zzragnar0kzz
Chieftain
- Joined
- May 2, 2021
- Messages
- 21
Shout out to the Community in general, and to anyone with an unhealthy interest in Goody Huts in particular!
Posting to create a common knowledge repository relating to Goody Huts and Lua, and to provide examples of what can be accomplished when a reward is received.
To fully utilize the tribal village reward system within Lua, there are a few key points to be aware of:
ImprovementActivated fires whenever any improvement is activated, which includes the Goody Hut. A properly-configured hook to this Event provides much, but not all, of the vital data needed to manipulate the granted reward using Lua, as follows:
Of particular interest here are the following:
Of particular interest here are the following:
Now the listeners for each Event can be modified to act only when needed, and to store their values in the correct queue and/or pull corresponding values from the other queue as needed.
The listener for GoodyHutReward should first check the boolean flag, and if that flag is true it should set it to false and abort. It should then check the reward (sub)type hash values, and if they correspond to the meteor strike reward, it should abort. If it passes both checks without aborting, it should store its current arguments in a table and check the ImprovementActivated queue to determine which Event fired first for the current reward. If the ImprovementActivated queue is empty, insert the current arguments table into the GoodyHutReward queue at the end; if the ImprovementActivated queue is NOT empty, remove the first set of arguments from it and add those to the current arguments table, then pass the consolidated arguments to another function for further processing. The updated listener looks like this:
The listener for ImprovementActivated should first check the activated improvement index to determine what type of improvement was activated. If the activated improvement was a barbarian camp, it should check the value of iOwnerID, and if that is > -1, it will try to determine if this value represents the Sumerian civilization, setting the boolean flag to true if it does. If iOwnerID is NOT > -1, it will repeat the same check using iImprovementOwnerID instead. It should then abort. If the activated improvement is NOT a barbarian camp, and is also NOT a goody hut, it should abort. If it passes both checks without aborting, it should store its current arguments in a table and check the GoodyHutReward queue to determine which Event fired first for the current reward. If the GoodyHutReward queue is empty, insert the current arguments table into the ImprovementActivated queue at the end; if the GoodyHutReward queue is NOT empty, remove the first set of arguments from it and add those to the current arguments table, then pass the consolidated arguments to another function for further processing. The updated listener looks like this:
Alternately, individual arguments can be stored/passed in lieu of a table of arguments. With the above, or similar, code in place, whichever Event fires first will store its arguments in the appropriate queue, and those arguments will be retrieved and used by whichever Event fires second. Any queued Event arguments should be removed from the appropriate queue in the order they were added.
So, why go through all of this hassle to gather and consolidate Event arguments? Once all the arguments from both Events have been consolidated, they can be passed to another function that performs whatever actual magic is desired, like shown by the calls to ValidateGoodyHutReward in the code above. This function might have a basic structure similar to the following:
The above example receives a table as an argument. Individual arguments can be passed to this function instead, if desired, but this configuration quickly and simply receives every stored argument from both Event listeners, so it is by far the easiest choice.
For a more concrete example, a future update to EGHV will utilize a system like this to enable the correct application of several enhanced goody hut rewards that cannot be granted using the built-in Modifiers system alone:
To prevent constant resource recycling, sVillagerSecrets and tHostileVillagers above can instead be defined with the other global configuration settings. Any needed arguments are retrieved from the consolidated set and passed to an appropriate implementation function as shown. The possibilities here are nearly endless.
Future edits and/or posts will clarify and/or correct existing information, and/or add new information. Questions, comments, and corrections are welcome.
Posting to create a common knowledge repository relating to Goody Huts and Lua, and to provide examples of what can be accomplished when a reward is received.
To fully utilize the tribal village reward system within Lua, there are a few key points to be aware of:
- Numerous ingame Events may fire when a Goody Hut reward is received. Testing indicates that for nearly all rewards, all pertinent data can be acquired from just two: ImprovementActivated and GoodyHutReward. Exceptions exist; see below.
- For rewards where both Events fire, they do NOT always fire in a consistent order. Testing indicates that ~ 90% of the time ImprovementActivated will fire first, but that isn't a guarantee, and sometimes GoodyHutReward will fire first instead.
- When both Events fire, they frequently exhibit stacking behavior, meaning that when a Player receives multiple rewards, whichever Event fires first will stack first.
- Sumeria's ability which grants a free reward upon clearing a barbarian camp. Technically speaking, ImprovementActivated DOES fire here, but it does so for the cleared barbarian camp, not a Goody Hut. Practically speaking, GoodyHutReward is the only one of the two Events that provides relevant information.
- The meteor strike reward in Gathering Storm. Of the two Events, it appears that only GoodyHutReward fires for this reward.
ImprovementActivated fires whenever any improvement is activated, which includes the Goody Hut. A properly-configured hook to this Event provides much, but not all, of the vital data needed to manipulate the granted reward using Lua, as follows:
Code:
function ImprovementActivatedListener( iX, iY, iOwnerID, iUnitID, iImprovementIndex, iImprovementOwnerID, iActivationType )
-- function body
end
- iX and iY provide direct map coordinates to the popped Goody Hut.
- iOwnerID and iImprovementOwnerID can both be used to determine the true target Player, as needed. One of these should equal the value of iPlayerID as provided by the GoodyHutReward Event.
- iUnitID here should be the same as the value provided by the GoodyHutReward Event. A value of -1 generally indicates the goody hut was popped by a method other than unit exploration, likely via border expansion.
- iActivationType appears to be superfluous. A situation where it does not equal 0 has not yet been identified during testing.
Code:
function GoodyHutRewardListener( iPlayerID, iUnitID, iTypeHash, iSubTypeHash )
-- function body
end
- iPlayerID is an additional means of determining the true target Player. It should be equal to the value of either iOwnerID or iImprovementOwnerID as provided by the ImprovementActivated Event. If it is greater than -1, it should represent the true target Player. Otherwise, iImprovementOwnerID likely represents the true target Player.
- iUnitID here should be the same as the value provided by the ImprovementActivated Event. A value of -1 generally indicates the goody hut was popped by a method other than unit exploration, likely via border expansion.
- iTypeHash indicates the received Goody Hut Type, or reward category.
- iSubTypeHash indicates the received Goody Hut SubType, or specific reward.
- Generic tables can be used for the queues.
- To ensure that any improvement other than a goody hut, and certain barbarian camps, is ignored, the Index value of the goody hut improvement must be known.
- The Index value of the barbarian camp improvement must also be known, to prevent any potential queue misalignments that might arise due to the Sumerian civilization being an active Player.
- A simple boolean flag, to indicate whether a barbarian camp was cleared by the Sumerian civilization; this also helps prevent any potential queue misalignments.
- To ensure that the meteor strike reward does not cause any queue misalignments, a means of identifying rewards by the provided hash values must be employed; tables can also be used for this purpose.
- Defining any reward (sub)type values to be ignored or otherwise acted upon as strings makes it easy to introduce new checks for other rewards that should also be ignored or otherwise acted upon.
Code:
tGoodyHutRewardQueue = {};
tImprovementActivatedQueue = {};
iGoodyHutIndex = GameInfo.Improvements["IMPROVEMENT_GOODY_HUT"].Index;
iBarbCampIndex = GameInfo.Improvements["IMPROVEMENT_BARBARIAN_CAMP"].Index;
bIsSumeria = false;
sMeteorType = "METEOR_GOODIES";
sMeteorSubType = "METEOR_GRANT_GOODIES";
tGoodyHutTypes = {};
for row in GameInfo.GoodyHuts() do
tGoodyHutTypes[DB.MakeHash(row.GoodyHutType)] = {
GoodyHutType = row.GoodyHutType,
Weight = row.Weight
};
end
tGoodyHutRewards = {};
for row in GameInfo.GoodyHutSubTypes() do
tGoodyHutRewards[DB.MakeHash(row.SubTypeGoodyHut)] = {
GoodyHut = row.GoodyHut,
SubTypeGoodyHut = row.SubTypeGoodyHut,
Weight = row.Weight,
ModifierID = row.ModifierID
};
end
The listener for GoodyHutReward should first check the boolean flag, and if that flag is true it should set it to false and abort. It should then check the reward (sub)type hash values, and if they correspond to the meteor strike reward, it should abort. If it passes both checks without aborting, it should store its current arguments in a table and check the ImprovementActivated queue to determine which Event fired first for the current reward. If the ImprovementActivated queue is empty, insert the current arguments table into the GoodyHutReward queue at the end; if the ImprovementActivated queue is NOT empty, remove the first set of arguments from it and add those to the current arguments table, then pass the consolidated arguments to another function for further processing. The updated listener looks like this:
Code:
function GoodyHutRewardListener( iPlayerID, iUnitID, iTypeHash, iSubTypeHash )
if (bIsSumeria) then
bIsSumeria = false;
return;
elseif (tGoodyHutTypes[iTypeHash].GoodyHutType == sMeteorType) and (tGoodyHutRewards[iSubTypeHash].SubTypeGoodyHut == sMeteorSubType) then
return;
end
local tCurrentArgs = {};
tCurrentArgs.PlayerID = iPlayerID;
tCurrentArgs.UnitID_GHR = iUnitID;
tCurrentArgs.TypeHash = iTypeHash;
tCurrentArgs.SubTypeHash = iSubTypeHash;
if (#tImprovementActivatedQueue == 0) then
table.insert(tGoodyHutRewardQueue, tCurrentArgs);
elseif (#tImprovementActivatedQueue > 0) then
for k, v in pairs(tImprovementActivatedQueue[1]) do
tCurrentArgs[k] = v;
end
table.remove(tImprovementActivatedQueue, 1);
ValidateGoodyHutReward(tCurrentArgs);
end
end
Code:
function ImprovementActivatedListener( iX, iY, iOwnerID, iUnitID, iImprovementIndex, iImprovementOwnerID, iActivationType )
if (iImprovementIndex == iBarbCampIndex) then
if (iOwnerID > -1) then
local pPlayer = Players[iOwnerID];
local pPlayerConfig = PlayerConfigurations[iOwnerID];
if (pPlayer ~= nil) and (pPlayerConfig ~= nil) then
local sCivTypeName = pPlayerConfig:GetCivilizationTypeName();
if (sCivTypeName == "CIVILIZATION_SUMERIA") then
bIsSumeria = true;
end
end
elseif (iImprovementOwnerID > -1) then
local pPlayer = Players[iImprovementOwnerID];
local pPlayerConfig = PlayerConfigurations[iImprovementOwnerID];
if (pPlayer ~= nil) and (pPlayerConfig ~= nil) then
local sCivTypeName = pPlayerConfig:GetCivilizationTypeName();
if (sCivTypeName == "CIVILIZATION_SUMERIA") then
bIsSumeria = true;
end
end
end
return;
elseif (iImprovementIndex ~= iGoodyHutIndex) then
return;
end
local tCurrentArgs = {};
tCurrentArgs.PlotX = iX;
tCurrentArgs.PlotY = iY;
tCurrentArgs.OwnerID = iOwnerID;
tCurrentArgs.UnitID_IA = iUnitID;
tCurrentArgs.ImprovementIndex = iImprovementIndex;
tCurrentArgs.ImprovementOwnerID = iImprovementOwnerID;
tCurrentArgs.ActivationType = iActivationType;
if (#tGoodyHutRewardQueue == 0) then
table.insert(tImprovementActivatedQueue, tCurrentArgs);
elseif (#tGoodyHutRewardQueue > 0) then
for k, v in pairs(tGoodyHutRewardQueue[1]) do
tCurrentArgs[k] = v;
end
table.remove(tGoodyHutRewardQueue, 1);
ValidateGoodyHutReward(tCurrentArgs);
end
end
So, why go through all of this hassle to gather and consolidate Event arguments? Once all the arguments from both Events have been consolidated, they can be passed to another function that performs whatever actual magic is desired, like shown by the calls to ValidateGoodyHutReward in the code above. This function might have a basic structure similar to the following:
Code:
function ValidateGoodyHutReward( tArgs )
-- function body
end
For a more concrete example, a future update to EGHV will utilize a system like this to enable the correct application of several enhanced goody hut rewards that cannot be granted using the built-in Modifiers system alone:
- Hostile villagers as a "reward" from a goody hut. In addition to the required Lua, this requires additional database configuration in the form of a new goody hut type and individual weighted subtypes for each such desired reward. See Addendum 2 in this thread for an example.
- A hidden building that becomes available when a particular reward is first received by a Player, and that upgrades to a more advanced version each subsequent time that reward is received by the same Player. This requires extensive additional database configuration in the form of multiple new buildings and corresponding Modifiers in multiple database tables, a new goody hut type, and a single new subtype to act as a gateway. Unlocking and upgrading the building involves attaching Modifiers to the target Player using Lua, so a generic Modifier can be used for the reward. * TO-DO: Add an Addendum to provide an example *
Code:
function ValidateGoodyHutReward( tArgs )
local iPlayerID = -2;
if (tArgs.PlayerID > -1) then iPlayerID = tArgs.PlayerID;
elseif (tArgs.OwnerID > -1) then iPlayerID = tArgs.OwnerID;
elseif (tArgs.ImprovementOwnerID > -1) then iPlayerID = tArgs.ImprovementOwnerID;
end
if (iPlayerID == -2) then
tGoodyHutRewardQueue = {};
tImprovementActivatedQueue = {};
return;
end
local sRewardSubType = tGoodyHutRewards[tArgs.SubTypeHash].SubTypeGoodyHut;
local sVillagerSecrets = "GOODYHUT_UNLOCK_VILLAGER_SECRETS";
local tHostileVillagers = {
["GOODYHUT_LOW_HOSTILITY_VILLAGERS"] = 1,
["GOODYHUT_MID_HOSTILITY_VILLAGERS"] = 2,
["GOODYHUT_HIGH_HOSTILITY_VILLAGERS"] = 3,
["GOODYHUT_MAX_HOSTILITY_VILLAGERS"] = 4
};
if (sRewardSubType == sVillagerSecrets) then
UnlockVillagerSecrets(iPlayerID, sRewardSubType);
elseif (tHostileVillagers[sRewardSubType] ~= nil) then
CreateHostileVillagers(tArgs.PlotX, tArgs.PlotY, iPlayerID, sRewardSubType);
end
end
Future edits and/or posts will clarify and/or correct existing information, and/or add new information. Questions, comments, and corrections are welcome.
Last edited: