diff --git a/1.4/Assemblies/0JecsTools.dll b/1.4/Assemblies/0JecsTools.dll
index 3f0619d0..7d2e445f 100644
Binary files a/1.4/Assemblies/0JecsTools.dll and b/1.4/Assemblies/0JecsTools.dll differ
diff --git a/1.4/Assemblies/AbilityUser.dll b/1.4/Assemblies/AbilityUser.dll
index 119acd95..be4b1e5f 100644
Binary files a/1.4/Assemblies/AbilityUser.dll and b/1.4/Assemblies/AbilityUser.dll differ
diff --git a/1.4/Assemblies/AbilityUserAI.dll b/1.4/Assemblies/AbilityUserAI.dll
index ef2844ef..2ee8a709 100644
Binary files a/1.4/Assemblies/AbilityUserAI.dll and b/1.4/Assemblies/AbilityUserAI.dll differ
diff --git a/1.4/Assemblies/CompActivatableEffect.dll b/1.4/Assemblies/CompActivatableEffect.dll
index f900c46e..fde4c14a 100644
Binary files a/1.4/Assemblies/CompActivatableEffect.dll and b/1.4/Assemblies/CompActivatableEffect.dll differ
diff --git a/1.4/Assemblies/CompAnimated.dll b/1.4/Assemblies/CompAnimated.dll
index 9a4d5991..ac3e9e84 100644
Binary files a/1.4/Assemblies/CompAnimated.dll and b/1.4/Assemblies/CompAnimated.dll differ
diff --git a/1.4/Assemblies/CompBalloon.dll b/1.4/Assemblies/CompBalloon.dll
index b2879f70..1f1c3ba0 100644
Binary files a/1.4/Assemblies/CompBalloon.dll and b/1.4/Assemblies/CompBalloon.dll differ
diff --git a/1.4/Assemblies/CompBigBox.dll b/1.4/Assemblies/CompBigBox.dll
index d6bf993a..0a51f761 100644
Binary files a/1.4/Assemblies/CompBigBox.dll and b/1.4/Assemblies/CompBigBox.dll differ
diff --git a/1.4/Assemblies/CompDeflector.dll b/1.4/Assemblies/CompDeflector.dll
index 498b0e4a..20653718 100644
Binary files a/1.4/Assemblies/CompDeflector.dll and b/1.4/Assemblies/CompDeflector.dll differ
diff --git a/1.4/Assemblies/CompDelayedSpawner.dll b/1.4/Assemblies/CompDelayedSpawner.dll
index 09a326c8..1c1eac86 100644
Binary files a/1.4/Assemblies/CompDelayedSpawner.dll and b/1.4/Assemblies/CompDelayedSpawner.dll differ
diff --git a/1.4/Assemblies/CompExtraSounds.dll b/1.4/Assemblies/CompExtraSounds.dll
index bbef5229..d4c352c9 100644
Binary files a/1.4/Assemblies/CompExtraSounds.dll and b/1.4/Assemblies/CompExtraSounds.dll differ
diff --git a/1.4/Assemblies/CompInstalledPart.dll b/1.4/Assemblies/CompInstalledPart.dll
index 5b6d60f4..0823cc02 100644
Binary files a/1.4/Assemblies/CompInstalledPart.dll and b/1.4/Assemblies/CompInstalledPart.dll differ
diff --git a/1.4/Assemblies/CompLumbering.dll b/1.4/Assemblies/CompLumbering.dll
index 38a1c8c4..ed7dcc57 100644
Binary files a/1.4/Assemblies/CompLumbering.dll and b/1.4/Assemblies/CompLumbering.dll differ
diff --git a/1.4/Assemblies/CompOverlays.dll b/1.4/Assemblies/CompOverlays.dll
index 8c3208a1..e0a3d2dd 100644
Binary files a/1.4/Assemblies/CompOverlays.dll and b/1.4/Assemblies/CompOverlays.dll differ
diff --git a/1.4/Assemblies/CompOversizedWeapon.dll b/1.4/Assemblies/CompOversizedWeapon.dll
index dc273e84..887bcb14 100644
Binary files a/1.4/Assemblies/CompOversizedWeapon.dll and b/1.4/Assemblies/CompOversizedWeapon.dll differ
diff --git a/1.4/Assemblies/CompSlotLoadable.dll b/1.4/Assemblies/CompSlotLoadable.dll
index 9b3fae54..9df18404 100644
Binary files a/1.4/Assemblies/CompSlotLoadable.dll and b/1.4/Assemblies/CompSlotLoadable.dll differ
diff --git a/1.4/Assemblies/CompToggleDef.dll b/1.4/Assemblies/CompToggleDef.dll
index a9146861..25956f33 100644
Binary files a/1.4/Assemblies/CompToggleDef.dll and b/1.4/Assemblies/CompToggleDef.dll differ
diff --git a/1.4/Assemblies/PawnShields.dll b/1.4/Assemblies/PawnShields.dll
index 680c59b6..f27bd20b 100644
Binary files a/1.4/Assemblies/PawnShields.dll and b/1.4/Assemblies/PawnShields.dll differ
diff --git a/1.4/Assemblies/ThinkNodes.dll b/1.4/Assemblies/ThinkNodes.dll
index b9c9ec70..22365ada 100644
Binary files a/1.4/Assemblies/ThinkNodes.dll and b/1.4/Assemblies/ThinkNodes.dll differ
diff --git a/Source/AllModdingComponents/JecsTools/ApparelExtension.cs b/Source/AllModdingComponents/JecsTools/ApparelExtension/ApparelExtension.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/ApparelExtension.cs
rename to Source/AllModdingComponents/JecsTools/ApparelExtension/ApparelExtension.cs
diff --git a/Source/AllModdingComponents/JecsTools/ApparelExtension/HarmonyPatches_ApparelExtension.cs b/Source/AllModdingComponents/JecsTools/ApparelExtension/HarmonyPatches_ApparelExtension.cs
new file mode 100644
index 00000000..f600b781
--- /dev/null
+++ b/Source/AllModdingComponents/JecsTools/ApparelExtension/HarmonyPatches_ApparelExtension.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using HarmonyLib;
+using RimWorld;
+using UnityEngine;
+using Verse;
+
+namespace JecsTools;
+
+public static partial class HarmonyPatches
+{
+ public static void HarmonyPatches_ApparelExtension(Harmony harmony, Type type)
+ {
+ //Checks apparel that uses the ApparelExtension
+ harmony.Patch(AccessTools.Method(typeof(ApparelUtility), nameof(ApparelUtility.CanWearTogether)),
+ postfix: new HarmonyMethod(type, nameof(Post_CanWearTogether)));
+
+ //Handles cases where gendered apparel swaps out for individual genders.
+ harmony.Patch(AccessTools.Method(typeof(PawnApparelGenerator), nameof(PawnApparelGenerator.GenerateStartingApparelFor)),
+ postfix: new HarmonyMethod(type, nameof(GenerateStartingApparelFor_PostFix)));
+ }
+
+
+ //PawnApparelGenerator
+ public static void GenerateStartingApparelFor_PostFix(Pawn pawn)
+ {
+ var allWornApparel = pawn.apparel?.WornApparel;
+ if (allWornApparel.NullOrEmpty())
+ return;
+ List<(Apparel, Apparel)> swapEntries = null;
+ foreach (var wornApparel in allWornApparel)
+ {
+ if (wornApparel.def?.GetApparelExtension()?.swapCondition is SwapCondition sc &&
+ sc.swapWhenGender is Gender gen &&
+ gen != Gender.None && gen == pawn.gender)
+ {
+ var swapApparel = (Apparel)ThingMaker.MakeThing(sc.swapTo, wornApparel.Stuff);
+ // Avoid modifying WornApparel during its enumeration by doing the swaps afterwards.
+ swapEntries ??= new List<(Apparel worn, Apparel swap)>();
+ swapEntries.Add((wornApparel, swapApparel));
+ }
+ }
+ if (swapEntries != null)
+ {
+ foreach (var (wornApparel, swapApparel) in swapEntries)
+ {
+ PawnGenerator.PostProcessGeneratedGear(swapApparel, pawn);
+ if (ApparelUtility.HasPartsToWear(pawn, swapApparel.def))
+ {
+ pawn.apparel.Wear(swapApparel, false);
+ DebugMessage($"apparel generation for {pawn}: swapped from {wornApparel} to {swapApparel}");
+ }
+ wornApparel.Destroy();
+ DebugMessage($"apparel generation for {pawn}: destroyed old {wornApparel}");
+ }
+ }
+ }
+
+ ///
+ /// Using the new ApparelExtension, we can have a string based apparel check.
+ ///
+ public static void Post_CanWearTogether(ThingDef A, ThingDef B, BodyDef body, ref bool __result)
+ {
+ static HashSet GetCoverage(ThingDef thingDef)
+ {
+ var coverage = thingDef.GetApparelExtension()?.Coverage;
+ return coverage == null || coverage.Count == 0 ? null : coverage;
+ }
+
+ if (A == null || B == null || body == null || __result == true)
+ return;
+ var coverageA = GetCoverage(A);
+ var coverageB = GetCoverage(B);
+ if (coverageA != null && coverageB != null)
+ {
+ foreach (var coverageItem in coverageB)
+ {
+ if (coverageA.Contains(coverageItem))
+ {
+ __result = false;
+ break;
+ }
+ }
+ }
+ else if ((coverageA != null && coverageB == null) || (coverageA == null && coverageB != null))
+ {
+ __result = true;
+ }
+ }
+}
diff --git a/Source/AllModdingComponents/JecsTools/Backstories/BackstoryDef.cs b/Source/AllModdingComponents/JecsTools/Backstories/BackstoryDef.cs
index af749a1d..45e911bb 100644
--- a/Source/AllModdingComponents/JecsTools/Backstories/BackstoryDef.cs
+++ b/Source/AllModdingComponents/JecsTools/Backstories/BackstoryDef.cs
@@ -12,7 +12,6 @@ namespace JecsTools
//Link -> https://github.com/RimWorld-CCL-Reborn/AlienRaces/blob/94bf6b6d7a91e9587bdc40e8a231b18515cb6bb7/Source/AlienRace/AlienRace/BackstoryDef.cs
public class BackstoryDef : RimWorld.BackstoryDef
{
-
public static HashSet checkBodyType = new HashSet();
public List forcedTraitsChance = new List();
@@ -27,21 +26,33 @@ public class BackstoryDef : RimWorld.BackstoryDef
public IntRange chronoAgeRange;
public List forcedItems = new List();
- public bool CommonalityApproved(Gender g) => Rand.Range(min: 0, max: 100) < (g == Gender.Female ? this.femaleCommonality : this.maleCommonality);
+ public bool CommonalityApproved(Gender g) => Rand.Range(min: 0, max: 100) <
+ (g == Gender.Female
+ ? this.femaleCommonality
+ : this.maleCommonality);
public bool Approved(Pawn p) => this.CommonalityApproved(p.gender) &&
- (this.bioAgeRange == default || (this.bioAgeRange.min < p.ageTracker.AgeBiologicalYears && p.ageTracker.AgeBiologicalYears < this.bioAgeRange.max)) &&
- (this.chronoAgeRange == default || (this.chronoAgeRange.min < p.ageTracker.AgeChronologicalYears && p.ageTracker.AgeChronologicalYears < this.chronoAgeRange.max));
+ (this.bioAgeRange == default ||
+ (this.bioAgeRange.min < p.ageTracker.AgeBiologicalYears &&
+ p.ageTracker.AgeBiologicalYears < this.bioAgeRange.max)) &&
+ (this.chronoAgeRange == default ||
+ (this.chronoAgeRange.min < p.ageTracker.AgeChronologicalYears &&
+ p.ageTracker.AgeChronologicalYears < this.chronoAgeRange.max));
public override void ResolveReferences()
{
this.identifier = this.defName;
base.ResolveReferences();
- this.forcedTraits = (this.forcedTraits ??= new List()).
- Concat(this.forcedTraitsChance.Where(predicate: trait => Rand.Range(min: 0, max: 100) < trait.chance).ToList().ConvertAll(converter: trait => new BackstoryTrait { def = trait.defName, degree = trait.degree })).ToList();
- this.disallowedTraits = (this.disallowedTraits ??= new List()).
- Concat(this.disallowedTraitsChance.Where(predicate: trait => Rand.Range(min: 0, max: 100) < trait.chance).ToList().ConvertAll(converter: trait => new BackstoryTrait { def = trait.defName, degree = trait.degree })).ToList();
+ this.forcedTraits = (this.forcedTraits ??= new List()).Concat(this.forcedTraitsChance
+ .Where(predicate: trait => Rand.Range(min: 0, max: 100) < trait.chance).ToList()
+ .ConvertAll(converter: trait => new BackstoryTrait { def = trait.defName, degree = trait.degree }))
+ .ToList();
+ this.disallowedTraits = (this.disallowedTraits ??= new List()).Concat(this
+ .disallowedTraitsChance.Where(predicate: trait => Rand.Range(min: 0, max: 100) < trait.chance)
+ .ToList()
+ .ConvertAll(converter: trait => new BackstoryTrait { def = trait.defName, degree = trait.degree }))
+ .ToList();
this.workDisables = (this.workAllows & WorkTags.AllWork) != 0 ? this.workDisables : ~this.workAllows;
if (this.bodyTypeGlobal == null && this.bodyTypeFemale == null && this.bodyTypeMale == null)
diff --git a/Source/AllModdingComponents/JecsTools/BuildingExtension.cs b/Source/AllModdingComponents/JecsTools/BuildingExtension/BuildingExtension.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/BuildingExtension.cs
rename to Source/AllModdingComponents/JecsTools/BuildingExtension/BuildingExtension.cs
diff --git a/Source/AllModdingComponents/JecsTools/BuildingExtension/HarmonyPatches_BuildingExtension.cs b/Source/AllModdingComponents/JecsTools/BuildingExtension/HarmonyPatches_BuildingExtension.cs
new file mode 100644
index 00000000..f4a293d6
--- /dev/null
+++ b/Source/AllModdingComponents/JecsTools/BuildingExtension/HarmonyPatches_BuildingExtension.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Collections.Generic;
+using HarmonyLib;
+using RimWorld;
+using UnityEngine;
+using Verse;
+
+namespace JecsTools;
+
+public static partial class HarmonyPatches
+{
+ public static void HarmonyPatches_BuildingExtension(Harmony harmony, Type type)
+ {
+ //BuildingExtension prevents some things from wiping other things when spawned/constructing/blueprinted.
+ harmony.Patch(AccessTools.Method(typeof(GenSpawn), nameof(GenSpawn.SpawningWipes)),
+ postfix: new HarmonyMethod(type, nameof(SpawningWipes_PostFix)));
+ harmony.Patch(AccessTools.Method(typeof(GenConstruct), nameof(GenConstruct.CanPlaceBlueprintOver)),
+ postfix: new HarmonyMethod(type, nameof(CanPlaceBlueprintOver_PostFix)));
+
+ //Ignores all structures as part of objects that disallow being fired through.
+ harmony.Patch(AccessTools.Method(typeof(Projectile), "CanHit"),
+ postfix: new HarmonyMethod(type, nameof(CanHit_PostFix)));
+
+ //Allows a bullet to pass through walls when fired.
+ harmony.Patch(AccessTools.Method(typeof(Verb), "CanHitCellFromCellIgnoringRange"),
+ prefix: new HarmonyMethod(type, nameof(CanHitCellFromCellIgnoringRange_Prefix)));
+ }
+
+
+ public static void SpawningWipes_PostFix(BuildableDef newEntDef, BuildableDef oldEntDef, ref bool __result)
+ {
+ // If SpawningWipes is already returning true, don't need to do anything.
+ if (__result == false && newEntDef is ThingDef newDef && oldEntDef is ThingDef oldDef)
+ {
+ if (HasSharedWipeCategory(newDef, oldDef))
+ __result = true;
+ }
+ }
+
+
+ public static void CanPlaceBlueprintOver_PostFix(BuildableDef newDef, ThingDef oldDef, ref bool __result)
+ {
+ // If CanPlaceBlueprintOver is already returning false, don't need to do anything.
+ if (__result == true && newDef is ThingDef thingDef)
+ {
+ if (HasSharedWipeCategory(thingDef, oldDef))
+ __result = false;
+ }
+ }
+
+ //Check wipe categories on BuildingExtension between two defs
+ private static bool HasSharedWipeCategory(ThingDef newDef, ThingDef oldDef)
+ {
+ static HashSet GetWipeCategories(ThingDef thingDef)
+ {
+ var buildingExtension = GenConstruct.BuiltDefOf(thingDef)?.GetBuildingExtension();
+ if (buildingExtension == null)
+ return null;
+ var wipeCategorySet = buildingExtension.WipeCategories;
+ return wipeCategorySet == null || wipeCategorySet.Count == 0 ? null : wipeCategorySet;
+ }
+
+ var wipeCategoriesA = GetWipeCategories(newDef);
+ DebugMessage($"{newDef} wipeCategoriesA: {wipeCategoriesA.ToStringSafeEnumerable()}");
+ var wipeCategoriesB = GetWipeCategories(oldDef);
+ DebugMessage($"{oldDef} wipeCategoriesB: {wipeCategoriesB.ToStringSafeEnumerable()}");
+ if (wipeCategoriesB == null && wipeCategoriesA == null)
+ {
+ DebugMessage("both wipeCategories null => false");
+ return false;
+ }
+ else if (wipeCategoriesA != null && wipeCategoriesB == null)
+ {
+ DebugMessage("wipeCategoriesB null => false");
+ return false;
+ }
+ else if (wipeCategoriesB != null && wipeCategoriesA == null)
+ {
+ DebugMessage("wipeCategoriesA null => false");
+ return false;
+ }
+ else
+ {
+ foreach (var strB in wipeCategoriesB)
+ {
+ if (wipeCategoriesA.Contains(strB))
+ {
+ DebugMessage($"found shared wipeCategories ({strB}) => true");
+ return true;
+ }
+ }
+ DebugMessage("no shared wipeCategories => false");
+ return false;
+ }
+ }
+
+
+
+ //Added B19, Oct 2019
+ //ProjectileExtension check
+ //Ignores all structures as part of objects that disallow being fired through.
+ public static void CanHit_PostFix(Projectile __instance, Thing thing, ref bool __result)
+ {
+ // TODO: This patch looks pointless since it can only change __result from false to ... false.
+ if (__result == false && __instance.def?.GetProjectileExtension() is ProjectileExtension ext)
+ {
+ if (ext.passesWalls)
+ {
+ //Mods will often have their own walls, so we cannot do a def check for ThingDefOf.Wall
+ //Most "walls" should either be in the structure category or be able to hold walls.
+ // TODO: In RW 1.3+, it seems like BuildingProperties.isPlaceOverableWall indicates whether something is a "wall",
+ // but it may be better to just look at ThingDef.Fillage/fillPercent instead,
+ // or maybe use PlaceWorker_OnTopOfWalls's heuristic of checking whether the defName contains "Wall"?
+ if (thing?.def is ThingDef def && (def.designationCategory == DesignationCategoryDefOf.Structure || def.holdsRoof))
+ {
+ __result = false;
+ return;
+ }
+ }
+ }
+ }
+
+
+ //Added B19, Oct 2019
+ //ProjectileExtension check
+ //Allows a bullet to pass through walls when fired.
+ public static bool CanHitCellFromCellIgnoringRange_Prefix(Verb __instance, ref bool __result)
+ {
+ if (__instance.EquipmentCompSource?.PrimaryVerb?.verbProps?.defaultProjectile?.GetProjectileExtension() is ProjectileExtension ext)
+ {
+ if (ext.passesWalls)
+ {
+ // TODO: While this does bypass the line-of-sight checks (and should it really bypass all LOS checks?),
+ // this also bypasses non-LOS checks, which doesn't look right.
+ __result = true;
+ }
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/Source/AllModdingComponents/JecsTools/PlaceWorker_OnTopOfWalls.cs b/Source/AllModdingComponents/JecsTools/BuildingExtension/PlaceWorker_OnTopOfWalls.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/PlaceWorker_OnTopOfWalls.cs
rename to Source/AllModdingComponents/JecsTools/BuildingExtension/PlaceWorker_OnTopOfWalls.cs
diff --git a/Source/AllModdingComponents/JecsTools/PlaceWorker_Outline.cs b/Source/AllModdingComponents/JecsTools/BuildingExtension/PlaceWorker_Outline.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/PlaceWorker_Outline.cs
rename to Source/AllModdingComponents/JecsTools/BuildingExtension/PlaceWorker_Outline.cs
diff --git a/Source/AllModdingComponents/JecsTools/PlaceWorker_UnderCeiling.cs b/Source/AllModdingComponents/JecsTools/BuildingExtension/PlaceWorker_UnderCeiling.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/PlaceWorker_UnderCeiling.cs
rename to Source/AllModdingComponents/JecsTools/BuildingExtension/PlaceWorker_UnderCeiling.cs
diff --git a/Source/AllModdingComponents/JecsTools/DamageSoak/HarmonyPatches_DamageSoak.cs b/Source/AllModdingComponents/JecsTools/DamageSoak/HarmonyPatches_DamageSoak.cs
new file mode 100644
index 00000000..3620bfb1
--- /dev/null
+++ b/Source/AllModdingComponents/JecsTools/DamageSoak/HarmonyPatches_DamageSoak.cs
@@ -0,0 +1,268 @@
+using System;
+using HarmonyLib;
+using RimWorld;
+using UnityEngine;
+using Verse;
+
+namespace JecsTools;
+
+public static partial class HarmonyPatches
+{
+
+ public static float? tempDamageAmount = null;
+ public static float? tempDamageAbsorbed = null;
+
+ public static void HarmonyPatches_DamageSoak(Harmony harmony, Type type)
+ {
+ //Allow fortitude (HediffComp_DamageSoak) to soak damage
+ //Adds HediffCompProperties_DamageSoak checks to damage
+ harmony.Patch(AccessTools.Method(typeof(Pawn_HealthTracker), nameof(Pawn_HealthTracker.PreApplyDamage)),
+ prefix: new HarmonyMethod(type, nameof(HarmonyPatches.PreApplyDamage_PrePatch)));
+ //Applies cached armor damage and absorption
+ harmony.Patch(AccessTools.Method(typeof(ArmorUtility), "ApplyArmor"),
+ prefix: new HarmonyMethod(type, nameof(HarmonyPatches.Pre_ApplyArmor)));
+ //Applies damage soak motes
+ harmony.Patch(AccessTools.Method(typeof(ArmorUtility), nameof(ArmorUtility.GetPostArmorDamage)),
+ postfix: new HarmonyMethod(type, nameof(HarmonyPatches.Post_GetPostArmorDamage)));
+ }
+
+ public static bool PreApplyDamage_PrePatch(Pawn ___pawn, ref DamageInfo dinfo, out bool absorbed)
+ {
+ DebugMessage($"c6c:: === Enter Harmony Prefix --- PreApplyDamage_PrePatch for {___pawn} and {dinfo} ===");
+ if (___pawn != null)
+ {
+ DebugMessage("c6c:: Pawn exists.");
+ var hediffSet = ___pawn.health.hediffSet;
+ if (hediffSet.hediffs.Count > 0)
+ {
+ DebugMessage("c6c:: Pawn has hediffs.");
+ // See above ArmorUtility comments.
+ if (PreApplyDamage_ApplyDamageSoakers(ref dinfo, hediffSet, ___pawn))
+ {
+ DebugMessage($"c6c:: === Exit Harmony Prefix --- PreApplyDamage_PrePatch for {___pawn} and {dinfo} ===");
+ absorbed = true;
+ return false;
+ }
+ }
+ }
+
+ // TODO: tempDamageAmount shouldn't be set if there are no damage soaks.
+ tempDamageAmount = dinfo.Amount;
+ DebugMessage($"c6c:: tempDamageAmount <= {tempDamageAmount}");
+ absorbed = false;
+ DebugMessage($"c6c:: === Exit Harmony Prefix --- PreApplyDamage_PrePatch for {___pawn} and {dinfo} ===");
+ return true;
+ }
+
+
+
+ private static void DamageSoakedMote(Pawn pawn, float soakedDamage)
+ {
+ if (soakedDamage > 0f && pawn != null && pawn.Spawned && pawn.MapHeld != null &&
+ pawn.DrawPos is Vector3 drawVecDos && drawVecDos.InBounds(pawn.MapHeld))
+ {
+ // To avoid any rounding bias, use RoundRandom for converting int to float.
+ var roundedSoakedDamage = GenMath.RoundRandom(soakedDamage);
+ DebugMessage($"c6c:: DamageSoakedMote for {pawn}: {soakedDamage} rounded to {roundedSoakedDamage}");
+ MoteMaker.ThrowText(drawVecDos, pawn.MapHeld, "JT_DamageSoaked".Translate(roundedSoakedDamage));
+ }
+ }
+
+ private static bool PreApplyDamage_ApplyDamageSoakers(ref DamageInfo dinfo, HediffSet hediffSet, Pawn pawn)
+ {
+ // Multiple damage soak hediff comps stack.
+ DebugMessage($"c6c:: --- Enter PreApplyDamage_ApplyDamageSoakers for {pawn} and {dinfo} ---");
+ var damageDef = dinfo.Def;
+ var totalSoakedDamage = 0f;
+ foreach (var hediffComp in hediffSet.GetAllComps())
+ {
+ if (!(hediffComp is HediffComp_DamageSoak damageSoakComp))
+ continue;
+ DebugMessage("c6c:: Soak Damage Hediff checked.");
+
+ var soakProps = damageSoakComp.Props;
+ if (soakProps == null)
+ {
+ DebugMessage("c6c:: Soak Damage Hediff has no damage soak XML properties.");
+ continue;
+ }
+ if (soakProps.settings.NullOrEmpty())
+ {
+ DebugMessage("c6c:: Soak Damage Hediff has no damage soak settings.");
+
+ // Null, here, means "all damage types", so null should pass this check.
+ if (soakProps.damageType != null && soakProps.damageType != damageDef)
+ {
+ DebugMessage($"c6c:: {damageDef.label.CapitalizeFirst()} is not in soak settings.");
+ continue;
+ }
+
+ if (soakProps.damageTypesToExclude != null &&
+ soakProps.damageTypesToExclude.Contains(damageDef))
+ {
+ DebugMessage($"c6c:: {damageDef.label.CapitalizeFirst()} is to be excluded from damage soak.");
+ continue;
+ }
+
+ var dmgAmount = dinfo.Amount;
+ var soakedDamage = Mathf.Min(soakProps.damageToSoak, dmgAmount);
+ DebugMessage($"c6c:: Soaked: Min({soakProps.damageToSoak}, {dinfo.Amount}) => {soakedDamage}");
+ dmgAmount -= soakedDamage;
+ DebugMessage($"c6c:: Damage amount: {dinfo.Amount} - {soakedDamage} => {dmgAmount}");
+ totalSoakedDamage += soakedDamage;
+ DebugMessage($"c6c:: Total soaked: {totalSoakedDamage}");
+ dinfo.SetAmount(dmgAmount);
+
+ if (dinfo.Amount > 0)
+ {
+ DebugMessage($"c6c:: More damage exists. Continuing check for soakers.");
+ continue;
+ }
+
+ DamageSoakedMote(pawn, totalSoakedDamage);
+ DebugMessage($"c6c:: Damage absorbed.");
+ DebugMessage($"c6c:: FINAL RESULT -- Total soaked: {totalSoakedDamage}, damage amount: {dinfo.Amount}.");
+ DebugMessage($"c6c:: --- Exit PreApplyDamage_ApplyDamageSoakers for {pawn} and {dinfo} ---");
+ return true;
+ }
+ else
+ {
+ DebugMessage("c6c:: Soak Damage Hediff has damage soak settings.");
+ foreach (var soakSettings in soakProps.settings)
+ {
+ DebugMessage($"c6c:: Hediff Damage: {damageDef}");
+ if (soakSettings.damageType != null)
+ DebugMessage($"c6c:: Soak Type: {soakSettings.damageType}");
+ else
+ DebugMessage($"c6c:: Soak Type: All");
+
+ //Null, here, means "all damage types"
+ //So Null should pass this check.
+ if (soakSettings.damageType != null && soakSettings.damageType != damageDef)
+ {
+ DebugMessage($"c6c:: No match. No soak.");
+ continue;
+ }
+
+ // This variable tracks whether the damage should be excluded by a damageTypesToExclude
+ // rule for breaking out of a nested for loop without using goto
+ bool damageExcluded = false;
+ if (!soakSettings.damageTypesToExclude.NullOrEmpty())
+ {
+ DebugMessage($"c6c:: Damage Soak Exlusions: ");
+ foreach (var exclusion in soakSettings.damageTypesToExclude)
+ {
+ DebugMessage($"c6c:: {exclusion}");
+ if (exclusion == damageDef)
+ {
+ DebugMessage($"c6c:: Exclusion match. Damage soak aborted.");
+ damageExcluded = true;
+ break;
+ }
+ }
+ if (damageExcluded)
+ continue;
+ }
+
+ var dmgAmount = dinfo.Amount;
+ var soakedDamage = Mathf.Min(soakSettings.damageToSoak, dmgAmount);
+ DebugMessage($"c6c:: Soaked: Min({soakSettings.damageToSoak}, {dinfo.Amount}) => {soakedDamage}");
+ dmgAmount -= soakedDamage;
+ DebugMessage($"c6c:: Damage amount: {dinfo.Amount} - {soakedDamage} => {dmgAmount}");
+ totalSoakedDamage += soakedDamage;
+ DebugMessage($"c6c:: Total soaked: {totalSoakedDamage}");
+ dinfo.SetAmount(dmgAmount);
+
+ if (dinfo.Amount > 0)
+ {
+ DebugMessage($"c6c:: Unsoaked damage remains. Checking for more soakers.");
+ continue;
+ }
+
+ DamageSoakedMote(pawn, totalSoakedDamage);
+ DebugMessage($"c6c:: Damage absorbed.");
+ DebugMessage($"c6c:: FINAL RESULT -- Total soaked: {totalSoakedDamage}, damage amount: {dinfo.Amount}.");
+ DebugMessage($"c6c:: --- Exit PreApplyDamage_ApplyDamageSoakers for {pawn} and {dinfo} ---");
+ return true;
+ }
+ }
+ }
+ if (totalSoakedDamage > 0)
+ {
+ DamageSoakedMote(pawn, totalSoakedDamage);
+ DebugMessage($"c6c:: FINAL RESULT -- Total soaked: {totalSoakedDamage}, damage amount: {dinfo.Amount}.");
+ }
+ DebugMessage($"c6c:: --- Exit PreApplyDamage_ApplyDamageSoakers for {pawn} and {dinfo} ---");
+ return false;
+ }
+
+ // ArmorUtility patches:
+ // These are a workaround for PreApplyDamage_PrePatch changes to the dinfo struct not being saved, due to
+ // Pawn_HealthTracker.PreApplyDamage dinfo parameter being passed by value (PreApplyDamage_PrePatch has it passed
+ // by reference, but this only affects the patch; Pawn_HealthTracker.PreApplyDamage still has it passed by value).
+ // Incidentally, these patches have another purpose: it allows other Pawn_HealthTracker.PreApplyDamage code like
+ // Apparel.CheckPreAbsorbDamage (like shield belts), various pawn-specific notifications affecting pawn behavior,
+ // and other mod's patches on the method to run, some of which could affect dinfo.Amount and absorbed flag.
+ // Indeed, the choice of prefix patching Pawn_HealthTracker.PreApplyDamage rather than a Pawn.PreApplyDamage prefix
+ // or a Pawn_HealthTracker.PreApplyDamage postfix is likely a compromise to allow as much change to dinfo as
+ // possible yet still apply damage soaks before shield belt absorption.
+ // Pawn_HealthTracker.PreApplyDamage notification specifics: if it runs (no ThingComp.PostPreApplyDamage sets
+ // absorbed flag), prisoner guilt, AI updates, and current danger are triggered. If no Apparel.CheckPreAbsorbDamage
+ // sets the absorbed flag, stun effects, pawn thought/memory, and tale recording are triggered.
+ // XXX: I do not think this patch is reliable because:
+ // 1) It's not guaranteed to run under certain conditions (e.g. if dinfo.IgnoreArmor) when it should.
+ // 2) dinfo.Amount can be divided into multiple DamageInfos under certain conditions (bomb/flame damage),
+ // which this doesn't take into account.
+ // 3) It assumes that all new damage amount since our PreApplyDamage_PrePatch ran should be damage soaked
+ // (as long as this patch runs, e.g. not absorbed, etc.), by setting the damage amount back to tempDamageAmount,
+ // the final damage amount recorded in PreApplyDamage_PrePatch, even if no damage soaks exist
+ // (see TODO in PreApplyDamage_PrePatch).
+ // 4) If damage amount decreased yet still non-zero since our PreApplyDamage_PrePatch ran, this patch will
+ // increase the damage amount back to tempDamageAmount, which is the total opposite of damage soaking.
+ // 5) The relationship of PreApplyDamage_PrePatch and this patch with respect to tempDamageAmount is fragile,
+ // especially since (1) and tempDamageAmount not always being set in PreApplyDamage_PrePatch.
+ // If another mod happens to use ArmorUtility without going through PreApplyDamage, this scheme will break.
+ // TODO:
+ // If we want to retain damage soaking before shield belt absorption:
+ // Instead of this patch, postfix patch (highest patch priority) Pawn.PreApplyDamage to update the original
+ // dinfo struct with any changes from PreApplyDamage_PrePatch. Make PreApplyDamage_PrePatch patch with lowest
+ // patch priority so that it runs right before Pawn_HealthTracker.PreApplyDamage. This should ensure that there
+ // no other changes to dinfo in between PreApplyDamage_PrePatch and the new Pawn.PreApplyDamage postfix patch
+ // that should've been tracked. tempDamageAmount is still needed to to transfer the damage amount info between
+ // these patches.
+ // If we're fine with damage soaks applying after shield belt absorption:
+ // Simplify into a single Pawn.PreApplyDamage postfix patch.
+ public static void Pre_ApplyArmor(ref float damAmount, Pawn pawn)
+ {
+ if (tempDamageAmount != null && damAmount > 0f)
+ {
+ var damageDiff = Mathf.Max(damAmount - tempDamageAmount.Value, 0f);
+ var newDamAmount = GenMath.RoundRandom(tempDamageAmount.Value);
+ DebugMessage($"c6c:: ApplyArmor prefix on {pawn}: tempDamageAmount {tempDamageAmount} => null, damAmount {damAmount} => {newDamAmount}");
+ damAmount = newDamAmount;
+ tempDamageAmount = null;
+ if (damageDiff > 0f)
+ tempDamageAbsorbed = damageDiff;
+ }
+ }
+
+
+
+ // XXX: Damage soak mote is already emitted in PreApplyDamage_ApplyDamageSoakers, so this leads to a misleading
+ // redundant soak mote. Worse, if the damage amount actually changes between PreApplyDamage_ApplyDamageSoakers
+ // and Pre_ApplyArmor, leading to a tempDamageAbsorbed that's different from PreApplyDamage_ApplyDamageSoakers's
+ // totalSoakedDamage, this is even more misleading.
+ public static void Post_GetPostArmorDamage(Pawn pawn)
+ {
+ if (tempDamageAbsorbed != null)
+ {
+ DebugMessage($"c6c:: GetPostArmorDamage postfix on {pawn}: tempDamageAbsorbed {tempDamageAbsorbed}");
+ if (pawn.GetHediffComp() != null)
+ {
+ DamageSoakedMote(pawn, tempDamageAbsorbed.Value);
+ }
+
+ tempDamageAbsorbed = null;
+ }
+ }
+}
diff --git a/Source/AllModdingComponents/JecsTools/HediffCompProperties_DamageSoak.cs b/Source/AllModdingComponents/JecsTools/DamageSoak/HediffCompProperties_DamageSoak.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/HediffCompProperties_DamageSoak.cs
rename to Source/AllModdingComponents/JecsTools/DamageSoak/HediffCompProperties_DamageSoak.cs
diff --git a/Source/AllModdingComponents/JecsTools/HediffComp_DamageSoak.cs b/Source/AllModdingComponents/JecsTools/DamageSoak/HediffComp_DamageSoak.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/HediffComp_DamageSoak.cs
rename to Source/AllModdingComponents/JecsTools/DamageSoak/HediffComp_DamageSoak.cs
diff --git a/Source/AllModdingComponents/JecsTools/DamageDefCleave.cs b/Source/AllModdingComponents/JecsTools/ExtraMeleeDamages/DamageDefCleave.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/DamageDefCleave.cs
rename to Source/AllModdingComponents/JecsTools/ExtraMeleeDamages/DamageDefCleave.cs
diff --git a/Source/AllModdingComponents/JecsTools/DamageWorker_Cleave.cs b/Source/AllModdingComponents/JecsTools/ExtraMeleeDamages/DamageWorker_Cleave.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/DamageWorker_Cleave.cs
rename to Source/AllModdingComponents/JecsTools/ExtraMeleeDamages/DamageWorker_Cleave.cs
diff --git a/Source/AllModdingComponents/JecsTools/ExtraMeleeDamages/HarmonyPatches_ExtraMeleeDamages.cs b/Source/AllModdingComponents/JecsTools/ExtraMeleeDamages/HarmonyPatches_ExtraMeleeDamages.cs
new file mode 100644
index 00000000..42f4373b
--- /dev/null
+++ b/Source/AllModdingComponents/JecsTools/ExtraMeleeDamages/HarmonyPatches_ExtraMeleeDamages.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Reflection.Emit;
+using HarmonyLib;
+using RimWorld;
+using UnityEngine;
+using Verse;
+
+namespace JecsTools;
+
+public static partial class HarmonyPatches
+{
+ public static void HarmonyPatches_ExtraMeleeDamages(Harmony harmony, Type type)
+ {
+ //Applies hediff-based extra damage to melee attacks.
+ harmony.Patch(typeof(Verb_MeleeAttackDamage).FindIteratorMethod("DamageInfosToApply"),
+ transpiler: new HarmonyMethod(type, nameof(Verb_MeleeAttackDamage_DamageInfosToApply_Transpiler)));
+ }
+
+ public static IEnumerable Verb_MeleeAttackDamage_DamageInfosToApply_Transpiler(
+ IEnumerable instructions, MethodBase method, ILGenerator ilGen)
+ {
+ // Transforms following:
+ // if (tool != null && tool.extraMeleeDamages != null)
+ // {
+ // foreach (ExtraDamage extraMeleeDamage in tool.extraMeleeDamages)
+ // ...
+ // }
+ // into:
+ // var extraDamages = DamageInfosToApply_ExtraDamages(this);
+ // if (extraDamages != null)
+ // {
+ // foreach (ExtraDamage extraMeleeDamage in extraDamages)
+ // ...
+ // }
+ // Note: We're actually modifying an iterator method, which delegates all of its logic to a compiler-generated
+ // IEnumerator class with a convoluted FSM with the primary logic in the MoveNext method.
+ // The logic surrounding yields within loops is especially complex, so it's best to just modify what's being
+ // looped over; in this case, that's replacing the tool.extraMeleeDamages with our own enumerable
+ // (along with adjusting the null check conditionals).
+
+ var fieldof_Verb_tool = AccessTools.Field(typeof(Verb), nameof(Verb.tool));
+ var fieldof_Tool_extraMeleeDamages = AccessTools.Field(typeof(Tool), nameof(Tool.extraMeleeDamages));
+ var methodof_List_GetEnumerator =
+ AccessTools.Method(typeof(List), nameof(IEnumerable.GetEnumerator));
+ var instructionList = instructions.AsList();
+ var locals = new Locals(method, ilGen);
+
+ var extraDamagesVar = locals.DeclareLocal>();
+
+ var verbToolFieldNullCheckIndex = instructionList.FindSequenceIndex(
+ locals.IsLdloc,
+ instr => instr.Is(OpCodes.Ldfld, fieldof_Verb_tool),
+ instr => instr.IsBrfalse());
+ var toolExtraDamagesIndex = instructionList.FindIndex(verbToolFieldNullCheckIndex + 3, // after above 3 predicates
+ instr => instr.Is(OpCodes.Ldfld, fieldof_Tool_extraMeleeDamages));
+ var verbToolFieldIndex = verbToolFieldNullCheckIndex + 1;
+ instructionList.SafeReplaceRange(verbToolFieldIndex, toolExtraDamagesIndex + 1 - verbToolFieldIndex, new[]
+ {
+ new CodeInstruction(OpCodes.Call,
+ AccessTools.Method(typeof(HarmonyPatches), nameof(DamageInfosToApply_ExtraDamages))),
+ extraDamagesVar.ToStloc(),
+ extraDamagesVar.ToLdloc(),
+ });
+
+ var verbToolExtraDamagesEnumeratorIndex = instructionList.FindSequenceIndex(verbToolFieldIndex,
+ locals.IsLdloc,
+ instr => instr.Is(OpCodes.Ldfld, fieldof_Verb_tool),
+ instr => instr.Is(OpCodes.Ldfld, fieldof_Tool_extraMeleeDamages),
+ instr => instr.Calls(methodof_List_GetEnumerator));
+ instructionList.SafeReplaceRange(verbToolExtraDamagesEnumeratorIndex, 4, new[] // after above 4 predicates
+ {
+ extraDamagesVar.ToLdloc(),
+ new CodeInstruction(OpCodes.Call,
+ AccessTools.Method(typeof(List), nameof(IEnumerable.GetEnumerator))),
+ });
+
+ return instructionList;
+ }
+
+ [ThreadStatic]
+ private static Dictionary<(Tool, Pawn), List> extraDamageCache;
+
+ // In the above transpiler, this replaces tool.extraMeleeDamages as the foreach loop enumeration target in
+ // Verb_MeleeAttackDamage.DamageInfosToApply.
+ // This must return a List rather than IEnumerator since Tool.extraMeleeDamages is a list.
+ // Specifically, the compiler-generated code calls List.GetEnumerator(), stores it in a
+ // List.Enumerator field in the internal iterator class (necessary for the FSM to work), then explicitly
+ // calls List.Enumerator methods/properties in multiple iterator class methods along with an initobj
+ // rather than ldnull for clearing it (since List.Enumerator is a struct). Essentially, it would be
+ // difficult to replace all this with IEnumerator versions in the above transpiler, we just have this
+ // method return the same type as Tool.extraMeleeDamages: List.
+ // If either tool.extraMeleeDamages and CasterPawn.GetHediffComp().Props.ExtraDamages
+ // are null, we can simply return the other, since both are lists. However, if both are non-null, we cannot simply
+ // return Enumerable.Concat of them both; we need to create a new list that contains both. Since list creation and
+ // getting the hediff extra damages are both relatively expensive operations, we utilize a cache.
+ // This cache is ThreadStatic to be optimized for single-threaded usage yet safe for multithreaded usage.
+ private static List DamageInfosToApply_ExtraDamages(Verb_MeleeAttackDamage verb)
+ {
+ extraDamageCache ??= new Dictionary<(Tool, Pawn), List>();
+ var key = (verb.tool, verb.CasterPawn);
+ if (!extraDamageCache.TryGetValue(key, out var extraDamages))
+ {
+ var toolExtraDamages = key.tool?.extraMeleeDamages;
+ var hediffExtraDamages = key.CasterPawn.GetHediffComp()?.Props?.ExtraDamages;
+ if (toolExtraDamages == null)
+ extraDamages = hediffExtraDamages;
+ else if (hediffExtraDamages == null)
+ extraDamages = toolExtraDamages;
+ else
+ {
+ extraDamages = new List(toolExtraDamages.Count + hediffExtraDamages.Count);
+ extraDamages.AddRange(toolExtraDamages);
+ extraDamages.AddRange(hediffExtraDamages);
+ }
+ DebugMessage($"DamageInfosToApply_ExtraDamages({verb}) => caching for {key}: {extraDamages.Join(ToString)}");
+ extraDamageCache[key] = extraDamages;
+ }
+ return extraDamages;
+ }
+
+
+}
diff --git a/Source/AllModdingComponents/JecsTools/HediffCompDamageOverTime.cs b/Source/AllModdingComponents/JecsTools/ExtraMeleeDamages/HediffCompDamageOverTime.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/HediffCompDamageOverTime.cs
rename to Source/AllModdingComponents/JecsTools/ExtraMeleeDamages/HediffCompDamageOverTime.cs
diff --git a/Source/AllModdingComponents/JecsTools/HediffCompProperties_DamageOverTime.cs b/Source/AllModdingComponents/JecsTools/ExtraMeleeDamages/HediffCompProperties_DamageOverTime.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/HediffCompProperties_DamageOverTime.cs
rename to Source/AllModdingComponents/JecsTools/ExtraMeleeDamages/HediffCompProperties_DamageOverTime.cs
diff --git a/Source/AllModdingComponents/JecsTools/HediffCompProperties_ExtraMeleeDamages.cs b/Source/AllModdingComponents/JecsTools/ExtraMeleeDamages/HediffCompProperties_ExtraMeleeDamages.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/HediffCompProperties_ExtraMeleeDamages.cs
rename to Source/AllModdingComponents/JecsTools/ExtraMeleeDamages/HediffCompProperties_ExtraMeleeDamages.cs
diff --git a/Source/AllModdingComponents/JecsTools/HediffComp_ExtraMeleeDamages.cs b/Source/AllModdingComponents/JecsTools/ExtraMeleeDamages/HediffComp_ExtraMeleeDamages.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/HediffComp_ExtraMeleeDamages.cs
rename to Source/AllModdingComponents/JecsTools/ExtraMeleeDamages/HediffComp_ExtraMeleeDamages.cs
diff --git a/Source/AllModdingComponents/JecsTools/Hediff_InjuryCustomLabel.cs b/Source/AllModdingComponents/JecsTools/ExtraMeleeDamages/Hediff_InjuryCustomLabel.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/Hediff_InjuryCustomLabel.cs
rename to Source/AllModdingComponents/JecsTools/ExtraMeleeDamages/Hediff_InjuryCustomLabel.cs
diff --git a/Source/AllModdingComponents/JecsTools/HarmonyPatches.cs b/Source/AllModdingComponents/JecsTools/HarmonyPatches.cs
deleted file mode 100644
index 08fa21cc..00000000
--- a/Source/AllModdingComponents/JecsTools/HarmonyPatches.cs
+++ /dev/null
@@ -1,744 +0,0 @@
-//#define DEBUGLOG
-
-using System;
-using System.Collections;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Reflection;
-using System.Reflection.Emit;
-using HarmonyLib;
-using RimWorld;
-using UnityEngine;
-using Verse;
-
-namespace JecsTools
-{
- [StaticConstructorOnStartup]
- public static partial class HarmonyPatches
- {
- //For alternating fire on some weapons
- public static Dictionary AlternatingFireTracker = new Dictionary();
-
- public static float? tempDamageAmount = null;
- public static float? tempDamageAbsorbed = null;
-
- static HarmonyPatches()
- {
- var harmony = new Harmony("jecstools.jecrell.main");
- var type = typeof(HarmonyPatches);
-
- //Debug Line
- //------------
- //harmony.Patch(AccessTools.Method(typeof(PawnGroupKindWorker_Normal), nameof(PawnGroupKindWorker_Normal.MinPointsToGenerateAnything)),
- // prefix: new HarmonyMethod(type, nameof(MinPointsTest)));
- //------------
-
- //Applies hediff-based extra damage to melee attacks.
- harmony.Patch(typeof(Verb_MeleeAttackDamage).FindIteratorMethod("DamageInfosToApply"),
- transpiler: new HarmonyMethod(type, nameof(Verb_MeleeAttackDamage_DamageInfosToApply_Transpiler)));
-
- //Allow fortitude (HediffComp_DamageSoak) to soak damage
- //Adds HediffCompProperties_DamageSoak checks to damage
- harmony.Patch(AccessTools.Method(typeof(Pawn_HealthTracker), nameof(Pawn_HealthTracker.PreApplyDamage)),
- prefix: new HarmonyMethod(type, nameof(PreApplyDamage_PrePatch)));
- //Applies cached armor damage and absorption
- harmony.Patch(AccessTools.Method(typeof(ArmorUtility), "ApplyArmor"),
- prefix: new HarmonyMethod(type, nameof(Pre_ApplyArmor)));
- //Applies damage soak motes
- harmony.Patch(AccessTools.Method(typeof(ArmorUtility), nameof(ArmorUtility.GetPostArmorDamage)),
- postfix: new HarmonyMethod(type, nameof(Post_GetPostArmorDamage)));
-
- //Applies knockback
- harmony.Patch(AccessTools.Method(typeof(Pawn), nameof(Pawn.PreApplyDamage)),
- prefix: new HarmonyMethod(type, nameof(Pawn_PreApplyDamage_Prefix)) { priority = Priority.High },
- postfix: new HarmonyMethod(type, nameof(Pawn_PreApplyDamage_Postfix)) { priority = Priority.Low });
- harmony.Patch(AccessTools.Method(typeof(Scenario), nameof(Scenario.TickScenario)),
- postfix: new HarmonyMethod(type, nameof(Scenario_TickScenario_Postfix)));
-
- //Allows for adding additional HediffSets when characters spawn using the StartWithHediff class.
- harmony.Patch(AccessTools.Method(typeof(PawnGenerator), nameof(PawnGenerator.GeneratePawn),
- new[] { typeof(PawnGenerationRequest) }),
- postfix: new HarmonyMethod(type, nameof(Post_GeneratePawn)));
-
- //Checks apparel that uses the ApparelExtension
- harmony.Patch(AccessTools.Method(typeof(ApparelUtility), nameof(ApparelUtility.CanWearTogether)),
- postfix: new HarmonyMethod(type, nameof(Post_CanWearTogether)));
-
- //Handles cases where gendered apparel swaps out for individual genders.
- harmony.Patch(AccessTools.Method(typeof(PawnApparelGenerator), nameof(PawnApparelGenerator.GenerateStartingApparelFor)),
- postfix: new HarmonyMethod(type, nameof(GenerateStartingApparelFor_PostFix)));
-
- //BuildingExtension prevents some things from wiping other things when spawned/constructing/blueprinted.
- harmony.Patch(AccessTools.Method(typeof(GenSpawn), nameof(GenSpawn.SpawningWipes)),
- postfix: new HarmonyMethod(type, nameof(SpawningWipes_PostFix)));
- harmony.Patch(AccessTools.Method(typeof(GenConstruct), nameof(GenConstruct.CanPlaceBlueprintOver)),
- postfix: new HarmonyMethod(type, nameof(CanPlaceBlueprintOver_PostFix)));
-
- harmony.Patch(AccessTools.Method(typeof(Projectile), "CanHit"),
- postfix: new HarmonyMethod(type, nameof(CanHit_PostFix)));
- harmony.Patch(AccessTools.Method(typeof(Verb), "CanHitCellFromCellIgnoringRange"),
- prefix: new HarmonyMethod(type, nameof(CanHitCellFromCellIgnoringRange_Prefix)));
-
- //Improve DamageInfo.ToString for debugging purposes.
- harmony.Patch(AccessTools.Method(typeof(DamageInfo), nameof(DamageInfo.ToString)),
- postfix: new HarmonyMethod(type, nameof(DamageInfo_ToString_Postfix)));
-
- //optionally use "CutoutComplex" shader for apparel that wants it
- //harmony.Patch(AccessTools.Method(typeof(ApparelGraphicRecordGetter), nameof(ApparelGraphicRecordGetter.TryGetGraphicApparel)),
- // transpiler: new HarmonyMethod(type, nameof(CutOutComplexApparel_Transpiler)));
- }
-
- [Conditional("DEBUGLOG")]
- private static void DebugMessage(string s)
- {
- Log.Message(s);
- }
-
- //Added B19, Oct 2019
- //ProjectileExtension check
- //Allows a bullet to pass through walls when fired.
- public static bool CanHitCellFromCellIgnoringRange_Prefix(Verb __instance, ref bool __result)
- {
- if (__instance.EquipmentCompSource?.PrimaryVerb?.verbProps?.defaultProjectile?.GetProjectileExtension() is ProjectileExtension ext)
- {
- if (ext.passesWalls)
- {
- // TODO: While this does bypass the line-of-sight checks (and should it really bypass all LOS checks?),
- // this also bypasses non-LOS checks, which doesn't look right.
- __result = true;
- }
- return false;
- }
- return true;
- }
-
- //Added B19, Oct 2019
- //ProjectileExtension check
- //Ignores all structures as part of objects that disallow being fired through.
- public static void CanHit_PostFix(Projectile __instance, Thing thing, ref bool __result)
- {
- // TODO: This patch looks pointless since it can only change __result from false to ... false.
- if (__result == false && __instance.def?.GetProjectileExtension() is ProjectileExtension ext)
- {
- if (ext.passesWalls)
- {
- //Mods will often have their own walls, so we cannot do a def check for ThingDefOf.Wall
- //Most "walls" should either be in the structure category or be able to hold walls.
- // TODO: In RW 1.3+, it seems like BuildingProperties.isPlaceOverableWall indicates whether something is a "wall",
- // but it may be better to just look at ThingDef.Fillage/fillPercent instead,
- // or maybe use PlaceWorker_OnTopOfWalls's heuristic of checking whether the defName contains "Wall"?
- if (thing?.def is ThingDef def && (def.designationCategory == DesignationCategoryDefOf.Structure || def.holdsRoof))
- {
- __result = false;
- return;
- }
- }
- }
- }
-
- public static void SpawningWipes_PostFix(BuildableDef newEntDef, BuildableDef oldEntDef, ref bool __result)
- {
- // If SpawningWipes is already returning true, don't need to do anything.
- if (__result == false && newEntDef is ThingDef newDef && oldEntDef is ThingDef oldDef)
- {
- if (HasSharedWipeCategory(newDef, oldDef))
- __result = true;
- }
- }
-
- public static void CanPlaceBlueprintOver_PostFix(BuildableDef newDef, ThingDef oldDef, ref bool __result)
- {
- // If CanPlaceBlueprintOver is already returning false, don't need to do anything.
- if (__result == true && newDef is ThingDef thingDef)
- {
- if (HasSharedWipeCategory(thingDef, oldDef))
- __result = false;
- }
- }
-
- private static bool HasSharedWipeCategory(ThingDef newDef, ThingDef oldDef)
- {
- static HashSet GetWipeCategories(ThingDef thingDef)
- {
- var buildingExtension = GenConstruct.BuiltDefOf(thingDef)?.GetBuildingExtension();
- if (buildingExtension == null)
- return null;
- var wipeCategorySet = buildingExtension.WipeCategories;
- return wipeCategorySet == null || wipeCategorySet.Count == 0 ? null : wipeCategorySet;
- }
-
- var wipeCategoriesA = GetWipeCategories(newDef);
- DebugMessage($"{newDef} wipeCategoriesA: {wipeCategoriesA.ToStringSafeEnumerable()}");
- var wipeCategoriesB = GetWipeCategories(oldDef);
- DebugMessage($"{oldDef} wipeCategoriesB: {wipeCategoriesB.ToStringSafeEnumerable()}");
- if (wipeCategoriesB == null && wipeCategoriesA == null)
- {
- DebugMessage("both wipeCategories null => false");
- return false;
- }
- else if (wipeCategoriesA != null && wipeCategoriesB == null)
- {
- DebugMessage("wipeCategoriesB null => false");
- return false;
- }
- else if (wipeCategoriesB != null && wipeCategoriesA == null)
- {
- DebugMessage("wipeCategoriesA null => false");
- return false;
- }
- else
- {
- foreach (var strB in wipeCategoriesB)
- {
- if (wipeCategoriesA.Contains(strB))
- {
- DebugMessage($"found shared wipeCategories ({strB}) => true");
- return true;
- }
- }
- DebugMessage("no shared wipeCategories => false");
- return false;
- }
- }
-
- //public static void MinPointsTest(PawnGroupMaker groupMaker)
- //{
- // if (!(groupMaker?.options?.Count > 0))
- // {
- // Log.Message("No options available.");
- // return;
- // }
- // foreach (var x in groupMaker.options)
- // {
- // Log.Message(x.kind.defName + " " + x.kind.isFighter.ToString() + " " + x.Cost);
- // }
- //}
-
- //PawnApparelGenerator
- public static void GenerateStartingApparelFor_PostFix(Pawn pawn)
- {
- var allWornApparel = pawn.apparel?.WornApparel;
- if (allWornApparel.NullOrEmpty())
- return;
- List<(Apparel, Apparel)> swapEntries = null;
- foreach (var wornApparel in allWornApparel)
- {
- if (wornApparel.def?.GetApparelExtension()?.swapCondition is SwapCondition sc &&
- sc.swapWhenGender is Gender gen &&
- gen != Gender.None && gen == pawn.gender)
- {
- var swapApparel = (Apparel)ThingMaker.MakeThing(sc.swapTo, wornApparel.Stuff);
- // Avoid modifying WornApparel during its enumeration by doing the swaps afterwards.
- swapEntries ??= new List<(Apparel worn, Apparel swap)>();
- swapEntries.Add((wornApparel, swapApparel));
- }
- }
- if (swapEntries != null)
- {
- foreach (var (wornApparel, swapApparel) in swapEntries)
- {
- PawnGenerator.PostProcessGeneratedGear(swapApparel, pawn);
- if (ApparelUtility.HasPartsToWear(pawn, swapApparel.def))
- {
- pawn.apparel.Wear(swapApparel, false);
- DebugMessage($"apparel generation for {pawn}: swapped from {wornApparel} to {swapApparel}");
- }
- wornApparel.Destroy();
- DebugMessage($"apparel generation for {pawn}: destroyed old {wornApparel}");
- }
- }
- }
-
- ///
- /// Using the new ApparelExtension, we can have a string based apparel check.
- ///
- public static void Post_CanWearTogether(ThingDef A, ThingDef B, BodyDef body, ref bool __result)
- {
- static HashSet GetCoverage(ThingDef thingDef)
- {
- var coverage = thingDef.GetApparelExtension()?.Coverage;
- return coverage == null || coverage.Count == 0 ? null : coverage;
- }
-
- if (A == null || B == null || body == null || __result == true)
- return;
- var coverageA = GetCoverage(A);
- var coverageB = GetCoverage(B);
- if (coverageA != null && coverageB != null)
- {
- foreach (var coverageItem in coverageB)
- {
- if (coverageA.Contains(coverageItem))
- {
- __result = false;
- break;
- }
- }
- }
- else if ((coverageA != null && coverageB == null) || (coverageA == null && coverageB != null))
- {
- __result = true;
- }
- }
-
- public static void Post_GeneratePawn(Pawn __result)
- {
- var hediffGiverSets = __result?.def?.race?.hediffGiverSets;
- if (hediffGiverSets != null)
- {
- foreach (var hediffGiverSet in hediffGiverSets)
- {
- foreach (var hediffGiver in hediffGiverSet.hediffGivers)
- {
- if (hediffGiver is HediffGiver_StartWithHediff hediffGiverStartWithHediff)
- {
- hediffGiverStartWithHediff.GiveHediff(__result);
- // TODO: Should this really only use the first found HediffGiver_StartWithHediff?
- return;
- }
- }
- }
- }
- }
-
- public static IEnumerable Verb_MeleeAttackDamage_DamageInfosToApply_Transpiler(
- IEnumerable instructions, MethodBase method, ILGenerator ilGen)
- {
- // Transforms following:
- // if (tool != null && tool.extraMeleeDamages != null)
- // {
- // foreach (ExtraDamage extraMeleeDamage in tool.extraMeleeDamages)
- // ...
- // }
- // into:
- // var extraDamages = DamageInfosToApply_ExtraDamages(this);
- // if (extraDamages != null)
- // {
- // foreach (ExtraDamage extraMeleeDamage in extraDamages)
- // ...
- // }
- // Note: We're actually modifying an iterator method, which delegates all of its logic to a compiler-generated
- // IEnumerator class with a convoluted FSM with the primary logic in the MoveNext method.
- // The logic surrounding yields within loops is especially complex, so it's best to just modify what's being
- // looped over; in this case, that's replacing the tool.extraMeleeDamages with our own enumerable
- // (along with adjusting the null check conditionals).
-
- var fieldof_Verb_tool = AccessTools.Field(typeof(Verb), nameof(Verb.tool));
- var fieldof_Tool_extraMeleeDamages = AccessTools.Field(typeof(Tool), nameof(Tool.extraMeleeDamages));
- var methodof_List_GetEnumerator =
- AccessTools.Method(typeof(List), nameof(IEnumerable.GetEnumerator));
- var instructionList = instructions.AsList();
- var locals = new Locals(method, ilGen);
-
- var extraDamagesVar = locals.DeclareLocal>();
-
- var verbToolFieldNullCheckIndex = instructionList.FindSequenceIndex(
- locals.IsLdloc,
- instr => instr.Is(OpCodes.Ldfld, fieldof_Verb_tool),
- instr => instr.IsBrfalse());
- var toolExtraDamagesIndex = instructionList.FindIndex(verbToolFieldNullCheckIndex + 3, // after above 3 predicates
- instr => instr.Is(OpCodes.Ldfld, fieldof_Tool_extraMeleeDamages));
- var verbToolFieldIndex = verbToolFieldNullCheckIndex + 1;
- instructionList.SafeReplaceRange(verbToolFieldIndex, toolExtraDamagesIndex + 1 - verbToolFieldIndex, new[]
- {
- new CodeInstruction(OpCodes.Call,
- AccessTools.Method(typeof(HarmonyPatches), nameof(DamageInfosToApply_ExtraDamages))),
- extraDamagesVar.ToStloc(),
- extraDamagesVar.ToLdloc(),
- });
-
- var verbToolExtraDamagesEnumeratorIndex = instructionList.FindSequenceIndex(verbToolFieldIndex,
- locals.IsLdloc,
- instr => instr.Is(OpCodes.Ldfld, fieldof_Verb_tool),
- instr => instr.Is(OpCodes.Ldfld, fieldof_Tool_extraMeleeDamages),
- instr => instr.Calls(methodof_List_GetEnumerator));
- instructionList.SafeReplaceRange(verbToolExtraDamagesEnumeratorIndex, 4, new[] // after above 4 predicates
- {
- extraDamagesVar.ToLdloc(),
- new CodeInstruction(OpCodes.Call,
- AccessTools.Method(typeof(List), nameof(IEnumerable.GetEnumerator))),
- });
-
- return instructionList;
- }
-
- [ThreadStatic]
- private static Dictionary<(Tool, Pawn), List> extraDamageCache;
-
- // In the above transpiler, this replaces tool.extraMeleeDamages as the foreach loop enumeration target in
- // Verb_MeleeAttackDamage.DamageInfosToApply.
- // This must return a List rather than IEnumerator since Tool.extraMeleeDamages is a list.
- // Specifically, the compiler-generated code calls List.GetEnumerator(), stores it in a
- // List.Enumerator field in the internal iterator class (necessary for the FSM to work), then explicitly
- // calls List.Enumerator methods/properties in multiple iterator class methods along with an initobj
- // rather than ldnull for clearing it (since List.Enumerator is a struct). Essentially, it would be
- // difficult to replace all this with IEnumerator versions in the above transpiler, we just have this
- // method return the same type as Tool.extraMeleeDamages: List.
- // If either tool.extraMeleeDamages and CasterPawn.GetHediffComp().Props.ExtraDamages
- // are null, we can simply return the other, since both are lists. However, if both are non-null, we cannot simply
- // return Enumerable.Concat of them both; we need to create a new list that contains both. Since list creation and
- // getting the hediff extra damages are both relatively expensive operations, we utilize a cache.
- // This cache is ThreadStatic to be optimized for single-threaded usage yet safe for multithreaded usage.
- private static List DamageInfosToApply_ExtraDamages(Verb_MeleeAttackDamage verb)
- {
- extraDamageCache ??= new Dictionary<(Tool, Pawn), List>();
- var key = (verb.tool, verb.CasterPawn);
- if (!extraDamageCache.TryGetValue(key, out var extraDamages))
- {
- var toolExtraDamages = key.tool?.extraMeleeDamages;
- var hediffExtraDamages = key.CasterPawn.GetHediffComp()?.Props?.ExtraDamages;
- if (toolExtraDamages == null)
- extraDamages = hediffExtraDamages;
- else if (hediffExtraDamages == null)
- extraDamages = toolExtraDamages;
- else
- {
- extraDamages = new List(toolExtraDamages.Count + hediffExtraDamages.Count);
- extraDamages.AddRange(toolExtraDamages);
- extraDamages.AddRange(hediffExtraDamages);
- }
- DebugMessage($"DamageInfosToApply_ExtraDamages({verb}) => caching for {key}: {extraDamages.Join(ToString)}");
- extraDamageCache[key] = extraDamages;
- }
- return extraDamages;
- }
-
- private static string ToString(ExtraDamage ed)
- {
- return $"(def={ed.def}, amount={ed.amount}, armorPenetration={ed.armorPenetration}, chance={ed.chance})";
- }
-
- // ArmorUtility patches:
- // These are a workaround for PreApplyDamage_PrePatch changes to the dinfo struct not being saved, due to
- // Pawn_HealthTracker.PreApplyDamage dinfo parameter being passed by value (PreApplyDamage_PrePatch has it passed
- // by reference, but this only affects the patch; Pawn_HealthTracker.PreApplyDamage still has it passed by value).
- // Incidentally, these patches have another purpose: it allows other Pawn_HealthTracker.PreApplyDamage code like
- // Apparel.CheckPreAbsorbDamage (like shield belts), various pawn-specific notifications affecting pawn behavior,
- // and other mod's patches on the method to run, some of which could affect dinfo.Amount and absorbed flag.
- // Indeed, the choice of prefix patching Pawn_HealthTracker.PreApplyDamage rather than a Pawn.PreApplyDamage prefix
- // or a Pawn_HealthTracker.PreApplyDamage postfix is likely a compromise to allow as much change to dinfo as
- // possible yet still apply damage soaks before shield belt absorption.
- // Pawn_HealthTracker.PreApplyDamage notification specifics: if it runs (no ThingComp.PostPreApplyDamage sets
- // absorbed flag), prisoner guilt, AI updates, and current danger are triggered. If no Apparel.CheckPreAbsorbDamage
- // sets the absorbed flag, stun effects, pawn thought/memory, and tale recording are triggered.
- // XXX: I do not think this patch is reliable because:
- // 1) It's not guaranteed to run under certain conditions (e.g. if dinfo.IgnoreArmor) when it should.
- // 2) dinfo.Amount can be divided into multiple DamageInfos under certain conditions (bomb/flame damage),
- // which this doesn't take into account.
- // 3) It assumes that all new damage amount since our PreApplyDamage_PrePatch ran should be damage soaked
- // (as long as this patch runs, e.g. not absorbed, etc.), by setting the damage amount back to tempDamageAmount,
- // the final damage amount recorded in PreApplyDamage_PrePatch, even if no damage soaks exist
- // (see TODO in PreApplyDamage_PrePatch).
- // 4) If damage amount decreased yet still non-zero since our PreApplyDamage_PrePatch ran, this patch will
- // increase the damage amount back to tempDamageAmount, which is the total opposite of damage soaking.
- // 5) The relationship of PreApplyDamage_PrePatch and this patch with respect to tempDamageAmount is fragile,
- // especially since (1) and tempDamageAmount not always being set in PreApplyDamage_PrePatch.
- // If another mod happens to use ArmorUtility without going through PreApplyDamage, this scheme will break.
- // TODO:
- // If we want to retain damage soaking before shield belt absorption:
- // Instead of this patch, postfix patch (highest patch priority) Pawn.PreApplyDamage to update the original
- // dinfo struct with any changes from PreApplyDamage_PrePatch. Make PreApplyDamage_PrePatch patch with lowest
- // patch priority so that it runs right before Pawn_HealthTracker.PreApplyDamage. This should ensure that there
- // no other changes to dinfo in between PreApplyDamage_PrePatch and the new Pawn.PreApplyDamage postfix patch
- // that should've been tracked. tempDamageAmount is still needed to to transfer the damage amount info between
- // these patches.
- // If we're fine with damage soaks applying after shield belt absorption:
- // Simplify into a single Pawn.PreApplyDamage postfix patch.
- public static void Pre_ApplyArmor(ref float damAmount, Pawn pawn)
- {
- if (tempDamageAmount != null && damAmount > 0f)
- {
- var damageDiff = Mathf.Max(damAmount - tempDamageAmount.Value, 0f);
- var newDamAmount = GenMath.RoundRandom(tempDamageAmount.Value);
- DebugMessage($"c6c:: ApplyArmor prefix on {pawn}: tempDamageAmount {tempDamageAmount} => null, damAmount {damAmount} => {newDamAmount}");
- damAmount = newDamAmount;
- tempDamageAmount = null;
- if (damageDiff > 0f)
- tempDamageAbsorbed = damageDiff;
- }
- }
-
- // XXX: Damage soak mote is already emitted in PreApplyDamage_ApplyDamageSoakers, so this leads to a misleading
- // redundant soak mote. Worse, if the damage amount actually changes between PreApplyDamage_ApplyDamageSoakers
- // and Pre_ApplyArmor, leading to a tempDamageAbsorbed that's different from PreApplyDamage_ApplyDamageSoakers's
- // totalSoakedDamage, this is even more misleading.
- public static void Post_GetPostArmorDamage(Pawn pawn)
- {
- if (tempDamageAbsorbed != null)
- {
- DebugMessage($"c6c:: GetPostArmorDamage postfix on {pawn}: tempDamageAbsorbed {tempDamageAbsorbed}");
- if (pawn.GetHediffComp() != null)
- {
- DamageSoakedMote(pawn, tempDamageAbsorbed.Value);
- }
-
- tempDamageAbsorbed = null;
- }
- }
-
- public static bool PreApplyDamage_PrePatch(Pawn ___pawn, ref DamageInfo dinfo, out bool absorbed)
- {
- DebugMessage($"c6c:: === Enter Harmony Prefix --- PreApplyDamage_PrePatch for {___pawn} and {dinfo} ===");
- if (___pawn != null)
- {
- DebugMessage("c6c:: Pawn exists.");
- var hediffSet = ___pawn.health.hediffSet;
- if (hediffSet.hediffs.Count > 0)
- {
- DebugMessage("c6c:: Pawn has hediffs.");
- // See above ArmorUtility comments.
- if (PreApplyDamage_ApplyDamageSoakers(ref dinfo, hediffSet, ___pawn))
- {
- DebugMessage($"c6c:: === Exit Harmony Prefix --- PreApplyDamage_PrePatch for {___pawn} and {dinfo} ===");
- absorbed = true;
- return false;
- }
- }
- }
-
- // TODO: tempDamageAmount shouldn't be set if there are no damage soaks.
- tempDamageAmount = dinfo.Amount;
- DebugMessage($"c6c:: tempDamageAmount <= {tempDamageAmount}");
- absorbed = false;
- DebugMessage($"c6c:: === Exit Harmony Prefix --- PreApplyDamage_PrePatch for {___pawn} and {dinfo} ===");
- return true;
- }
-
- // Stores original dinfo.Amount in __state, that below Pawn_PreApplyDamage_Postfix can access.
- public static void Pawn_PreApplyDamage_Prefix(ref DamageInfo dinfo, ref float __state)
- {
- __state = dinfo.Amount;
- }
-
- // This should happen after all modifications to dinfo and any possible setting of absorbed flag,
- // i.e. after all ThingComp.PostPreApplyDamage and Apparel.CheckPreAbsorbDamage (shield belts).
- public static void Pawn_PreApplyDamage_Postfix(Pawn __instance, ref DamageInfo dinfo, ref bool absorbed,
- float __state)
- {
- if (dinfo.Weapon is ThingDef weaponDef && !weaponDef.IsRangedWeapon &&
- dinfo.Instigator is Pawn instigator)
- {
- DebugMessage($"c6c:: Instigator using non-ranged weapon: {dinfo}");
- var hediffCompKnockback = instigator.GetHediffComp();
- if (hediffCompKnockback != null)
- {
- // Hack to prevent multiple knockbacks occurring due to multiple damage infos (e.g. extra damage) for same instigator+target:
- // prevent knockback if tick hasn't passed since last knockback for instigator+target pair.
- // This requires a (instigator+target)=>tick cache, which is cleared after every tick via Scenario_TickScenario_Postfix.
- var pair = new Pair(instigator, __instance);
- var ticks = Find.TickManager.TicksGame;
- if (knockbackLastTicks.TryGetValue(pair, out var lastTicks) && lastTicks == ticks)
- return;
- knockbackLastTicks[pair] = ticks;
- hediffCompKnockback.ApplyKnockback(__instance,
- damageAbsorbedPercent: absorbed ? 1f : 1f - Mathf.Clamp01(dinfo.Amount / __state));
- }
- }
- }
-
- public static void Scenario_TickScenario_Postfix()
- {
- knockbackLastTicks.Clear();
- }
-
- private static readonly ConcurrentDictionary, int> knockbackLastTicks =
- new ConcurrentDictionary, int>();
-
- private static bool PreApplyDamage_ApplyDamageSoakers(ref DamageInfo dinfo, HediffSet hediffSet, Pawn pawn)
- {
- // Multiple damage soak hediff comps stack.
- DebugMessage($"c6c:: --- Enter PreApplyDamage_ApplyDamageSoakers for {pawn} and {dinfo} ---");
- var damageDef = dinfo.Def;
- var totalSoakedDamage = 0f;
- foreach (var hediffComp in hediffSet.GetAllComps())
- {
- if (!(hediffComp is HediffComp_DamageSoak damageSoakComp))
- continue;
- DebugMessage("c6c:: Soak Damage Hediff checked.");
-
- var soakProps = damageSoakComp.Props;
- if (soakProps == null)
- {
- DebugMessage("c6c:: Soak Damage Hediff has no damage soak XML properties.");
- continue;
- }
- if (soakProps.settings.NullOrEmpty())
- {
- DebugMessage("c6c:: Soak Damage Hediff has no damage soak settings.");
-
- // Null, here, means "all damage types", so null should pass this check.
- if (soakProps.damageType != null && soakProps.damageType != damageDef)
- {
- DebugMessage($"c6c:: {damageDef.label.CapitalizeFirst()} is not in soak settings.");
- continue;
- }
-
- if (soakProps.damageTypesToExclude != null &&
- soakProps.damageTypesToExclude.Contains(damageDef))
- {
- DebugMessage($"c6c:: {damageDef.label.CapitalizeFirst()} is to be excluded from damage soak.");
- continue;
- }
-
- var dmgAmount = dinfo.Amount;
- var soakedDamage = Mathf.Min(soakProps.damageToSoak, dmgAmount);
- DebugMessage($"c6c:: Soaked: Min({soakProps.damageToSoak}, {dinfo.Amount}) => {soakedDamage}");
- dmgAmount -= soakedDamage;
- DebugMessage($"c6c:: Damage amount: {dinfo.Amount} - {soakedDamage} => {dmgAmount}");
- totalSoakedDamage += soakedDamage;
- DebugMessage($"c6c:: Total soaked: {totalSoakedDamage}");
- dinfo.SetAmount(dmgAmount);
-
- if (dinfo.Amount > 0)
- {
- DebugMessage($"c6c:: More damage exists. Continuing check for soakers.");
- continue;
- }
-
- DamageSoakedMote(pawn, totalSoakedDamage);
- DebugMessage($"c6c:: Damage absorbed.");
- DebugMessage($"c6c:: FINAL RESULT -- Total soaked: {totalSoakedDamage}, damage amount: {dinfo.Amount}.");
- DebugMessage($"c6c:: --- Exit PreApplyDamage_ApplyDamageSoakers for {pawn} and {dinfo} ---");
- return true;
- }
- else
- {
- DebugMessage("c6c:: Soak Damage Hediff has damage soak settings.");
- foreach (var soakSettings in soakProps.settings)
- {
- DebugMessage($"c6c:: Hediff Damage: {damageDef}");
- if (soakSettings.damageType != null)
- DebugMessage($"c6c:: Soak Type: {soakSettings.damageType}");
- else
- DebugMessage($"c6c:: Soak Type: All");
-
- //Null, here, means "all damage types"
- //So Null should pass this check.
- if (soakSettings.damageType != null && soakSettings.damageType != damageDef)
- {
- DebugMessage($"c6c:: No match. No soak.");
- continue;
- }
-
- // This variable tracks whether the damage should be excluded by a damageTypesToExclude
- // rule for breaking out of a nested for loop without using goto
- bool damageExcluded = false;
- if (!soakSettings.damageTypesToExclude.NullOrEmpty())
- {
- DebugMessage($"c6c:: Damage Soak Exlusions: ");
- foreach (var exclusion in soakSettings.damageTypesToExclude)
- {
- DebugMessage($"c6c:: {exclusion}");
- if (exclusion == damageDef)
- {
- DebugMessage($"c6c:: Exclusion match. Damage soak aborted.");
- damageExcluded = true;
- break;
- }
- }
- if (damageExcluded)
- continue;
- }
-
- var dmgAmount = dinfo.Amount;
- var soakedDamage = Mathf.Min(soakSettings.damageToSoak, dmgAmount);
- DebugMessage($"c6c:: Soaked: Min({soakSettings.damageToSoak}, {dinfo.Amount}) => {soakedDamage}");
- dmgAmount -= soakedDamage;
- DebugMessage($"c6c:: Damage amount: {dinfo.Amount} - {soakedDamage} => {dmgAmount}");
- totalSoakedDamage += soakedDamage;
- DebugMessage($"c6c:: Total soaked: {totalSoakedDamage}");
- dinfo.SetAmount(dmgAmount);
-
- if (dinfo.Amount > 0)
- {
- DebugMessage($"c6c:: Unsoaked damage remains. Checking for more soakers.");
- continue;
- }
-
- DamageSoakedMote(pawn, totalSoakedDamage);
- DebugMessage($"c6c:: Damage absorbed.");
- DebugMessage($"c6c:: FINAL RESULT -- Total soaked: {totalSoakedDamage}, damage amount: {dinfo.Amount}.");
- DebugMessage($"c6c:: --- Exit PreApplyDamage_ApplyDamageSoakers for {pawn} and {dinfo} ---");
- return true;
- }
- }
- }
- if (totalSoakedDamage > 0)
- {
- DamageSoakedMote(pawn, totalSoakedDamage);
- DebugMessage($"c6c:: FINAL RESULT -- Total soaked: {totalSoakedDamage}, damage amount: {dinfo.Amount}.");
- }
- DebugMessage($"c6c:: --- Exit PreApplyDamage_ApplyDamageSoakers for {pawn} and {dinfo} ---");
- return false;
- }
-
- private static void DamageSoakedMote(Pawn pawn, float soakedDamage)
- {
- if (soakedDamage > 0f && pawn != null && pawn.Spawned && pawn.MapHeld != null &&
- pawn.DrawPos is Vector3 drawVecDos && drawVecDos.InBounds(pawn.MapHeld))
- {
- // To avoid any rounding bias, use RoundRandom for converting int to float.
- var roundedSoakedDamage = GenMath.RoundRandom(soakedDamage);
- DebugMessage($"c6c:: DamageSoakedMote for {pawn}: {soakedDamage} rounded to {roundedSoakedDamage}");
- MoteMaker.ThrowText(drawVecDos, pawn.MapHeld, "JT_DamageSoaked".Translate(roundedSoakedDamage));
- }
- }
-
- // Not sure if another mod is using this, so obsoleting it rather than deleting it.
- [Obsolete]
- public static Vector3 PushResult(Thing Caster, Thing thingToPush, int pushDist, out bool collision)
- {
- return HediffComp_Knockback.PushResult(Caster, thingToPush, pushDist, out var _, out collision);
- }
-
- // Not sure if another mod is using this, so obsoleting it rather than deleting it.
- [Obsolete]
- public static void PushEffect(Thing Caster, Thing target, int distance, bool damageOnCollision = false)
- {
- HediffComp_Knockback.PushEffect(Caster, target, damageAbsorbedPercent: 0f, new HediffCompProperties_Knockback
- {
- knockDistance = new FloatRange(distance, distance),
- knockDistanceAbsorbedPercentCurve = HediffComp_Knockback.AlwaysOneCurve,
- knockDistanceMassCurve = HediffComp_Knockback.AlwaysOneCurve,
- knockImpactDamage = damageOnCollision ? new FloatRange(8f, 10f) : default,
- knockImpactDamageDistancePercentCurve = HediffComp_Knockback.AlwaysOneCurve,
- knockImpactDamageType = DamageDefOf.Blunt,
- });
- }
-
- public static string DamageInfo_ToString_Postfix(string result, ref DamageInfo __instance)
- {
- var insertIndex = result.IndexOf(", angle=");
- return result.Insert(insertIndex, $", hitPart={__instance.HitPart.ToStringSafe()}, " +
- $"weapon={__instance.Weapon.ToStringSafe()}, armorPenetration={__instance.ArmorPenetrationInt}");
- }
-
- //added 2018/12/13 - Mehni.
- //Uses CutoutComplex shader for apparel that wants it.
- //private static IEnumerable CutOutComplexApparel_Transpiler(IEnumerable instructions)
- //{
- // MethodInfo shader = AccessTools.Method(typeof(HarmonyPatches), nameof(HarmonyPatches.Shader));
- // FieldInfo cutOut = AccessTools.Field(typeof(ShaderDatabase), nameof(ShaderDatabase.Cutout));
-
- // foreach (CodeInstruction codeInstruction in instructions)
- // {
- // if (codeInstruction.opcode == OpCodes.Ldsfld && codeInstruction.operand == cutOut)
- // {
- // yield return new CodeInstruction(OpCodes.Ldarg_0); //apparel
- // yield return new CodeInstruction(OpCodes.Call, shader); //return shader type
- // continue; //skip instruction.
- // }
- // yield return codeInstruction;
- // }
- //}
-
- //private static Shader Shader(Apparel apparel)
- //{
- // if (apparel.def.graphicData.shaderType.Shader == ShaderDatabase.CutoutComplex)
- // return ShaderDatabase.CutoutComplex;
-
- // return ShaderDatabase.Cutout;
- //}
- }
-}
diff --git a/Source/AllModdingComponents/JecsTools/Knockback/HarmonyPatches_Knockback.cs b/Source/AllModdingComponents/JecsTools/Knockback/HarmonyPatches_Knockback.cs
new file mode 100644
index 00000000..5a62996e
--- /dev/null
+++ b/Source/AllModdingComponents/JecsTools/Knockback/HarmonyPatches_Knockback.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Concurrent;
+using HarmonyLib;
+using RimWorld;
+using UnityEngine;
+using Verse;
+
+namespace JecsTools;
+
+public static partial class HarmonyPatches
+{
+ public static void HarmonyPatches_Knockback(Harmony harmony, Type type)
+ {
+ //Applies knockback
+ harmony.Patch(AccessTools.Method(typeof(Pawn), nameof(Pawn.PreApplyDamage)),
+ prefix: new HarmonyMethod(type, nameof(Pawn_PreApplyDamage_Prefix)) { priority = Priority.High },
+ postfix: new HarmonyMethod(type, nameof(Pawn_PreApplyDamage_Postfix)) { priority = Priority.Low });
+ harmony.Patch(AccessTools.Method(typeof(Scenario), nameof(Scenario.TickScenario)),
+ postfix: new HarmonyMethod(type, nameof(Scenario_TickScenario_Postfix)));
+ }
+
+
+ private static readonly ConcurrentDictionary, int> knockbackLastTicks =
+ new ConcurrentDictionary, int>();
+
+
+ // Stores original dinfo.Amount in __state, that below Pawn_PreApplyDamage_Postfix can access.
+ public static void Pawn_PreApplyDamage_Prefix(ref DamageInfo dinfo, ref float __state)
+ {
+ __state = dinfo.Amount;
+ }
+
+
+ // This should happen after all modifications to dinfo and any possible setting of absorbed flag,
+ // i.e. after all ThingComp.PostPreApplyDamage and Apparel.CheckPreAbsorbDamage (shield belts).
+ public static void Pawn_PreApplyDamage_Postfix(Pawn __instance, ref DamageInfo dinfo, ref bool absorbed,
+ float __state)
+ {
+ if (dinfo.Weapon is ThingDef weaponDef && !weaponDef.IsRangedWeapon &&
+ dinfo.Instigator is Pawn instigator)
+ {
+ DebugMessage($"c6c:: Instigator using non-ranged weapon: {dinfo}");
+ var hediffCompKnockback = instigator.GetHediffComp();
+ if (hediffCompKnockback != null)
+ {
+ // Hack to prevent multiple knockbacks occurring due to multiple damage infos (e.g. extra damage) for same instigator+target:
+ // prevent knockback if tick hasn't passed since last knockback for instigator+target pair.
+ // This requires a (instigator+target)=>tick cache, which is cleared after every tick via Scenario_TickScenario_Postfix.
+ var pair = new Pair(instigator, __instance);
+ var ticks = Find.TickManager.TicksGame;
+ if (knockbackLastTicks.TryGetValue(pair, out var lastTicks) && lastTicks == ticks)
+ return;
+ knockbackLastTicks[pair] = ticks;
+ hediffCompKnockback.ApplyKnockback(__instance,
+ damageAbsorbedPercent: absorbed ? 1f : 1f - Mathf.Clamp01(dinfo.Amount / __state));
+ }
+ }
+ }
+
+ public static void Scenario_TickScenario_Postfix()
+ {
+ knockbackLastTicks.Clear();
+ }
+
+}
diff --git a/Source/AllModdingComponents/JecsTools/HediffCompProperties_Knockback.cs b/Source/AllModdingComponents/JecsTools/Knockback/HediffCompProperties_Knockback.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/HediffCompProperties_Knockback.cs
rename to Source/AllModdingComponents/JecsTools/Knockback/HediffCompProperties_Knockback.cs
diff --git a/Source/AllModdingComponents/JecsTools/HediffComp_Knockback.cs b/Source/AllModdingComponents/JecsTools/Knockback/HediffComp_Knockback.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/HediffComp_Knockback.cs
rename to Source/AllModdingComponents/JecsTools/Knockback/HediffComp_Knockback.cs
diff --git a/Source/AllModdingComponents/JecsTools/OtherHarmonyPatches/HarmonyPatches.cs b/Source/AllModdingComponents/JecsTools/OtherHarmonyPatches/HarmonyPatches.cs
new file mode 100644
index 00000000..e865d33a
--- /dev/null
+++ b/Source/AllModdingComponents/JecsTools/OtherHarmonyPatches/HarmonyPatches.cs
@@ -0,0 +1,52 @@
+//#define DEBUGLOG
+
+using System;
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Reflection;
+using System.Reflection.Emit;
+using HarmonyLib;
+using RimWorld;
+using UnityEngine;
+using Verse;
+
+namespace JecsTools
+{
+ [StaticConstructorOnStartup]
+ public static partial class HarmonyPatches
+ {
+ //For alternating fire on some weapons
+ public static Dictionary AlternatingFireTracker = new Dictionary();
+
+ static HarmonyPatches()
+ {
+ var harmony = new Harmony("jecstools.jecrell.main");
+ var type = typeof(HarmonyPatches);
+
+ HarmonyPatches_ApparelExtension(harmony, type);
+ HarmonyPatches_BuildingExtension(harmony, type);
+ HarmonyPatches_DamageSoak(harmony, type);
+ HarmonyPatches_Debug(harmony, type);
+ HarmonyPatches_ExtraMeleeDamages(harmony, type);
+ HarmonyPatches_Knockback(harmony, type);
+ HarmonyPatches_StartWithHediff(harmony, type);
+ HarmonyPatches_StartWithGenes(harmony, type);
+ }
+
+ [Conditional("DEBUGLOG")]
+ private static void DebugMessage(string s)
+ {
+ Log.Message(s);
+ }
+
+ private static string ToString(ExtraDamage ed)
+ {
+ return $"(def={ed.def}, amount={ed.amount}, armorPenetration={ed.armorPenetration}, chance={ed.chance})";
+ }
+
+
+
+ }
+}
diff --git a/Source/AllModdingComponents/JecsTools/OtherHarmonyPatches/HarmonyPatches_Debug.cs b/Source/AllModdingComponents/JecsTools/OtherHarmonyPatches/HarmonyPatches_Debug.cs
new file mode 100644
index 00000000..5301a3bc
--- /dev/null
+++ b/Source/AllModdingComponents/JecsTools/OtherHarmonyPatches/HarmonyPatches_Debug.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using HarmonyLib;
+using RimWorld;
+using UnityEngine;
+using Verse;
+
+namespace JecsTools;
+
+public static partial class HarmonyPatches
+{
+ public static void HarmonyPatches_Debug(Harmony harmony, Type type)
+ {
+ //Improve DamageInfo.ToString for debugging purposes.
+ harmony.Patch(AccessTools.Method(typeof(DamageInfo), nameof(DamageInfo.ToString)),
+ postfix: new HarmonyMethod(type, nameof(DamageInfo_ToString_Postfix)));
+ }
+
+ public static string DamageInfo_ToString_Postfix(string result, ref DamageInfo __instance)
+ {
+ var insertIndex = result.IndexOf(", angle=");
+ return result.Insert(insertIndex, $", hitPart={__instance.HitPart.ToStringSafe()}, " +
+ $"weapon={__instance.Weapon.ToStringSafe()}, armorPenetration={__instance.ArmorPenetrationInt}");
+ }
+
+}
diff --git a/Source/AllModdingComponents/JecsTools/HarmonyPatches_GUI.cs b/Source/AllModdingComponents/JecsTools/OtherHarmonyPatches/HarmonyPatches_GUI.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/HarmonyPatches_GUI.cs
rename to Source/AllModdingComponents/JecsTools/OtherHarmonyPatches/HarmonyPatches_GUI.cs
diff --git a/Source/AllModdingComponents/JecsTools/OtherHarmonyPatches/HarmonyPatches_Obsolete.cs b/Source/AllModdingComponents/JecsTools/OtherHarmonyPatches/HarmonyPatches_Obsolete.cs
new file mode 100644
index 00000000..d1a25db5
--- /dev/null
+++ b/Source/AllModdingComponents/JecsTools/OtherHarmonyPatches/HarmonyPatches_Obsolete.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using HarmonyLib;
+using RimWorld;
+using UnityEngine;
+using Verse;
+
+namespace JecsTools;
+
+public static partial class HarmonyPatches
+{
+ // Not sure if another mod is using this, so obsoleting it rather than deleting it.
+ [Obsolete]
+ public static Vector3 PushResult(Thing Caster, Thing thingToPush, int pushDist, out bool collision)
+ {
+ return HediffComp_Knockback.PushResult(Caster, thingToPush, pushDist, out var _, out collision);
+ }
+
+ // Not sure if another mod is using this, so obsoleting it rather than deleting it.
+ [Obsolete]
+ public static void PushEffect(Thing Caster, Thing target, int distance, bool damageOnCollision = false)
+ {
+ HediffComp_Knockback.PushEffect(Caster, target, damageAbsorbedPercent: 0f, new HediffCompProperties_Knockback
+ {
+ knockDistance = new FloatRange(distance, distance),
+ knockDistanceAbsorbedPercentCurve = HediffComp_Knockback.AlwaysOneCurve,
+ knockDistanceMassCurve = HediffComp_Knockback.AlwaysOneCurve,
+ knockImpactDamage = damageOnCollision ? new FloatRange(8f, 10f) : default,
+ knockImpactDamageDistancePercentCurve = HediffComp_Knockback.AlwaysOneCurve,
+ knockImpactDamageType = DamageDefOf.Blunt,
+ });
+ }
+}
diff --git a/Source/AllModdingComponents/JecsTools/ProjectileExtension.cs b/Source/AllModdingComponents/JecsTools/ProjectileExtension/ProjectileExtension.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/ProjectileExtension.cs
rename to Source/AllModdingComponents/JecsTools/ProjectileExtension/ProjectileExtension.cs
diff --git a/Source/AllModdingComponents/JecsTools/Projectile_Laser.cs b/Source/AllModdingComponents/JecsTools/ProjectileExtension/Projectile_Laser.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/Projectile_Laser.cs
rename to Source/AllModdingComponents/JecsTools/ProjectileExtension/Projectile_Laser.cs
diff --git a/Source/AllModdingComponents/JecsTools/ThingDef_LaserProjectile.cs b/Source/AllModdingComponents/JecsTools/ProjectileExtension/ThingDef_LaserProjectile.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/ThingDef_LaserProjectile.cs
rename to Source/AllModdingComponents/JecsTools/ProjectileExtension/ThingDef_LaserProjectile.cs
diff --git a/Source/AllModdingComponents/JecsTools/StartWithGenes/HarmonyPatches_StartWithGenes.cs b/Source/AllModdingComponents/JecsTools/StartWithGenes/HarmonyPatches_StartWithGenes.cs
new file mode 100644
index 00000000..00d9d696
--- /dev/null
+++ b/Source/AllModdingComponents/JecsTools/StartWithGenes/HarmonyPatches_StartWithGenes.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using HarmonyLib;
+using RimWorld;
+using UnityEngine;
+using Verse;
+
+namespace JecsTools;
+
+public static partial class HarmonyPatches
+{
+ public static void HarmonyPatches_StartWithGenes(Harmony harmony, Type type)
+ {
+ //Allows for adding additional genes when characters spawn using the StartWithHediff class.
+ harmony.Patch(AccessTools.Method(typeof(PawnGenerator), nameof(PawnGenerator.GeneratePawn),
+ new[] { typeof(PawnGenerationRequest) }),
+ postfix: new HarmonyMethod(type, nameof(Post_GeneratePawn_Genes)));
+ }
+
+ public static void Post_GeneratePawn_Genes(Pawn __result)
+ {
+ if (!ModsConfig.BiotechActive)
+ return;
+
+ if (__result?.kindDef?.modExtensions is { } extensions)
+ {
+ foreach (var extension in extensions)
+ {
+ if (extension is PawnKindGeneExtension newGenes)
+ {
+ foreach (var gene in newGenes.Genes.Where(gene => Rand.Range(min: 0, max: 100) < gene.chance))
+ {
+ __result.genes.AddGene(DefDatabase.GetNamed(gene.defName), false);
+ }
+ }
+ }
+ }
+
+ var hediffGiverSets = __result?.def?.race?.hediffGiverSets;
+ if (hediffGiverSets != null)
+ {
+ foreach (var hediffGiverSet in hediffGiverSets)
+ {
+ foreach (var hediffGiver in hediffGiverSet.hediffGivers)
+ {
+ if (hediffGiver is HediffGiver_StartWithHediff hediffGiverStartWithHediff)
+ {
+ hediffGiverStartWithHediff.GiveHediff(__result);
+ // TODO: Should this really only use the first found HediffGiver_StartWithHediff?
+ return;
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/Source/AllModdingComponents/JecsTools/StartWithGenes/PawnKindGeneExtension.cs b/Source/AllModdingComponents/JecsTools/StartWithGenes/PawnKindGeneExtension.cs
new file mode 100644
index 00000000..fdce6f81
--- /dev/null
+++ b/Source/AllModdingComponents/JecsTools/StartWithGenes/PawnKindGeneExtension.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using Verse;
+
+namespace JecsTools
+{
+ public class ChancedGeneEntry
+ {
+ public string defName;
+ public float chance = 100;
+ }
+
+ public class PawnKindGeneExtension : DefModExtension
+ {
+ private List genes; // set via reflection
+
+ [Unsaved]
+ private HashSet geneSet;
+
+ public HashSet Genes
+ {
+ get
+ {
+ // DefModExtension lacks a ResolveReferences hook, so must use lazy initialization in property instead.
+ if (geneSet == null && genes != null)
+ {
+ geneSet = new HashSet(genes.Count);
+ foreach (var gene in genes)
+ geneSet.Add(gene);
+ }
+ return geneSet;
+ }
+ }
+
+ public override IEnumerable ConfigErrors()
+ {
+ if (genes != null && Genes.Count != genes.Count)
+ yield return nameof(genes) + " has duplicate genes: " + genes.ToStringSafeEnumerable();
+ }
+ }
+}
diff --git a/Source/AllModdingComponents/JecsTools/StartWithHediff/HarmonyPatches_StartWithHediff.cs b/Source/AllModdingComponents/JecsTools/StartWithHediff/HarmonyPatches_StartWithHediff.cs
new file mode 100644
index 00000000..20280574
--- /dev/null
+++ b/Source/AllModdingComponents/JecsTools/StartWithHediff/HarmonyPatches_StartWithHediff.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using HarmonyLib;
+using RimWorld;
+using UnityEngine;
+using Verse;
+
+namespace JecsTools;
+
+public static partial class HarmonyPatches
+{
+ public static void HarmonyPatches_StartWithHediff(Harmony harmony, Type type)
+ {
+ //Allows for adding additional HediffSets when characters spawn using the StartWithHediff class.
+ harmony.Patch(AccessTools.Method(typeof(PawnGenerator), nameof(PawnGenerator.GeneratePawn),
+ new[] { typeof(PawnGenerationRequest) }),
+ postfix: new HarmonyMethod(type, nameof(Post_GeneratePawn)));
+ }
+
+ public static void Post_GeneratePawn(Pawn __result)
+ {
+ var hediffGiverSets = __result?.def?.race?.hediffGiverSets;
+ if (hediffGiverSets != null)
+ {
+ foreach (var hediffGiverSet in hediffGiverSets)
+ {
+ foreach (var hediffGiver in hediffGiverSet.hediffGivers)
+ {
+ if (hediffGiver is HediffGiver_StartWithHediff hediffGiverStartWithHediff)
+ {
+ hediffGiverStartWithHediff.GiveHediff(__result);
+ // TODO: Should this really only use the first found HediffGiver_StartWithHediff?
+ return;
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/Source/AllModdingComponents/JecsTools/HediffExpandedDef.cs b/Source/AllModdingComponents/JecsTools/StartWithHediff/HediffExpandedDef.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/HediffExpandedDef.cs
rename to Source/AllModdingComponents/JecsTools/StartWithHediff/HediffExpandedDef.cs
diff --git a/Source/AllModdingComponents/JecsTools/HediffGiver_StartWithHediff.cs b/Source/AllModdingComponents/JecsTools/StartWithHediff/HediffGiver_StartWithHediff.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/HediffGiver_StartWithHediff.cs
rename to Source/AllModdingComponents/JecsTools/StartWithHediff/HediffGiver_StartWithHediff.cs
diff --git a/Source/AllModdingComponents/JecsTools/HediffWithComps_Expanded.cs b/Source/AllModdingComponents/JecsTools/StartWithHediff/HediffWithComps_Expanded.cs
similarity index 100%
rename from Source/AllModdingComponents/JecsTools/HediffWithComps_Expanded.cs
rename to Source/AllModdingComponents/JecsTools/StartWithHediff/HediffWithComps_Expanded.cs
diff --git a/Source/AllModdingComponents/JecsTools/_HumanlikeOrdersUtility.cs b/Source/AllModdingComponents/JecsTools/_HumanlikeOrdersUtility.cs
index ecb7e18e..0c189e6a 100644
--- a/Source/AllModdingComponents/JecsTools/_HumanlikeOrdersUtility.cs
+++ b/Source/AllModdingComponents/JecsTools/_HumanlikeOrdersUtility.cs
@@ -23,7 +23,6 @@ namespace JecsTools
/// are loaded into FloatMenuOptionList;
/// Next, I've created two harmony patches to make a saved list
/// of float menu options with a unique ID tag.
- /// Using this system should lower
///
[StaticConstructorOnStartup]
public static class _HumanlikeOrdersUtility