diff --git a/spec/System/TestCommon_spec.lua b/spec/System/TestCommon_spec.lua index 61a848e14e..4ff1906d83 100644 --- a/spec/System/TestCommon_spec.lua +++ b/spec/System/TestCommon_spec.lua @@ -5,7 +5,7 @@ describe("Common", function() function ParentClass:ConstructorTestParentClass() return self end - local ChildClass = newClass("ConstructorTestProblemChildClass", "ConstructorTestParentClass") + local ChildClass = newClass("ConstructorTestProblemChild", "ConstructorTestParentClass") function ChildClass:ConstructorTestProblemChild() -- Intentionally does not call self:ConstructorTestParentClass() return self @@ -40,7 +40,7 @@ describe("Common", function() return self end - local ChildClass = newClass("ConstructorTestProblemChildClass", "ConstructorTestParentClass") + local ChildClass = newClass("ConstructorTestProblemChild", "ConstructorTestParentClass") function ChildClass:ConstructorTestProblemChild() self.ConstructorTestParentClass() return self diff --git a/src/Classes/ModDB.lua b/src/Classes/ModDB.lua index c71599b5a0..ffaf1e5ea5 100644 --- a/src/Classes/ModDB.lua +++ b/src/Classes/ModDB.lua @@ -135,7 +135,7 @@ end function ModDBClass:SumInternal(context, modType, cfg, flags, keywordFlags, source, ...) local result = 0 - local globalLimits = { } + local globalLimits for i = 1, select('#', ...) do local modList = self.mods[select(i, ...)] if modList then @@ -143,6 +143,9 @@ function ModDBClass:SumInternal(context, modType, cfg, flags, keywordFlags, sour local mod = modList[i] if mod.type == modType and band(flags, mod.flags) == mod.flags and MatchKeywordFlags(keywordFlags, mod.keywordFlags) and (not source or ( mod.source and mod.source:match("[^:]+") == source )) then if mod[1] then + if not globalLimits then + globalLimits = {} + end local value = context:EvalMod(mod, cfg, globalLimits) or 0 result = result + value else @@ -161,7 +164,7 @@ end function ModDBClass:MoreInternal(context, cfg, flags, keywordFlags, source, ...) local result = 1 local modPrecision = nil - local globalLimits = { } + local globalLimits for i = 1, select('#', ...) do local modList = self.mods[select(i, ...)] local modResult = 1 --The more multipliers for each mod are computed to the nearest percent then applied. @@ -171,6 +174,9 @@ function ModDBClass:MoreInternal(context, cfg, flags, keywordFlags, source, ...) if mod.type == "MORE" and band(flags, mod.flags) == mod.flags and MatchKeywordFlags(keywordFlags, mod.keywordFlags) and (not source or mod.source:match("[^:]+") == source) then local value if mod[1] then + if not globalLimits then + globalLimits = {} + end value = context:EvalMod(mod, cfg, globalLimits) or 0 else value = mod.value or 0 @@ -270,7 +276,7 @@ function ModDBClass:ListInternal(context, result, cfg, flags, keywordFlags, sour end function ModDBClass:TabulateInternal(context, result, modType, cfg, flags, keywordFlags, source, ...) - local globalLimits = { } + local globalLimits for i = 1, select('#', ...) do local modName = select(i, ...) local modList = self.mods[modName] @@ -280,6 +286,9 @@ function ModDBClass:TabulateInternal(context, result, modType, cfg, flags, keywo if (mod.type == modType or not modType) and band(flags, mod.flags) == mod.flags and MatchKeywordFlags(keywordFlags, mod.keywordFlags) and (not source or mod.source:match("[^:]+") == source) then local value if mod[1] then + if not globalLimits then + globalLimits = {} + end value = context:EvalMod(mod, cfg, globalLimits) else value = mod.value diff --git a/src/Modules/CalcSetup.lua b/src/Modules/CalcSetup.lua index 5b2d481a3c..780fd7e7c0 100644 --- a/src/Modules/CalcSetup.lua +++ b/src/Modules/CalcSetup.lua @@ -114,8 +114,21 @@ function calcs.initModDB(env, modDB) modDB.conditions["Effective"] = env.mode_effective end -function calcs.buildModListForNode(env, node) - local modList = new("ModList"):ModList() +-- Recycle a modlist so that we do not allocate many tables for each node. +local function resetModList(list) + for i = #list, 1, -1 do + list[i] = nil + end + list.multipliers = wipeTable(list.multipliers) + list.conditions = wipeTable(list.conditions) + list.actor = wipeTable(list.actor) + list.parent = false + return list +end + +---@param reuse table|nil A ModList to recycle instead of allocating. Only safe when the caller discards the result. +function calcs.buildModListForNode(env, node, reuse) + local modList = reuse and resetModList(reuse) or new("ModList"):ModList() if node.type == "Keystone" then modList:AddMod(node.keystoneMod) else @@ -123,51 +136,118 @@ function calcs.buildModListForNode(env, node) end -- Run first pass radius jewels - for _, rad in pairs(env.radiusJewelList) do - if rad.type == "Other" and rad.nodes[node.id] and rad.nodes[node.id].type ~= "Mastery" then - rad.func(node, modList, rad.data) + for i = 1, #env.radiusJewelList do + local rad = env.radiusJewelList[i] + if rad.type == "Other" then + local radNode = rad.nodes[node.id] + if radNode and radNode.type ~= "Mastery" then + rad.func(node, modList, rad.data) + end end end - if modList:Flag(nil, "PassiveSkillHasNoEffect") or (env.allocNodes[node.id] and modList:Flag(nil, "AllocatedPassiveSkillHasNoEffect")) then + -- prefilter the modlist so that every :Flag() call does not have to go through the entire mod list + local hasNoEffect, hasAllocNoEffect, hasScale, hasOtherEffect, hasExtraSkill, hasExplode + for i = 1, #modList do + local name = modList[i].name + if name == "PassiveSkillHasNoEffect" then + hasNoEffect = true + elseif name == "AllocatedPassiveSkillHasNoEffect" then + hasAllocNoEffect = true + elseif name == "PassiveSkillEffect" then + hasScale = true + elseif name == "PassiveSkillHasOtherEffect" then + hasOtherEffect = true + elseif name == "ExtraSkill" then + hasExtraSkill = true + elseif name == "CanExplode" then + hasExplode = true + end + end + + if (hasNoEffect and modList:Flag(nil, "PassiveSkillHasNoEffect")) or (env.allocNodes[node.id] and (hasAllocNoEffect and modList:Flag(nil, "AllocatedPassiveSkillHasNoEffect"))) then wipeTable(modList) + hasScale = false + hasOtherEffect = nil + hasExtraSkill = nil + hasExplode = nil end -- Apply effect scaling - local scale = calcLib.mod(modList, nil, "PassiveSkillEffect") - if scale ~= 1 then - local scaledList = new("ModList"):ModList() - scaledList:ScaleAddList(modList, scale) - modList = scaledList + if hasScale then + local scale = calcLib.mod(modList, nil, "PassiveSkillEffect") + if scale ~= 1 then + local scaledList = new("ModList"):ModList() + scaledList:ScaleAddList(modList, scale) + modList = scaledList + end end -- Run second pass radius jewels - for _, rad in pairs(env.radiusJewelList) do + local rescan = false + for i = 1, #env.radiusJewelList do + local rad = env.radiusJewelList[i] if rad.nodes[node.id] and rad.nodes[node.id].type ~= "Mastery" and (rad.type == "Threshold" or (rad.type == "Self" and env.allocNodes[node.id]) or (rad.type == "SelfUnalloc" and not env.allocNodes[node.id])) then rad.func(node, modList, rad.data) + rescan = true + hasOtherEffect = nil + hasExtraSkill = nil + hasExplode = nil + end + end + + if rescan then + for i = 1, #modList do + local name = modList[i].name + if name == "PassiveSkillHasOtherEffect" then + hasOtherEffect = true + elseif name == "ExtraSkill" then + hasExtraSkill = true + elseif name == "CanExplode" then + hasExplode = true + end end end - if modList:Flag(nil, "PassiveSkillHasOtherEffect") then - for i, mod in ipairs(modList:List(skillCfg, "NodeModifier")) do - if i == 1 then wipeTable(modList) end - modList:AddMod(mod.mod) + if hasOtherEffect and modList:Flag(nil, "PassiveSkillHasOtherEffect") then + local newMods = modList:List(nil, "NodeModifier") + for i = 1, #newMods do + local mod = newMods[i].mod + if i == 1 then + wipeTable(modList) + hasExtraSkill = nil + hasExplode = nil + end + if mod.name == "ExtraSkill" then + hasExtraSkill = true + elseif mod.name == "CanExplode" then + hasExplode = true + end + modList:AddMod(mod) end end - node.grantedSkills = { } - for _, skill in ipairs(modList:List(nil, "ExtraSkill")) do - if skill.name ~= "Unknown" then - t_insert(node.grantedSkills, { - skillId = skill.skillId, - level = skill.level, - noSupports = true, - source = "Tree:"..node.id - }) + node.grantedSkills = wipeTable(node.grantedSkills) + if hasExtraSkill then + local list = modList:List(nil, "ExtraSkill") + for i = 1, #list do + local skill = list[i] + if skill.name ~= "Unknown" then + t_insert(node.grantedSkills, { + skillId = skill.skillId, + level = skill.level, + noSupports = true, + source = "Tree:" .. node.id + }) + end end end - return modList, modList:Flag(nil, "CanExplode") and node + if hasExplode then + return modList, modList:Flag(nil, "CanExplode") and node + else + return modList + end end -- Build list of modifiers from the listed tree nodes @@ -181,8 +261,12 @@ function calcs.buildModListForNodeList(env, nodeList, finishJewels) -- Add node modifiers local modList = new("ModList"):ModList() local explodeSources = {} + -- Outside MAIN mode the per-node list is merged into modList and then + -- dropped, so a single list can be recycled for every node instead of + -- allocating one each time. + local scratch = env.mode ~= "MAIN" and new("ModList"):ModList() or nil for _, node in pairs(nodeList) do - local nodeModList, explode = calcs.buildModListForNode(env, node) + local nodeModList, explode = calcs.buildModListForNode(env, node, scratch) t_insert(explodeSources, explode) modList:AddList(nodeModList) if env.mode == "MAIN" then @@ -193,7 +277,7 @@ function calcs.buildModListForNodeList(env, nodeList, finishJewels) if finishJewels then -- Process extra radius nodes; these are unallocated nodes near conversion or threshold jewels that need to be processed for _, node in pairs(env.extraRadiusNodeList) do - local nodeModList = calcs.buildModListForNode(env, node) + local nodeModList = calcs.buildModListForNode(env, node, scratch) if env.mode == "MAIN" then node.finalModList = nodeModList end diff --git a/src/Modules/Common.lua b/src/Modules/Common.lua index 20e3346330..d9f7115282 100644 --- a/src/Modules/Common.lua +++ b/src/Modules/Common.lua @@ -76,6 +76,25 @@ local function getClass(className) return class end +-- wrap constructor to check that the constructors for all parent and superparent classes have been called +local function wrapConstructor(class, className, originalFunc) + return function(self, ...) + local ret = originalFunc(self, ...) + if class._parents then + for parent in pairs(class._superParents) do + if parent[parent._className] and not self._parentInit[parent] then + error("Parent class '" .. + parent._className .. "' of class '" .. className .. "' must be initialised") + end + end + end + if not ret then + error(string.format("Class %s constructor did not return a value", className)) + end + return ret + end +end + ---@generic T ---@param className `T` ---@param ... string parent class names @@ -90,8 +109,11 @@ function newClass(className, ...) end return obj end + -- a list of metatables. one for each parent + class._metaList = {} class._className = className local numVarArg = select("#", ...) + local parentIndex if numVarArg > 0 then -- Build list of parent classes class._parents = { } @@ -102,21 +124,79 @@ function newClass(className, ...) class._superParents = { } addSuperParents(class, class) -- Set up inheritance - setmetatable(class, { - __index = function(self, key) - for _, parent in ipairs(class._parents) do - local val = parent[key] - if val ~= nil then - self[key] = val - return val - end + function parentIndex(self, key) + for _, parent in ipairs(class._parents) do + local val = parent[key] + if val ~= nil then + rawset(self, key, val) + return val end end - }) + end end + setmetatable(class, { + __index = parentIndex, + __newindex = function(self, k, v) + if k == className then + -- Check that the constructors for all parent and superparent classes have been called + v = wrapConstructor(class, className, v) + end + rawset(self, k, v) + end + }) + class._unconstructedMeta = { + __index = function(obj, key) + if key == className then + setmetatable(obj, class) + return class[className] + end + error(s_format( + "Object of class '%s' was used before it was constructed (accessed '%s'). Did you forget to call new(\"%s\"):%s()?", + className, tostring(key), className, className)) + end, + } return class end +-- avoid rebuilding metatables constantly. this is done by caching class-parent pair metatables +local function getMeta(class, parent) + local metaList = rawget(class, "_metaList") + local meta = metaList[parent] + if not meta then + local parentName = parent._className + meta = { + __index = function(proxy, key) + local object = rawget(proxy, "_object") + local v = rawget(object, key) + if v ~= nil then + return v + else + return parent[key] + end + end, + __newindex = function(proxy, k, v) + local object = rawget(proxy, "_object") + object[k] = v + end, + __call = function(proxy, self, ...) + local object = rawget(proxy, "_object") + if not parent[parentName] then + error("Parent class '" .. parentName .. "' of class '" .. class._className .. "' has no constructor") + end + if object._parentInit[parent] then + error("Parent class '" .. parentName .. "' of class '" .. class._className .. "' has already been initialised") + end + if self ~= object then + error(string.format("Parent class %s constructor of class %s was not provided self. Are you perhaps calling it with self.%s instead of self:%s?", parentName, class._className, parentName, parentName)) + end + parent[parent._className](self, ...) + object._parentInit[parent] = true + end, + } + metaList[parent] = meta + end + return meta +end ---@generic T ---@param className `T` ---@param extraArg nil Never pass extra parameters. Defined purely to guard against old syntax. @@ -130,77 +210,14 @@ function new(className, extraArg) end local class = getClass(className) -- protect against calling new("Foo") without calling :Foo() - local object - if class[className] then - if not rawget(class, "_unconstructedMeta") then - class._unconstructedMeta = { - __index = function(obj, key) - if key == className then - setmetatable(obj, class) - return class[className] - end - error(s_format( - "Object of class '%s' was used before it was constructed (accessed '%s'). Did you forget to call new(\"%s\"):%s()?", - className, tostring(key), className, className)) - end, - } - end - object = setmetatable({}, class._unconstructedMeta) - else - object = setmetatable({}, class) - end + local object = setmetatable({}, class._unconstructedMeta or class) object.Object = object if class._parents then -- Add parent and superparent class proxies object._parentInit = { } for parent in pairs(class._superParents) do - local proxyMeta = { - __index = function(self, key) - local v = rawget(object, key) - if v ~= nil then - return v - else - return parent[key] - end - end, - __newindex = object, - __call = function(_, self, ...) - if not parent[parent._className] then - error("Parent class '"..parent._className.."' of class '"..class._className.."' has no constructor") - end - if object._parentInit[parent] then - error("Parent class '"..parent._className.."' of class '"..class._className.."' has already been initialised") - end - if self ~= object then - error(string.format("Parent class %s constructor of class %s was not provided self. Are you perhaps calling it with self.%s instead of self:%s?", parent._className, className, parent._className, parent._className)) - end - parent[parent._className](self, ...) - object._parentInit[parent] = true - end, - } - object[parent._className] = setmetatable(proxyMeta, proxyMeta) - end - end - - if class[className] and not rawget(class, "_constructorInitialised") then - local originalFunc = class[className] - class[className] = function(self, ...) - local ret = originalFunc(self, ...) - if class._parents then - -- Check that the constructors for all parent and superparent classes have been called - for parent in pairs(class._superParents) do - if parent[parent._className] and not self._parentInit[parent] then - error("Parent class '" .. - parent._className .. "' of class '" .. className .. "' must be initialised") - end - end - end - if not ret then - error(string.format("Class %s constructor did not return a value", className)) - end - return ret + object[parent._className] = setmetatable({ _object = object }, getMeta(class, parent)) end - class._constructorInitialised = true end return object end