Chrisy15
Flower, Beautiful
- Joined
- Jul 9, 2015
- Messages
- 2,132
GameEvents.CanSaveUnit is a very flawed GameEvent for a number of reasons, and I'm sorry to say that I can't do anything about those. Instead my interest has been spurned over a new issue, the GameEvent's nature as a Test All event.
Test events allow Lua functions to weigh in on whether a certain game process should happen as normal or whether an exceptional behaviour should occur. An example of a standard Test All event would be PlayerCanConstruct, which allows a building to be constructed if all functions return
The issue is that CanSaveUnit does not work as a Test All event because the boolean polarity is reversed; doing the exceptional thing with CanSaveUnit requires a
The solution is for CanSaveUnit to instead be a Test Any event, an
In the absence of a DLL mod, I have instead devised a system which can be used to (effectively) convert CanSaveUnit into a Test All event. Although it is named in homage to Machiavelli's SerialEventUnitCreatedGood, the user-end implementation is quite different so modders should pay attention to the comments in my provided example. The most pressing drawback of this system is that it does not solve the problem being discussed here, as that can only be done by a DLL mod, but merely circumvents it; thus CanSaveUnitGood does not work if ANY other function is subscribed to CanSaveUnit, and without a DLL solution it is required that every mod which uses CanSaveUnit must be made to instead use this system. While this sounds like a very unattainable goal, I would hope that the infrequent use of CanSaveUnit and the easy deployability of the utility would mean that the limited number of relevant mods can be patched either by their creators or by their users.
How To Use It
This utility comes in the form of a Lua file which must be added through InGameUIAddin, NOT the VFS. If desired or required, the code can instead be ported into an existent InGameUIAddin Lua file so long as it is copied in its entirety. This utility creates two LuaEvents which your own InGameUIAddin Lua script will need to interact with, like so:
The core utility code, for easy reference or easy copying:
Test events allow Lua functions to weigh in on whether a certain game process should happen as normal or whether an exceptional behaviour should occur. An example of a standard Test All event would be PlayerCanConstruct, which allows a building to be constructed if all functions return
true
but blocks construction if any function returns false
. This and
gate works well here because a true
return to PlayerCanConstruct does not serve to override any obstruction to construction imposed by the database, such as tech prereqs; a return of true
is merely a function's way of not blocking construction, of pretending it isn't there.The issue is that CanSaveUnit does not work as a Test All event because the boolean polarity is reversed; doing the exceptional thing with CanSaveUnit requires a
true
return, and subsequently a return of false
should mean that the function is ignored. Unfortunately, since CanSaveUnit is a Test All event, all functions are required to return true
for a unit to be saved; if any function decides it does not want to save a unit, the and
gate fails and the unit is forced to die regardless of how many other functions wanted to save it. This renders CanSaveUnit logically flawed, by virtue of how a different outcome is derived from a function's logic failing to said failed function not being there in the first place.The solution is for CanSaveUnit to instead be a Test Any event, an
or
gate. This way if any function returns true
then the exception is made and the unit is saved, and any return of false
does not override any exception generated by another function. Unfortunately, while this conclusion is easily drawn, it requires a DLL mod to implement - and I would implore that @whoward69 and I guess @Recursive (idk I'll make a github post later) implement it into their respective DLL patches because as far as I can conceive this is an objective improvement and necessary bugfix with no negative consequences or side effects.In the absence of a DLL mod, I have instead devised a system which can be used to (effectively) convert CanSaveUnit into a Test All event. Although it is named in homage to Machiavelli's SerialEventUnitCreatedGood, the user-end implementation is quite different so modders should pay attention to the comments in my provided example. The most pressing drawback of this system is that it does not solve the problem being discussed here, as that can only be done by a DLL mod, but merely circumvents it; thus CanSaveUnitGood does not work if ANY other function is subscribed to CanSaveUnit, and without a DLL solution it is required that every mod which uses CanSaveUnit must be made to instead use this system. While this sounds like a very unattainable goal, I would hope that the infrequent use of CanSaveUnit and the easy deployability of the utility would mean that the limited number of relevant mods can be patched either by their creators or by their users.
How To Use It
This utility comes in the form of a Lua file which must be added through InGameUIAddin, NOT the VFS. If desired or required, the code can instead be ported into an existent InGameUIAddin Lua file so long as it is copied in its entirety. This utility creates two LuaEvents which your own InGameUIAddin Lua script will need to interact with, like so:
Code:
-- Standard normal function which you would have otherwise just subscribed to CanSaveUnit.
-- This solution should be applied to both traditional CanSaveUnit functions and to functions merely using the event to detect Trade Route pillaging.
function C15_CanSaveUnit(playerID, unitID)
local pPlayer = Players[playerID]
local pUnit = pPlayer:GetUnitByID(unitID)
if pUnit:GetUnitType() == GameInfoTypes["UNIT_WARRIOR"] then
return true
else
return false
end
end
-- This function needs to be defined by the user to resolve load order issues.
-- Names and methods don't matter, all that matters is that your normal function gets linearly inserted into the table provided by the function parameters.
-- Remember, no function arguments when inserting your function! You're inserting the function itself, not the returned value.
function C15_CanSaveUnit_Import(tTable)
tTable[#tTable+1] = C15_CanSaveUnit
end
-- Make sure that you subscribe the import function to this LuaEvent, not your normal function!
-- The second LuaEvent then serves to call the first, "doing" that which you have just set up.
LuaEvents.C15_CanSaveUnitGood_Import.Add(C15_CanSaveUnit_Import)
LuaEvents.C15_CanSaveUnitGood_Refresh.Call()
The core utility code, for easy reference or easy copying:
Spoiler Nerd Code for nerds :
Code:
-- C15_CanSaveUnitGood
-- Author: Chrisy15
-- DateCreated: 4/8/2024 4:07:29 PM
--------------------------------------------------------------
local iVersionNumber = 2
local tStoredFunctions = {}
local bPrintSaveResult = false -- This will create A LOT of print statements, but can serve to identify if another mod's CanSaveUnit subscription is interfering with this system.
-- Refresh list of cached functions by telling user files to insert their CanSaveUnit functions into tStoredFunctions. Directly taken from MCIS lol
function C15_CanSaveUnitGood_RefreshList()
tStoredFunctions = {}
LuaEvents.C15_CanSaveUnitGood_Import.Call(tStoredFunctions)
end
LuaEvents.C15_CanSaveUnitGood_Refresh.Add(C15_CanSaveUnitGood_RefreshList)
-- Remove all content from this instance of the utility. Version check avoids self-destruction.
function C15_CanSaveUnitGood_Disable(iNewVersion)
if iNewVersion > iVersionNumber then
Events.SequenceGameInitComplete.Remove(C15_CanSaveUnitGood_Init)
LuaEvents.C15_CanSaveUnitGood_Refresh.Remove(C15_CanSaveUnitGood_RefreshList)
tStoredFunctions = {}
end
end
LuaEvents.C15_CanSaveUnitGood_Disable.Add(C15_CanSaveUnitGood_Disable)
-- All version discrepancies should be resolved by the time SequenceGameInitComplete is called
function C15_CanSaveUnitGood_Init()
LuaEvents.C15_CanSaveUnitGood_Refresh.Call()
-- Additions to tStoredFunctions made after this point can still be recognised, so there's no impetus on mods to make their Imports before SequenceGameInitComplete.
local function C15_CanSaveUnitGood_CanSaveUnit(playerID, unitID)
local bSaveUnit = false -- Default state is that Unit should die
for i = 1, #tStoredFunctions do
if tStoredFunctions[i](playerID, unitID) then
if bPrintSaveResult then
print("CanSaveUnit: true!")
end
bSaveUnit = true -- One of the functions have concluded true, and thus the Unit should be saved
end
end
if bPrintSaveResult then
print("CanSaveUnit: ", bSaveUnit)
end
return bSaveUnit -- Don't return until all functions have been called, to ensure that all their code gets executed
end
GameEvents.CanSaveUnit.Add(C15_CanSaveUnitGood_CanSaveUnit)
MapModData.C15_CanSaveUnitGood = nil
end
-- Prevent Duplicates - derived from MCIS
if MapModData.C15_CanSaveUnitGood then
local iCurrentVersion = MapModData.C15_CanSaveUnitGood
if iCurrentVersion >= iVersionNumber then
print("Version check failed!", iVersionNumber)
else
LuaEvents.C15_CanSaveUnitGood_Disable.Call(iVersionNumber)
LuaEvents.C15_CanSaveUnitGood_Refresh.Call()
MapModData.C15_CanSaveUnitGood = iVersionNumber
Events.SequenceGameInitComplete.Add(C15_CanSaveUnitGood_Init)
print("Version check passed!", iVersionNumber)
end
else
MapModData.C15_CanSaveUnitGood = iVersionNumber
Events.SequenceGameInitComplete.Add(C15_CanSaveUnitGood_Init)
print("Version check passed!", iVersionNumber)
end
Attachments
Last edited: