Updating Functions Without Overwriting Files (Minimizing Conflicts) - Updated 2025/2/16

Conexion

Chieftain
Joined
Feb 14, 2025
Messages
3
Conflicts are never enjoyable - especially in modding. They can frustrate both mod creators and users alike. This is particularly true for UI mods, which require their files to be overwritten.

No longer! Well... Sort of.

There is still the issue of updating the HTML files themselves, which I will be doing more digging into, but the biggest obstacle right now is overwriting JS files.

With this method, mods should be able to modify different functions in the same file without having to overwrite the whole file (and another mod's work).

I've been digging into solving this problem, and I think this should work for most people's use cases. The answer is in events, and prototypes.

I won't bore you with the details - If you're interested in learning more about promises and prototypes, check out:

Let's set up a minimal project:
XML:
<?xml version="1.0" encoding="utf-8"?>
<Mod id="no_conflict_test" version="1"
    xmlns="ModInfo">
    <Properties>
        <Name>No Conflict Test</Name>
        <Description>Testing a method of adding UI changes without conflicts.</Description>
        <Authors>Conexion</Authors>
        <Package>Mod</Package>
        <AffectsSavedGames>0</AffectsSavedGames>
    </Properties>
    <ActionCriteria>
        <Criteria id="always">
            <AlwaysMet></AlwaysMet>
        </Criteria>
    </ActionCriteria>
    <ActionGroups>
        <ActionGroup id="game-no-conflict-test-always" scope="game" criteria="always">
            <Actions>
                <UIScripts>
                    <Item>no-conflict-test.js</Item>
                </UIScripts>
            </Actions>
        </ActionGroup>
    </ActionGroups>
</Mod>

You can see that I've created a minimal `no-conflict-test.modinfo` file - All I have is a single item declared that sits in the same folder. Nothing fancy. Let's add a simple js file.

JavaScript:
const yourModInitFunction = () => {
  // Your code here
}

// Add your function to the engine's ready event
engine.whenReady.then(yourModInitFunction);

And that's all you need to get code executing without custom panels or otherwise. Let's add a small test to make sure it is working:

JavaScript:
const yourModInitFunction = () => {
    setInterval(() => {
        console.error("<----<<  TEST  >>---->");
    }, 2000);
}
// Add your function to the engine's ready event
engine.whenReady.then(yourModInitFunction);

Now when you open the debugger, in the console you should see the test message show up every two seconds.

Now let's make it do something:

JavaScript:
// Panel you want to modify
import { PanelSystemBar } from '/base-standard/ui/system-bar/panel-system-bar.js';

const yourModInitFunction = () => {
    // Overwrite the onMetagamingStatusChanged method to remove 2K icon
    PanelSystemBar.prototype.onMetagamingStatusChanged = () => {
        if (!Network.supportsSSO()) return;
        const container = document.getElementById("ps-icons");
   
        if (container) {
            const newConnectionButton = document.createElement('fxs-activatable');
            newConnectionButton.setAttribute('caption', Locale.compose("LOC_UI_METAPROGRESSION"));
            newConnectionButton.setAttribute("radial-tag", "ps-bar-metaprogression");
            newConnectionButton.id = "metaprogression";
            newConnectionButton.classList.add("ml-3", "size-4", "bg-cover", "mt-1");
            if (Network.isMetagamingAvailable()) {
                newConnectionButton.style.backgroundImage = "url('')"; // Remove 2K icon
                newConnectionButton.setAttribute('data-tooltip-content', Locale.compose("LOC_UI_ENABLED_METAPROGRESSION"));
            }
            else {
                newConnectionButton.style.backgroundImage = "url('')"; // Remove 2K icon
                newConnectionButton.setAttribute('data-tooltip-content', Locale.compose("LOC_UI_DISABLED_METAPROGRESSION"));
            }
            const oldConnectionButton = document.getElementById("metaprogression");
            if (oldConnectionButton) {
                container.replaceChild(newConnectionButton, oldConnectionButton);
            }
            else {
                container.appendChild(newConnectionButton);
            }
        }
    }
}

// Add your function to the engine's ready event
engine.whenReady.then(yourModInitFunction);

All I do is import the panel I want to modify, overwrite the function I want to modify on the Panel's prototype, and done! In this case, I remove the URL of the 2K icon so that it doesn't show up in the upper right corner in what's called the System Bar.

I've attached the code here so you can open it and play with it yourself.

Threw this together pretty quickly, but I felt it was important to get out there. Let me know if you run into issues or have any questions.

---

Update: 2025/2/16

As @RealityMeltdown mentions below, it is often a good idea to run the original function, then add your code that runs after (where you can, this cannot always be done). If you do this, multiple people using this same 'additive' approach would further reduce conflicts!

Not only that, but you can often simplify your change and only include the slice of what you need. Continuing with our code above, this approach would look like:
JavaScript:
// Panel you want to modify
import { PanelSystemBar } from '/base-standard/ui/system-bar/panel-system-bar.js';

const yourModInitFunction = () => {
    // Save a reference to the original function
    const prevFunction = PanelSystemBar.prototype.onMetagamingStatusChanged;

    PanelSystemBar.prototype.onMetagamingStatusChanged = function (...args) {
        // Call the original function
        prevFunction.apply(this, args);

        // Make your changes here
        const button = document.getElementById("metaprogression");
        if (button) button.style.backgroundImage = "url('')"; // Remove 2K icon
    };
};

// Add your function to the engine's ready event
engine.whenReady.then(yourModInitFunction);

Now, if two people touch the same function, there is reduced risk of them stepping on each other's toes! The result:

1739732404883.png


1739732458632.png
 

Attachments

Last edited:
This method keeps you from having to constantly have to update when fxs changes the underlying source code, which is a huge win!

If I'm reading this correctly (and I might not be!), and two mods change "PanelSystemBar.prototype.onMetagamingStatusChanged" one would overwrite the other. Or did I miss where the original was preserved?

Can you copy the old function to a variable and call it inside, effectively "appending" to the original hook? I'm not sure if JavaScript lets you copy functions that way.

I should mention, though, that if you're changing enough code, you might have issues overwriting the same method anyway. You may "want* to make sure your version is the only version, and let users tweak the priority load order to "pick".

Now if we could have a warning when both are using this method...
 
Last edited:
This is awesome. I was trying to figure out how to do modular design like this for another use-case. I was trying to solve without replacing JS file since mods wholly replacing JS/LUA files are mods that are more easily broken by updates and less compatible with other mods.

If you have a moment, could you let me know how you'd go about this specific challenge? Basically, when user enters City View, I want it to show the city banner. The code that handles this is in the CityView class in the getRules() method which alot of these types of classes share.

JavaScript:
    getRules() {
        return [
            { name: "harness", type: UISystem.HUD, visible: "true" },
            { name: "city-banners", type: UISystem.World, visible: "true" },  // changed from false
            { name: "plot-icons", type: UISystem.World, visible: "true" },
            { name: "plot-tooltips", type: UISystem.World, visible: "true" },
            { name: "plot-vfx", type: UISystem.World, visible: "true" },
            { name: "unit-flags", type: UISystem.World, visible: "false" },
            { name: "unit-info-panel", type: UISystem.World, visible: "false" },
            { name: "small-narratives", type: UISystem.World, visible: "false" },
            { name: "world", type: UISystem.Events, selectable: false }
        ];
    }

The CityView class gets added as a "Handler" to the ViewManager module where it is then invoked as needed.

I'm trying to figure out if I can override the getRules method at runtime. If not, it might be instantiated already in the ViewManager module needing updating in there somehow.
 
If I'm reading this correctly (and I might not be!), and two mods change "PanelSystemBar.prototype.onMetagamingStatusChanged" one would overwrite the other. Or did I miss where the original was preserved?
Can you copy the old function to a variable and call it inside, effectively "appending" to the original hook? I'm not sure if JavaScript lets you copy functions that way.

I should mention, though, that if you're changing enough code, you might have issues overwriting the same method anyway. You may "want* to make sure your version is the only version, and let users tweak the priority load order to "pick".

I've added a section on preserving the original and adding your own on top, thanks for the suggestion! Regarding priority, I haven't confirmed it, but I know In @sukritact 's mod, there is:


XML:
<ActionGroup id="game-suk-simple-ui-always" scope="game" criteria="always">
    <Properties>
        <LoadOrder>100</LoadOrder>
    </Properties>

Which I believe may set the 'priority' - Using this, in theory, mod makers could make sure their code loads before or after others, which would be great if that's the case! Hoping that once official mod support is ready, they'll have better ways to manage priority and conflicts.

This is awesome. I was trying to figure out how to do modular design like this for another use-case. I was trying to solve without replacing JS file since mods wholly replacing JS/LUA files are mods that are more easily broken by updates and less compatible with other mods.

If you have a moment, could you let me know how you'd go about this specific challenge? Basically, when user enters City View, I want it to show the city banner. The code that handles this is in the CityView class in the getRules() method which alot of these types of classes share.

I'll take a look at that in just a moment!
 
The CityView class gets added as a "Handler" to the ViewManager module where it is then invoked as needed.

I'm trying to figure out if I can override the getRules method at runtime. If not, it might be instantiated already in the ViewManager module needing updating in there somehow.

Alright! So I took a look a bit - Unfortunately I think you have a bit of a complex case. Obviously, overwriting the file is the easiest way. Beyond that, I think you're going to need to start by either adding a `removeHandler` function to the `ViewManagerSingleton`, or replace `addHandler` so that you can overwrite existing handlers. You'd then make a copy of the `CityView`, modify the prototype to change the `getRules` function, and the new `CityView` to the `ViewManager`.

Hope that helps!
 
Is there a way to call the original function? Then you could assign the result somewhere, and then modify just the flag you want to toggle.
 
I've added a section on preserving the original and adding your own on top, thanks for the suggestion! Regarding priority, I haven't confirmed it, but I know In @sukritact 's mod, there is:
XML:
<ActionGroup id="game-suk-simple-ui-always" scope="game" criteria="always">
    <Properties>
        <LoadOrder>100</LoadOrder>
    </Properties>

Which I believe may set the 'priority'....
i've played around with LoadOrder extensively & have not seen any effect from it. i don't think the new mod framework supports explicit load order yet (or if it does, it uses a different syntax)
 
I think <LoadOrder> works for <UpdateDatabase>
 
I think <LoadOrder> works for <UpdateDatabase>
after some more experimentation, it does seem to work for scripts too, but scope is important. if you want to override a mod with <ActionGroup> set to LoadOrder 100, you also need to set the LoadOrder property for <ActionGroup> – it won't work if you set it on <ImportFiles> instead.
 
Alright! So I took a look a bit - Unfortunately I think you have a bit of a complex case. Obviously, overwriting the file is the easiest way. Beyond that, I think you're going to need to start by either adding a `removeHandler` function to the `ViewManagerSingleton`, or replace `addHandler` so that you can overwrite existing handlers. You'd then make a copy of the `CityView`, modify the prototype to change the `getRules` function, and the new `CityView` to the `ViewManager`.

Hope that helps!
Thanks mate. Hopefully when modding support is released, they might document this. I'm not optimistic though. If I figure it out I'll let everyone know.
 
While it's working for system-bar/panel-system-bar.js it seems to crash silently when importing city-details/panel-city-details.js. I wonder what' s the difference.
 
Is there a way to do this with a global function or variable instead of a class function? I'm trying to override a function in production-chooser-helpers.js but haven't found anything that works so far.
 
Last edited:
If the function is unexported I think there's no way save for replacing the whole file with <ImportFiles>
 
Ah! Because PanelCityDetails is unexported! RIP

PanelCityDetails works better with the decorator approach discussed here:
https://forums.civfanatics.com/threads/additive-ui-elements.695406/

however, its render method isn't included in that decorator protocol, so i also need to use the techniques here to patch it. that's harder to do with a non-exported class, although i was able to get the necessary prototype with Object.getPrototypeOf(panel) which fetches the prototype from the instance you're decorating.

i'll have an example in my next City Hall release! or you can check out the discussion on the official Civ discord – i just described my technique in more detail there, in the #modding-technical-discussion channel.
 
Any idea on best practices when what you want to change isn't in a named function?

For example, look at
core/ui/options/options.js

There's a ton done via:
Options.addInitCallback(() => { /*the whole damn file*/ }

So if I want to override just part of that callback's logic, not sure good way to do that.
 
Back
Top Bottom