300 introduces many new concepts. One is missing among them: variables. Because the PR is already massive, this new concept has been set aside to limit potential issues.
Concept
There are currently 4 types of trigger:
Aura
Action Usable
Talent
Holy Power
There is a Trigger class, which is no more than a bit field telling which of these types of trigger are required for each effect.
Currently, each of these types is spread out in many locations. The goal of the variables concept is to make possible to support a new type of trigger easily, without having to write code in so many places.
Textbook case
In order to illustrate how spread the code is, let's take a look at an example: Holy Power.
It will help imagining the kind of tasks necessary to achieve the ultimate variable implementation.
Trigger type
Near the beginning of trigger.lua all types of triggers are defined. Currently, the list is:
Variables are updated by two means: getting the current state, and updating the state through events. Manual checks cover the former.
Currently, manual checks are written in trigger.lua in the TriggerManualChecks map:
local TriggerManualChecks = {
[SAO.TRIGGER_HOLY_POWER] = function(bucket)
if Enum and Enum.PowerType and Enum.PowerType.HolyPower then
local holyPower = UnitPower("player", Enum.PowerType.HolyPower);
bucket:setHolyPower(holyPower);
else
SAO:Debug(Module, "Cannot fetch Holy Power because this resource is unknown from Enum.PowerType");
end
end,
}
As seen in this example, it calls the bucket:setHolyPower method. It will be discussed later on.
Hash type
Each trigger end up in the hash value used to index displays uniquely in buckets.
Hash types are defined in hash.lua. They are computed using bit fields, different from trigger bit fields. Hash bit fields carry data, while trigger bit fields only carry flags of whether or not we need this data to begin with.
Hash type bit fields are defined as the list of possible values, their mask, and are prefixed by a comment explaining the formula to go from the actual value to the bit field, or vice versa:
-- Holy Power
-- hash = HASH_HOLY_POWER_0 * (1 + holy_power)
local HASH_HOLY_POWER_0 = 0x0800
local HASH_HOLY_POWER_1 = 0x1000
local HASH_HOLY_POWER_2 = 0x1800
local HASH_HOLY_POWER_3 = 0x2000
local HASH_HOLY_POWER_MASK = 0x3800
Hash conflict check
To make sure there aren't any conflict with bit fields in hashes, hash.lua defines a list checked at start:
local masks = {
HASH_AURA_MASK,
HASH_ACTION_USABLE_MASK,
HASH_TALENT_MASK,
HASH_HOLY_POWER_MASK
}
In case you're wondering if there is such check with trigger bit fields, the answer is yes. It is done automatically, based on the content of TriggerNames.
Hash stringifier
In order to help how to read and write hash values, hash.lua defines an internal concept: hash stringifiers.
HashStringifier:register(
HASH_HOLY_POWER_MASK,
"holy_power", -- key
function(hash) -- toValue()
local holyPower = hash:getHolyPower();
return tostring(holyPower);
end,
function(hash, value) -- fromValue()
if tostring(tonumber(value)) == value then
hash:setHolyPower(tonumber(value));
return true;
else
return nil; -- Not good
end
end,
function(hash) -- getHumanReadableKeyValue
local holyPower = hash:getHolyPower();
return string.format(HOLY_POWER_COST, holyPower);
end,
function(hash) -- optionIndexer
return hash:getHolyPower();
end
);
Stringifiers are interesting, because they are an attempt at simplifying how to add new trigger types. It can definitely serve as an inspiration for the variable concept.
Hash methods
The Hash class defined in hash.lua has methods for getting and setting each hash type.
-- Holy Power
hasHolyPower = function(self)
return bit.band(self.hash, HASH_HOLY_POWER_MASK) ~= 0;
end,
setHolyPower = function(self, holyPower)
if type(holyPower) ~= 'number' or holyPower < 0 then
SAO:Warn(Module, "Invalid Holy Power "..tostring(holyPower));
elseif holyPower > 3 then
SAO:Debug(Module, "Holy Power overflow ("..holyPower..") truncated to 3");
setMaskedHash(self, HASH_HOLY_POWER_3, HASH_HOLY_POWER_MASK);
else
setMaskedHash(self, HASH_HOLY_POWER_0 * (1 + holyPower), HASH_HOLY_POWER_MASK);
end
end,
getHolyPower = function(self)
local maskedHash = getMaskedHash(self, HASH_HOLY_POWER_MASK);
if maskedHash == nil then return nil; end
return (maskedHash / HASH_HOLY_POWER_0) - 1;
end,
It should be noted that there is no "holy power member". In hashes, everything is baked in the unique hash number, which is a 32-bit integer (maybe more than 32 bits, but we at least 32 for sure).
Another important point, is that there is no method in the Hash class for reading or writing strings. These are computed automagically thanks to hash stringifiers.
Member in bucket
The Bucket class defined in bucket.lua holds a 'current state' of each variable.
The member is set in the class constructor and reset method:
And there is a method for setting them and apply these changes internally for e.g. showing or hiding displays:
setHolyPower = function(self, holyPower)
if self.currentHolyPower == holyPower then
return;
end
self.currentHolyPower = holyPower;
self.trigger:inform(SAO.TRIGGER_HOLY_POWER);
self.hashCalculator:setHolyPower(holyPower);
self:applyHash();
end,
Talking about displays, there is no need to define explicitly what holy power is, how it changes, or even why it exists in the first place. Displays only care about hash numbers as unique indexes. They don't care about about how the hash was computed.
Effect doc
At the beginning of effect.lua, a big comment showcases what a Native Optimized Effect (NOE) looks like. Among this comment, is this:
--[[
...
triggers = { -- Default is false for every trigger; at least one trigger must be set
aura = true, -- The aura with spell ID 'spellID' is gained or lost by the player
action = false, -- The action with spell ID 'spellID' is usable or not
talent = false, -- The player spent at least one point in effect's talent, or spent none
holyPower = false, -- Number of charges of Holy Power (Paladin only, Cataclysm)
},
overlays = {{
project = SAO.WRATH, -- Default is project from effect
condition = { -- Default is the default value for each trigger defined in 'triggers'
aura = 0, -- Default is 0. -1 for 'aura missing', 0 for 'any stacks', 1 or more tells the number of stacks
action = nil, -- Default is true. true for action usable, false for action not usable
talent = nil, -- Default is true. true for talent picked (at least one point), false for talent not picked
holyPower = nil, -- Default is 3
},
...
]]
There is no equivalent documentation for showcasing a Human Readable Effect (HRE). If we ever write one, variables should be mentioned here as well.
Condition Builder
Similar to hash stringifiers, effect.lua defines an internal concept of condition builders. This helps building the condition, mentioned in effect documentation:
ConditionBuilder:register(
"holyPower", -- Name used by NOE
"holyPower", -- Name used by HRE
0, -- Default (NOE only)
"setHolyPower", -- Setter method for Hash
"Holy Power value",
function(value) return type(value) == 'number' and value >= 0 and value <= 3 end,
function(value) return value end
);
Import trigger
Each HRE ends up converted into a NOE. This conversion process performs operations called 'imports', which fetches content from the HRE to put it into the corresponding NOE.
Among those imports is the step of "import trigger". But not all triggers are imported. Each HRE may only import triggers useful to them, and either discard or force imports to the NOE, whether or not they are part of the HRE. For example, the "aura" HRE forces the usage of the 'aura trigger', while the "counter" HRE forces the usage of the 'action usable' trigger.
The Holy Power uses the (poorly worded) importResource function:
local function importResource(effect, props)
importTrigger(effect, props, "holyPower", "useHolyPower");
end
This function is then used in appropriate HRE creators, for example when creating a counter:
local function createCounter(effect, props)
-- Import triggers
effect.triggers.action = true; -- Forced
importTalent(effect, props); -- Optional
importResource(effect, props); -- Optional
importCounterButton(effect, props);
return effect;
end
When variables will be centralized, the importX functions can be generalized, but extra caution should be taken to not call all trigger imports for all types of HREs.
Events
Variables are updated by two means: getting the current state, and updating the state through events. Events cover the latter.
In its most basic form, events just grab a "the state I'm looking at has changed" event. This is what Holy Power does.
function SAO.UNIT_POWER_FREQUENT(self, unitTarget, powerType)
if unitTarget == "player" and powerType == "HOLY_POWER" then
self:CheckManuallyAllBuckets(SAO.TRIGGER_HOLY_POWER);
end
end
Calling CheckManuallyAllBuckets(SAO.TRIGGER_HOLY_POWER) is straightforward, and could be used as a lazy way to start testing new variables. But when performance comes into play, more refined calls should be made.
And of course, events must be caught by a frame, using the game's Frame:RegisterEvent method. For example in SpellActivationOverlay.lua:
if ( SAO.IsCata() and classFile == "PALADIN" ) then
self:RegisterEvent("UNIT_POWER_FREQUENT"); -- For Holy Power, introduced in Cataclysm
end
The "if paladin" test is a bit overkill. But it shows that events should not be registered bluntly. There are cases (here, based on class) where we do not need to register to every event out there. Event parsing has a cost and should be handled with caution, especially if we want the addon to avoid wasting precious resources, which has been a core objective since its inception.
Suggestion
Based on the above analysis, here are a few key points that come to mind:
each variable could be indexed from the Trigger type e.g., from SAO.TRIGGER_HOLY_POWER for Holy Power
most code can be derived from a single 'variable' containing members or methods that perform necessary operations described above
variable.lua
Here is a quick start for implementing variables in what could be the future variable.lua file.
local AddonName, SAO = ...
local Module
-- Variable definition map
-- key = trigger flag, value = variable object
SAO.Variables = {}
local function check(var, member, expectedType)
if type(var) ~= 'table' then
return;
elseif not var[member] then
SAO:Warn(Module, "Variable does not define a "..tostring(member));
elseif type(var[member]) ~= expectedType then
SAO:Warn(Module, "Variable defines member "..tostring(member).." of type '"..type(var[member]).."' instead of '"..expectedType.."'");
end
end
SAO.Variable = {
register = function(self, var)
check(var, trigger, 'table');
check(var.trigger, flag, 'number'); -- TRIGGER_HOLY_POWER
check(var.trigger, name, 'string'); -- "holyPower"
check(var, hash, 'table');
check(var.hash, mask, 'number'); -- HASH_HOLY_POWER_MASK
check(var.hash, key, 'number'); -- "holy_power"
check(var.hash, setter, 'string'); -- "setHolyPower"
--[[ function(self, holyPower, bucket)
if type(holyPower) ~= 'number' or holyPower < 0 then
SAO:Warn(Module, "Invalid Holy Power "..tostring(holyPower));
elseif holyPower > 3 then
SAO:Debug(Module, "Holy Power overflow ("..holyPower..") truncated to 3");
setMaskedHash(self, HASH_HOLY_POWER_3, HASH_HOLY_POWER_MASK);
else
setMaskedHash(self, HASH_HOLY_POWER_0 * (1 + holyPower), HASH_HOLY_POWER_MASK);
end
end]]
check(var.hash, setterFunc, 'function');
check(var.hash, getter, 'string'); -- "getHolyPower"
--[[ function(self)
local maskedHash = getMaskedHash(self, HASH_HOLY_POWER_MASK);
if maskedHash == nil then return nil; end
return (maskedHash / HASH_HOLY_POWER_0) - 1;
end]]
check(var.hash, getterFunc, 'function');
-- function(hash) return tostring(hash:getHolyPower()) end
check(var.hash, toValue, 'function');
--[[ function(hash, value)
if tostring(tonumber(value)) == value then
hash:setHolyPower(tonumber(value));
return true;
else
return nil; -- Not good
end
end]]
-- function(hash) return string.format(HOLY_POWER_COST, hash:getHolyPower()) end
check(var.hash, getHumanReadableKeyValue, 'function');
-- function(hash) return hash:getHolyPower() end
check(var.hash, optionIndexer, 'function');
check(var, bucket, 'table');
check(var.bucket, member, 'string'); -- "currentHolyPower"
-- check(var.bucket, impossibleValue, 'any'); -- can be anything, usually nil or -1
check(var.bucket, setter, 'string'); -- "setHolyPower"
check(var, event, 'table');
check(var.event, names, 'table'); -- { "UNIT_POWER_FREQUENT" }
-- function() return SAO.IsCata() and select(2, UnitClass("player")) == "PALADIN" end
check(var.event, isRequired, 'function');
--[[ function(bucket)
if Enum and Enum.PowerType and Enum.PowerType.HolyPower then
local holyPower = UnitPower("player", Enum.PowerType.HolyPower);
bucket:setHolyPower(holyPower);
else
SAO:Debug(Module, "Cannot fetch Holy Power because this resource is unknown from Enum.PowerType");
end
end]]
check(var, fetchAndSet, 'function'); -- Formerly in TriggerManualChecks
check(var, condition, 'table');
check(var.condition, noeVar, 'string'); -- "holyPower"
check(var.condition, hreVar, 'string'); -- "holyPower"
-- check(var.condition, noeDefault, 'any'); -- can be anything, usually 0
check(var.condition, description, 'string'); -- "Holy Power value"
-- function(value) return type(value) == 'number' and value >= 0 and value <= 3 end
check(var.condition, checker, 'function');
-- function(value) return value end
check(var.condition, noeToHash, 'function');
check(var, import, 'table');
check(var.import, noeTrigger, 'string'); -- "holyPower"
check(var.import, hreTrigger, 'string'); -- "useHolyPower"
-- Add the hash setter and getter directly to the Hash class definition
Hash[var.hash.setter] = var.hash.setterFunc;
Hash[var.hash.getter] = var.hash.getterFunc;
-- Add the bucket setter directly to the bucket class declaration
Bucket[var.bucket.setter] = function(self, value)
if self[var.bucket.member] == value then
return;
end
self[var.bucket.member] = value;
self.trigger:inform(var.trigger.flag);
self.hashCalculator[var.hash.setter](value, bucket);
self:applyHash();
end,
self.__index = nil;
setmetatable(var, self);
self.__index = nil;
Variables[var.trigger.flag] = var;
end
}
And then, we would need to update the code to:
replace existing code for variables in buckets, hashes, etc. and even tables such as TriggerNames
for example, bucket's create and reset methods would parse Variables
initializing/resetting variables would use and use var.bucket.member and var.bucket.impossibleValue
bit field checks should parse the list of Variables instead of e.g. TriggerNames
port code for hash stringifiers and condition builders to base their list on the Variables table
Events
Event are declared in the Variable class, but not actually plugged in.
Code has to be added to SpellActivationOverlay_OnLoad to parse the list of variable events, and call RegisterEvent to register those which return true to their isRequired() function.
And event handling code has to be added to events.lua as well.
Homemade Development
Some code still needs to be done manually by developers:
the new trigger type must be added to trigger.lua
the trigger must be mentioned in effect documentation
and as mentioned previously, event code must be written to events.lua
File structure
Because each variable may have a significant amount of lines of code, and because variables never really interact with each other, they could end up within a new folder structure of variables. For example:
the above source code would be in components/variable.lua
the Holy Power code would be in variables/holypower.lua
and this folder would have others e.g. variables/talent.lua
These files should be added to SpellActivationOverlay.toc and the new folder variables folder should be included when packaging flavors. Some flavors could also exclude unused variables. For example, holypower.lua would be included in the Cataclysm flavor only.
Introduction
300 introduces many new concepts. One is missing among them: variables. Because the PR is already massive, this new concept has been set aside to limit potential issues.
Concept
There are currently 4 types of trigger:
There is a
Trigger
class, which is no more than a bit field telling which of these types of trigger are required for each effect.Currently, each of these types is spread out in many locations. The goal of the variables concept is to make possible to support a new type of trigger easily, without having to write code in so many places.
Textbook case
In order to illustrate how spread the code is, let's take a look at an example: Holy Power.
It will help imagining the kind of tasks necessary to achieve the ultimate variable implementation.
Trigger type
Near the beginning of
trigger.lua
all types of triggers are defined. Currently, the list is:Adding a new variable should start by adding a new value in this list.
Trigger name
In
trigger.lua
, close to the above list, is the list of trigger names:Manual Check
Variables are updated by two means: getting the current state, and updating the state through events. Manual checks cover the former.
Currently, manual checks are written in
trigger.lua
in theTriggerManualChecks
map:As seen in this example, it calls the
bucket:setHolyPower
method. It will be discussed later on.Hash type
Each trigger end up in the hash value used to index displays uniquely in buckets.
Hash types are defined in
hash.lua
. They are computed using bit fields, different from trigger bit fields. Hash bit fields carry data, while trigger bit fields only carry flags of whether or not we need this data to begin with.Hash type bit fields are defined as the list of possible values, their mask, and are prefixed by a comment explaining the formula to go from the actual value to the bit field, or vice versa:
Hash conflict check
To make sure there aren't any conflict with bit fields in hashes,
hash.lua
defines a list checked at start:In case you're wondering if there is such check with trigger bit fields, the answer is yes. It is done automatically, based on the content of
TriggerNames
.Hash stringifier
In order to help how to read and write hash values,
hash.lua
defines an internal concept: hash stringifiers.Stringifiers are interesting, because they are an attempt at simplifying how to add new trigger types. It can definitely serve as an inspiration for the variable concept.
Hash methods
The
Hash
class defined inhash.lua
has methods for getting and setting each hash type.It should be noted that there is no "holy power member". In hashes, everything is baked in the unique hash number, which is a 32-bit integer (maybe more than 32 bits, but we at least 32 for sure).
Another important point, is that there is no method in the
Hash
class for reading or writing strings. These are computed automagically thanks to hash stringifiers.Member in bucket
The
Bucket
class defined inbucket.lua
holds a 'current state' of each variable.The member is set in the class constructor and reset method:
And there is a method for setting them and apply these changes internally for e.g. showing or hiding displays:
Talking about displays, there is no need to define explicitly what holy power is, how it changes, or even why it exists in the first place. Displays only care about hash numbers as unique indexes. They don't care about about how the hash was computed.
Effect doc
At the beginning of
effect.lua
, a big comment showcases what a Native Optimized Effect (NOE) looks like. Among this comment, is this:There is no equivalent documentation for showcasing a Human Readable Effect (HRE). If we ever write one, variables should be mentioned here as well.
Condition Builder
Similar to hash stringifiers,
effect.lua
defines an internal concept of condition builders. This helps building thecondition
, mentioned in effect documentation:Import trigger
Each HRE ends up converted into a NOE. This conversion process performs operations called 'imports', which fetches content from the HRE to put it into the corresponding NOE.
Among those imports is the step of "import trigger". But not all triggers are imported. Each HRE may only import triggers useful to them, and either discard or force imports to the NOE, whether or not they are part of the HRE. For example, the
"aura"
HRE forces the usage of the 'aura trigger', while the"counter"
HRE forces the usage of the 'action usable' trigger.The Holy Power uses the (poorly worded)
importResource
function:This function is then used in appropriate HRE creators, for example when creating a counter:
When variables will be centralized, the
importX
functions can be generalized, but extra caution should be taken to not call all trigger imports for all types of HREs.Events
Variables are updated by two means: getting the current state, and updating the state through events. Events cover the latter.
In its most basic form, events just grab a "the state I'm looking at has changed" event. This is what Holy Power does.
Calling
CheckManuallyAllBuckets(SAO.TRIGGER_HOLY_POWER)
is straightforward, and could be used as a lazy way to start testing new variables. But when performance comes into play, more refined calls should be made.And of course, events must be caught by a frame, using the game's
Frame:RegisterEvent
method. For example inSpellActivationOverlay.lua
:The "if paladin" test is a bit overkill. But it shows that events should not be registered bluntly. There are cases (here, based on class) where we do not need to register to every event out there. Event parsing has a cost and should be handled with caution, especially if we want the addon to avoid wasting precious resources, which has been a core objective since its inception.
Suggestion
Based on the above analysis, here are a few key points that come to mind:
SAO.TRIGGER_HOLY_POWER
for Holy Powervariable.lua
Here is a quick start for implementing variables in what could be the future
variable.lua
file.And then, we would need to update the code to:
TriggerNames
create
andreset
methods would parseVariables
var.bucket.member
andvar.bucket.impossibleValue
Variables
instead of e.g.TriggerNames
Events
Event are declared in the Variable class, but not actually plugged in.
Code has to be added to
SpellActivationOverlay_OnLoad
to parse the list of variable events, and callRegisterEvent
to register those which returntrue
to theirisRequired()
function.And event handling code has to be added to
events.lua
as well.Homemade Development
Some code still needs to be done manually by developers:
trigger.lua
events.lua
File structure
Because each variable may have a significant amount of lines of code, and because variables never really interact with each other, they could end up within a new folder structure of
variables
. For example:components/variable.lua
variables/holypower.lua
variables/talent.lua
These files should be added to
SpellActivationOverlay.toc
and the new foldervariables
folder should be included when packaging flavors. Some flavors could also exclude unused variables. For example,holypower.lua
would be included in the Cataclysm flavor only.