About making ModLib (or any other mod I guess) a soft-dependency.

Users who are viewing this thread

bm01

Sergeant
Someone asked me how I made ModLib a soft-dependency for my mod, and suggested to me that I should post my explanation somewhere for other people to reference. So here it is. I'm not an expert in programming and C# is quite new to me, so it's likely that my solution is sub-optimal.

This can also apply to MBOptionScreen but I've had issues with it so I would recommend waiting until a stable version is released.

So first of all, we obviously can't have any reference to ModLib in our mod. And since we need to declare a class that inherits from one of ModLib's types (SettingsBase) to pass to SettingsDatabase.Register(), we have to move that class to an external DLL and load it only if ModLib is active. Any access to its methods or properties will have to be done through reflection.

Unfortunately, it means that we cannot use that class to store our actual settings, since it will just be a facade for ModLib to have something to read. So we end up with two settings classes and copying data from one to the other when needed. But more on that later.

So here's what I do. I have my normal settings class that I use throughout my code regardless of whether ModLib is loaded or not. Then I have a ModLibSettingsHandler static class with a Initialize method that I call when my mod is loaded. Inside, I check for ModLib with this bit of code:

C#:
if (AppDomain.CurrentDomain.GetAssemblies().Where(x => x.GetName().Name == "ModLib").FirstOrDefault() == null)
{
  return;
}
if (!Utilities.GetModulesNames().ToList().Contains("ModLib"))
{
  return;
}

First it checks the domain. Don't ask me what that is, I'm not entirely sure. But if a DLL is present in the game folder, that DLL ends up in the domain (even if the mod isn't loaded, which by the way is one of the reason why even unloaded mod can screw up your game). Then, it checks if ModLib is actually loaded. I believe we could skip the first check, but I prefer to be safe.

Ok, so we know that ModLib is loaded, now we can load our DLL holding our settings class that inherits from SettingsBase. This is how it's done:

C#:
string path = System.IO.Path.GetFullPath(System.IO.Path.Combine(Utilities.GetBasePath(), "Modules", DontStopMeNow.ModuleShortName, "bin", "Win64_Shipping_Client", "DontStopMeNow.ModLib.dll"));
_modLibSettingsType = Assembly.LoadFile(path).GetType("DontStopMeNow.ModLib.ModLibSettings");
Activator.CreateInstance(_modLibSettingsType);

The second line does two things, it loads our DLL and return the type of our class. With that type, I can create an instance of it. I don't store it because, again, that would force us to reference our secondary DLL, which we can't do. Also that instance can become invalid at some point, who knows what ModLib is going to do with it.

We can do a bunch of stuff in the constructor (remember it has to be public, non-static and without arguments, because that's what ModLib wants). Personally this is where I handle the reset button since it turns out that ModLib creates a new instance of our class when we click on it.

Now the problem is, how do we link our settings with ModLib's settings, so that changes are actually reflected? Well that's the part I'm not sure I'm doing right. Basically, I have a LinkSettings() method that populates a List<Action<bool>>. A list storing a bunch of delegates (anonymous methods) taking a bool as argument. One for each property. Here what it looks like inside:

C#:
private static void LinkSettings(SettingsFields settings, ModLibSettings modLibSettings)
{
  _links = new List<Action<bool>>()
  {
    delegate(bool toModLib)
    {
      if (toModLib)
      {
        modLibSettings.Affect_Ai = settings.RearUp.Affect_Ai;
      }
      else
      {
        settings.RearUp.Affect_Ai = modLibSettings.Affect_Ai;
      }
    },
  // And so on
  };
}

I'm doing this for each properties. It's not easily maintainable, and it's error prone. But I couldn't find a better solution (while keeping serialization possible).

I can then iterate through it, and depending on the boolean, it copies from one side to another:

C#:
private static void CopySettingsToModLib(SettingsFields settings, ModLibSettings modLibSettings)
{
  LinkSettings(settings, modLibSettings);
  foreach (var link in _links)
  {
    link(true);
  }
}

I also have CopyModLibToSettings() that will be called externally when changes have been made in ModLib's menu. Unfortunately, we can't register any kind of callback method so we don't know when the user is entering or leaving the menu. Since ModLib forces a restart, it's fine, I just call it when my mod is unloaded. After that I save my config file and all is good.

The thing is, as I said, ModLib creates another instance of our class whenever we click on "reset to default". Then, depending on what the user does, either it keeps that new instance as the default one, or it goes back to the original one. There is no way for us to know for sure which one will be used, so we have to call SettingsDatabase.GetSettings() every time we want to retrieve the values.

C#:
public static void CopyModLibToSettings(SettingsFields settings)
{
  // ModLib creates a new instance when the reset button is hit, so we're forced to retrieve the latest one.
  // We can't use the constructor to store the new instance because we don't know if it's actually going to be used or not (the user may cancel).
  LinkSettings(settings, (ModLibSettings)SettingsDatabase.GetSettings("DontStopMeNow"));
  foreach (var link in _links)
  {
    link(false);
  }
}

And that's pretty much it. Finally, here's the constructor:

C#:
public ModLibSettings()
{
  if (firstInstanciation)
  {
    firstInstanciation = false;
    CopySettingsToModLib(SettingsManager.Settings, this);
    SettingsDatabase.RegisterSettings(this);
  }
  else
  {
    // ModLib creates a new instance when the reset button is pressed. So we give it our default settings.
    CopySettingsToModLib(new SettingsFields(), this);
  }
}

There are two bigs advantages in doing this. Firstly, I can ditch ModLib, and my mod will still work since I handle my own config file. I really suggest not to rely on an external mod to handle core aspects of your mod. Secondly, mod order is no longer an issue.

I hope my explanation makes sense and people will find it useful.
 
Last edited:
To make copying from one setting class to the other I've been told to use an interface and make both implement it, but in my case the structure is different (one class and two nested classes) so it doesn't really work. That's probably what people should do though.

(also I removed an useless call in the constructor, the call to LinkSettings() was a leftover from an earlier version).
 
Last edited:
I did it with reflection. The ModLib settings file and the 'Normal' settings file have the same named properties.
This isn't the most optimized way, but it only runs once during startup so I wasn't worried about performance too much.
The ModLib version of the settings can return a 'Normal' version like so:

C#:
       public TournamentXPSettings GetSettings()
        {                   
            TournamentXPSettings dto = new TournamentXPSettings();
            PropertyInfo[] propertiesML = typeof(TournamentXPSettingsModLib).GetProperties();
            foreach (PropertyInfo pTXP in typeof(TournamentXPSettings).GetProperties())
            {
                foreach (PropertyInfo pML in propertiesML)
                {
                    try
                    {
                        if (pTXP.Name == pML.Name && pML.Name != "Instance")
                        {
                            pTXP.SetValue(dto, pML.GetValue(TournamentXPSettingsModLib.Instance));
                            break;
                        }
                    }
                    catch (Exception ex)
                    {
                        ErrorLog.Log("Error in assigning ModLib property to TXPSettings: " + pTXP.Name + "\n" + ex.ToStringFull());
                    }
                }
            }
            return dto;
        }

Doing it this way also means as long as I add new settings to both classes, I don't have to worry about the update/link between the two. I tried using Automapper as well, but was running into issues with it not loading all the time.
 
Thanks, I ended up doing something similar, but since I had nested types in my settings class, I had to do some recursion. Performance wise, it takes less than 2 milliseconds. I'm using a Linkedlist for the inner loop and I remove the value once found, but for only 26 properties it actually doesn't make any difference at all compared to a List (and it would probably be worse if the properties weren't iin the same order in both collections).

Anyway here's what it looks like. Thanks again for the idea.
C#:
private static void LinkSettings(SettingsFields settings, ModLibSettings modLibSettings)
{
    _links = new List<Action<bool>>();
    List<PropertyInfo> settingsProperties = new List<PropertyInfo>(typeof(SettingsFields).GetProperties().Where(x => x.Name != "Config_Version"));
    string[] forbiddenPropertyNames = { "ID", "ModName", "ModuleFolderName", "SubFolder" };
    LinkedList<PropertyInfo> modLibProperties = new LinkedList<PropertyInfo>(typeof(ModLibSettings).GetProperties().Where(x => !forbiddenPropertyNames.Contains(x.Name)));
    Link(settingsProperties, modLibProperties, settings, modLibSettings);
}

private static void Link(List<PropertyInfo> settingsProperties, LinkedList<PropertyInfo> modLibProperties, object settings, ModLibSettings modLibSettings)
{
    foreach (PropertyInfo property in settingsProperties)
    {
        bool found = false;
        List<PropertyInfo> nestedProperties = property.PropertyType.GetProperties().ToList();
        if (nestedProperties.Count > 0)
        {
            Link(nestedProperties, modLibProperties, property.GetValue(settings), modLibSettings);
            continue;
        }
        for (LinkedListNode<PropertyInfo> node = modLibProperties.First; node != null; node = node.Next)
        {
            if (property.Name != node.Value.Name)
            {
                continue;
            }
            found = true;
            _links.Add((bool toModLib) =>
            {
                if (toModLib)
                {
                    node.Value.SetValue(modLibSettings, property.GetValue(settings));
                }
                else
                {
                    property.SetValue(settings, node.Value.GetValue(modLibSettings));
                }
            });
            modLibProperties.Remove(node);
            break;
        }
        if (!found)
        {
            DontStopMeNow.HandleError("Couldn't find '" + property.Name + "' in ModLibSettings", null, true);
        }
    }
}
 
Back
Top Bottom