1. We have added a Gift Upgrades feature that allows you to gift an account upgrade to another member, just in time for the holiday season. You can see the gift option when going to the Account Upgrades screen, or on any user profile screen.
    Dismiss Notice

Lua UI Mod Compatibility

Discussion in 'Mod Creation Help' started by Red Key, Feb 26, 2020.

  1. Red Key

    Red Key Modder

    Joined:
    Sep 24, 2011
    Messages:
    418
    Gender:
    Male
    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: Feb 26, 2020
  2. LeeS

    LeeS Imperator Supporter

    Joined:
    Jul 23, 2013
    Messages:
    7,089
    Location:
    Illinois, USA
    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.
     
  3. Gedemon

    Gedemon Modder Super Moderator

    Joined:
    Oct 4, 2004
    Messages:
    9,569
    Location:
    France
    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.
     
  4. Red Key

    Red Key Modder

    Joined:
    Sep 24, 2011
    Messages:
    418
    Gender:
    Male
    Location:
    USA
    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.
     
  5. Gedemon

    Gedemon Modder Super Moderator

    Joined:
    Oct 4, 2004
    Messages:
    9,569
    Location:
    France
    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 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.
     
  6. Red Key

    Red Key Modder

    Joined:
    Sep 24, 2011
    Messages:
    418
    Gender:
    Male
    Location:
    USA
    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?
     
  7. Gedemon

    Gedemon Modder Super Moderator

    Joined:
    Oct 4, 2004
    Messages:
    9,569
    Location:
    France
    Yes I tried it in the past and I suspect that this criteria is not working.
     
  8. Red Key

    Red Key Modder

    Joined:
    Sep 24, 2011
    Messages:
    418
    Gender:
    Male
    Location:
    USA
    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.
     
  9. Red Key

    Red Key Modder

    Joined:
    Sep 24, 2011
    Messages:
    418
    Gender:
    Male
    Location:
    USA
    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>
     

    Attached Files:

  10. Red Key

    Red Key Modder

    Joined:
    Sep 24, 2011
    Messages:
    418
    Gender:
    Male
    Location:
    USA
    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
     
  11. Red Key

    Red Key Modder

    Joined:
    Sep 24, 2011
    Messages:
    418
    Gender:
    Male
    Location:
    USA
    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
     
  12. LeeS

    LeeS Imperator Supporter

    Joined:
    Jul 23, 2013
    Messages:
    7,089
    Location:
    Illinois, USA
    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.
     

Share This Page