Bleeding Rate Multiplier (C#) (Solved)

Started by ilikegoodfood, February 11, 2018, 06:10:10 PM

Previous topic - Next topic

ilikegoodfood

I'm trying to create a venom for my newest monster for my Monster Mash mod that acts as an anticoagulant (increases bleeding rate).

To do that I need to create a HediffDef that modifies the bleedingRateMultiplier, which is set as 1f in the game's code (BodyPartDef, line 95).
// Token: 0x04002469 RID: 9321
public float bleedingRateMultiplier = 1f;


By expanding my limited assembly, is it possible to implement a bleedingRateMultiplier that is defined by the creature, default of 1, and add a HediffDef stage to allow a HediffDef to modify it?

If yes, how hard is it likely to be and what would be required (this is my first mod)?

Thanks.

jamaicancastle

It's totally possible. It will require C# and probably the Harmony patching library.

Your mod will need to do three things. One, as you mentioned, is a hediff that will be applied to the target.

The second is a hook for the hediff to affect. The easiest way to get one of these is a stat, because stats are built primarily in XML (in Defs/Stats). You'll want a simple stat that defaults to 1 (=100%). You can also experiment with some of the other stat options, but that's not necessary.

Third, you'll need to patch the hediffGiver that controls blood loss and have it reference the new stat.

Note that this will be a systemic effect - all of the pawn's wounds will bleed more, not just the poisoned ones. A topical effect is much more difficult.

ilikegoodfood

#2
HungerRate is a topical (per creature) effect that has hooks similar to what I would need.
Am I correct in thinking that I could use it's code as a template for my new bleedRate property?

Also, The bleed rate is only calculated on the overall bleeding and isn't displayed on the individual wounds. This would result in the bleed Rate total being higher than the sum of its parts.
Would that confuse you, as a user, or would the +X% bleed Rate in the Hediff be clear enough?

EDIT:

This doesn't actually look as bad as I feared.
The BleedRateTotal is already calculated per pawn in the HediffSet file using the follwoing calculation:
// Token: 0x17000AA2 RID: 2722
// (get) Token: 0x06004388 RID: 17288 RVA: 0x001E90F4 File Offset: 0x001E74F4
public float PainTotal
{
get
{
if (this.cachedPain < 0f)
{
this.cachedPain = this.CalculatePain();
}
return this.cachedPain;
}
}


As such, all I should need is to set a bleedRateFactorOffset, a get function, and substitute the CalculatedPain() function to include the offset value, which I believe can be done using the Harmony Library which I already have.

ilikegoodfood

#3
So, as far as I can tell, there are few methods that I need to create and add to various places in the game using Harmony patches.

The first step is to add the following two values to Verse.HediffStages:
        public float bleedRateFactor = 1f;

        public float bleedRateFactorOffset;


Then I need to add the following bleedRateFactor method to HediffSet:
        public float bleedRateFactor
        {
            get
            {
                float num = 1f;
                for (int i = 0; i < this.pawn.hediffs.Count; i++)
                {
                    HediffStage curStage = this.hediffs[i].CurStage;
                    if (curStage != null)
                    {
                        num *= curStage.bleedRateFactor;
                    }
                }
                for (int j = 0; j < this.hediffs.Count; j++)
                {
                    HediffStage curStage2 = this.hediffs[j].CurStage;
                    if (curStage2 != null)
                    {
                        num += curStage2.bleedRateFactorOffset;
                    }
                }
                return Mathf.Max(num, 0f);
            }
        }


And finally, I need to use the Hamrony Transpiler to replace the default CalculateBleedRate in HediffSet to the following:
        private float CalculateBleedRate()
        {
            if (!this.pawn.RaceProps.IsFlesh || this.pawn.health.Dead)
            {
                return 0f;
            }
            float num = 0f;
            for (int i = 0; i < this.hediffs.Count; i++)
            {
                num += this.hediffs[i].BleedRate;
            }
            float num2 = num / this.pawn.HealthScale;
            for (int j = 0; j < this.hediffs.Count; j++)
            {
                num2 *= this.hediffs[j].bleedRateFactor;
            }
            return num2;
        }


Using the Various Space Ship Chunks mod as a template (link to Transpiler use), I think I can duplicate the Transpiler code, but even after reading the Harmony wiki tutorial, I have no idea what I need in order to add the other three methods.

Could someone please help me understand it all?
And am I missing any steps?

Thanks again.

jamaicancastle

Quote from: ilikegoodfood on February 12, 2018, 06:27:58 AM
The first step is to add the following two values to Verse.HediffStages:
        public float bleedRateFactor = 1f;

        public float bleedRateFactorOffset;
This is broadly why I would suggest using a stat, because stat offsets in hediffs are referred to by defName and the whole framework of manipulating them already exists. I'm not sure that it's possible to add properties to a class using Harmony. (There are a few other workarounds, like a comp or a worldcomponent, that could do the same thing, I just think a stat is easiest in this case.)

You should be able to postfix CalculateBleedRate instead of transpiling it if you want (I find postfixes easier to manage, myself). Just take the normal return value and multiply it by the stat factor. If the creature isn't supposed to be bleeding for whatever reason, 0 * the stat is still 0 so you're fine.

As far as user visibility - I would think the poison hediff would be in addition to the injuries themselves, and should show up under "whole body" like illnesses do. So you'd have a line under whole body for say "venom" that, on mouseover, would include a line like "bleeding rate +30%". That should be enough for people to tell what's going on.

ilikegoodfood

#5
I think that I have found a way to make the equation work specifically to each character while still being an XML value.
I have created a new PawnCapacityDef that is present but hidden on all pawn types called BleedRate and applied a capMod to the Hediff.

I have tested the venom in game and it seems to be working just fine, however, I don't know if it is possible for my new CalculateBleedRate equation to read the Capacity and use it as a multiplier, or what call I would need to put in place to do it.
Also, If I am calling a value from the pawn, would it still be achievable as a Postfix?

EDIT: Okay, now I understand stats a little better. I'll have to come back to this at another time. I have other things need doing too.

jamaicancastle

Quote from: ilikegoodfood on February 12, 2018, 06:20:30 PM
Also, If I am calling a value from the pawn, would it still be achievable as a Postfix?
Yes. A Harmony postfix can call a number of parameters:
Postfix(ref var __result, var __instance, var original_function_parameter_1, var original_function_parameter_2, ...)
where all of the "var"s are replaced by the actual variable type in question. Result is the return value of the method, and instance is the object that called the method.

In this case, CalculateBleedRate is a method of HediffSet that doesn't take any parameters and returns a float. So your postfix would look like this:
Postfix(ref float __result, HediffSet __instance)

Then in the body of the function, you can refer to the properties of __instance in the normal way. In this case the pawn is stored as __instance.pawn. Fortunately it's a public variable; it's possible to access private variables but it's a lot more verbose. To return one of the pawn's attributes, you'd use one of the following:
__instance.pawn.getStatValue([your stat def]); //find a stat
__instance.pawn.health.capacities.getLevel([your capacity def]); //find a capacity

ilikegoodfood

I've tried a number of things and I can't seem to work out where the problem is:

At first I added the BleedRate PawnCapacityDef, but left out the workerClass so that it would auto-create one, as the code seems to do when it returns null, but then I couldn't make a reference to it, because it didn't have a PawnCapacityDefOf entry.
Then I tried creating a simple workerClass and PawnCapacityDefOf in my own namespace, but, of course, the game doesn't look anywhere but in its own directory for them, so that didn't seem to work.

Finally, I tried bypassing my new PawnCapacity altogether, tying it to loss of blood pumping, but also without success.
At this point I don't know if the error is in the calculation or the patch. If it's not applying the patch, then that would certainly explain it nor working... Here is the patch itself:

namespace MonsterMash
{
    class initializeHamrony
    {
        static void patchHarmony()
        {
            var harmony = HarmonyInstance.Create("com.mash.monster");
            harmony.PatchAll(Assembly.GetExecutingAssembly());
        }
    }

    [HarmonyPatch(typeof(HediffSet), "CalculateBleedRate")]
    static class HarmonyPatches
    {
        static void Postfix(ref float _result, HediffSet _instance)
        {
            float num = _result * (1f + (1f - _instance.pawn.health.capacities.GetLevel(PawnCapacityDefOf.BloodPumping)));
        }
    }
}


Also, just out of curiosity, what chaos would occur if you created a duplicate file in the original namespace used by an existing RimWorld file, such as PawnCapacityDefOf?

jamaicancastle

Harmony expects __instance and __result to have two preceding underscores, not one. I don't know if that's the root of the problem, but I'd double-check that.

You can use the Log.Message() or Log.Error() functions to have your code send messages to the in-game dev log (with dev mode on, the far left button at the top of the screen). You can use that to test whether your patch is being run in the first place, or whether it's getting the right information passed to it.

There's a way to create your own *DefOf classes, but I'd have to look up the syntax on that because I rarely do it. (For most defs, you can call *Def.Named() to look them up by name instead. PawnCapacityDef doesn't seem to have that feature.)

ilikegoodfood

#9
So after some minor tinkering and changes I managed to get both the initialization message and the BleedRate Postfix messages showing up in the log, however, the bleeding isn't actually updating.

{
    [StaticConstructorOnStartup]
    class Main
    {
        static Main()
        {
            var harmony = HarmonyInstance.Create("com.mash.monster");
            harmony.PatchAll(Assembly.GetExecutingAssembly());
            Log.Message("Monster Mash :: Harmony initialized.");
        }
    }

    [HarmonyPatch(typeof(HediffSet), "CalculateBleedRate")]
    static class harmonyPatches
    {
        static void Postfix(ref float __result, HediffSet __instance)
        {
            float num = __result * (1f + (1f - __instance.pawn.health.capacities.GetLevel(PawnCapacityDefOf.BloodPumping)));
            Log.Message("Monster Mash :: Bleeding Postfix applied.");
        }
    }
}

Jaxxa

From the look of it the line:

float num = __result * (1f + (1f - __instance.pawn.health.capacities.GetLevel(PawnCapacityDefOf.BloodPumping)));

You are calculating num but never using it. You should assign it to __result instead of num like so:

__result = __result * (1f + (1f - __instance.pawn.health.capacities.GetLevel(PawnCapacityDefOf.BloodPumping)));

ilikegoodfood

#11
Thank you very much for your help.
I managed to get the version using BloodPumping working after I made that change.

EDIT 2: Creating a BleedRate Stat that requires the BleedRate Capacity with a weight of 1 worked perfectly! You don't need to read the rest of this comment.
Thank you very much for your assistance. :D


After that I began trying to formulate a workaround to not having a PawnCapacityDefOf my new BleedRate Capacity and I came up with the following:
{
    [StaticConstructorOnStartup]
    class Main
    {
        static Main()
        {
            var harmony = HarmonyInstance.Create("com.mash.monster");
            harmony.PatchAll(Assembly.GetExecutingAssembly());
        }
    }

    [HarmonyPatch(typeof(HediffSet), "CalculateBleedRate")]
    static class harmonyPatches
    {
        static void Postfix(ref float __result, HediffSet __instance)
        {
            bleedOffset bo = new bleedOffset();
            float bleedOffset = bo.get(__instance);
            __result *= bleedOffset;
            // __result *= (1f + (1f - __instance.pawn.health.capacities.GetLevel(PawnCapacityDefOf.BloodPumping)));
        }
    }

    class bleedOffset
    {
        public float get(HediffSet __instance)
        {
            float bleedOffset = 0f;

            if (__instance.hediffs.Count != 0)
            {
                foreach (Hediff T in __instance.hediffs)
                {
                    foreach (PawnCapacityModifier U in T.CapMods)
                    {
                        if (U.capacity.defName == "BleedRate")
                        {
                            bleedOffset += U.offset;
                        }
                    }
                }
            }

            return bleedOffset;
        }
    }
}


As you can see, I have created a custom method that takes the HediffSet __instance and reads the offset directly from the Hediffs, thus bypassing the need to reference the Capacity altogether.
It doesn't flag up any errors in Visual Studios, but on loading RimWorld I get the following error for every living thing in the ticker list:
QuoteException ticking [NAME]: System.NullReferenceException: Object reference not set to and instance of an object at MonsterMash.bleedOffset.get(Verse.HediffSet) <0x000a9>

I'm unsure of what I need to do to fix it, having already followed the MyClass steps that I found in online tutorials.
Also, such a workaround may well be terrible for the overall performance, so if you do know/have access to the Syntax for creating a new Capacity, or you know of an easier way to assign a code-readable per-pawn value, that would be fantastic. I don't even know where I would look to try and work that out.

Thanks again for your help.

EDIT: I am now experimenting with a stat that uses the BleedRate Capacity as an alternative and less resource-intensive work-around.

ilikegoodfood

#12
Solution:
In order to create the offset controls for the bleed rate I needed three things:

The first is a Capacity Def for the pawns:
<PawnCapacityDef>
<defName>BleedRate</defName>
<label>bleed rate</label>
<listOrder>9</listOrder>
<showOnHumanlikes>false</showOnHumanlikes>
<showOnAnimals>false</showOnAnimals>
<showOnMechanoids>false</showOnMechanoids>
</PawnCapacityDef>


The second is a Stat that uses the new hidden capacity:
<StatDef>
<defName>BleedRate</defName>
<label>bleed rate</label>
<description>Multiplier on bleed rate.</description>
<category>BasicsPawn</category>
<defaultBaseValue>1</defaultBaseValue>
<toStringStyle>PercentZero</toStringStyle>
<hideAtValue>1</hideAtValue>
<minValue>0.1</minValue>
<showOnMechanoids>false</showOnMechanoids>
<capacityFactors>
<li>
<capacity>BleedRate</capacity>
<weight>1</weight>
</li>
</capacityFactors>
</StatDef>


The final step is to create a Harmony Patch in your assembly that modifies the CalculateBleedRate method:
{
    [StaticConstructorOnStartup]
    class Main
    {
        static Main()
        {
            var harmony = HarmonyInstance.Create("com.mash.monster");
            harmony.PatchAll(Assembly.GetExecutingAssembly());
        }
    }

    [HarmonyPatch(typeof(HediffSet), "CalculateBleedRate")]
    static class harmonyPatches
    {
        static void Postfix(ref float __result, HediffSet __instance)
        {
            __result *= __instance.pawn.GetStatValue(StatDef.Named("BleedRate"));
        }
    }
}


This method uses the Harmony library to function, so you must have it in the assemblies folder of your mod if you wish to use patches like this.
A number of useful tutorials exist for setting up your own assembly, but you will need at least some knowledge of C# if you wish to edit the patch.

You can then use a capMod offset in the Hediff to increase the BleedRate capacity, which in turn increases the BleedRate stat and multiplies the bleeding from injuries by that raised stat. For Example, here is the SanguineDrake Venom from my Monster Mash mod:
<HediffDef>
<defName>MM_SanguineDrakeVenom</defName>
<hediffClass>HediffWithComps</hediffClass>
<defaultLabelColor>(1, 0.2, 0.2)</defaultLabelColor>
<label>sanguine drake venom</label>
<lethalSeverity>1</lethalSeverity>
<makesSickThought>true</makesSickThought>
<scenarioCanAdd>true</scenarioCanAdd>
<comps>
<li Class="HediffCompProperties_Immunizable">
<severityPerDayNotImmune>-0.1</severityPerDayNotImmune>
</li>
</comps>
<stages>
<li>
<label>initial</label>
<becomeVisible>true</becomeVisible>
</li>
<li>
<label>initial</label>
<minSeverity>0.04</minSeverity>
<capMods>
<li>
<capacity>BleedRate</capacity>
<offset>0.1</offset>
</li>
</capMods>
</li>
<li>
<label>minor</label>
<minSeverity>0.14</minSeverity>
<capMods>
<li>
<capacity>BleedRate</capacity>
<offset>0.25</offset>
</li>
</capMods>
</li>
<li>
<label>moderate</label>
<minSeverity>0.39</minSeverity>
<capMods>
<li>
<capacity>BleedRate</capacity>
<offset>0.7</offset>
</li>
</capMods>
</li>
<li>
<label>serious</label>
<minSeverity>0.64</minSeverity>
<capMods>
<li>
<capacity>BleedRate</capacity>
<offset>1.25</offset>
</li>
</capMods>
</li>
<li>
<label>extreme</label>
<minSeverity>0.84</minSeverity>
<capMods>
<li>
<capacity>BleedRate</capacity>
<offset>1.8</offset>
</li>
</capMods>
</li>
</stages>
</HediffDef>