Jump to content

4 posts in this topic Last Reply

Highlighted Posts

Posted:
Last Online:  
 
Quote

Note: This is a work in progress, and we are still working on some updates in TM:PE for the 10.21 release.

In this guide I'm going to explain how part of the mod incompatibility detection used in TM:PE works, specifically:

  • Detecting known-incompatible mods
  • Detecting other versions of your own mod

It does not cover:

  • Detecting "currently unknown" incompatibilities
    • You'll need a redirection framework (eg. Harmony) to do stuff like that
  • Persisting settings such as "Run on startup" or "Ignore disabled mods"
    • Implementation is specific to how you choose to store settings

I've tried to keep the guide as simple and generic as possible.

When to use?

First, an incompatible mod checker can be somewhat contentious so only implement it if you really need it. For example, these are valid reasons to implement a checker:

  • Another mod conflicts with your mod, breaking it's functionality or causing weird bugs
  • Some users have obsolete versions of your mod
  • You keep forgetting to remove other builds of your mod during development & testing

The first two items on that list should be obvious, but the third might seem a little strange. While developing and testing TM:PE, we realised that the game (well, Mono, actually) is really bad at coping with multiple versions of the same mod - you can, and should, read more about it in our issue tracker. The tl;dr is: If there are multiple versions of your mod installed, such as a local dev version and a workshop version, the game struggles to differentiate them. The more complex your mod, particularly if it uses lots of static classes, etc., the more nightmarish this issue becomes.

With over a million subscribers (probably only about 300k active), 46 known incompatible mods, ~10 older versions of TM:PE, and 2 live versions (LABS and STABLE), we really needed this infrastructure to help tame support workload! If you spot any errors in the guide below, please let us know as  you could potentially make hundreds of thousands of people a bit happier!

Top-level overview

The checker has three main stages:

  1. Determine when the mod checker should run:
    • If the mod is already enabled when cities.exe is launched, wait until the plugin manager is ready
    • Or run when the mod is enabled by the user via Main Menu > Content Manager > Mods
  2. Scan for known incompatible mods, including other versions of your mod
  3. Display a pop-up dialog allowing the user to unsubscribe/delete problematic mods

See comments below for each stage in turn.

Credits

Krzychu124 developed the initial version of the mod checker. LinuxFan identified the issue with Mono not dealing with multiple instances of the same mod, and Dymanoid later elaborated on why that happens. Dymanoid also came up with the neat way to use a Guid to identify the current instance of the mod when scanning through other mods. FireController helped with testing. The UI code is largely based on the games' own code by Colossal Order. I've just helped out a bit on the fringes.


  Edited by aubergine18  

Added credits section

Share this post


Link to post
Share on other sites
  • Original Poster
  • Posted:
    Last Online:  
     

    Stage 1: Determine when the mod checker should run

    This is the easy bit :) For sake of brevity, I've left out the using clauses, I'm sure your IDE will help you fill in the gaps.

    The TM:PE implementation can be viewed online: TrafficManagerMod.cs

    namespace YourNamespace {
        public class YourMod : IUserMod {
    
            public string Name => "YourModName";
    
            public string Description => "Your mod description";
    
            public void OnEnabled() {
    
                if (UIView.GetAView() != null) {
                    // The user just enabled mod via Content Manager > Mods
                    // So we can run the checker right now
                    CheckForIncompatibleMods();
                } else {
                    // The mod was already enabled when Cities.exe launched
                    // We need to wait for plugin manager to be ready, so use this event:
                    LoadingManager.instance.m_introLoaded += CheckForIncompatibleMods;
                }
            }
    
            public void OnDisabled() {
                // Remove the event listener
                LoadingManager.instance.m_introLoaded -= CheckForIncompatibleMods;
            }
    
            private static void CheckForIncompatibleMods() {
                // You'd pull this from your mod settings, but we'll just define inline for now
                bool ScanOnStartup = true;
    
                if (ScanOnStartup) {
                    // This performs the next stage - checking for incompatible mods
                    ModsCompatibilityChecker mcc = new ModsCompatibilityChecker();
                    mcc.PerformModCheck();
                }
            }
        }
    }

    You'll notice we're using the LoadingManager.instance.m_introLoaded event in the code above; that occurs after the game has finished downloading any new or updated subscriptions and initialised the PluginManager.


      Edited by aubergine18  

    simplified

    Share this post


    Link to post
    Share on other sites
  • Original Poster
  • Posted:
    Last Online:  
     

    Stage 2: Scan for incompatible mods

    In stage 1, you will see we instantiate a ModsCompatibilityChecker instance, and then PerformModCheck() - here's the code that does that...

    The TM:PE implementation can be viewed online: ModsCompatibilityChecker.cs 

    namespace YourNamespace
    {
        public class ModsCompatibilityChecker
        {
            private readonly Dictionary<ulong, string> knownIncompatibleMods;
    
            public ModsCompatibilityChecker()
            {
                knownIncompatibleMods = LoadListOfIncompatibleMods();
            }
    
            // In TM:PE, we actually load these from an embedded text file resource,
            // but to keep this guide simple we'll just manually define them in code.
            private Dictionary<ulong, string> LoadListOfIncompatibleMods()
            {
                // You don't really need the string, tbh, so you could use a much simpler data structure here
                Dictionary<ulong, string> results = new Dictionary<ulong, string>();
    
                results.Add(409184143u, "Traffic++");
                results.Add(626024868u, "Traffic++ V2");
    
                return results;
            }
    
            // continued below...

    Now, before we continue, it's important to set yourself some rules about adding mods to the list. If your mod is popular, you're potentially going to nerf the subscriber count of the mods on the list. As an example, you can view the rules we defined for TM:PE in our wiki.

    Furthermore, you should also post a page somewhere explaining why the mods are incompatible, and, where possible, list alternate mods (particularly if the incompatible mods are obsolete) to help users retain functionality they would otherwise lose. As an example, see this post we made in the Steam workshop.

            // continued from above...
    
            // Scan for incompatibilities, and if any are found show a dialog
            public void PerformModCheck()
            {
                Dictionary<PluginInfo, string> detected = ScanForIncompatibleMods();
    
                if (detected.Count > 0)
                {
                    IncompatibleModsPanel panel = UIView.GetAView().AddUIComponent(typeof(IncompatibleModsPanel)) as IncompatibleModsPanel;
    
                    panel.IncompatibleMods = detected;
                    panel.Initialize();
    
                    UIView.PushModal(panel);
                    UIView.SetFocus(panel);
                }
            }
    
            // Iterate installed mods looking for any on the knownIncompatibleMods list, or other
            // versions of our mod.
            public Dictionary<PluginInfo, string> ScanForIncompatibleMods()
            {
                // This gives a unique id for our mod so we can avoid adding it to the results
                Guid selfGuid = Assembly.GetExecutingAssembly().ManifestModule.ModuleVersionId;
    
                Dictionary<PluginInfo, string> results = new Dictionary<PluginInfo, string>();
    
                // You'd pull this from your mod settings, but to keep things simple we'll just define inline
                bool filterToEnabled = false;
    
                foreach (PluginInfo mod in Singleton<PluginManager>.instance.GetPluginsInfo())
                {
                    // Skip bundled mods, camera scripts, and, if filtered, non-enabled mods
                    if (!mod.isBuiltin && !mod.isCameraScript && (!filterToEnabled || mod.isEnabled))
                    {
                        string modName = GetModName(mod);
                        ulong workshopID = mod.publishedFileID.AsUInt64;
    
                        if (knownIncompatibleMods.ContainsKey(workshopID))
                        {
                            results.Add(mod, modName);
                        }
                        else if (modName.Contains("YourModName"))
                        {
                            Guid currentGuid = GetModGuid(mod);
    
                            if (currentGuid != selfGuid)
                            {
                                results.Add(mod, $"{modName} in /{Path.GetFileName(mod.modPath)}");
                            }
                        }
                    }
                }
    
                return results;
            }
    
            public string GetModName(PluginInfo mod)
            {
                return ((IUserMod)mod.userModInstance).Name;
            }
    
            public Guid GetModGuid(PluginInfo plugin)
            {
                return plugin.userModInstance.GetType().Assembly.ManifestModule.ModuleVersionId;
            }
        }
    }

    Phew! So, that's stage 2 done. We've built up a list of identified incompatible mods and, assuming we found some, it's now time to do the dreaded UI stage...


      Edited by aubergine18  

    simplified comments

    Share this post


    Link to post
    Share on other sites
  • Original Poster
  • Posted:
    Last Online:  
     

    Stage 3: User interface

    This stage will be added in a few days, we're currently hunting down some bugs.

    The TM:PE implementation can be viewed online: IncompatibleModsPanel.cs

    todo


      Edited by aubergine18  

    Some initial content

    Share this post


    Link to post
    Share on other sites

    Sign In or register to comment...

    To comment in reply, you must be a community member

    Sign In  

    Already have an account? Sign in here.

    Sign In Now

    Create an Account  

    Sign up to join our friendly community. It's easy!  

    Register a New Account


    ×

    Thank You for the Continued Support!

    Simtropolis depends on donations to fund site maintenance costs.
    Without your support, we just would not be in our 24th year online!  You really help make this a great community. *:thumb:

    But we still need your support to stay online. If you're able to, please consider a donation to help us stay up and running. This helps sustain a platform where we can share our community creations for years to come.

    Make a Donation, Get a Gift!

    Expand your city with the best from the Simtropolis Exchange.
    Make a Donation and get one or all three discs today!

    STEX Collections

    By way of a "Thank You" gift, we'd like to send you our STEX Collector's DVD. It's some of the best buildings, lots, maps and mods collected for you over the years. Check out the STEX Collections for more info.

    Each donation helps keep Simtropolis online, open and free!

    Thank you for reading and enjoy the site!

    More About STEX Collections