DefendBase Harmony Patch Help Please

Started by AlexandrosPrice, February 02, 2020, 07:29:08 AM

Previous topic - Next topic

AlexandrosPrice

Hi everyone, I had requested an "Offensive Stealth/Detection Indicator" in the mod forum. I don't have much experience coding, but now I want to try and create the mod myself, but stripped down to just change the variables in the triggers for their counter attack. How would I go about patching the LordJob_DefendBase transitions with Harmony? So that the triggers for the third transition are now:

FractionPawnsLost(0.15f) //to make it so that if the enemy losses are at 15% and not 20%, they attack
PawnHarmed(0.2f, false, null) //to make it so that if enemies injured are 20% and not 30%, they attack
ChanceOnTickInterval(60000, 0.05f) //enemies check every day not every hour, with a 5% chance instead of 3% of detecting
TicksPassed(300000) //enemies will automatically attack after 5 days instead of 4.2 days
UrgentlyHungry() //keep this the same
ChanceOnPlayerHarmNPCBuilding(0.5f) //enemies will attack 50% of the time if you harm their stuff, not 40%

Just to re-balance the time that it takes to be detected by the enemies on their faction base map, but increase the risk in attacking first. As long as the enemies will automatically attack if colonists get in their weapon range, just making these changes is all I'm looking to have to do.

I've looked at the examples on the Harmony Ludeon page and the GitHub Harmony Wiki, but i'm still confused on how the patches are put together. And I don't understand how one would hook up the transpiler to the transition3.

Any help with this would be much appreciated!

K

It doesn't look like you need a transpiler for this, which is good since they're pretty hard to work with.

Instead you could postfix CreateGraph in LordJob_DefendBase and modify the state graph however you want. The list of StateGraph transitions is publicly accessible so you could easily remove the original transition3 from it, then create a new transition with your modified values and add it to the StateGraph.

AlexandrosPrice

Thank you for responding, I've been getting some help  from the rimworld discord, but I have more questions. Making a class that runs the Harmony instance first right? Then use the __return function to get back the graph, but they were talking about "removing" the triggers from the code. I don't suppose you know a good example of Harmony in action with the __return.transition in use? Any help would be much appreciated , I've made a promise to myself of not playing my save until I make this mod. *And have it be stable XD I'll send what I have now soon, if that's alright with you, could you give me some tips? I have some programming experience, but not at all with C#.

K

Here's a mock-up of how the Harmony patch would probably look:

[HarmonyPatch(typeof(LordJob_DefendBase), "CreateGraph")]
public class HealthTooltipPatch {

public static void Postfix(ref StateGraph __result, IntVec3 ___baseCenter)
{
__result.transitions.RemoveAll(trans => trans.triggers.Count == 6);

LordToil_DefendBase lordToil_DefendBase = new LordToil_DefendBase(baseCenter);
LordToil_AssaultColony lordToil_AssaultColony = new LordToil_AssaultColony(true);
Transition myTransition = new Transition(lordToil_DefendBase, lordToil_AssaultColony, false, true);
// ...
// Configure the transition here
// ...

__result.AddTransition(myTransition);
}
}


I just typed this up so don't trust it too much. I have no idea if it would work.

The hardest part is determining which transition to remove. In this case I opted to look at the amount of triggers the transition had. This could conflict with other mods if they also changed the transitions. Configuring the transition should just be a copy-paste from the decompiled code and some number changes.

AlexandrosPrice

#4
So started with the creation of the mod and initialization of harmony in a 'Main'.

using System.Collections.Generic;
using System.Messaging;
using System.Reflection;
using Harmony;
using RimWorld;
using Verse;
using Verse.AI.Group;

namespace RimGetReady
{
    [StaticConstructorOnStartup]
    public class RimGetReadyMain
    {
        public HarmonyInstance instance = HarmonyInstance.Create("LordJob_DefendBase_Patch");
    }
}

And with your help I made this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using RimWorld;
using RimWorld.Planet;
using UnityEngine;
using Harmony;
using Verse.AI.Group;
using Verse;

namespace RimGetReady
{
   
   
    [HarmonyPatch(typeof(LordJob_DefendBase))]
    [HarmonyPatch("CreateGraph")]
    public class LordJob_DefendBase_Patch
    {
        public static void Postfix(ref StateGraph __result, IntVec3 __baseCenter)
        {
            __result.transitions.RemoveAll(trans => trans.triggers.Count == 8);

        LordToil_DefendBase lordToil_DefendBase = new LordToil_DefendBase(__baseCenter);
            LordToil_DefendBase lordToil_DefendBase2 = new LordToil_DefendBase(__baseCenter);
            LordToil_AssaultColony lordToil_AssaultColony = new LordToil_AssaultColony(true);
            lordToil_AssaultColony.useAvoidGrid = true;
            Transition myTransition = new Transition(lordToil_DefendBase, lordToil_AssaultColony, false, true);
            myTransition.AddTrigger(new Trigger_BecameNonHostileToPlayer());
            __result.AddTransition(myTransition);
            Transition myTransition2 = new Transition(lordToil_DefendBase2, lordToil_DefendBase, false, true);
            myTransition2.AddTrigger(new Trigger_BecamePlayerEnemy());
            __result.AddTransition(myTransition2);
            Transition myTransition3 = new Transition(lordToil_DefendBase, lordToil_AssaultColony, false, true);
            myTransition3.AddTrigger(new Trigger_FractionPawnsLost(0.15f));
            myTransition3.AddTrigger(new Trigger_PawnHarmed(0.2f, false, null));
            myTransition3.AddTrigger(new Trigger_ChanceOnTickInteval(60000, 0.05f));
            myTransition3.AddTrigger(new Trigger_TicksPassed(300000));
            myTransition3.AddTrigger(new Trigger_UrgentlyHungry());
            myTransition3.AddTrigger(new Trigger_ChanceOnPlayerHarmNPCBuilding(0.5f));
            __result.AddTransition(myTransition3);
        }
    }
}

I figured that we'd have to remove all triggers and set up new transitions, should I not have?
When I tested it by sending an SRTS (Maybe a conflict with this?) to a faction base map, it froze at the "Creating map for new encounter". Do you know what might be the problem?

Oh, wait no, it worked! I mean it loaded the map, but the enemy immediately attacked...
So something must have happened or there's a conflict I guess. Would increasing the priority of the Harmony Instance help?

... or maybe somebody's just urgently hungry...

AlexandrosPrice

I GOT IT!!! ;D
You are awesome K!
I'll be sure to give credit where credit is due when I try and submit this!
I don't know if I should keep all the triggers or values,  or if I should try and make a mod settings section. But I'll distribute this to get people to test it out.

LWM

Don't you need

    instance.PatchAll();

in your static constructor to make the harmony patches actually get patched?

AlexandrosPrice

In the documentation I said that my main class starts the mod, and the next { } after is where I created the Harmony instance. So I'm pretty sure that the instance.PatchAll(); is in there... I'll check when I get home.

AlexandrosPrice

Hmmm. I don't have a PatchAll but it seems like the patch is working anyway...
Where would I put it in the code? Is there anything that I'm doing that would negate the need for it?

K

Just so you know that "RemoveAll(trans => trans.triggers.count = 8)" would only remove transitions with 8 triggers. If you want to remove every trigger and change them then just use "__result.transitions.RemoveAll()" and then replace them.

That way at least you can guarantee that the transitions are the ones you want.

AlexandrosPrice

#10
Yeah, but now its saying: "There is no argument given that corresponds to the required formal parameter 'match' of 'List<Transition>.RemoveAll(Predicate<Transition>)'" because there is nothing in between the ().
What "Predicate<Transition>"'s do I add after the RemoveAll?

Something like this?
(trans => trans.triggers.TrueForAll(new Trigger))
or
(new Transition(LordToil,LordToil,bool,bool));

K

Actually the command to remove everything from a list is "list.Clear()". I forgot that RemoveAll required a predicate.

AlexandrosPrice

Okay, so this is what I have now:


using RimWorld;
using Harmony;
using Verse.AI.Group;
using Verse;
using System.Reflection;

namespace RimGetReady
{
    public class RimGetReadyMain
    {
        public static void DoRimGetReadyMain()
        {

            var harmony = HarmonyInstance.Create("com.Steam.Rimworld.Mod.RimGetReady");
            harmony.PatchAll(Assembly.GetExecutingAssembly());

        }
   
    }
    [HarmonyPatch(typeof(LordJob_DefendBase))]
    [HarmonyPatch("CreateGraph")]
    public class LordJob_DefendBase_Patch
    {
        public static StateGraph Postfix(ref StateGraph __result,Faction __faction, IntVec3 __baseCenter)
        {
            __result.transitions.Clear();
            LordToil_DefendBase lordToil_DefendBase = new LordToil_DefendBase(__baseCenter);
            LordToil_DefendBase lordToil_DefendBase2 = new LordToil_DefendBase(__baseCenter);
            LordToil_AssaultColony lordToil_AssaultColony = new LordToil_AssaultColony(true)
            {
                useAvoidGrid = true
            };
            Transition myTransition = new Transition(lordToil_DefendBase, lordToil_AssaultColony, false, true);
            myTransition.AddSource(lordToil_AssaultColony);
            myTransition.AddTrigger(new Trigger_BecameNonHostileToPlayer());
            __result.AddTransition(myTransition, false);
            Transition myTransition2 = new Transition(lordToil_DefendBase, lordToil_AssaultColony, false, true);
            //myTransition2.AddTrigger(new Trigger_FractionPawnsLost(0.15f));
            //myTransition2.AddTrigger(new Trigger_PawnHarmed(0.2f, false, null));
            //myTransition2.AddTrigger(new Trigger_ChanceOnTickInteval(60000, 0.05f));
            myTransition2.AddTrigger(new Trigger_TicksPassed(300000));
            //myTransition2.AddTrigger(new Trigger_UrgentlyHungry());
            //myTransition2.AddTrigger(new Trigger_PawnExperiencingDangerousTemperatures());
            //myTransition2.AddTrigger(new Trigger_ChanceOnPlayerHarmNPCBuilding(0.5f));
            myTransition2.AddPostAction(new TransitionAction_WakeAll());
            string message = "MessageDefendersAttacking".Translate(__faction.def.pawnsPlural, __faction.Name, Faction.OfPlayer.def.pawnsPlural).CapitalizeFirst();
            myTransition2.AddPreAction(new TransitionAction_Message(message, MessageTypeDefOf.ThreatBig, null, 1f));
            __result.AddTransition(myTransition2, false);
            Transition myTransition3 = new Transition(lordToil_DefendBase2, lordToil_DefendBase, false, true);
            myTransition3.AddTrigger(new Trigger_BecamePlayerEnemy());
            __result.AddTransition(myTransition3, false);
            return __result;
        }
    }
}


It seems to still not be working. I have made sure that when I test, I just have the TicksPassed so that it is the only trigger. But they counter attack earlier than five days. Do I need to move the third transition back to after the first transition?

K

I'm not very familiar with how the game processes transitions so I couldn't tell you but I'd recommend that you mirror the vanilla code as closely as possible.

Also, you don't really need to return the StateGraph if you've got a reference to the return value. While it doesn't hurt, Harmony postfixes typically either intercept the return value and return some new value or modify the return value (when combined with the ref keyword). Most patches don't try to do both, and the returned value would probably overwrite the modified return value.

LWM

I see there's AddPostAction() - that's something that happens when the transition occurs?  And you can have more than one?

You might add some Log.Warning/Message() statements as post actions for each transition, just saying it's done.  Then you can check the debug log to see what's going on.  If you look at the RimWorld cookiecutter recipe or my Deep Storage code (the Deep_Storage.cs), you can see how you can specify that the trace only shows up in Debug builds, for AddPostAction(), you could put it in blocks of #if DEBUG ... #end if, so you could even leave it in.

I find that sort of thing helps, as it gives a window into what your code is actually doing in the game.  You might have them print out tick counts, so you can compare directly, etc.

Good luck!