[LIB] Harmony v1.2.0.1

Started by Brrainz, January 13, 2017, 04:59:21 PM

Previous topic - Next topic

Nightinggale

Quote from: pardeike on December 11, 2017, 01:36:21 AM
You need to extend the official "Mod" class. It is the preferred way to start a mod and runs much earlier.
I'm not really sure what I'm supposed to do. It's not possible to make an extension constructor. A normal extension method is not called by vanilla. Class inheritance doesn't seem to be the answer either as no instance of the child class will be allocated, hence no constructor. The only thing left I could think of would be .cctor, but it looks like that doesn't work with RimWorld. I'm back to being out of ideas on how to execute C# code prior to LoadDefs() being called for the first time.

If there is some other way of activating Harmony earlier, then please enlighten me.
ModCheck - boost your patch loading times and include patchmods in your main mod.

CannibarRechter

>>  You need to extend the official "Mod" class

> It's not possible to make an extension constructor.

Pretty sure this got garbled. The game has first class support for running classes you inherit from the Mod class. If I remember when I get home at night, I'll look for the pattern for you. But I doubt he meant C# style extension classes here at all.

CR All Mods and Tools Download Link
CR Total Texture Overhaul : Gives RimWorld a Natural Feel
CR Moddable: make RimWorld more moddable.
CR CompFX: display dynamic effects over RimWorld objects

Nightinggale

I got it working (finally)
    public sealed class HarmonyStarter : Mod
    {
        public HarmonyStarter(ModContentPack content) : base (content)
        {
            var harmony = HarmonyInstance.Create("com.rimworld.modcheck");
            harmony.PatchAll(Assembly.GetExecutingAssembly());
        }
    }

It's so beautifully clean and simple once you see the result and once again I'm like "I spent this long on that???".

Thanks for the replies. I'm not sure I would have thought of inheriting the Mod class on my own.
ModCheck - boost your patch loading times and include patchmods in your main mod.

CannibarRechter

Yup. You got it. That's what I was gonna post for you. ;-P
CR All Mods and Tools Download Link
CR Total Texture Overhaul : Gives RimWorld a Natural Feel
CR Moddable: make RimWorld more moddable.
CR CompFX: display dynamic effects over RimWorld objects

Kiame

A heads up to other developers. I was having issues on Mac and Linux (Ubuntu specifically) systems where RW would crashing when my mods were enabled. I tracked the problem down to the simple act of patching Window.PreClose:

[HarmonyPatch(typeof(Window), "PreClose")]

Just patching it will causes the game to crash. More info here: https://ludeon.com/forums/index.php?topic=37269.msg385072#msg385072

dburgdorf

#215
My question was answered in another thread.
- Rainbeau Flambe (aka Darryl Burgdorf) -
Old. Short. Grumpy. Bearded. "Yeah, I'm a dorf."



Buy me a Dr Pepper?

Nightinggale

Quote from: Nightinggale on December 01, 2017, 02:31:54 PM
I have run into an issue with ModCheck and either Harmony suffers from it too or I would like to know how it's solved. The problem is when multiple mods adds the DLL. The game will load one of them and reject the rest, which is ok if all of them are updated. However if some fail to update, then the goal becomes to load the newest. However based on my experience and reading Verse.LoadedModManager.LoadAllActiveMods(), my impression is that it loads the first dll it encounters. This mean it will use the dll from the first mod in the modlist, regardless of the version number.
I finally managed to solve this. Even better I have a generic solution, which seems to work for all DLL files including Harmony. Because of this I have decided to ask for feedback here before making a real release. It's a standalone DLL and while it can handle multiple versions of other DLL files, it can't handle multiple different versions of itself, which is why I want to get it right in the first release.

The project url: https://github.com/Nightinggale/VersionedAssemblyLoader

The approach is as follows:
It relies on mod class init, which is the first thing to run in vanilla after loading DLL files. On init, it loops all mods and stores data on each DLL file located in a folder called VersionedAssemblies. Once completed, it loads one copy of each filename and then it makes a call to init code in the newly loaded DLL file.
The following rules apply:

  • Filenames are sorted alphabetically (0Harmony.dll is first)
  • When multiple files have the same name, it will use the file with the highest AssemblyFileVersion
    AssemblyFileVersion is assumed to be of type Version, that is x.x, x.x.x or x.x.x.x where x is ints
  • It calls title.VersionedAssemblyInit where title is AssemblyTitle
    AssemblyTitle is assumed to be a static method without parameters and is ignored if the method is missing
I think this is enough to allow it to apply to any DLL file, which is meant to be included in multiple mods while ensuring that the DLL file is only read once and only the newest version is used.

Any comments about this? Feel free to test this, but please do not start distributing until I release it as a stable release.
ModCheck - boost your patch loading times and include patchmods in your main mod.

scuba156

Quote from: Nightinggale on January 22, 2018, 01:14:00 PM
It's a standalone DLL and while it can handle multiple versions of other DLL files, it can't handle multiple different versions of itself, which is why I want to get it right in the first release.
You may be interested in this. It allows easier updates to .dll mods that get bundled with other mods. It stores which version of the assembly is the latest into a unity GameObject and only executes on that assembly. It will only need a few small changes to work for you.

It starts at the Main.cs constructor, the first instance ran will call Init() which finds all available assemblies and determines the newest version (my code also uses GameObjects to store other data, so Init() does some other things too). Then back at the constructor, it calls Execute() if the current instance is the latest version, otherwise it skips this version and will not execute. Everything inside Execute() is then safe to change in future versions if needed, everything before it cannot be updated without ensuring it will be called since only the first instance will actually run it and that could be any version. From what I remember, RimWorld will only load one instance of an assembly that has the same name and version, no matter where it comes from, so you don't have to check for that.

Other classes you will need are GameObjectUtils.cs (you can remove StoredModIdentifiers and the ComponentModIdentifiers class) and AssemblyUtility.cs.

Side note: I rediscovered this code snippet yesterday for anyone intersted. If you want to use use any translated strings for any sort of UI when you have a dll like this that gets bundled with other mods, but want to everything contained in a single dll file, then set all your language files as an EmbeddedResource and then you can use this class to extract and load them when you need them. Only thing is I cannot remember if I tested it with a dll that has multiple languages, so a check might need to be added to determine the current language and only load that one, but it also might not need it, no idea. It can also be adapted to work on other file types like images.

scuba156

Quote from: Kiame on December 16, 2017, 02:44:08 PM
A heads up to other developers. I was having issues on Mac and Linux (Ubuntu specifically) systems where RW would crashing when my mods were enabled. I tracked the problem down to the simple act of patching Window.PreClose:

[HarmonyPatch(typeof(Window), "PreClose")]

Just patching it will causes the game to crash. More info here: https://ludeon.com/forums/index.php?topic=37269.msg385072#msg385072
The same happens with Window.ExtraOnGUI(). Since the code I was executing wasn't all that important, my temporary solution was to manually patch it only on windows platforms, leaving unix unpatched. It works, but it's situation dependent.

Nightinggale

Quote from: scuba156 on January 22, 2018, 11:40:01 PMYou may be interested in this. It allows easier updates to .dll mods that get bundled with other mods. It stores which version of the assembly is the latest into a unity GameObject and only executes on that assembly. It will only need a few small changes to work for you.
Thanks, but after close examination of the source code as well as testing, I ended up not believing it to work for me. Say we have a DLL where version 1.1 and 1.2 are present in Assemblies and loaded in that order. Your approach is to call both in [StaticConstructorOnStartup]. First 1.1 detects that it's outdated and does nothing, then 1.2 is called and it executes. ModCheck has to work on PatchOperations, which are executed prior to [StaticConstructorOnStartup] calls. This mean all I have available is method calls from vanilla. It will start by calling 1.1 and even if I decide not to do anything, it will not call 1.2 afterwards. ModCheck has caused a number of issues from being loaded this early because not all features are available until some time after patching.
ModCheck - boost your patch loading times and include patchmods in your main mod.

scuba156

Quote from: Nightinggale on January 23, 2018, 04:20:11 AM
Quote from: scuba156 on January 22, 2018, 11:40:01 PMYou may be interested in this. It allows easier updates to .dll mods that get bundled with other mods. It stores which version of the assembly is the latest into a unity GameObject and only executes on that assembly. It will only need a few small changes to work for you.
Thanks, but after close examination of the source code as well as testing, I ended up not believing it to work for me. Say we have a DLL where version 1.1 and 1.2 are present in Assemblies and loaded in that order. Your approach is to call both in [StaticConstructorOnStartup]. First 1.1 detects that it's outdated and does nothing, then 1.2 is called and it executes. ModCheck has to work on PatchOperations, which are executed prior to [StaticConstructorOnStartup] calls. This mean all I have available is method calls from vanilla. It will start by calling 1.1 and even if I decide not to do anything, it will not call 1.2 afterwards. ModCheck has caused a number of issues from being loaded this early because not all features are available until some time after patching.
If I understand correctly, then you should be able to load the latest assembly into memory and invoke a class in there. Activator.CreateInstance(string,string) should help, there is also Activator.CreateInstance(Type). It has examples there that should help, also check the remarks section.

You could then store a bool on whether you have executed and use reflection to read the variable. HugsLibChecker has an example here.

Nightinggale

Quote from: scuba156 on January 23, 2018, 06:58:46 AMIf I understand correctly, then you should be able to load the latest assembly into memory and invoke a class in there. Activator.CreateInstance(string,string) should help, there is also Activator.CreateInstance(Type). It has examples there that should help, also check the remarks section.
I am actually using CreateInstance. However all those proposals makes me wonder if we had a case of miscommunication. I did manage to make a working solution here: https://github.com/Nightinggale/VersionedAssemblyLoader (same link as before). I could go ahead and release it as it is, but since it will be a mess if multiple versions are floating around in different mods, I decided to ask if people would have requests or could spot issues before releasing anything. Harmony has the same issue if updating, which is why I decided to ask the question here. I'm not asking for a new design just for the sake of change. It should be if it's an improvement over the current implementation.

Having said that, I don't feel like I wasted time reading code for other possible solutions. Not only is it always good to consider if the chosen solution is the best, I also realized that AssemblyUtility.CurrentAssemblyVersion can be used to provide an error if it is using an outdated DLL file. Not ideal, but better than using an outdated version without acting on it.
ModCheck - boost your patch loading times and include patchmods in your main mod.

fiziologus

using Verse;
using Harmony;
using System.Reflection;

namespace Prototype
{
        [StaticConstructorOnStartup]
        class Main
        {
                static Main()
                {
                        var harm = HarmonyInstance.Create("rimworld.mod.nutrition");
                        harm.PatchAll(Assembly.GetExecutingAssembly());
                }
        }
        [HarmonyPatch(typeof(IngredientValueGetter_Nutrition))]
        [HarmonyPatch("ValuePerUnitOf")]
        class Mod
        {
                [HarmonyPostfix]
                static void ValueForMeat(ref float __result, ThingDef t, IngredientValueGetter_Nutrition __instanse)
                {
                        float meat = t.IsMeat ? 2.6f : 1;
                        __result *= meat;
                }
        }
}

Where may be issue, this code just not work.

Kiame

Quote from: fiziologus on February 18, 2018, 11:56:56 PM
using Verse;
using Harmony;
using System.Reflection;

namespace Prototype
{
        [StaticConstructorOnStartup]
        class Main
        {
                static Main()
                {
                        var harm = HarmonyInstance.Create("rimworld.mod.nutrition");
                        harm.PatchAll(Assembly.GetExecutingAssembly());
                }
        }
        [HarmonyPatch(typeof(IngredientValueGetter_Nutrition))]
        [HarmonyPatch("ValuePerUnitOf")]
        class Mod
        {
                [HarmonyPostfix]
                static void ValueForMeat(ref float __result, ThingDef t, IngredientValueGetter_Nutrition __instanse)
                {
                        float meat = t.IsMeat ? 2.6f : 1;
                        __result *= meat;
                }
        }
}

Where may be issue, this code just not work.

Have you verified the patch is being called? Add

Log.ErrorOnce("IngredientValueGetter_Nutrition.ValuePerUnitOf Patch Called", __instance.GetHashCode());

To the beginning of the method to make sure it is successfully overridden. If it is then it could be the reference is not getting set. I've had other issues at times where reference values were not getting persisted and being verbose fixed the problem. In this case try:

float meat = t.IsMeat ? 2.6f : 1f;
meat = meat * __result;
__result = meat;

fiziologus

@Kiame, thanks with loging, I crack this.

Some help for modders.

In only-assembly-patch mods (as my example) init class (Main in my code) must inherit from Mod class, and init class constructor must use Verse.Mod constructor and have public access.

using Verse;
using Harmony;
using System;
using System.Reflection;

namespace Prototype
{
//      [StaticConstructorOnStartup]  ** no need, constructor cannot be static here
        class Main : Mod
        {
                public Main(ModContentPack content) : base (content) // always, or engine cannot call .ctor
                {
                        var harm = HarmonyInstance.Create("rimworld.mod.nutrition");
                        harm.PatchAll(Assembly.GetExecutingAssembly());
                        Log.Warning("Patch Init Called"); // just log message for debug
                }
        }
        [HarmonyPatch(typeof(IngredientValueGetter_Nutrition))]
        [HarmonyPatch("ValuePerUnitOf")]
        class Mod_Meat
        {
                [HarmonyPostfix]
                static void ValueForMeat(ref float __result, ThingDef t, IngredientValueGetter_Nutrition __instance)
                {
                        Log.ErrorOnce("IngredientValueGetter_Nutrition.ValuePerUnitOf Patch Called", __instance.GetHashCode());
                        float meat = t.IsMeat ? 2.6f : 1;
                        __result *= meat; // possible
                }
        }
}