[GS] Adding Building Resource Use to the UI

Sostratus

Deity
Joined
Jul 31, 2017
Messages
2,383
Location
Minnesota, USA
After some helpful guidance in the Quick Questions thread, i felt this was getting pointed enough to warrant a little thread.

Goal:
The stated goal of this mod is to update the function RefreshResources() in the expansion2 file TopPanel_Expansion2.lua to support displaying having strategic resource costs from buildings.

Background
This effort is part of my first civ6 modding project, which adds buildings that consume strategic resources as per turn maintenance. This is done using the otherwise unused table Building_ResourceCosts. While this table is fully functional in gameplay, it is not connected to the UI at all. So when you hover over a resource like, say, oil, normally one might see a tooltip like "consuming X per turn, -Y from Power, -Z from units." This would be adding on to that "...-W from buildings."

Current State
I understand XML, C, Python type languages, but I am otherwise green to Lua. Some of the syntax is a little odd to look at but from a "pseudo code" level I see what's going on.
There are two parts in RefreshResources() which I have identified as the areas to attack. First, there are a few lines in RefreshResources() that seem to be a target:
Spoiler :

...
local unitConsumptionPerTurn:number = pPlayerResources:GetUnitResourceDemandPerTurn(resource.ResourceType);
local powerConsumptionPerTurn:number = pPlayerResources:GetPowerResourceDemandPerTurn(resource.ResourceType);
local totalConsumptionPerTurn:number = unitConsumptionPerTurn + powerConsumptionPerTurn;
...

Where it would seem the logical thing to do would be something like so:
Spoiler :

...
local unitConsumptionPerTurn:number = pPlayerResources:GetUnitResourceDemandPerTurn(resource.ResourceType);
local powerConsumptionPerTurn:number = pPlayerResources:GetPowerResourceDemandPerTurn(resource.ResourceType);
local buildingConsumptionPerTurn:number = pPlayerResources:GetBuildingResourceDemandPerTurn(resource.ResourceType);
local totalConsumptionPerTurn:number = unitConsumptionPerTurn + powerConsumptionPerTurn + buildingConsumptionPerTurn;
...

My first hunch was to look around and try to find where "GetXResourceDemandPerTurn" is defined, since i am assuming for eg units it is summing up the instances of a unit and multiplying by the unit's <ResourceMaintenanceAmount> column entry in the Units_XP2 table, or something to that effect. Naturally I would just change the relevant types to buildings and refer to the columns in the aforementioned Building_ResourceCosts table.

A few lines below this chunk, where things are getting processed into being displayed, we have:
Spoiler :

if (totalConsumptionPerTurn > 0) then
tooltip = tooltip .. "[NEWLINE]" .. Locale.Lookup("LOC_RESOURCE_CONSUMPTION", totalConsumptionPerTurn);
if (unitConsumptionPerTurn > 0) then
tooltip = tooltip .. "[NEWLINE]" .. Locale.Lookup("LOC_RESOURCE_UNIT_CONSUMPTION_PER_TURN", unitConsumptionPerTurn);
end
if (powerConsumptionPerTurn > 0) then
tooltip = tooltip .. "[NEWLINE]" .. Locale.Lookup("LOC_RESOURCE_POWER_CONSUMPTION_PER_TURN", powerConsumptionPerTurn);
end
end

And again, logically, it would seem like the obvious answer would be to just extend the addition above here:
Spoiler :

if (totalConsumptionPerTurn > 0) then
tooltip = tooltip .. "[NEWLINE]" .. Locale.Lookup("LOC_RESOURCE_CONSUMPTION", totalConsumptionPerTurn);
if (unitConsumptionPerTurn > 0) then
tooltip = tooltip .. "[NEWLINE]" .. Locale.Lookup("LOC_RESOURCE_UNIT_CONSUMPTION_PER_TURN", unitConsumptionPerTurn);
end
if (powerConsumptionPerTurn > 0) then
tooltip = tooltip .. "[NEWLINE]" .. Locale.Lookup("LOC_RESOURCE_POWER_CONSUMPTION_PER_TURN", powerConsumptionPerTurn);
end
if (buildingConsumptionPerTurn > 0) then
tooltip = tooltip .. "[NEWLINE]" .. Locale.Lookup("LOC_RESOURCE_BUILDING_CONSUMPTION_PER_TURN",buildingConsumptionPerTurn);
end

end

Outstanding Problems
-Where or how would I add the functionality "GetBuildingResourceDemandPerTurn" ? (Assuming this is the correct approach.) As I understand I will need to tell the game to replace the top panel file with my new file, but this seems like it would live somewhere else.
-How does Locale.Lookup("LOC_RESOURCE_POWER_CONSUMPTION_PER_TURN", powerConsumptionPerTurn); function? This looks like the game is using those two arguments to produce "-X resource per turn due to Power" type text.
-Alternatively to computing based on a "GetBuildingResourceDemandPerTurn" basis, I could infer the number consumed by buildings as a workaround; because if there's a difference between units+power and the total consumed, it must be buildings. (The Building_ResourceCosts table may not be hooked up to the UI, but it functions numerically, and the UI does correctly say the net income of the resource.)
 
Player:GetResources():GetPowerResourceDemandPerTurn() and Player:GetResources():GetUnitResourceDemandPerTurn() are not defined within any lua file. They are defined at the DLL level and exposed to usage in lua files: the game's DLL engine handles all the calculations, so in essence these two methods are merely querries ot the game's DLL engine for the desired data.

You would have to write your own function to create a "GetBuildingResourceDemandPerTurn". You can create new functions for a player object or for the Player:GetResources() object but it is often just as easy to create a "standard-type" lua function to handle such calculations.

Locale.Lookup("TEXT_KEY_NAME") is used to look up the text string for the appropriate language the human player is running under for the specified TXT-TAG. Locale.Lookup accepts "wildcard" arguments whose data is inserted in the designated positions within the text-string. The actual text-string within the definition of the <Text> portion of the Tag determines exactly where within the string the data for the wildcard will be inserted.

Unfortunately nearly all text Tags for GS or RaF are not included in the DebugLocalization.sqlite file so we cannot open up and look at the exact structure of the <Text> field for this Tag-Name. But it would look something like
Code:
"- 1{} resource per turn due to Power"
or
Code:
"- %1 resource per turn due to Power"
for the tag-name LOC_RESOURCE_POWER_CONSUMPTION_PER_TURN for the English language. The number 1 in these possible examples is signifying that the data for the 1st wildcard argument passed to the Locale.Lookup function should be used in that spot. If two or more wildcard arguments are to be passed then the <Text> field for the Tag might look more like
Code:
"- 1{} resource per turn due to  2{}"
In such a "fake" code example the word "Power" might be passed as a wildcard value for the #2 wildcard.
 
Generally when I add a new function to a Firaxis-made lua file, I place my new function as close to the top of the file as is practical so that it is easier to see and to debug later when necessary. I will generally place such a function below any lines which state
Code:
include(Something)
and beow any lines in which Firaxis is defining variables that are needed by the UI file.

So when I made a customized version of CityPanelOverview I first did this at the top of the file
Code:
-- Copyright 2017-2018, Firaxis Games
-- AKA: "City Details", (Left) side panel with details on a selected city
--edited by LeeS 15Sept2019
and then in this section of the file I added a new function I could use later within the text of the file
Code:
local YIELD_STATE :table = {
		NORMAL  = 0,
		FAVORED = 1,
		IGNORED = 2
}

-- ===========================================================================
--	FUNCTION ADDED BY LeeS
-- ===========================================================================
function GetBuildingsTableData(BuildingTextName)
	local sBuildingTypename = "NONE"
	for row in GameInfo.Buildings() do
		if Locale.Lookup(row.Name) == BuildingTextName then
			sBuildingTypename = row.BuildingType
			break
		end
	end
	if sBuildingTypename ~= "NONE" then
		return GameInfo.Buildings[sBuildingTypename]
	else return nil
	end
end
local bDummiesEnabled = (GameInfo.Buildings[0].IsDummy ~= nil)
print("CityPanelOverview.lua loaded from LeeS Dummy Building Systems version for 13Sept19 Patch")

-- ===========================================================================
--	END FUNCTION ADDED BY LeeS
-- ===========================================================================
My stuff that I added is thus right below lua-table creations and definitions made by Firaxis.
 
Player:GetResources():GetPowerResourceDemandPerTurn() and Player:GetResources():GetUnitResourceDemandPerTurn() are not defined within any lua file. They are defined at the DLL level and exposed to usage in lua files: the game's DLL engine handles all the calculations, so in essence these two methods are merely querries ot the game's DLL engine for the desired data.
Thanks for the replies. I have so much to learn!
I suspected that some of that good stuff might be in the DLL... I do hope they release it soon.

With the wildcard text it might be easiest for me to just get the implied building usage by subtracting total vs units+power.
I realize that at some point in the future I might become more experienced and want to extend this to the reports screen or something. In that case I would want to have a function that actually adds up what buildings use that can be a bit agnostic towards what's modded in on the content side.
Referring to your modding guide, ~page 342 where you refer to City:GetBuildings() there is a submethod :HasBuilding. I could manually program in the mod's buildings (there are only 2!) and their relevant costs. That would be unwieldy as a general solution but, it could work.

Is there a way to extract data from an arbitrary game table into lua, such as the Building_ResourceCosts?
 
See the section about using GameInfo in the lua chapters.

And also, carefully read the code in the code-block contained within my previous message on this thread. The custom function I wrote is looking through the data-rows in XML/SQL table "Buildings".
 
And also, carefully read the code in the code-block contained within my previous message on this thread. The custom function I wrote is looking through the data-rows in XML/SQL table "Buildings".
Looking it over again with an eye for what [] vs () do it makes a lot more sense how this functions. My brain was defaulting to [] being like C arrays instead of a way to deal with text/keys. Oops!

So in your code example, Buildingtextname and row.Name is going to be a value like LOC_BUILDING_MONUMENT_NAME, and then if that found a match, sBuildingTypeName would be assigned BUILDING_MONUMENT. (The buildingtype associated with LOC_BUILDING_MONUMENT_NAME.)
Then, if you did find the monument, you're returning the entire row for the monument, which would give you all the columns associated with the monument to process however you need.

(Do I understand that correctly?)

See the section about using GameInfo in the lua chapters.
If I understand this section correctly, I can call any table using GameInfo, but I can only do
GameInfo.Buildings[BUILDING_MONUMENT] (or otherwise asking directly about a primary key) for the main tables like units/buildings, not a table like building_ResourceCosts? And to do something similar to what you have done in your example I could search for the index in a loop (imagining a slightly modified version of the GetGameInfoIndex where one iterates a table to find the matching type instead of direct query) and then say
GameInfo.Building_ResourceCosts(index) ?
 
(Do I understand that correctly?)
yes

Although what function GetBuildingsTableData is doing is actually translating the localized text-string for a Building like "Library" back into it's row-data from table <Buildings> because in certain functions as written by Firaxis they're using the localized name of the Building like "Library" or "Biblioteque" instead of the SQL BuildingType name like "BUILDING_LIBRARY".
If I understand this section correctly, I can call any table using GameInfo, but I can only do
GameInfo.Buildings[BUILDING_MONUMENT] (or otherwise asking directly about a primary key) for the main tables like units/buildings, not a table like building_ResourceCosts? And to do something similar to what you have done in your example I could search for the index in a loop (imagining a slightly modified version of the GetGameInfoIndex where one iterates a table to find the matching type instead of direct query) and then say
GameInfo.Building_ResourceCosts(index) ?
Mostly correct. You can only directly access a row's data when accessing primary tables like Buildings, Policies, Units via a GameInfo.Buildings[BUILDING_MONUMENT] syntax. But when directly typing in the name of a BuildingType, UnitType, etc.,. you need to do as GameInfo.Buildings["BUILDING_MONUMENT"] rather than GameInfo.Buildings[BUILDING_MONUMENT] because in the red-colored example the game will interpret that as the name of a local lua variable and you'll get a nil value error.

But no you cannot do anything like GameInfo.Building_ResourceCosts(index). You have to traverse through all the rows in the table looking for the data you are interested in, because table Building_ResourceCosts is not a primary table where there is a single SQL PRIMARY KEY which is unique and is also listed as a "Type" in table "Types". You would have to do as
Code:
local HamburgerResourceType = -1
local HamburgerStartProductionCost = -1
local HamburgerPerTurnMaintenanceCost = -1

for row in GameInfo.Building_ResourceCosts() do
     if row.BuildingType == "BUILDING_HAMBURGER_HUT" then
          print("Hamburger Hut found in table Building_ResourceCosts")
          HamburgerResourceType = row.ResourceType
          HamburgerStartProductionCost = row.StartProductionCost
          HamburgerPerTurnMaintenanceCost = row.PerTurnMaintenanceCost
           break
     end
end
Just be aware that table Building_ResourceCosts allows one BuildingType to have more than one ResourceType "Cost" associated with it, so your code would need to be a bit more complicated than the simple example above.
 
Last edited:
Just be aware that table Building_ResourceCosts allows one BuildingType to have more than one ResourceType "Cost" associated with it, so your code would need to be a bit more complicated than the simple example above.
That is a good point to keep in mind!
Trying to keep complexity down, but I was brainstorming that since the computation is looped to go through the strategic resources, it would make the most sense to have:
-a function1 that accepts only the ResourceType, and returns a table of the buildings that consume it and their maintenance cost for that resource (since the Building_ResourceCosts table is empty in vanilla this shouldn't be computationally intensive, although in general it would be better to compute this once when the game loads/is created since the database is set at that point)
-a function2 that iterates the list of the player's cities, and for each building in the table from function1 uses :HasBuilding() to ascertain if that cost should be added to the total
-the result would be a variable representing building resource demand which would be identical to the ones for units and power, and thus a simple fill in.

One other thing on implementation: you had veyr helpfully mentioned the replaceUIscript action:
Code:
<ReplaceUIScript id="Expansion2_TopPanel" criteria="Expansion2">
            <Properties>
                <LuaContext>TopPanel</LuaContext>
                <LuaReplace>UI/Replacements/TopPanel_Expansion2.lua</LuaReplace>
            </Properties>
        </ReplaceUIScript>
Since I'm a beginner I am trying to keep this inside modbuddy. I'm assuming that the "criteria" and the "properties" sections in modbuddy would be taking on the relevant values.
But in this snippet, if I were to make a modified version of TopPanel_Expansion2.lua, is it the vanilla TopPanel I want to replace (Because that's what the XP2 file is doing) or the TopPanel_Expansion2 I want to replace (because that's what I am updating?)
(Assuming I just did something like, copy the entire XP2 file into a lua script and added the lines I need.)
I ask because I was playing around trying to print the pPlayerResources table used to do everything in RefreshResources() to the lua.log to see what exactly it contains and definitely messed up a few times with getting things to load/replace properly.
 
Top Bottom