I was asked to explain a bit of code in one of my mod, and while I was looking for a short answer, I thought it would be better to give a complete one on a method I use since civ5.
More specifically, I'll expand on this question:
https://forums.civfanatics.com/thre...ame-of-a-civilization-during-the-game.642711/
The problem here is that we don't have methods to simply change the name of a civilization/leader/player once the game is started, the only simple way to fake it would be to edit UI files to replace all the functions to get a player/civilization/leader name, which are:
pPlayerConfig:GetPlayerName() (125 hits in 50 files)
pPlayerConfig:GetLeaderName() (28 hits in 18 files)
pPlayerConfig:GetCivilizationDescription() (40 hits in 21 files)
pPlayerConfig:GetCivilizationShortDescription() (61 hits in 24 files)
That a lot of lines to change, and a lot to check when merging update.
But there is a good old civ5 method that is still partially working in civ6 and we can use, see here:
https://forums.civfanatics.com/threads/how-to-add-a-function-to-a-metatable.458083/
This was allowing to replace functions of the metatable of an object, which was shared in all contexts, for example instead of editing 125 lines to change the pPlayerConfig:GetPlayerName() by our own custom function, we could simply replace the GetPlayerName function in the metatable of pPlayerConfig once in a file, and all 125 calls in the other files would use our custom function.
Sadly, I've said "partially" because, yes, for civ5 the metatable was shared for all lua context files, which allowed what's described above.
But for civ6, well, you already know we have 2 main contexts that are not synchronized (GamePlay and UserInterface) and as expected the metatable of "similar" objects (for example an unit) are different, but the main problem for us is that the metatable are also not shared for lua files of the same main context.
Now, there is an intermediate solution, "include" is working, and there are common files that are included in multiple files, so we can still use the metatable method by including it in a few key files instead of editing 50+ files, and anyway, even if you need to edit 50 files once it's still less than editing 125 lines (just for GetPlayerName)
So how does it works exactly ?
I have one lua file (that is not set in <AddGameplayScripts> or <AddUserInterfaces>, but just in <ImportFiles>) that contains the base functions.
That file is included in some key files (CityBannerManager.lua, CityPanel.lua, CivilizationIcon.lua, LeaderIcon.lua, UnitFlagManager.lua)
It contains this particular function at the end of the file that is called after all files are loaded to replace the old functions of pPlayerConfig (GetPlayerName(), ...) by the new ones (set earlier in the file)
The first line
allows the function to be called with or without a pPlayerConfig object.
if it's called without, like that
it will get the pPlayerConfig of player #0 to get the metatable.
then it add new functions to it, like:
means that using pPlayerConfig:OldGetLeaderName() returns the Leader Name as it's known by the core engine.
and it replace some function by new ones, for example
means that using pPlayerConfig:GetLeaderName() now call GetLeaderName() from my lua file which in turn can return my custom value, not the core engine value.
Now let's have a look at my GetLeaderName() function:
First, note that calling
is the same as calling
That function try to get the table where the value is stored, if the table exists it try to return the entry (eventually) stored with the key "LeaderName" or, if that key doesn't exist it call the old method to return the core engine value.
It also call the old method if the table doesn't exist.
The other replacing function (GetPlayerName, ...) work the same way, by checking a specific key or returning the old value if they can't find a new one.
I could have set new function like SetPlayerName(), SetLeaderName() to set specific values, but the goal here was to (quickly) replace the existing functions call from the game's UI files.
Instead, I use for example pPlayerConfig:SetKeyValue("LeaderName", "Napoleon") in my own code where I need it.
let's look at SetKeyValue
Yeah, this one may need a bit of explanations...
And for that I must start with GetData
First, ExposedMembers is THE table that is shared between all contexts. Thanks god (or Salec ?) for its existence, without that table I would have stopped modding civ6 a loooong time ago. I use it, for, well, everything I can hack in it.
So this table is available from everywhere, and in other script files, I create the "GCO" table that contains all data for my mod (Gedemon's Civilization Overhaul if you ask)
For this specific example, the PlayerConfigData table is created in another file, and filled with previously saved values when reloading a game (and saved with the game using a variant of this method)
Now why [self:OldGetCivilizationTypeName()] ?
the pPlayerConfig objects are PlayerConfigurations[0] or PlayerConfigurations[1] where 0 or 1 are PlayerIDs
For example, the local player being 0, if he is playing with Germany then PlayerConfigurations[0].GetCivilizationTypeName() returns CIVILIZATION_GERMANY
I'm afraid I can't remember precisely why ATM, but I wanted to have a key for each player entries in the PlayerConfigData table that would not be the playerID (yeah, it must have been important, I need to remember...), so I used the CivilizationType for the key.
And once the metatable is edited, to be sure to get the same key whatever change I made to the various PlayerConfig attributes, I needed to use a function that would always return the same value for a specific player, so I use the pPlayerConfig:OldGetCivilizationTypeName() which return the locked game core value.
When used with the Germans player of my example, PlayerConfigurations[0].GetData() would return the content of ExposedMembers.GCO.PlayerConfigData["CIVILIZATION_GERMANY"]
Back to SetKeyValue, if GetData returns nothing in configData (could happen if we've never stored anything for "CIVILIZATION_GERMANY" in that table), then it create it here
and then reference it to configData, reminder this
is a reference to a table, not a copy to a table (yeah, you can start laughing, it took me ages to realize that)
And now that we have a valid reference to a table for "CIVILIZATION_GERMANY", we can finally store our key, value in it:
Done. Now you can have fun.
For example, still with our German player, if, in one of the files (or the tuner) including this script, I use
Now, because the file is included in UI files managing icons, because those file call pPlayerConfig:GetCivilizationTypeName(), and because this is now my function that fetch the data in ExposedMembers.GCO.PlayerConfigData, and that data contains "CIVILIZATION_FRANCE" for "CivilizationTypeName", our German player will now have the French Civilization icon everywhere in the User Interface (after it has updated, which I force in my mod using LuaEvents added in CityBanner and UnitFlag files)
Finally, this is not an universal method, some tables (UI for example, which prevents me to do the same for Colors) have their metatable write-protected and can't be edited.
More specifically, I'll expand on this question:
https://forums.civfanatics.com/thre...ame-of-a-civilization-during-the-game.642711/
The problem here is that we don't have methods to simply change the name of a civilization/leader/player once the game is started, the only simple way to fake it would be to edit UI files to replace all the functions to get a player/civilization/leader name, which are:
pPlayerConfig:GetPlayerName() (125 hits in 50 files)
pPlayerConfig:GetLeaderName() (28 hits in 18 files)
pPlayerConfig:GetCivilizationDescription() (40 hits in 21 files)
pPlayerConfig:GetCivilizationShortDescription() (61 hits in 24 files)
That a lot of lines to change, and a lot to check when merging update.
But there is a good old civ5 method that is still partially working in civ6 and we can use, see here:
https://forums.civfanatics.com/threads/how-to-add-a-function-to-a-metatable.458083/
This was allowing to replace functions of the metatable of an object, which was shared in all contexts, for example instead of editing 125 lines to change the pPlayerConfig:GetPlayerName() by our own custom function, we could simply replace the GetPlayerName function in the metatable of pPlayerConfig once in a file, and all 125 calls in the other files would use our custom function.
Sadly, I've said "partially" because, yes, for civ5 the metatable was shared for all lua context files, which allowed what's described above.
But for civ6, well, you already know we have 2 main contexts that are not synchronized (GamePlay and UserInterface) and as expected the metatable of "similar" objects (for example an unit) are different, but the main problem for us is that the metatable are also not shared for lua files of the same main context.
Now, there is an intermediate solution, "include" is working, and there are common files that are included in multiple files, so we can still use the metatable method by including it in a few key files instead of editing 50+ files, and anyway, even if you need to edit 50 files once it's still less than editing 125 lines (just for GetPlayerName)
So how does it works exactly ?
I have one lua file (that is not set in <AddGameplayScripts> or <AddUserInterfaces>, but just in <ImportFiles>) that contains the base functions.
That file is included in some key files (CityBannerManager.lua, CityPanel.lua, CivilizationIcon.lua, LeaderIcon.lua, UnitFlagManager.lua)
It contains this particular function at the end of the file that is called after all files are loaded to replace the old functions of pPlayerConfig (GetPlayerName(), ...) by the new ones (set earlier in the file)
Code:
-----------------------------------------------------------------------------------------
-- Initialize PlayerConfig Functions
-----------------------------------------------------------------------------------------
function InitializePlayerConfigFunctions(pPlayerConfig) -- Note that those functions are limited to this file context (and those which include it)
local pPlayerConfig = pPlayerConfig or PlayerConfigurations[0]
local p = getmetatable(pPlayerConfig).__index
if not p.GetData then -- initialize only once !
p.GetData = GetData
p.SetKeyValue = SetKeyValue
p.GetKeyValue = GetKeyValue
-- Old functions
p.OldGetCivilizationTypeName = p.GetCivilizationTypeName
p.OldGetCivilizationShortDescription = p.GetCivilizationShortDescription
p.OldGetPlayerName = p.GetPlayerName
p.OldGetLeaderName = p.GetLeaderName
p.OldGetLeaderTypeName = p.GetLeaderTypeName
p.OldGetCivilizationDescription = p.GetCivilizationDescription
-- Override functions
p.GetCivilizationTypeName = GetCivilizationTypeName
p.GetCivilizationShortDescription = GetCivilizationShortDescription
p.GetPlayerName = GetPlayerName
p.GetLeaderName = GetLeaderName
p.GetLeaderTypeName = GetLeaderTypeName
p.GetCivilizationDescription = GetCivilizationDescription
end
end
The first line
Code:
local pPlayerConfig = pPlayerConfig or PlayerConfigurations[0]
if it's called without, like that
Code:
InitializePlayerConfigFunctions()
then it add new functions to it, like:
- GetData : allows to get the table (shared using ExposedMembers) where variables are stored (like new names) and which is saved/loaded in another file
- SetKeyValue : used to store a value in that table, for example pPlayerConfig:SetKeyValue("LeaderName", "Napoleon")
- GetKeyValue : used to get a value from the table, for example pPlayerConfig:GetKeyValue("LeaderName") would now return "Napoleon"
Code:
p.OldGetLeaderName = p.GetLeaderName
and it replace some function by new ones, for example
Code:
p.GetLeaderName = GetLeaderName
Now let's have a look at my GetLeaderName() function:
Code:
function GetLeaderName(self)
local configData = self:GetData()
if configData then
return configData.LeaderName or self:OldGetLeaderName()
else
return self:OldGetLeaderName()
end
end
First, note that calling
Code:
pPlayerConfig:GetLeaderName()
Code:
GetLeaderName(pPlayerConfig)
That function try to get the table where the value is stored, if the table exists it try to return the entry (eventually) stored with the key "LeaderName" or, if that key doesn't exist it call the old method to return the core engine value.
It also call the old method if the table doesn't exist.
The other replacing function (GetPlayerName, ...) work the same way, by checking a specific key or returning the old value if they can't find a new one.
I could have set new function like SetPlayerName(), SetLeaderName() to set specific values, but the goal here was to (quickly) replace the existing functions call from the game's UI files.
Instead, I use for example pPlayerConfig:SetKeyValue("LeaderName", "Napoleon") in my own code where I need it.
let's look at SetKeyValue
Code:
function SetKeyValue(self, key, value)
local configData = self:GetData()
if not configData then
ExposedMembers.GCO.PlayerConfigData[self:OldGetCivilizationTypeName()] = {}
configData = ExposedMembers.GCO.PlayerConfigData[self:OldGetCivilizationTypeName()]
end
configData[key] = value
end
And for that I must start with GetData
Code:
function GetData(self)
return ExposedMembers.GCO.PlayerConfigData[self:OldGetCivilizationTypeName()]
end
First, ExposedMembers is THE table that is shared between all contexts. Thanks god (or Salec ?) for its existence, without that table I would have stopped modding civ6 a loooong time ago. I use it, for, well, everything I can hack in it.
So this table is available from everywhere, and in other script files, I create the "GCO" table that contains all data for my mod (Gedemon's Civilization Overhaul if you ask)
For this specific example, the PlayerConfigData table is created in another file, and filled with previously saved values when reloading a game (and saved with the game using a variant of this method)
Now why [self:OldGetCivilizationTypeName()] ?
the pPlayerConfig objects are PlayerConfigurations[0] or PlayerConfigurations[1] where 0 or 1 are PlayerIDs
For example, the local player being 0, if he is playing with Germany then PlayerConfigurations[0].GetCivilizationTypeName() returns CIVILIZATION_GERMANY
I'm afraid I can't remember precisely why ATM, but I wanted to have a key for each player entries in the PlayerConfigData table that would not be the playerID (yeah, it must have been important, I need to remember...), so I used the CivilizationType for the key.
And once the metatable is edited, to be sure to get the same key whatever change I made to the various PlayerConfig attributes, I needed to use a function that would always return the same value for a specific player, so I use the pPlayerConfig:OldGetCivilizationTypeName() which return the locked game core value.
When used with the Germans player of my example, PlayerConfigurations[0].GetData() would return the content of ExposedMembers.GCO.PlayerConfigData["CIVILIZATION_GERMANY"]
Back to SetKeyValue, if GetData returns nothing in configData (could happen if we've never stored anything for "CIVILIZATION_GERMANY" in that table), then it create it here
Code:
ExposedMembers.GCO.PlayerConfigData[self:OldGetCivilizationTypeName()] = {}
Code:
configData = ExposedMembers.GCO.PlayerConfigData[self:OldGetCivilizationTypeName()]
And now that we have a valid reference to a table for "CIVILIZATION_GERMANY", we can finally store our key, value in it:
Code:
configData[key] = value
Done. Now you can have fun.
For example, still with our German player, if, in one of the files (or the tuner) including this script, I use
Code:
local pPlayerConfig = PlayerConfigurations[0]
pPlayerConfig:SetKeyValue("CivilizationTypeName", "CIVILIZATION_FRANCE")
Now, because the file is included in UI files managing icons, because those file call pPlayerConfig:GetCivilizationTypeName(), and because this is now my function that fetch the data in ExposedMembers.GCO.PlayerConfigData, and that data contains "CIVILIZATION_FRANCE" for "CivilizationTypeName", our German player will now have the French Civilization icon everywhere in the User Interface (after it has updated, which I force in my mod using LuaEvents added in CityBanner and UnitFlag files)
Finally, this is not an universal method, some tables (UI for example, which prevents me to do the same for Colors) have their metatable write-protected and can't be edited.