[LIB] Harmony v1.2.0.1

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

Previous topic - Next topic

Spdskatr

Quote from: RawCode on August 01, 2017, 10:00:10 AM
calling base virtual method is not mandatory, implementation methods may call only override.
in such case unspecific code injection put on base method will not fire, because base method will not fire.
Since the base method looks like its code size is small, another possibility is that in lower grade code the method got inlined as a compiler optimization, therefore losing its instance. Though virtual methods should not be affected by compile time optimization, only runtime...
My mods

If 666 is evil, does that make 25.8069758011 the root of all evil?

CannibarRechter

I'm having both success and difficulty with Harmony. I think the code will be self-explanatory (when my comment says it FAILS or SUCCEEDS, it is because the print statements in the functions do or do not print). Can someone pretty please tell me why the functions aren't firing? My main desire is to actually get the POSTFIX function on PawnKindLifeStage.ResolveReferences() to fire. But I have figured out a long the way I cannot predict what makes harmony work or not work.

Help?

using Harmony;
using System;
//using System.Linq;
//using System.Text;
//using System.Diagnostics;
//using System.Collections.Generic;
using RimWorld;
using Verse;
using UnityEngine;
//using Verse.AI;
//using Verse.Sound;
namespace CR
{
    [StaticConstructorOnStartup]
    static class GraphicsPatches
    {
        static GraphicsPatches()
        {
            HarmonyInstance harmony = HarmonyInstance.Create("rimworld.CR.graphix");
         
            // FAILS: defined as public class PawnKindDef { public override void ResolveReferences() ... }
         harmony.Patch(typeof(PawnKindDef).GetMethod("ResolveReferences"), new HarmonyMethod(typeof(GraphicsPatches).GetMethod("MakeTransparentPreFix1")), null);

         // FAILS; defined as public class PawnKindLifeStage { public void ResolveReferences() ... }
            harmony.Patch(typeof(PawnKindLifeStage).GetMethod("ResolveReferences"), new HarmonyMethod(typeof(GraphicsPatches).GetMethod("MakeTransparentPreFix2")), null);
         
         // SUCCEEDS: defined as public class PawnGraphicSet { public void ResolveAllGraphics)() ... }
            harmony.Patch(typeof(PawnGraphicSet).GetMethod("ResolveAllGraphics"), new HarmonyMethod(typeof(GraphicsPatches).GetMethod("MakeTransparentPreFix3")), null);

         // FAILS; defined as public class PawnKindLifeStage { public void ResolveReferences() ... }         
            harmony.Patch(typeof(PawnKindLifeStage).GetMethod("ResolveReferences"), null, new HarmonyMethod(typeof(GraphicsPatches).GetMethod("MakeTransparentPostFix")));                             
      }
      
      public static bool MakeTransparentPreFix1(PawnKindDef __instance)
      {   
         UnityEngine.Debug.Log( string.Concat(new object[] { "CR. Transparent Prefix (PawnKindDef) CALLED" } ));      
         return true;
      }
      
      public static bool MakeTransparentPreFix2(PawnKindLifeStage __instance)
      {   
         UnityEngine.Debug.Log( string.Concat(new object[] { "CR. Transparent Prefix (PawnKindLifeStage) CALLED" } ));      
         return true;
      }
      
      public static bool MakeTransparentPreFix3(PawnGraphicSet __instance)
      {   
         UnityEngine.Debug.Log( string.Concat(new object[] { "CR. Transparent Prefix (PawnGraphicSet) CALLED" } ));      
         return true;
      }
      
      public static void MakeTransparentPostFix(PawnKindLifeStage __instance)
      {   
         if (__instance.dessicatedBodyGraphicData != null)
         {
            UnityEngine.Debug.Log( string.Concat(new object[] { "CR. Making corpse graphic transparent SUCCEEDED" } ));      
            __instance.dessicatedBodyGraphicData.shaderType = ShaderType.Transparent;
         }
         else UnityEngine.Debug.Log( string.Concat(new object[] { "CR. Making corpse graphic transparent FAILED:", __instance.label } ));   
      }
    }
}
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

Brrainz

Let's start with the most obvious. I haven't tested or looked it up myself but are you sure your naming and access rights are correct on those manual method fetching calls? If any of those return null, you won't patch successfully. So please add logging to make sure your variations of ResolveReference actually return a valid MethodInfo.

CannibarRechter

Yep. I explored that.


    static class GraphicsPatches
    {
        static GraphicsPatches()
        {
            HarmonyInstance harmony = HarmonyInstance.Create("rimworld.CR.graphix");

// FAILS; defined as public class PawnKindLifeStage { public void ResolveReferences() ... }

            MethodInfo method = typeof(Verse.PawnKindLifeStage).GetMethod("ResolveReferences");
UnityEngine.Debug.Log( string.Concat(new object[] { "CR. Initing GraphicsPatches: ", (method!=null ? method.ToString(): "NULL") } ));
            harmony.Patch( method, null, new HarmonyMethod(typeof(GraphicsPatches).GetMethod("MakeTransparentPostFix1")));
                   
}
public static void MakeTransparentPostFix1(PawnKindLifeStage __instance)
{
UnityEngine.Debug.Log( string.Concat(new object[] { "CR. Lifestage postfix called: ", __instance.label } ));
}
}
}


This code prints "Void ResolveReferences()".

The postfix as shown never executes. I have a work-around (higher up the call tree), but this is really the best place for it, so it's irking me. ;-P

I feel like I need to learn how to run rimworld in a debugger, and put a breakpoint in the function, just to prove that rimworlds is calling it at all. Any suggestions in that direction?

Anyway PawnKindLifeStage is a super simple class, and fully public. Thus:


using System;

namespace Verse
{
public class PawnKindLifeStage
{
// ... member variables

public void ResolveReferences()
{
// ... small bit of graphics load/setup code
}
}
}


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

Brrainz

No idea what you are doing but this mini mod works just fine for me:


using Harmony;
using System.Reflection;
using Verse;

namespace Test
{
public class TestMain : Mod
{
public TestMain(ModContentPack content) : base(content)
{
var harmony = HarmonyInstance.Create("net.pardeike.test");
harmony.PatchAll(Assembly.GetExecutingAssembly());
}
}

[HarmonyPatch(typeof(PawnKindLifeStage))]
[HarmonyPatch("ResolveReferences")]
static class PawnKindLifeStage_ResolveReferences_Patch
{
static void Prefix()
{
Log.Error("CALLING ResolveReferences");
}
}
}


Quote from: CannibarRechter on September 03, 2017, 06:27:03 AM
Yep. I explored that.


    static class GraphicsPatches
    {
        static GraphicsPatches()
        {
            HarmonyInstance harmony = HarmonyInstance.Create("rimworld.CR.graphix");

// FAILS; defined as public class PawnKindLifeStage { public void ResolveReferences() ... }

            MethodInfo method = typeof(Verse.PawnKindLifeStage).GetMethod("ResolveReferences");
UnityEngine.Debug.Log( string.Concat(new object[] { "CR. Initing GraphicsPatches: ", (method!=null ? method.ToString(): "NULL") } ));
            harmony.Patch( method, null, new HarmonyMethod(typeof(GraphicsPatches).GetMethod("MakeTransparentPostFix1")));
                   
}
public static void MakeTransparentPostFix1(PawnKindLifeStage __instance)
{
UnityEngine.Debug.Log( string.Concat(new object[] { "CR. Lifestage postfix called: ", __instance.label } ));
}
}
}


This code prints "Void ResolveReferences()".

The postfix as shown never executes. I have a work-around (higher up the call tree), but this is really the best place for it, so it's irking me. ;-P

I feel like I need to learn how to run rimworld in a debugger, and put a breakpoint in the function, just to prove that rimworlds is calling it at all. Any suggestions in that direction?

Anyway PawnKindLifeStage is a super simple class, and fully public. Thus:


using System;

namespace Verse
{
public class PawnKindLifeStage
{
// ... member variables

public void ResolveReferences()
{
// ... small bit of graphics load/setup code
}
}
}


CannibarRechter

Thank you. Different question. I am trying to Prefix patch a generic. Nothing complex right now, I just want the Prefix to execute, print a line, and then let the original generic do its work.

I have  this:


[HarmonyPatch(typeof(Verse.ContentFinder<>), new Type[] { typeof(Texture2D) } ) ]
[HarmonyPatch("Get")]
static class ContentFinder_Get_Patch
{
static bool Prefix( Texture2D __instance, Texture2D __result, string itemPath, bool reportFailure = true )
{
if (itemPath == "UI/Commands/Forbidden")
Log.Error( "CR. Intercepted content finder.");

return true;
}
}


So, this method is never invoked, but it borks the game. I get a series of errors like this before the game bails out and invalidates the mod (I'm thinking an internal exception happens or some such, but can't really tell):


(Filename: C:/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 42)

Could not load UnityEngine.Texture2D at Items/Drugs/BottleBeer in any active mod or in base resources.

(Filename: C:/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 42)

MatFrom with null sourceTex.

(Filename: C:/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 42)

Could not load UnityEngine.Texture2D at Items/Drugs/BoxAmbrosia in any active mod or in base resources.

(Filename: C:/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 42)

MatFrom with null sourceTex.


ContentFinder is pretty basic as a class. It looks like this:



namespace Verse
{
public static class ContentFinder<T> where T : class
{
public static T Get(string itemPath, bool reportFailure = true)
{
...


Am I missing something about how to patch generics?
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

Brrainz

Generic methods are split by the compiler into individual methods, one for each type or interface that can be used. So you cannot patch all at once since Harmony only can redirect code flow of a concrete implementation of a method. One can loop through all types you want to patch and patch them with code instead of annotations. You don't have to patch all types if you are only interested in a few cases.


MethodInfo method = typeof(Sample).GetMethod("GenericMethod");
MethodInfo generic = method.MakeGenericMethod(myType);

CannibarRechter

It's a generic class, not a generic method. I tried the manual approach here:


namespace CR
{
    [StaticConstructorOnStartup]
    public class ModdableUI : Mod
    {
        public ModdableUI( ModContentPack content ) : base( content )
        {
try
{
HarmonyInstance harmony = HarmonyInstance.Create("rimworld.CR.moddableUI");

Type genericClass = typeof( ContentFinder<Texture2D> );

UnityEngine.Debug.Log( string.Concat(new object[] { "CR: ModdableUI: ", (genericClass!=null ? genericClass.ToString(): "NULL") } ));

MethodInfo method = genericClass.GetMethod("Get");

UnityEngine.Debug.Log( string.Concat(new object[] { "CR: ModdableUI: ", (method!=null ? method.ToString(): "NULL") } ));

//harmony.Patch( method, new HarmonyMethod(typeof(GraphicsPatches).GetMethod("MakeTransparentPreFix")), null);
harmony.Patch( method, null, new HarmonyMethod(typeof(ModdableUI).GetMethod("GetPostfx")));
}
catch( Exception e )
{
UnityEngine.Debug.Log( string.Concat(new object[] { "CR: Exception: ", e } ));
}
}

static public void GetPostfix( Texture2D __instance, string itemPath, bool reportFailure )
{
// if (itemPath == "UI/Commands/Forbidden")
UnityEngine.Debug.Log( string.Concat(new object[] { "CR: Intercepted GET: ", itemPath } ));

return;
}
    }
}


What the debug statements print is here:

CR: ModdableUI: Verse.ContentFinder`1[UnityEngine.Texture2D]

CR: ModdableUI: UnityEngine.Texture2D Get(System.String, Boolean)

Those appear to be the correctly reflected generic class and method. Rimworld loses the ability to process any textures at all when this patch is applied. Which is weird, as it's a postfix patch which changes nothing, so it should have no impact.

BTW, while I went ahead and used the manual method invocation here, the Harmony website discusses generics, and gives recommendations that are similar to the stuff I tried the first time (class decorations).
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

Brrainz

Quote from: CannibarRechter on September 04, 2017, 10:39:50 AM
It's a generic class, not a generic method. I tried the manual approach here:

static public void GetPostfix( Texture2D __instance, string itemPath, bool reportFailure )
{
}


Since ContentFinder.Get is a static method, I am not sure why your postfix wants Harmony to inject "Texture2D __instance". It's static and even if it were not, Texture2D would need to be ContentFinder.

CannibarRechter

Welp, that was a silly error. I was hopeful, but the same problem continues. Here's the entire classfile now (as you can see I've tried it with the decorator method and manual method both, adjusted properly for a static method without an __instance type). The game issues several pages of can't load texture errors and then bongs out. Anyway, thank you so much for your help.

Is there a good way to debug Harmony? I suppose I could download the code and establish some traps...


using Harmony;
using System;
//using System.Linq;
//using System.Text;
//using System.Diagnostics;
//using System.Collections.Generic;
using System.Reflection;
using RimWorld;
using Verse;
using UnityEngine;
//using Verse.AI;
//using Verse.Sound;
namespace CR
{
    public class ModdableUI : Mod
    {
        public ModdableUI( ModContentPack content ) : base( content )
        {
try
{
HarmonyInstance harmony = HarmonyInstance.Create("rimworld.CR.moddableUI");
//harmony.PatchAll( Assembly.GetExecutingAssembly() );

Type genericClass = typeof( ContentFinder<Texture2D> );

UnityEngine.Debug.Log( string.Concat(new object[] { "CR: ModdableUI: ", (genericClass!=null ? genericClass.ToString(): "NULL") } ));

MethodInfo method = genericClass.GetMethod("Get");

UnityEngine.Debug.Log( string.Concat(new object[] { "CR: ModdableUI: ", (method!=null ? method.ToString(): "NULL") } ));

//harmony.Patch( method, new HarmonyMethod(typeof(GraphicsPatches).GetMethod("MakeTransparentPreFix")), null);
harmony.Patch( method, null, new HarmonyMethod(typeof(ModdableUI).GetMethod("GetPostfx")));

UnityEngine.Debug.Log( string.Concat(new object[] { "CR: Inited Moddable UI" } ));
}
catch( Exception e )
{
UnityEngine.Debug.Log( string.Concat(new object[] { "CR: Exception: ", e, "; this mod failed. We suggest you disable the mod and contact the author." } ));
}
}

static public void GetPostfix( string itemPath, bool reportFailure )
{
// if (itemPath == "UI/Commands/Forbidden")
UnityEngine.Debug.Log( string.Concat(new object[] { "CR: Intercepted GET: ", itemPath } ));

return;
}

//[HarmonyPatch(typeof(Verse.ContentFinder<Texture2D>))]
//[HarmonyPatch("Get")]
//static class ContentFinder_Get_Patch
//{
//static bool Prefix( Texture2D __instance, Texture2D __result, string itemPath, bool reportFailure )
// static void Postfix( string itemPath, bool reportFailure )
// {
// if (itemPath == "UI/Commands/Forbidden")
// UnityEngine.Debug.Log( string.Concat(new object[] { "CR. Intercepted GET: ", itemPath } ));

// return;
// }
//}
    }
}

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

CannibarRechter

So, if I declare ContentFinder<Texture2D>, and then GetMethod ("Get"), and then apply a null prefix (one that simply returns true, but does nothing) or a postfix to it, I get the exceptions. It took me a while to really understand what's happening: the exception is being issued by ContentFinder<Texture2D>.Get() itself. This is notable, because it suggests that Harmony is changing the behavior of Get(). If I am right, that's a Harmony bug. I believe I know where the problem occurs, if not why. Look here:


public static class ContentFinder<T> where T : class
{
public static T Get(string itemPath, bool reportFailure = true)
{
if (!UnityData.IsInMainThread)
{
Log.Error("Tried to get a resource \"" + itemPath + "\" from a different thread. All resources must be loaded in the main thread.");
return (T)((object)null);
}
T t = (T)((object)null);
foreach (ModContentPack current in LoadedModManager.RunningMods)
{
t = current.GetContentHolder<T>().Get(itemPath);
if (t != null)
{
T result = t;
return result;
}
}
// BEGIN ERROR BLOCK
if (typeof(T) == typeof(Texture2D))
{
t = (T)((object)Resources.Load<Texture2D>(GenFilePaths.ContentPath<Texture2D>() + itemPath));
}
if (typeof(T) == typeof(AudioClip))
{
t = (T)((object)Resources.Load<AudioClip>(GenFilePaths.ContentPath<AudioClip>() + itemPath));
}
// END ERROR BLOCK
if (t != null)
{
return t;
}
if (reportFailure)
{
// THIS IS THE EXCEPTION THAT IS RAISED:
Log.Error(string.Concat(new object[]
{
"Could not load ",
typeof(T),
" at ",
itemPath,
" in any active mod or in base resources."
}));
}
return (T)((object)null);
}


So my theory is that when Harmony patches a method on a generic class, something goes wrong with the original method. If you look at the error block that I annotated, I believe (although I am not 100% sure) what is happening is that those conditional checks are not returning true for whatever reason, and therefore being skipped. We are therefore reaching the reportFailure block. The other possibility is that Resources.Load() is returning null. I doubt this, though. That's a Unity method.

I've noticed another anomaly that might be helpful. I went ahead and attempted to implement an override style prefix (one that returns false). This would prevent the original possibly borked method from executing at all, and use my method instead. I've noticed that even though I specifically bound to ContentFinder<Texture2D>.Get(), the game/harmony is invoking the method
with AudioClip requests. I don't know if this is relevant information for you or not. I would think that shouldn't be able to happen. The game only looks for sound on bound generics like this: ContentFinder<AudioClip>.Get().

I think I have enough information to implement workarounds on my end at this point, but I thought I would raise the possibility of a bug in Harmony for you to look at.

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

Brrainz

This might be a limitation in how Harmony redirects the original method by inserting an assembler jump to the assembler code of an compiled dynamic method. My guess is that generic methods in generic classes have some extra logic inside the assembler code that simply cannot be replicated with a dynamic method. That, or the way to configure the dynamic method is incomplete (or simply does not support this scenario). To be honest, I have no clue how to address this.

Quote from: CannibarRechter on September 07, 2017, 07:41:28 AM
So, if I declare ContentFinder<Texture2D>, and then GetMethod ("Get"), and then apply a null prefix (one that simply returns true, but does nothing) or a postfix to it, I get the exceptions. It took me a while to really understand what's happening: the exception is being issued by ContentFinder<Texture2D>.Get() itself. This is notable, because it suggests that Harmony is changing the behavior of Get(). If I am right, that's a Harmony bug. I believe I know where the problem occurs, if not why. Look here:


public static class ContentFinder<T> where T : class
{
public static T Get(string itemPath, bool reportFailure = true)
{
if (!UnityData.IsInMainThread)
{
Log.Error("Tried to get a resource \"" + itemPath + "\" from a different thread. All resources must be loaded in the main thread.");
return (T)((object)null);
}
T t = (T)((object)null);
foreach (ModContentPack current in LoadedModManager.RunningMods)
{
t = current.GetContentHolder<T>().Get(itemPath);
if (t != null)
{
T result = t;
return result;
}
}
// BEGIN ERROR BLOCK
if (typeof(T) == typeof(Texture2D))
{
t = (T)((object)Resources.Load<Texture2D>(GenFilePaths.ContentPath<Texture2D>() + itemPath));
}
if (typeof(T) == typeof(AudioClip))
{
t = (T)((object)Resources.Load<AudioClip>(GenFilePaths.ContentPath<AudioClip>() + itemPath));
}
// END ERROR BLOCK
if (t != null)
{
return t;
}
if (reportFailure)
{
// THIS IS THE EXCEPTION THAT IS RAISED:
Log.Error(string.Concat(new object[]
{
"Could not load ",
typeof(T),
" at ",
itemPath,
" in any active mod or in base resources."
}));
}
return (T)((object)null);
}


So my theory is that when Harmony patches a method on a generic class, something goes wrong with the original method. If you look at the error block that I annotated, I believe (although I am not 100% sure) what is happening is that those conditional checks are not returning true for whatever reason, and therefore being skipped. We are therefore reaching the reportFailure block. The other possibility is that Resources.Load() is returning null. I doubt this, though. That's a Unity method.

I've noticed another anomaly that might be helpful. I went ahead and attempted to implement an override style prefix (one that returns false). This would prevent the original possibly borked method from executing at all, and use my method instead. I've noticed that even though I specifically bound to ContentFinder<Texture2D>.Get(), the game/harmony is invoking the method
with AudioClip requests. I don't know if this is relevant information for you or not. I would think that shouldn't be able to happen. The game only looks for sound on bound generics like this: ContentFinder<AudioClip>.Get().

I think I have enough information to implement workarounds on my end at this point, but I thought I would raise the possibility of a bug in Harmony for you to look at.

CannibarRechter

Okay. I'll let you know if I can get my work around working. If it works ti will assume that when invoked that the prefix method must assume that it could be EITHER version of the generic, and cannot return TRUE. I think this can be made to work; it will be dependent on me being able to determine if what is passed into the function is Texture2D or AudioClip. If I can do that, I can get what I need done, I think. It does mean that one couldn't ever safely use a Postfix method on this type of generic. That makes me a sad panda.
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

CannibarRechter

Unfortunately my work-around doesn't work. While the code I had  in mind executes perfectly well (I did it with 'object'), the value of __result is ignored by the game. So while I set __result to a value using code functionally similar to the original ContentFinder, the game after making the load complains about a MatFrom null. That's what the game does when it's going to set a texture to that red box, if you have seen it before, but now it's pretty much doing that to all graphics in Rimworld.

I'll see about creating an isolated non-Rimworld program that can replicate this bug for you...
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

Brrainz

@CannibarRechter:

I have analyzed your problem and it seems that there are two issues here:

1) [HarmonyPatch(typeof(ContentFinder<>), new Type[] { typeof(Texture2D) })] seems to be not working right now so you have to use a manual approach instead:


static MethodBase TargetMethod()
{
var type = typeof(ContentFinder<>);
type = type.MakeGenericType(new Type[] { typeof(Texture2D) });
var method = type.GetMethod("Get", AccessTools.all);
return method;
}


This is annoying and I will look into it.

2) The main problem is somewhere else. Right now, Harmony has no support for try-catch structures in the IL code of a method. And unfortunately, ContentFinder<T>.Get() has a try-catch structure that has code logic in its finally handler. As a result, the wrong part of the code is executed because the try-catch is not executed (in IL code, try-catch is not purely done by IL codes but also meta data that Harmony would need to maintain and insert at the correct places again).

I already have a pull request for handling try-catch metadata but my feeling is that it solves the problem only partially since I am pretty sure that it does not allow restructuring IL code with transpilers. So while pulling in this fix would most likely solve this issue, it would not work 100% in all cases.

The only workaround for you left at this point (until Harmony supports try/catch) would be to rewrite the method in a prefix that returns false.

/Andreas