Lua UI Mod Compatibility

Red Key

Modder
Joined
Sep 24, 2011
Messages
424
Location
USA
I need some help understanding how to make UI mods compatible. I use @sukritact 's Simple UI Adjustments and would like to make my mod compatible with it if possible. Simple UI makes some changes to specific functions in CityBannerManager.lua through the use of ReplaceUIScript. I am changing a different function in the same file that Sukritact did not touch and also using ReplaceUIScript. Seems like there should be a way to make things compatible since there is no overlap in the functions we are modifying.

Currently my mod is loading after Sukritact's mod and it appears my changes to CityBannerManager are working, but Sukritact's do not when my mod is active. For example, when hovering over the city banner it does not show me what tiles the citizens are working like it normally does when Simple UI is working.

One question I had - is there a difference between "include("CityBannerManager.lua");" and "include("CityBannerManager");" ?

Here is a condensed version of Simple UI's modinfo and LUA. I think the details of the functions are probably not important to the discussion, but I could include more code if needed.

Spoiler Simple UI Adjustment Code :

Code:
    <ReplaceUIScript id="Replace_CityBannerManager">
      <Properties>
        <LuaContext>CityBannerManager</LuaContext>
        <LuaReplace>UI/Common/CityBannerManager_Suk_UI.lua</LuaReplace>
      </Properties>
    </ReplaceUIScript>
CityBannerManager_Suk_UI.lua:
Code:
include("CityBannerManager.lua");

BASE_CityBanner_Initialize = CityBanner.Initialize

function CityBanner:Initialize( playerID: number, cityID : number, districtID : number, bannerType : number, bannerStyle : number)

    BASE_CityBanner_Initialize(self, playerID, cityID, districtID, bannerType, bannerStyle)
...
end

if not OnPopulationIconClicked then
    function OnPopulationIconClicked(playerID: number, cityID: number)
...
    end
end

function SetReligionTooltip(tControl, pCity)
...
end

BASE_CityBanner_UpdateReligion = CityBanner.UpdateReligion

function CityBanner.UpdateReligion(self)

    BASE_CityBanner_UpdateReligion(self)
...
end

Here is the related portion of my modinfo and LUA. I think I do not need to create BASE_* function pointers and call them like Sukritact did for some functions because I copied the entire contents of the original function and then changed a few lines. Correct me if I am wrong about that.

Spoiler Key Loyalties Code :
Code:
    <ReplaceUIScript id="ReplaceCityBannerLoyalty">
      <Properties>
        <LuaContext>CityBannerManager</LuaContext>
        <LuaReplace>CityBannerManager_KeyLoyalty.lua</LuaReplace>
        <LoadOrder>333</LoadOrder>
      </Properties>
    </ReplaceUIScript>
CityBannerManager_KeyLoyalty.lua:
Code:
include("CityBannerManager");

function CityBanner:UpdateLoyalty()
...
end

P.S. I see no errors in the log files.
 
Last edited:
The game uses entire files, not chunks of code from within files.

As part of the game loading and mod loading process, User Interface files are loaded as complete units (1 file = 1 unit), but only the final version of any file loaded into the game is used.

A ReplaceUIScript action literally tells the game to replace the Vanilla UI context with the one specified in the action. So when you state for example
Code:
<LuaContext>TechTree</LuaContext>
You are telling the game to replace whatever is usually used as the lua file for the "TechTree" context with the file you specify from within your mod in the next line of the modinfo, like Firaxis does here for Gathering Storm:
Code:
<LuaContext>TechTree</LuaContext>
<LuaReplace>UI/Replacements/TechTree_Expansion2.lua</LuaReplace>
But when two or more mods state the same LuaContext within an ReplaceUIScript Action, only the final Action implemented during the load process will be used by the game. The Entire File that is given for the <LuaReplace> will be file used for that "Context", not portions of code from within it. If some other mod has a file with differing code then all those difference are lost since the game only uses the entire contents of the final file to be loaded.

The same "final file to be loaded" rule also applies to simple <ImportFiles> actions as well -- the entire file is used, not portions of it or only portions which are different from the previous file loaded with the same filename.
 
I wonder why they've not implemented this (2 last lines of EndGameMenu.lua and see various EndGameMenu_[Something].lua in the game's folder)
Code:
include("EndGameMenu_", true);
Initialize();

in every UI files yet...

I don't see why that wouldn't allow 2 or more mods to changes specific functions of one UI files and still be compatible as long as they don't change the same function.
 
Thanks for the replies. I am new to Lua modding so this is helpful. I was hoping that when I include "CityBannerManager" that it is including the last defined version of any functions in that context - so it would include Sukritact's changes when I import it into my file if I had Simple UI enabled. If it is always just the Vanilla version of the file I see how that would be a problem.

Is there a way to include a file from another mod if that mod is enabled and loaded before mine? Is that what the example Gedemon gave is doing? Could I use a statement like "include("CityBannerManager_Suk_UI")" even if that file is not present in my mod?

With the "include("EndGameMenu_", true);" does this mean include all files with names matching the pattern EndGameMenu_* - even those from mods? I didn't understand exactly how this resolves the issue.
 
Is there a way to include a file from another mod if that mod is enabled and loaded before mine? Is that what the example Gedemon gave is doing? Could I use a statement like "include("CityBannerManager_Suk_UI")" even if that file is not present in my mod?

Yes for files that are defined in a <ImportFiles> section (and even if they are loaded after, the include would be called after all mods are loaded), but I don't know for files defined in <ReplaceUIScript>

With the "include("EndGameMenu_", true);" does this mean include all files with names matching the pattern EndGameMenu_* - even those from mods? I didn't understand exactly how this resolves the issue.
With the current implementation it won't resolve the issue I'm afraid, it's a different method, one that would allow multiple mods to change functions in the same UI file.
 
Yes for files that are defined in a <ImportFiles> section (and even if they are loaded after, the include would be called after all mods are loaded), but I don't know for files defined in <ReplaceUIScript>

I did some testing last night and it does not work with ReplaceUIScript. For now I am including a copy of "CityBannerManager_Suk_UI.lua" from Sukritact's mod in mine. I use ImportFiles with a "ModInUse" criteria to load the file. Then I have both of these lines in my own "CityBannerManager_KeyLoyalty.lua" file:

Code:
include("CityBannerManager");
include("CityBannerManager_Suk_UI");

This method works from what I could tell, but I don't like having to include a copy of Sukritact's file in my mod. If he makes an update to that file I would have to copy the update into my mod or there is a possibility of errors.

Also, I could not figure out how to use ModBuddy to create the "ModInUse" criteria. I tried multiple times/ways but ModBuddy kept crashing. ModBuddy seems to only support certain criteria types and ModInUse isn't among them. I ended up using notepad++ to add the "ModInUse" criteria manually, but if I rebuild in ModBuddy I assume I will lose those changes. Has anyone been able to add a "ModInUse" criteria within ModBuddy?
 
Yes I tried it in the past and I suspect that this criteria is not working.
 
The criteria itself seems to work. In the log I could see that it did not perform the import action unless I had the mod enabled. I just don't know how to create the criteria in ModBuddy.
 
Another question - why does the attached simple mod break the loyalty lens? When this mod is enabled the city banner no longer has the loyalty bar below it when you turn on the loyalty lens. I tested it with all other mods disabled, so I know it isn't coming from something else. I didn't notice anything in the logs.

I have created this simple mod to narrow down an issue with my other mod. This mod does not actually change anything. It does a ReplaceUIScript action on the CityBannerManager but the contents of what I am replacing it with are just a copy and paste of "function CityBanner:UpdateLoyalty()". I also confirmed that this function is the same in both RnF and GS. I am playing with both expansions enabled.

Spoiler CityBannerManager_Loyalty.lua :

Code:
include("CityBannerManager");

function CityBanner:UpdateLoyalty()

    -- Always update the loyalty warning, even if the lens isnt active
    self:UpdateLoyaltyWarning();

    local instance:table = self.m_Instance.LoyaltyInfo;
    if instance then
        if not m_isLoyaltyLensActive then
            instance.Top:SetHide(true);
            return;
        end
        local pCity:table = self:GetCity();
        if pCity then
            local pCityCulturalIdentity:table = pCity:GetCulturalIdentity();
            if pCityCulturalIdentity then
                local ownerID:number = pCity:GetOwner();

                local playerIdentitiesInCity = pCityCulturalIdentity:GetPlayerIdentitiesInCity();
                local cityIdentityPressures = pCityCulturalIdentity:GetCityIdentityPressures();
                local identitySourcesBreakdown = pCityCulturalIdentity:GetIdentitySourcesBreakdown();

                -- Update owner icon
                local pOwnerConfig:table = PlayerConfigurations[ownerID];
                local ownerIcon:string = "ICON_" .. pOwnerConfig:GetCivilizationTypeName();
                local ownerSecondaryColor, ownerPrimaryColor = UI.GetPlayerColors( ownerID );
                local ownerCivIconTooltip:string = Locale.Lookup("LOC_LOYALTY_CITY_IS_LOYAL_TO_TT", pOwnerConfig:GetCivilizationDescription());
                instance.OwnerCivIcon:SetIcon(ownerIcon);
                instance.OwnerCivIcon:SetColor(ownerPrimaryColor);
                instance.OwnerCivIcon:SetToolTipString(ownerCivIconTooltip);
                instance.OwnerCivIconBacking:SetColor(ownerSecondaryColor);
                instance.OwnerCivIconExtended:SetIcon(ownerIcon);
                instance.OwnerCivIconExtended:SetColor(ownerPrimaryColor);
                instance.OwnerCivIconExtended:SetToolTipString(ownerCivIconTooltip);
                instance.OwnerCivIconBackingExtended:SetColor(ownerSecondaryColor);

                -- Update potential transfer player icon
                local transferPlayerID:number = pCityCulturalIdentity:GetPotentialTransferPlayer();
                if transferPlayerID ~= -1 then
                    instance.TopCivIconBacking:SetHide(false);
                    instance.TopCivIconBackingExtended:SetHide(false);

                    local pTopConfig:table = PlayerConfigurations[transferPlayerID];
                    local topIcon:string = "ICON_" .. pTopConfig:GetCivilizationTypeName();
                    local topSecondaryColor, topPrimaryColor = UI.GetPlayerColors( transferPlayerID );
                    local topCivIconTooltip:string = Locale.Lookup("LOC_LOYALTY_CITY_WILL_FALL_TO_TT", pTopConfig:GetCivilizationDescription());
                    instance.TopCivIcon:SetIcon(topIcon);
                    instance.TopCivIcon:SetColor(topPrimaryColor);
                    instance.TopCivIcon:SetToolTipString(topCivIconTooltip);

                    instance.TopCivIconBacking:SetColor(topSecondaryColor);
                    instance.TopCivIconExtended:SetIcon(topIcon);
                    instance.TopCivIconExtended:SetColor(topPrimaryColor);
                    instance.TopCivIconBackingExtended:SetColor(topSecondaryColor);
                    instance.TopCivIconExtended:SetToolTipString(topCivIconTooltip);
                else
                    instance.TopCivIconBacking:SetHide(true);
                    instance.TopCivIconBackingExtended:SetHide(false);
                end

                -- Determine which pressure font icon to use
                local loyaltyPerTurn:number = pCityCulturalIdentity:GetLoyaltyPerTurn();
                local loyaltyFontIcon:string = loyaltyPerTurn >= 0 and "[ICON_PressureUp]" or "[ICON_PressureDown]";

                -- Update loyalty precentage
                local currentLoyalty:number = pCityCulturalIdentity:GetLoyalty();
                local maxLoyalty:number = pCityCulturalIdentity:GetMaxLoyalty();
                local loyalPercent:number = currentLoyalty / maxLoyalty;
                instance.LoyaltyFill:SetPercent(loyalPercent);
                instance.LoyaltyFillExtended:SetPercent(loyalPercent);

                local loyalStatusTooltip:string = GetLoyaltyStatusTooltip(pCity);
                local loyaltyFillToolTip:string = Locale.Lookup("LOC_LOYALTY_STATUS_TT", loyaltyFontIcon, Round(currentLoyalty,1), maxLoyalty, loyalStatusTooltip);
                instance.LoyaltyFill:SetToolTipString(loyaltyFillToolTip);
                instance.LoyaltyFillExtended:SetToolTipString(loyaltyFillToolTip);

                -- Update loyalty percentage string
                local loyaltyText:string = Locale.Lookup("LOC_CULTURAL_IDENTITY_LOYALTY_PERCENTAGE", Round(currentLoyalty, 1), maxLoyalty, loyaltyFontIcon, Round(loyaltyPerTurn, 1));
                instance.LoyaltyPercentageLabel:SetText(loyaltyText);
                instance.LoyaltyPercentageLabel:SetToolTipString(loyaltyFillToolTip);
                instance.LoyaltyPressureIcon:SetText(loyaltyFontIcon);
                instance.LoyaltyPressureIcon:SetToolTipString(GetLoyaltyPressureIconTooltip(loyaltyPerTurn, ownerID));

                --Update Loyalty breakdown
                if self.m_LoyaltyBreakdownIM ~= nil then
                    self.m_LoyaltyBreakdownIM:ResetInstances();

                    --Populate the breakdown
                    local localPlayerID = Game.GetLocalPlayer();
                    local pCulturalIdentity = pCity:GetCulturalIdentity();
                    local identitiesInCity = pCulturalIdentity:GetPlayerIdentitiesInCity();
                    local firstIdentityInCity = next(identitiesInCity);
                    if firstIdentityInCity == nil then
                        --We have no presences, or we are the only one
                        self.m_Instance.LoyaltyInfo.IdentityBreakdownStack:SetHide(true);
                    else
                        self.m_Instance.LoyaltyInfo.IdentityBreakdownStack:SetHide(false);
                        table.sort(identitiesInCity, function(left, right)
                            return left.IdentityTotal > right.IdentityTotal;
                        end);

                        local numInfluencers = 0;
                        for i, playerPresence in ipairs(identitiesInCity) do
                            if playerPresence.IdentityTotal ~= nil and playerPresence.IdentityTotal > 0 then
                                if numInfluencers < 2 or playerPresence.Player == localPlayerID then
                                    numInfluencers = numInfluencers + 1;
                                    local instance = self.m_LoyaltyBreakdownIM:GetInstance();
                                    local localPlayer = Players[localPlayerID];
                                    local pPlayerConfig = PlayerConfigurations[playerPresence.Player];
                                    local civName = Locale.Lookup(pPlayerConfig:GetCivilizationShortDescription());
                                    local lineVal = (i == 1 and "[ICON_Bolt] " or "") .. Round(playerPresence.IdentityTotal, 1);
                                    local hasBeenMet = localPlayer:GetDiplomacy():HasMet(playerPresence.Player) or localPlayerID == playerPresence.Player;
                                    instance.LineTitle:SetText(hasBeenMet and civName or Locale.Lookup("LOC_LOYALTY_PANEL_UNMET_CIV"));
                                    instance.LineValue:SetText(lineVal);

                                    local civIconManager = CivilizationIcon:AttachInstance(instance.CivilizationIcon);
                                    civIconManager:UpdateIconFromPlayerID(playerPresence.Player);
                                end
                            end
                        end
                    end
                    self.m_Instance.LoyaltyInfo.MainStack:CalculateSize();
                end
                

                -- Update loyalty pressure breakdown
                instance.FreeCityTop:SetHide(true);
                instance.CityStateTop:SetHide(true);
                for i, pressure in ipairs(identitySourcesBreakdown) do
                    if pressure[PRESSURE_BREAKDOWN_TYPE_POPULATION_PRESSURE] then
                        SetPressureBreakdownColumn(instance.PopulationPressureValue, instance.PopulationPressureFontIcon, Round(pressure[PRESSURE_BREAKDOWN_TYPE_POPULATION_PRESSURE], 1));
                        local tooltip:string = Locale.Lookup("LOC_CULTURAL_IDENTITY_POPULATION_PRESSURE_TOOLTIP");
                        local ownerPlayer = Players[ownerID];
                        if (ownerPlayer ~= nil) then
                            if (ownerPlayer:IsMajor()) then
                                tooltip = tooltip .. "[NEWLINE][NEWLINE]" .. Locale.Lookup("LOC_CULTURAL_IDENTITY_POPULATION_PRESSURE_TOOLTIP_MAJOR_CIVS");
                            else
                                tooltip = tooltip .. "[NEWLINE][NEWLINE]" .. Locale.Lookup("LOC_CULTURAL_IDENTITY_POPULATION_PRESSURE_TOOLTIP_MINOR_CIVS");
                            end
                        end
                        instance.PopulationTop:SetToolTipString(tooltip);
                    elseif pressure[PRESSURE_BREAKDOWN_TYPE_GOVERNORS] then
                        SetPressureBreakdownColumn(instance.GovernorPressureValue, instance.GovernorPressureFontIcon, Round(pressure[PRESSURE_BREAKDOWN_TYPE_GOVERNORS], 1));
                    elseif pressure[PRESSURE_BREAKDOWN_TYPE_HAPPINESS] then
                        SetPressureBreakdownColumn(instance.HappinessPressureValue, instance.HappinessPressureFontIcon, Round(pressure[PRESSURE_BREAKDOWN_TYPE_HAPPINESS], 1));
                    elseif pressure[PRESSURE_BREAKDOWN_TYPE_OTHER] then
                        SetPressureBreakdownColumn(instance.OtherPressureValue, instance.OtherPressureFontIcon, Round(pressure[PRESSURE_BREAKDOWN_TYPE_OTHER], 1));
                    elseif pressure[PRESSURE_BREAKDOWN_TYPE_CITY_STATE_BONUS] then
                        SetPressureBreakdownColumn(instance.CityStatePressureValue, instance.CityStatePressureFontIcon, Round(pressure[PRESSURE_BREAKDOWN_TYPE_CITY_STATE_BONUS], 1));
                        instance.CityStateTop:SetHide(false);
                    elseif pressure[PRESSURE_BREAKDOWN_TYPE_FREE_CITY_BONUS] then
                        SetPressureBreakdownColumn(instance.FreeCityPressureValue, instance.FreeCityPressureFontIcon, Round(pressure[PRESSURE_BREAKDOWN_TYPE_FREE_CITY_BONUS], 1));
                        instance.FreeCityTop:SetHide(false);
                    end
                end
                self.m_Instance.LoyaltyInfo.MainStack:CalculateSize();
                local newSizeY = self.m_Instance.LoyaltyInfo.MainStack:GetSizeY();
                self.m_Instance.LoyaltyInfo.CulturalIdentityExpandedButton:SetSizeY(newSizeY + 10);
                instance.Top:SetHide(false);
            end
        end
    end
end


Spoiler modinfo :

Code:
<?xml version="1.0" encoding="utf-8"?>
<Mod id="f50f3f9e-344b-446b-a555-c3698897bff3" version="1">
  <Properties>
    <Name>Loyalty Lens Test</Name>
    <Description>This is a brief description of the mod.</Description>
    <Created>1583219749</Created>
    <Teaser>This is a brief description of the mod.</Teaser>
    <Authors>RedKey</Authors>
    <CompatibleVersions>1.2,2.0</CompatibleVersions>
  </Properties>
  <Dependencies>
    <Mod id="1B28771A-C749-434B-9053-D1380C553DE9" title="Expansion: Rise and Fall" />
  </Dependencies>
  <References>
    <Mod id="4873eb62-8ccc-4574-b784-dda455e74e68" title="Expansion: Gathering Storm" />
  </References>
  <InGameActions>
    <ReplaceUIScript id="ReplaceUpdateLoyalty">
      <Properties>
        <LuaContext>CityBannerManager</LuaContext>
        <LuaReplace>CityBannerManager_Loyalty.lua</LuaReplace>
      </Properties>
    </ReplaceUIScript>
  </InGameActions>
  <Files>
    <File>CityBannerManager_Loyalty.lua</File>
  </Files>
</Mod>
 

Attachments

  • LoyaltyLensTest.zip
    2.9 KB · Views: 63
Screenshot of the issue described in my previous post:

With mod enabled there is no loyalty bar:
Spoiler :
upload_2020-3-2_23-55-21.png



Without mod:
Spoiler :
upload_2020-3-3_0-1-50.png

 
For others reference, I think I have figured out my issue. The reason copying and pasting the entire original function does not work is because the function uses a variable (m_isLoyaltyLensActive) which is declared local in CityBannerManager.lua outside the function. If I understand correctly, this variable is scoped to only be accessible from with CityBannerManager.lua and an include statement does not make it available in my new file. Thus in the following lines of code the variable will be nil/undefined and considered false which causes the function to exit early.

Code:
if not m_isLoyaltyLensActive then
            instance.Top:SetHide(true);
            return;
 end
 
Yup.

Variables that are defined as local within FileA.lua are nil values when FileA.lua is "included" into FileB.lua

This issue is probably the fundamental reason Firaxis uses three different versions of CityBannerManager.lua, none of which appear to be "included" into any other lua User Interface file.
 
Top Bottom