-- Base Class Helpers -- Contains necessary logic for defining, creating and working with all Class structures in a consistent manner local appName, app = ...; -- Global locals local type, pairs, tonumber, setmetatable, rawget, tinsert = type, pairs, tonumber, setmetatable, rawget, tinsert; -- App locals local GetRelativeValue = app.GetRelativeValue; -- Lib Helpers local constructor = function(id, t, typeID) if t then if not t.g and t[1] then return { g=t, [typeID]=id }; else t[typeID] = id; return t; end else return {[typeID] = id}; end end local returnZero = function() return 0; end; -- Provides a Unique Counter value for the Key referenced on each reference local uniques = setmetatable({}, { __index = function(t, key) return 0; end }); local UniqueCounter = setmetatable({}, { __index = function(t, key) local count = uniques[key] + 1; -- app.PrintDebug("UniqueCounter",key,count) uniques[key] = count; return count; end }); app.UniqueCounter = UniqueCounter; -- Proper unique hash for a Class Object is not as simple as ID..Value, there are many situations where that does not provide adequate uniqueness local function CreateHash(t) local key = t.key or t.text; if key then local hash = key .. (t[key] or "NOKEY"); if key == "criteriaID" and t.achievementID then hash = hash .. ":" .. t.achievementID; elseif key == "itemID" and t.modItemID and t.modItemID ~= t.itemID then hash = key .. t.modItemID; elseif key == "creatureID" then if t.encounterID then hash = hash .. ":" .. t.encounterID; end local difficultyID = GetRelativeValue(t, "difficultyID"); if difficultyID then hash = hash .. "-" .. difficultyID; end elseif key == "encounterID" then if t.creatureID then hash = hash .. ":" .. t.creatureID; end local difficultyID = GetRelativeValue(t, "difficultyID"); if difficultyID then hash = hash .. "-" .. difficultyID; end if t.crs then local numCrs = #t.crs; if numCrs == 1 then hash = hash .. t.crs[1]; elseif numCrs == 2 then hash = hash .. t.crs[1] .. t.crs[2]; elseif numCrs > 2 then hash = hash .. t.crs[1] .. t.crs[2] .. t.crs[3]; end end elseif key == "difficultyID" then local instanceID = GetRelativeValue(t, "instanceID") or GetRelativeValue(t, "headerID"); if instanceID then hash = hash .. "-" .. instanceID; end elseif key == "headerID" then -- for custom headers, they may be used in conjunction with other bits of data that we don't want to merge together (because it makes no sense) -- Separate if using Class requirements if t.c then for _,class in pairs(t.c) do hash = hash .. "C" .. class; end end -- Separate if using Faction/Race requirements if t.r then hash = "F" .. t.r .. hash; elseif t.races then for _,race in pairs(t.races) do hash = hash .. "R" .. race; end end elseif key == "spellID" and t.itemID then -- Some recipes teach the same spell, so need to differentiate by their itemID as well hash = hash .. ":" .. t.itemID; end if t.rank then hash = hash .. "." .. t.rank; -- app.PrintDebug("hash.rank",hash) end if t.nomerge then hash = hash.."__"..UniqueCounter["Hash"]; end t.hash = hash; return hash; end end app.CreateHash = CreateHash; -- Represents default field evaluation logic for all Classes unless defined within the Class local DefaultFields = { -- Cloned groups will not directly have a parent, but they will instead have a sourceParent, so fill in with that instead ["parent"] = function(t) return t.sourceParent; end, -- A semi-unique string value that identifies this object based on its key, or text if it doesn't have one. ["hash"] = function(t) return CreateHash(t); end, -- Default text should be a valid link or name ["text"] = function(t) return t.link or t.name; end, -- Whether or not something is repeatable. ["repeatable"] = function(t) return t.isDaily or t.isWeekly or t.isMonthly or t.isYearly or t.isWorldQuest; end, ["costProgress"] = returnZero, ["costTotal"] = returnZero, ["progress"] = returnZero, ["total"] = returnZero, }; -- Creates a Base Object Table which will evaluate the provided set of 'fields' (each field value being a keyed function) local classDefinitions, _cache = {}; local BaseObjectFields = function(fields, className) if not className then print("A Class Name must be declared when using BaseObjectFields"); end local class = { __type = function() return className; end }; if not classDefinitions[className] then classDefinitions[className] = class; else print("A Class has already been defined with that name!", className); end if fields then for key,method in pairs(fields) do class[key] = method; end end -- Inject the default fields into the class for key,method in pairs(DefaultFields) do if not rawget(class, key) then class[key] = method; end end return { __index = function(t, key) _cache = rawget(class, key); if _cache then return _cache(t); end end }; end app.BaseObjectFields = BaseObjectFields; app.BaseClass = BaseObjectFields(nil, "BaseClass"); app.CreateClass = function(className, classKey, fields, ...) -- Validate arguments if not className then print("A Class Name must be declared when using CreateClass"); end if not classKey then print("A Class Key must be declared when using CreateClass"); end if not fields then print("Fields must be declared when using CreateClass"); end -- Ensure that a key field exists! if not fields.key then fields.key = function() return classKey; end; end -- If this object supports collectibleAsCost, that means it needs a way to fallback to a version of itself without any cost evaluations should it detect that it doesn't use it anywhere. if fields.collectibleAsCost then local simpleclass = {}; for key,method in pairs(fields) do simpleclass[key] = method; end simpleclass.collectibleAsCost = app.ReturnFalse; simpleclass.collectedAsCost = nil; local simplemeta = BaseObjectFields(simpleclass, "Simple" .. className); fields.simplemeta = function(t) return simplemeta; end; end local args = { ... }; local total = #args; if total > 0 then local conditionals = {}; for i=1,total,3 do local class = args[i + 1]; tinsert(conditionals, args[i + 2]); if class then for key,method in pairs(fields) do if not rawget(class, key) then class[key] = method; end end if class.collectibleAsCost then local simpleclass = {}; for key,method in pairs(class) do simpleclass[key] = method; end simpleclass.collectibleAsCost = app.ReturnFalse; simpleclass.collectedAsCost = nil; local simplemeta = BaseObjectFields(simpleclass, "Simple" .. className .. args[i]); class.simplemeta = function(t) return simplemeta; end; end tinsert(conditionals, BaseObjectFields(class, className .. args[i])); else tinsert(conditionals, {}); end end total = #conditionals; fields.conditionals = conditionals; local Class = BaseObjectFields(fields, className); return function(id, t) t = constructor(id, t, classKey); for i=1,total,2 do if conditionals[i](t) then return setmetatable(t, conditionals[i + 1]); end end return setmetatable(t, Class); end, Class; else local Class = BaseObjectFields(fields, className); return function(id, t) return setmetatable(constructor(id, t, classKey), Class); end, Class; end end app.ExtendClass = function(baseClassName, className, classKey, fields, ...) local baseClass = classDefinitions[baseClassName]; if baseClass then if not fields then fields = {}; end for key,method in pairs(baseClass) do if not fields[key] then fields[key] = method; end end fields.__type = nil; fields.key = nil; else print("Could not find specified base class:", baseClassName); end return app.CreateClass(className, classKey, fields, ...); end -- Allows wrapping one Type Object with another Type Object. This allows for fall-through field logic -- without requiring a full copied definition of identical field functions and raw Object content app.WrapObject = function(object, baseObject) if not object or not baseObject then error("Tried to WrapObject with none provided!",object,baseObject) end -- need to preserve the existing object's meta AND return the object being wrapped while also allowing fallback to the base object local objectMeta = getmetatable(object) if not objectMeta then error("Tried to WrapObject which has no metatable! (Wrapping not necessary)") end objectMeta = objectMeta.__index if not objectMeta then error("Tried to WrapObject which has no index!") end if type(objectMeta) == "function" then return setmetatable(object, { __index = function(t, key) return objectMeta(t, key) or baseObject[key]; end }); end return setmetatable(object, { __index = function(t, key) return objectMeta[key] or baseObject[key]; end }); end --[[ -- Proof of Concept with Class Conditionals local fields = { ["name"] = function(t) return "Loki"; end, ["OnTest"] = function() return function(t) print(t.name .. " (" .. t.__type .. "): I'm a god!"); end end, }; local fieldsWithArgs = { OnTest = function() return function(t) print(t.name .. " (" .. t.__type .. "): I'm a variant!"); end end }; local fieldsWithFeeling = { OnTest = function() return function(t) print(t.name .. " (" .. t.__type .. "): I'm a variant... with feeling!"); end end }; app.CreateExample = app.CreateClass("Example", "exampleID", fields, "WithArgs", fieldsWithArgs, (function(t) return t.args; end), "WithFeeling", fieldsWithFeeling, (function(t) return t.feeling; end)); for i,instance in ipairs({ app.CreateExample(1), app.CreateExample(2, { name = "Alligator Loki", args = "I'm a Crocodile!" }), app.CreateExample(3, { name = "Sylvie", feeling = "Pretty Neat" }), }) do instance.OnTest(instance); end ]]--