code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using HarmonyLib;
using Multiplayer.API;
using UnityEngine;
using Verse;
using RimWorld;
using rjw.Modules.Testing;

using Seeded = rjw.Modules.Rand.Seeded;
using LudeonTK;

namespace rjw
{
	[HarmonyPatch(typeof(Hediff_Pregnant), "DoBirthSpawn")]
	internal static class PATCH_Hediff_Pregnant_DoBirthSpawn
	{
		/// <summary>
		/// This one overrides vanilla pregnancy hediff behavior.
		/// 0 - try to find suitable father for debug pregnancy
		/// 1st part if character pregnant and rjw pregnancies enabled - creates rjw pregnancy and instantly births it instead of vanilla
		/// 2nd part if character pregnant with rjw pregnancy - birth it
		/// 3rd part - debug - create rjw/vanila pregnancy and birth it
		/// </summary>
		/// <param name="mother"></param>
		/// <param name="father"></param>
		/// <returns></returns>
		[HarmonyPrefix]
		[SyncMethod]
		private static bool on_begin_DoBirthSpawn(ref Pawn mother, ref Pawn father)
		{
			//--Log.Message("patches_pregnancy::PATCH_Hediff_Pregnant::DoBirthSpawn() called");

			if (mother == null)
			{
				ModLog.Error("Hediff_Pregnant::DoBirthSpawn() - no mother defined -> exit");
				return false;
			}

			//CompEggLayer compEggLayer = mother.TryGetComp<CompEggLayer>();
			//if (compEggLayer != null)
			//{
			//	ProcessVanillaEggPregnancy(mother, father);
			//	return false;
			//}

			//vanilla debug?
			if (mother.gender == Gender.Male)
			{
				ModLog.Error("Hediff_Pregnant::DoBirthSpawn() - mother is male -> exit");
				return false;
			}

			// get a reference to the hediff we are applying
			//do birth for vanilla pregnancy Hediff
			//if using rjw pregnancies - add RJW pregnancy Hediff and birth it instead
			Hediff_Pregnant self = (Hediff_Pregnant)mother.health.hediffSet.GetFirstHediffOfDef(HediffDefOf.Pregnant);
			if (self != null)
			{
				return ProcessVanillaPregnancy(self, mother, father);
			}

			// do birth for existing RJW pregnancies
			if (ProcessRJWPregnancy(mother, father))
			{
				return false;
			}

			return ProcessDebugPregnancy(mother, father);
		}

		private static bool ProcessVanillaEggPregnancy(Pawn mother, Pawn father)
		{
			CompEggLayer compEggLayer = mother.TryGetComp<CompEggLayer>();
			if (compEggLayer != null)
			{
				if (!compEggLayer.FullyFertilized)
				{
					if (father == null)
						compEggLayer.Fertilize(mother);
					else
						compEggLayer.Fertilize(father);

					compEggLayer.ProduceEgg();
				}
			}

			CompHatcher compHatcher = mother.TryGetComp<CompHatcher>();
			if (compHatcher != null)
			{
				compHatcher.Hatch();
			}

			ModLog.Message("patches_pregnancy::PATCH_Hediff_Pregnant::DoBirthSpawn():ProcessVanillaEggPregnancy birthing:" + xxx.get_pawnname(mother));
			return true;
		}

		private static bool ProcessVanillaPregnancy(Hediff_Pregnant pregnancy, Pawn mother, Pawn father)
		{
			void CreateAndBirth<T>() where T : Hediff_BasePregnancy
			{
				T hediff = Hediff_BasePregnancy.Create<T>(mother, father);
				hediff.GiveBirth();
				if (pregnancy != null)
					mother.health.RemoveHediff(pregnancy);
			}

			if (father == null)
			{
				father = Hediff_BasePregnancy.Trytogetfather(ref mother);
			}

			ModLog.Message("patches_pregnancy::PATCH_Hediff_Pregnant::DoBirthSpawn():Vanilla_pregnancy birthing:" + xxx.get_pawnname(mother));
			if (RJWPregnancySettings.animal_pregnancy_enabled && ((father == null || xxx.is_animal(father)) && xxx.is_animal(mother)))
			{
				//RJW Bestial pregnancy animal-animal
				ModLog.Message(" override as Bestial birthing(animal-animal): Father-" + xxx.get_pawnname(father) + " Mother-" + xxx.get_pawnname(mother));
				CreateAndBirth<Hediff_BestialPregnancy>();
				return false;
			}
			else if (RJWPregnancySettings.bestial_pregnancy_enabled && ((xxx.is_animal(father) && xxx.is_human(mother)) || (xxx.is_human(father) && xxx.is_animal(mother))))
			{
				//RJW Bestial pregnancy human-animal
				ModLog.Message(" override as Bestial birthing(human-animal): Father-" + xxx.get_pawnname(father) + " Mother-" + xxx.get_pawnname(mother));
				CreateAndBirth<Hediff_BestialPregnancy>();
				return false;
			}
			else if (RJWPregnancySettings.humanlike_pregnancy_enabled && (xxx.is_human(father) && xxx.is_human(mother)))
			{
				//RJW Humanlike pregnancy
				ModLog.Message(" override as Humanlike birthing: Father-" + xxx.get_pawnname(father) + " Mother-" + xxx.get_pawnname(mother));
				CreateAndBirth<Hediff_HumanlikePregnancy>();
				return false;
			}
			else
			{
				ModLog.Warning("Hediff_Pregnant::DoBirthSpawn() - checks failed, vanilla pregnancy birth");
				ModLog.Warning("Hediff_Pregnant::DoBirthSpawn(): Father-" + xxx.get_pawnname(father) + " Mother-" + xxx.get_pawnname(mother));
				//vanilla pregnancy code, no effects on rjw

				return true;
			}
		}

		private static bool ProcessRJWPregnancy(Pawn mother, Pawn father)
		{
			var p = PregnancyHelper.GetPregnancies(mother);
			if (p.NullOrEmpty())
			{
				return false;
			}

			var birth = false;
			foreach (var x in p)
			{
				if (x is Hediff_BasePregnancy)
				{
					var preg = x as Hediff_BasePregnancy;
					ModLog.Message($"patches_pregnancy::{preg.GetType().Name}::DoBirthSpawn() birthing:" + xxx.get_pawnname(mother));
					preg.GiveBirth();
					birth = true;
				}
			}

			return birth;
		}

		private static bool ProcessDebugPregnancy(Pawn mother, Pawn father)
		{
			void CreateAndBirth<T>() where T : Hediff_BasePregnancy
			{
				T hediff = Hediff_BasePregnancy.Create<T>(mother, father);
				hediff.GiveBirth();
			}
			//CreateAndBirth<Hediff_HumanlikePregnancy>();
			//CreateAndBirth<Hediff_BestialPregnancy>();
			//CreateAndBirth<Hediff_MechanoidPregnancy>();
			//return false;

			//debug, add RJW pregnancy and birth it
			ModLog.Message("patches_pregnancy::PATCH_Hediff_Pregnant::DoBirthSpawn():Debug_pregnancy birthing:" + xxx.get_pawnname(mother));
			if (father == null)
			{
				father = Hediff_BasePregnancy.Trytogetfather(ref mother);

				if (RJWPregnancySettings.bestial_pregnancy_enabled && ((xxx.is_animal(father) || xxx.is_animal(mother)))
					|| (xxx.is_animal(mother) && RJWPregnancySettings.animal_pregnancy_enabled))
				{
					//RJW Bestial pregnancy
					ModLog.Message(" override as Bestial birthing, mother: " + xxx.get_pawnname(mother));
					CreateAndBirth<Hediff_BestialPregnancy>();
				}
				else if (RJWPregnancySettings.humanlike_pregnancy_enabled && ((father == null || xxx.is_human(father)) && xxx.is_human(mother)))
				{
					//RJW Humanlike pregnancy
					ModLog.Message(" override as Humanlike birthing, mother: " + xxx.get_pawnname(mother));
					CreateAndBirth<Hediff_HumanlikePregnancy>();
				}
				else
				{
					ModLog.Warning("Hediff_Pregnant::DoBirthSpawn() - debug vanilla pregnancy birth");
					return true;
				}
			}
			return false;
		}
	}

	[HarmonyPatch(typeof(Hediff_Pregnant), "Tick")]
	class PATCH_Hediff_Pregnant_Tick
	{
		[HarmonyPrefix]
		static bool abort_on_missing_genitals(Hediff_Pregnant __instance)
		{
			if (__instance.pawn.IsHashIntervalTick(1000))
			{
				if (!Genital_Helper.has_vagina(__instance.pawn))
				{
					__instance.pawn.health.RemoveHediff(__instance);
				}
			}
			return true;
		}
	}

	// Enables pregnancy approach for same-gender couples as long as they have the right genitals,
	// prevents the UI from bugging out if the first pawn is genderless, and ensures that the
	// "Pawns are sterile" report is not misapplied in case of mpreg.
	[HarmonyPatch(typeof(PregnancyUtility), nameof(PregnancyUtility.CanEverProduceChild))]
	class PregnancyUtility_CanEverProduceChild
	{
		static Type thisType = typeof(PregnancyUtility_CanEverProduceChild);

		static readonly MethodInfo SterileMethod =
			AccessTools.Method(typeof(Pawn), nameof(Pawn.Sterile));
		static readonly MethodInfo GetPregnancyHediffMethod =
			AccessTools.Method(typeof(PregnancyUtility), nameof(PregnancyUtility.GetPregnancyHediff));

		// TODO from Feb-2023: Uncomment the second version once `rjw-race-support` gets
		// its shit together and patches their gender check.
		static bool BypassGenderChecks => true;
		// static bool BypassGenderChecks =>
		// 	!ModsConfig.IsActive("erdelf.HumanoidAlienRaces") ||
		// 	!ModsConfig.IsActive("ASMR.RJW.RaceSupport");

		[HarmonyPrefix]
		static bool CheckPlumbing(Pawn first, Pawn second, ref AcceptanceReport __result)
		{
			if (first.Dead || second.Dead)
			{
				return true;
			}

			var plumbingReport = GetPlumbingReport(first, second);
			if (plumbingReport.Accepted)
			{
				return true;
			}

			__result = plumbingReport;
			return false;
		}

		[HarmonyTranspiler]
		static IEnumerable<CodeInstruction> SkipGenderCheckAndMakeSterilityCheckNotSexist(
			IEnumerable<CodeInstruction> instructionEnumberable, ILGenerator generator)
		{
			var instructions = instructionEnumberable.ToList();

			bool foundGenderCheck = false;
			bool foundSupposedLocalFemaleAssignment = false;
			bool foundFemaleSterilityCheck = false;
			bool foundMaleSterilityCheck = false;

			// Break this method up into blocks, either where a value is returned or where
			// we set a local variable.
			static bool findBlock(CodeInstruction il) => il.opcode == OpCodes.Ret || il.IsStloc();
			(var curIns, var nextIns) = instructions.SplitAfter(findBlock);

			while(curIns.Count > 0)
			{
				if (curIns.Any((il) => il.Is(OpCodes.Ldstr, "PawnsHaveSameGender")))
				{
					// Processing the same-gender check.

					// We want to bypass this check, unless the Alien Races mod is active.
					// RJW's compatibility mod can instead patch its `GenderReproductionCheck`
					// method to alter its behavior.
					if (!BypassGenderChecks)
					{
						foundGenderCheck = true;
						goto yieldOriginal;
					}

					// Due to a quirk in how the optimizer handles branches, we must insert our
					// bypass AFTER the conditional, otherwise the compiler just removes it.
					// Isn't it great when documentation doesn't mention stuff like this?
					(var beforeBr, var afterBr) = curIns.SplitAfter((il) => il.Branches(out _));

					// Verify that we found the branch.
					if (afterBr.Count is 0) goto yieldOriginal;

					foreach (var il in beforeBr) yield return il;
					if (beforeBr.LastOrDefault() is { } br && br.Branches(out var bypassLabel))
					{
						foundGenderCheck = true;

						// Adds `&& false` after the conditional.
						yield return new(OpCodes.Ldc_I4_0);
						yield return new(OpCodes.Brfalse_S, bypassLabel);
					}
					foreach (var il in afterBr) yield return il;
					goto setupNextLoop;
				}
				else if (curIns.Any((il) => il.IsStlocOf(1)))
				{
					// Processing the initialization of `pawn2` into local #1:
					// Pawn pawn2 = (first.gender == Gender.Female) ? first : second;

					// This needs to be changed to just select the other pawn, the one that is
					// not the assumed father.  It is possible that Alien Races has already
					// patched this, so we'll need to take that into consideration.

					// Locate the branch instruction.
					(var beforeBr, var afterBr) = curIns.SplitAfter((il) => il.Branches(out _));

					// Verify that we found the branch.
					if (afterBr.Count is 0) goto yieldOriginal;
					if (beforeBr.LastOrDefault() is not { } br) goto yieldOriginal;
					if (!br.Branches(out var label)) goto yieldOriginal;

					foundSupposedLocalFemaleAssignment = true;

					// Rewrite to:
					// Pawn pawn2 = (first != pawn) ? first : second;

					// Make sure the labels are preserved.
					yield return curIns[0].Replace(new CodeInstruction(OpCodes.Ldarg_0));
					yield return new(OpCodes.Ldloc_0);
					yield return new(OpCodes.Bne_Un_S, label);
					// The remaining instructions handle the selection of the pawn, so we
					// can spit those out without changes.
					foreach (var il in afterBr) yield return il;
					goto setupNextLoop;
				}
				else if (curIns.Any((il) => il.IsStlocOf(6)))
				{
					// Processing the initialization of `flag5` into local #6:
					// bool flag5 = pawn2.Sterile(false) && PregnancyUtility.GetPregnancyHediff(pawn2) == null;

					// This needs to be changed to call `PregnancyHelper.GetPregnancy`.

					// Look for the call `pawn2.Sterile(false)`.
					(var callToSterile, var remainderSterile) = curIns.CutMatchingSequence(
						(il) => il.opcode == OpCodes.Ldloc_1,
						(il) => il.Calls(SterileMethod),
						(il) => il.opcode == OpCodes.Brfalse_S
					);

					// Verify we found this sequence and abort if not.
					if (callToSterile.Count == 0) goto yieldOriginal;

					// Look for the call `PregnancyUtility.GetPregnancyHediff(pawn2)`.
					(var callToPregnancy, var remainderPregnancy) = remainderSterile.CutMatchingSequence(
						(il) => il.opcode == OpCodes.Ldloc_1,
						(il) => il.Calls(GetPregnancyHediffMethod)
					);

					// Verify we found this sequence and abort if not.
					if (callToPregnancy.Count == 0) goto yieldOriginal;

					foundFemaleSterilityCheck = true;

					// Rewrite this to call `PregnancyHelper.GetPregnancy` instead.
					// We're keeping the call to `Sterile`.
					foreach (var il in callToSterile) yield return il;
					// Setup and call.
					yield return new(OpCodes.Ldloc_1);
					yield return CodeInstruction.Call(typeof(PregnancyHelper), nameof(PregnancyHelper.GetPregnancy));
					// The remaining instructions just check for `null`, so should be good.
					foreach (var il in remainderPregnancy) yield return il;
					goto setupNextLoop;
				}
				else if (curIns.Any((il) => il.IsStlocOf(7)))
				{
					// Processing the initialization of `flag6` into local #7:
					// bool flag6 = pawn.Sterile(false);

					// We need to add a call to `PregnancyHelper.GetPregnancy` just like for
					// the female, since this pawn may actually be a futa or something.

					// Look for the call `pawn.Sterile(false)`.
					(var callToSterile, var remainder) = curIns.CutMatchingSequence(
						(il) => il.opcode == OpCodes.Ldloc_0,
						(il) => il.Calls(SterileMethod)
					);

					// Verify we found this sequence and abort if not.
					if (callToSterile.Count == 0) goto yieldOriginal;

					// There should only be 1 instruction left, which sets the variable.
					if (remainder.Count != 1) goto yieldOriginal;

					foundMaleSterilityCheck = true;

					// We need to setup a label pointing to the instruction that will set
					// `false` if the call to `pawn.Sterile(false)` returns false.
					var labelToFalse = generator.DefineLabel();
					var IL_SetToFalse = new CodeInstruction(OpCodes.Ldc_I4_0);
					IL_SetToFalse.labels.Add(labelToFalse);

					// We also need a label on the instruction that sets the variable.
					var labelToSet = generator.DefineLabel();
					remainder[0].labels.Add(labelToSet);

					foreach (var il in callToSterile) yield return il;
					// If the return value was false, skip to `IL_SetToFalse`.
					yield return new(OpCodes.Brfalse_S, labelToFalse);
					// Now, write `PregnancyHelper.GetPregnancy(pawn) == null`.
					yield return new(OpCodes.Ldloc_0);
					yield return CodeInstruction.Call(typeof(PregnancyHelper), nameof(PregnancyHelper.GetPregnancy));
					yield return new(OpCodes.Ldnull);
					yield return new(OpCodes.Ceq);
					// If it was `true`, skip over the next instruction so that is set to the variable.
					yield return new(OpCodes.Br_S, labelToSet);
					// Otherwise, we'll set `false`.
					yield return IL_SetToFalse;
					// The remaining instruction just sets the variable to whatever was last pushed
					// to the stack.
					foreach (var il in remainder) yield return il;
					goto setupNextLoop;
				}

			yieldOriginal:
				foreach (var il in curIns)
					yield return il;

			setupNextLoop:
				(curIns, nextIns) = nextIns.SplitAfter(findBlock);
			}

			if (!foundGenderCheck)
				ModLog.Error("Failed to patch PregnancyUtility.CanEverProduceChild: Could not find gender check");
			if (!foundSupposedLocalFemaleAssignment)
				ModLog.Error("Error when patching PregnancyUtility.CanEverProduceChild: Could not find assignment to local variable `pawn2`");
			if (!foundFemaleSterilityCheck)
				ModLog.Error("Error when patching PregnancyUtility.CanEverProduceChild: Could not find sterility check on local variable `pawn2`");
			if (!foundMaleSterilityCheck)
				ModLog.Error("Error when patching PregnancyUtility.CanEverProduceChild: Could not find sterility check on local variable `pawn`");
		}

		static AcceptanceReport GetPlumbingReport(Pawn first, Pawn second)
		{
			bool firstHasPenis = Genital_Helper.has_penis_fertile(first);
			bool firstHasVagina = Genital_Helper.has_vagina(first);

			if (!firstHasPenis && !firstHasVagina)
			{
				return "PawnLacksFunctionalGenitals".Translate(first.Named("PAWN")).Resolve();
			}

			bool secondHasPenis = Genital_Helper.has_penis_fertile(second);
			bool secondHasVagina = Genital_Helper.has_vagina(second);

			if (!secondHasPenis && !secondHasVagina)
			{
				return "PawnLacksFunctionalGenitals".Translate(second.Named("PAWN")).Resolve();
			}
			if ((firstHasPenis && secondHasVagina) || (firstHasVagina && secondHasPenis))
			{
				return true;
			}
			if (firstHasPenis && !secondHasVagina)
			{
				return "PawnLacksVagina".Translate(second.Named("PAWN")).Resolve();
			}
			if (firstHasVagina && !secondHasPenis)
			{
				return "PawnLacksFunctionalPenis".Translate(second.Named("PAWN")).Resolve();
			}
			return true;
		}

		/// <summary>
		/// <para>Tests various pairings to make sure that that the patch is operating
		/// correctly.  The results of this test may be affected by other mods,
		/// so keep that in mind.</para>
		/// <para>This doesn't go into too much depth, mostly just checking colonists
		/// and a few animals.  It does not currently test integration with Alien
		/// Races or anything.  RJW's Alien Races compatibility mod should provide
		/// its own auto-test for that.</para>
		/// </summary>
		[DebugOutput("RJW Auto-Tests", onlyWhenPlaying = true)]
		public static void TestCanEverProduceChild()
		{
			var logger = Modules.Shared.Logs.LogManager.GetLogger("TestCanEverProduceChild");
			var sb = new StringBuilder();
			var passedColor = GenColor.FromHex("66CC00");
			var failedColor = GenColor.FromHex("FF3300");

			var maleColonistRequest = new PawnGenerationRequest(
				kind: PawnKindDefOf.Colonist,
				fixedGender: Gender.Male,
				excludeBiologicalAgeRange: new(0f, 18f)
			).RequestDefaults();

			var femaleColonistRequest = new PawnGenerationRequest(
				kind: PawnKindDefOf.Colonist,
				fixedGender: Gender.Female,
				allowPregnant: false,
				excludeBiologicalAgeRange: new(0f, 18f)
			).RequestDefaults();

			var maleThrumboRequest = new PawnGenerationRequest(
				kind: PawnKindDefOf.Thrumbo,
				fixedGender: Gender.Male,
				excludeBiologicalAgeRange: new(0f, 8f)
			).RequestDefaults();

			var femaleThrumboRequest = new PawnGenerationRequest(
				kind: PawnKindDefOf.Thrumbo,
				fixedGender: Gender.Female,
				allowPregnant: false,
				excludeBiologicalAgeRange: new(0f, 8f)
			).RequestDefaults();

			// Checks regarding basic sex-parts.

			Trial("Male Colonist + Female Colonist", () =>
			{
				var male = TestHelper.GenerateSeededPawn(maleColonistRequest);
				var female = TestHelper.GenerateSeededPawn(femaleColonistRequest);
				TryToReport(male, female, true);
			});

			Trial("Male Colonist + Male Colonist", () =>
			{
				var male1 = TestHelper.GenerateSeededPawn(maleColonistRequest);
				var male2 = TestHelper.GenerateSeededPawn(maleColonistRequest);
				TryToReport(male1, male2, false);
			});

			Trial("Female Colonist + Female Colonist", () =>
			{
				var female1 = TestHelper.GenerateSeededPawn(femaleColonistRequest);
				var female2 = TestHelper.GenerateSeededPawn(femaleColonistRequest);
				TryToReport(female1, female2, false);
			});

			Trial("Futa Colonist + Female Colonist", () =>
			{
				var male = TestHelper.GenerateSeededPawn(maleColonistRequest);
				var female = TestHelper.GenerateSeededPawn(femaleColonistRequest);
				if (TestHelper.MakeIntoFuta(male))
					TryToReport(male, female, true);
				else
				{
					var text = "Failed to Create Futa".Colorize(failedColor);
					sb.AppendLineTagged($"  {text}");
				}
			});

			Trial("Male Colonist + Futa Colonist", () =>
			{
				var male = TestHelper.GenerateSeededPawn(maleColonistRequest);
				var female = TestHelper.GenerateSeededPawn(femaleColonistRequest);
				if (TestHelper.MakeIntoFuta(female))
					TryToReport(male, female, true);
				else
				{
					var text = "Failed to Create Futa".Colorize(failedColor);
					sb.AppendLineTagged($"  {text}");
				}
			});

			Trial("Futa Colonist + Futa Colonist", () =>
			{
				var male = TestHelper.GenerateSeededPawn(maleColonistRequest);
				var female = TestHelper.GenerateSeededPawn(femaleColonistRequest);
				if (TestHelper.MakeIntoFuta(male) && TestHelper.MakeIntoFuta(female))
					TryToReport(male, female, true);
				else
				{
					var text = "Failed to Create Futa".Colorize(failedColor);
					sb.AppendLineTagged($"  {text}");
				}
			});

			// Tests regarding pregnancy.

			Trial("Male Colonist + Pregnant Female Colonist (Vanilla)", () =>
			{
				if (!ModsConfig.BiotechActive)
				{
					sb.AppendLine("  Biotech Not Active");
					return;
				}

				var male = TestHelper.GenerateSeededPawn(maleColonistRequest);
				var female = TestHelper.GenerateSeededPawn(femaleColonistRequest);
				if (VanillaHumanPregnancy(female, male) is { } hediff)
				{
					female.health.AddHediff(hediff);
					TryToReport(male, female, true);
				}
				else
				{
					var text = "Failed to Impregnate".Colorize(failedColor);
					sb.AppendLineTagged($"  {text}");
				}
			});

			Trial("Male Colonist + Pregnant Female Colonist (RJW)", () =>
			{
				var male = TestHelper.GenerateSeededPawn(maleColonistRequest);
				var female = TestHelper.GenerateSeededPawn(femaleColonistRequest);
				PregnancyHelper.AddPregnancyHediff(female, male);
				TryToReport(male, female, true);
			});

			Trial("Pregnant Male Colonist + Female Colonist (Vanilla)", () =>
			{
				if (!ModsConfig.BiotechActive)
				{
					sb.AppendLine("  Biotech Not Active");
					return;
				}

				var male = TestHelper.GenerateSeededPawn(maleColonistRequest);
				var female = TestHelper.GenerateSeededPawn(femaleColonistRequest);
				if (VanillaHumanPregnancy(male, female) is { } hediff)
				{
					male.health.AddHediff(hediff);
					TryToReport(male, female, true);
				}
				else
				{
					var text = "Failed to Impregnate".Colorize(failedColor);
					sb.AppendLineTagged($"  {text}");
				}
			});

			Trial("Pregnant Male Colonist + Female Colonist (RJW)", () =>
			{
				var male = TestHelper.GenerateSeededPawn(maleColonistRequest);
				var female = TestHelper.GenerateSeededPawn(femaleColonistRequest);
				PregnancyHelper.AddPregnancyHediff(male, female);
				TryToReport(male, female, true);
			});

			// Tests regarding artificial genitalia and sterilization.

			Trial("Sterilized Male Colonist + Female Colonist", () =>
			{
				if (!ModsConfig.BiotechActive)
				{
					sb.AppendLine("  Biotech Not Active");
					return;
				}

				var male = TestHelper.GenerateSeededPawn(maleColonistRequest);
				var female = TestHelper.GenerateSeededPawn(femaleColonistRequest);
				male.health.AddHediff(HediffDefOf.Sterilized);
				TryToReport(male, female, false);
			});

			Trial("Artificially Male Colonist + Female Colonist", () =>
			{
				var male = TestHelper.GenerateSeededPawn(maleColonistRequest);
				var female = TestHelper.GenerateSeededPawn(femaleColonistRequest);
				if (TestHelper.GiveArtificialGenitals(male))
					TryToReport(male, female, false);
				else
				{
					var text = "Failed to Render Artifical".Colorize(failedColor);
					sb.AppendLineTagged($"  {text}");
				}
			});

			Trial("Male Colonist + Sterilized Female Colonist", () =>
			{
				if (!ModsConfig.BiotechActive)
				{
					sb.AppendLine("  Biotech Not Active");
					return;
				}

				var male = TestHelper.GenerateSeededPawn(maleColonistRequest);
				var female = TestHelper.GenerateSeededPawn(femaleColonistRequest);
				female.health.AddHediff(HediffDefOf.Sterilized);
				TryToReport(male, female, false);
			});

			Trial("Male Colonist + Artificially Female Colonist", () =>
			{
				// The hydraulic vagina is still somehow fertile, btw.
				var male = TestHelper.GenerateSeededPawn(maleColonistRequest);
				var female = TestHelper.GenerateSeededPawn(femaleColonistRequest);
				if (TestHelper.GiveArtificialGenitals(female))
					TryToReport(male, female, true);
				else
				{
					var text = "Failed to Render Artifical".Colorize(failedColor);
					sb.AppendLineTagged($"  {text}");
				}
			});

			// Checks to verify the gender checks were disabled properly.

			Trial("Male Colonist + Cuntboy Colonist", () =>
			{
				var male = TestHelper.GenerateSeededPawn(maleColonistRequest);
				var female = TestHelper.GenerateSeededPawn(femaleColonistRequest);
				female.gender = Gender.Male;
				TryToReport(male, female, true);
			});

			Trial("Dickgirl Colonist + Female Colonist", () =>
			{
				var male = TestHelper.GenerateSeededPawn(maleColonistRequest);
				var female = TestHelper.GenerateSeededPawn(femaleColonistRequest);
				male.gender = Gender.Female;
				TryToReport(male, female, true);
			});

			// Tests versus animals.

			Trial("Male Thrumbo + Female Thrumbo", () =>
			{
				var male = TestHelper.GenerateSeededPawn(maleThrumboRequest);
				var female = TestHelper.GenerateSeededPawn(femaleThrumboRequest);
				TryToReport(male, female, true);
			});

			Trial("Male Thrumbo + Female Colonist", () =>
			{
				var male = TestHelper.GenerateSeededPawn(maleThrumboRequest);
				var female = TestHelper.GenerateSeededPawn(femaleColonistRequest);
				TryToReport(male, female, true);
			});

			Trial("Male Colonist + Female Thrumbo", () =>
			{
				var male = TestHelper.GenerateSeededPawn(maleColonistRequest);
				var female = TestHelper.GenerateSeededPawn(femaleThrumboRequest);
				TryToReport(male, female, true);
			});

			Find.WindowStack.Add(new Dialog_MessageBox(sb.ToString().Trim()));

			void Trial(string title, Action action)
			{
				try
				{
					sb.AppendLine(title);
					action();
				}
				catch (Exception ex)
				{
					var result = "Exception".Colorize(failedColor);
					sb.AppendLineTagged($"  {result} (see console logs)");
					logger.Error("Auto-test exception.", ex);
				}
				finally
				{
					sb.AppendLine();
				}
			}

			// Tries to give the `mother` a human pregnancy.
			Hediff_Pregnant VanillaHumanPregnancy(Pawn mother, Pawn father)
			{
				using var _seeded = Seeded.With(mother);
				var hediff = (Hediff_Pregnant)HediffMaker.MakeHediff(HediffDefOf.PregnantHuman, mother);
				hediff.Severity = 0.1f;
				var inheritedGeneSet = PregnancyUtility.GetInheritedGeneSet(father, mother, out var success);
				if (success)
				{
					hediff.SetParents(null, father, inheritedGeneSet);
					return hediff;
				}
				return null;
			}

			// Try the two pawns in both argument positions.
			void TryToReport(Pawn pawn1, Pawn pawn2, bool expectAccepted)
			{
				var text = expectAccepted ? "Accepted" : "Rejected";
				sb.AppendLine($"  Expecting: {text}");
				var result1 = PregnancyUtility.CanEverProduceChild(pawn1, pawn2);
				sb.AppendLine("  Given Order");
				sb.AppendLineTagged($"    Status: {ReportToStatus(result1, expectAccepted)}");
				sb.AppendLineTagged($"    Reason: {ReportToReason(result1)}");
				var result2 = PregnancyUtility.CanEverProduceChild(pawn2, pawn1);
				sb.AppendLine("  Reversed Order");
				sb.AppendLineTagged($"    Status: {ReportToStatus(result2, expectAccepted)}");
				sb.AppendLineTagged($"    Reason: {ReportToReason(result2)}");
			}

			string ReportToStatus(AcceptanceReport report, bool expectAccepted) =>
				(report.Accepted, expectAccepted) switch
				{
					(true, true) or (false, false) => "Passed".Colorize(passedColor),
					_ => "Failed".Colorize(failedColor)
				};

			string ReportToReason(AcceptanceReport report) =>
				report.Reason.NullOrEmpty() ? "(None)" : report.Reason;
		}
	}

	[HarmonyPatch(typeof(PregnancyUtility), "RandomLastName")]
	static class Patch_PregnancyUtility_RandomLastName
	{
		public static bool Prefix(ref Pawn geneticMother, ref Pawn birthingMother, ref Pawn father)
		{
			if (geneticMother != null)
				if (geneticMother.Name == null || !xxx.is_human(geneticMother))
				{
					geneticMother = null;
				}
			if (father != null)
				if (father.Name == null || !xxx.is_human(father))
				{
					father = null;
				}
			if (birthingMother != null)
				if (birthingMother.Name == null || !xxx.is_human(birthingMother))
				{
					birthingMother = null;
				}
			return true;
		}
	}

	// Fixes CompEggLayer and mating for humanlikes.
	// Note that this involves skipping over a check for the Biotech DLC, but as of build 1.4.3534 this doesn't unlock
	// any Biotech-only features or anything like that.
	[HarmonyPatch(typeof(Pawn), nameof(Pawn.Sterile))]
	class Pawn_Sterile
	{
		[HarmonyTranspiler]
		static IEnumerable<CodeInstruction> NotOwningBiotechWillNotMakeYouSterileButModdingProbablyWill(
			IEnumerable<CodeInstruction> instructions)
		{
			PropertyInfo biotechActive = AccessTools.DeclaredProperty(typeof(ModsConfig), nameof(ModsConfig.BiotechActive));
			bool foundBiotechCheck = false;

			foreach (var instruction in instructions)
			{
				// if (ModsConfig.BiotechActive) ...
				// => if (true) ...
				if (instruction.Calls(biotechActive.GetMethod))
				{
					yield return new CodeInstruction(OpCodes.Ldc_I4_1);
					foundBiotechCheck = true;
					continue;
				}
				yield return instruction;
			}

			if (!foundBiotechCheck)
			{
				ModLog.Error("Failed to patch Pawn.Sterile: Could not find `ModsConfig.BiotechActive`");
			}
		}
	}

	// Make the game use RJW's fertility capacity in vanilla pregnancy chance calculations
	[HarmonyPatch]
	class Various_GetStatValue
	{
		static IEnumerable<MethodBase> TargetMethods()
		{
			yield return AccessTools.Method(typeof(PregnancyUtility), nameof(PregnancyUtility.CanEverProduceChild));
			yield return AccessTools.Method(typeof(PregnancyUtility), nameof(PregnancyUtility.PregnancyChanceForPawn));
			yield return AccessTools.Method(typeof(Pawn), nameof(Pawn.Sterile));
		}

		[HarmonyTranspiler]
		static IEnumerable<CodeInstruction> UseRjwFertilityInstead(IEnumerable<CodeInstruction> instructionEnumerable, MethodBase original)
		{
			bool foundFertilityStatDefLoad = false;
			FieldInfo statDefOfFertility = AccessTools.DeclaredField(typeof(StatDefOf), nameof(StatDefOf.Fertility));

			var instructions = instructionEnumerable.ToList();
			for (int i = 0; i < instructions.Count; i++)
			{
				// pawn.GetStatValue(StatDefOf.Fertility)
				// => pawn.health.capacities.GetLevel(xxx.reproduction)
				if (instructions[i].LoadsField(statDefOfFertility) && i + 3 < instructions.Count)
				{
					foundFertilityStatDefLoad = true;
					yield return CodeInstruction.LoadField(typeof(Pawn), nameof(Pawn.health)).WithLabels(instructions[i].ExtractLabels());
					yield return CodeInstruction.LoadField(typeof(Pawn_HealthTracker), nameof(Pawn_HealthTracker.capacities));
					yield return CodeInstruction.LoadField(typeof(xxx), nameof(xxx.reproduction));
					yield return CodeInstruction.Call(typeof(PawnCapacitiesHandler), nameof(PawnCapacitiesHandler.GetLevel));

					// Skip GetStatValue call
					i += 3;
					continue;
				}
				yield return instructions[i];
			}

			if (!foundFertilityStatDefLoad)
			{
				ModLog.Error($"Failed to patch {original.Name}: Could not find `pawn.GetStatValue(StatDefOf.Fertility)`");
			}
		}
	}

	// Mostly prevent pawns born from RJW pregnancies from getting named "Baby".
	[HarmonyPatch(typeof(PawnBioAndNameGenerator), nameof(PawnBioAndNameGenerator.GiveAppropriateBioAndNameTo))]
	class PawnBioAndNameGenerator_GiveAppropriateBioAndNameTo
	{
		static readonly MethodInfo getPawnDevelopmentalStage = AccessTools.Method(typeof(Pawn), "get_DevelopmentalStage");

		[HarmonyTranspiler]
		static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructionEnumerable, MethodBase original)
		{
			var instructions = instructionEnumerable.ToList();
			bool found = false;

			for (int i = 0; i < instructions.Count; i++)
			{
				// replace bool flag = pawn.DevelopmentalStage.Baby(); with bool flag = isBiotechBaby(Pawn pawn);
				if (instructions[i].Calls(getPawnDevelopmentalStage))
				{
					yield return CodeInstruction.Call(typeof(PawnBioAndNameGenerator_GiveAppropriateBioAndNameTo), nameof(isBiotechBaby));
					i++;
					found = true;

				}
				else
				{
					yield return instructions[i];
				}

			}
			if (!found)
			{
				ModLog.Error($"Failed to patch {original.Name}: could not find pawn.DevelopmentalStage");
			}
		}

		public static bool isBiotechBaby(Pawn pawn)
		{
			return pawn.DevelopmentalStage.Baby() && RJWPregnancySettings.UseVanillaPregnancy;
		}

	}

	// Adjust the pregnancy approach tooltip to reflect the fact that it no longer affects the chance of pregnancy directly.
	[HarmonyPatch(typeof(PregnancyUtility), nameof(PregnancyUtility.GetDescription))]
	class PregnancyUtility_GetDescription
	{
		[HarmonyTranspiler]
		static IEnumerable<CodeInstruction> ModifyPregnancyApproachTooltip(IEnumerable<CodeInstruction> instructions)
		{
			foreach (var instruction in instructions)
			{
				if (instruction.LoadsConstant("PregnancyChance"))
				{
					yield return new CodeInstruction(OpCodes.Ldstr, "VaginalWeight");
				}
				else
				{
					yield return instruction;
				}
			}
		}
	}

	[HarmonyPatch(typeof(PawnColumnWorker_Pregnant), "GetIconFor")]
	public class PawnColumnWorker_Patch_Icon
	{
		public static void Postfix(Pawn pawn, ref Texture2D __result)
		{
			if (pawn.IsVisiblyPregnant()) __result = ContentFinder<Texture2D>.Get("UI/Icons/Animal/Pregnant", true);
		}
	}

	[HarmonyPatch(typeof(PawnColumnWorker_Pregnant), "GetTooltipText")]
	public class PawnColumnWorker_Patch_Tooltip
	{
		public static bool Prefix(Pawn pawn, ref string __result)
		{
			// Handles multi-pregnancy by getting the one nearest to birth.
			var pregHediff = PregnancyHelper.GetPregnancy(pawn);
			(var ticksCompleted, var ticksToBirth) = PregnancyHelper.GetProgressTicks(pregHediff);
			__result = "PregnantIconDesc".Translate(
				ticksCompleted.ToStringTicksToDays("F0"),
				ticksToBirth.ToStringTicksToDays("F0")
			);
			return false;
		}
	}

	[HarmonyPatch(typeof(TransferableUIUtility), "DoExtraIcons")]
	public class TransferableUIUtility_Patch_Icon
	{
		//private static readonly Texture2D PregnantIcon = ContentFinder<Texture2D>.Get("UI/Icons/Animal/Pregnant", true);
		public static void Postfix(Transferable trad, Rect rect, ref float curX, Texture2D ___PregnantIcon)
		{
			Pawn pawn = trad.AnyThing as Pawn;
			if (pawn?.health?.hediffSet != null && pawn.IsVisiblyPregnant())
			{
				Rect rect3 = new Rect(curX - 24f, (rect.height - 24f) / 2f, 24f, 24f);
				curX -= 24f;
				if (Mouse.IsOver(rect3))
				{
					TooltipHandler.TipRegion(rect3, PawnColumnWorker_Pregnant.GetTooltipText(pawn));
				}
				GUI.DrawTexture(rect3, ___PregnantIcon);
			}
		}
	}

	//[HarmonyPatch(typeof(AutoSlaughterManager))]
	//public class AutoSlaughterManager_AnimalsToSlaughter_rjw_preg
	//{
	//	public static void Postfix(AutoSlaughterManager __instance)
	//	{
	//		List<Pawn> __result = new List<Pawn>();
	//		var any_ins = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
	//		__result = (List<Pawn>)(__instance.GetType().GetField("AnimalsToSlaughter", any_ins).GetValue(__instance));
	//		//(__instance.GetType().GetField("AnimalsToSlaughter", any_ins).SetValue(__instance, newresult));

	//		//List<Pawn> __result = new List<Pawn>((List < Pawn > )typeof(AutoSlaughterManager).GetField("AnimalsToSlaughter", BindingFlags.Instance | BindingFlags.NonPublic));
	//		if (!__result.NullOrEmpty())
	//		{
	//			var newresult = __result;
	//			foreach (var pawn in __result)
	//			{
	//				if (pawn?.health?.hediffSet != null && pawn.IsPregnant(true))
	//				{
	//					if (__instance.configs.Any(x => x.allowSlaughterPregnant != true && x.animal == pawn.def))
	//					{
	//						newresult.Remove(pawn);
	//					}
	//				}

	//			}
	//			__result = newresult;
	//		}

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


code:
using System.Linq;
using Verse;
using HarmonyLib;
using RimWorld;
using System.Collections;
using System.Collections.Generic;

namespace rjw
{

	/// <summary>
	/// Necessary to prevent duplicate stat entries on harvested part, since stats are populated by each recipe that
	/// installs it.
	/// </summary>
	[HarmonyPatch(typeof(MedicalRecipesUtility), nameof(MedicalRecipesUtility.GetMedicalStatsFromRecipeDefs))]
	public static class FixDuplicatePartStats
	{
		class StatDrawEntryEqualityComparer : IEqualityComparer<StatDrawEntry>
		{
			public bool Equals(StatDrawEntry x, StatDrawEntry y) => x.LabelCap == y.LabelCap && x.ValueString == y.ValueString;

			public int GetHashCode(StatDrawEntry entry) => entry.LabelCap.GetHashCode() ^ entry.ValueString.GetHashCode();
		}

		static IEqualityComparer<StatDrawEntry> comparer = new StatDrawEntryEqualityComparer();

		static IEnumerable<StatDrawEntry> Postfix(IEnumerable<StatDrawEntry> recipes) => recipes.Distinct(comparer);
	}

	/// <summary>
	/// Patch all races into rjw parts recipes
	/// </summary>
	[StaticConstructorOnStartup]
	public static class MakeAllRacesUseSexPartRecipes
	{
		static MakeAllRacesUseSexPartRecipes()
		{
			//summons carpet bombing

			//inject races into rjw recipes
			foreach (RecipeDef x in	DefDatabase<RecipeDef>.AllDefsListForReading.Where(x => x.IsSurgery && (x.targetsBodyPart || !x.appliedOnFixedBodyParts.NullOrEmpty())))
			{
				if (x.appliedOnFixedBodyParts.Contains(xxx.genitalsDef)
					|| x.appliedOnFixedBodyParts.Contains(xxx.breastsDef)
					|| x.appliedOnFixedBodyParts.Contains(xxx.anusDef)
					|| x.modContentPack?.PackageId == "rim.job.world"
					|| x.modContentPack?.PackageId == "Safe.Job.World" //*sigh*
					//|| x.modContentPack?.PackageId == "Abraxas.RJW.RaceSupport" // for udders?
					)

					foreach (ThingDef thingDef in DefDatabase<ThingDef>.AllDefs.Where(thingDef =>
							thingDef.race != null && (
							thingDef.race.Humanlike ||
							thingDef.race.Animal
							)))
					{
						//filter out something, probably?
						//if (thingDef.race. == "Human")
						//	continue;

						if (!x.recipeUsers.Contains(thingDef))
						{
							x.recipeUsers.Add(item: thingDef);
							//Log.Message("recipe: " + x.defName + ", thing: " + thingDef.defName);

						}
					}
			}
		}
	}
}

repo_name: ElToro/rjw

license: mit


code:
using HarmonyLib;
using RimWorld;
using System;
using Verse;

namespace rjw
{
	///<summary>
	///RJW Designators checks/update
	///update designators only for selected pawn, once, instead of every tick(60 times per sec)
	///</summary>
	[HarmonyPatch(typeof(Selector), "Select")]
	[StaticConstructorOnStartup]
	static class PawnSelect
	{
		[HarmonyPrefix]
		private static bool Update_Designators_Permissions(Selector __instance, ref object obj)
		{
			if (obj is Pawn)
			{
				//ModLog.Message("Selector patch");
				Pawn pawn = (Pawn)obj;
				//ModLog.Message("pawn: " + xxx.get_pawnname(pawn));
				pawn.UpdatePermissions();
			}
			return true;
		}
	}

	//[HarmonyPatch(typeof(Dialog_InfoCard), "Setup")]
	////[HarmonyPatch(typeof(Dialog_InfoCard), "Dialog_InfoCard", new Type[] {typeof(Def)})]
	//[StaticConstructorOnStartup]
	//static class Button
	//{
	//	[HarmonyPostfix]
	//	public static bool Postfix()
	//	{
	//		ModLog.Message("InfoCardButton");
	//		return true;
	//	}
	//}
}


code:
namespace rjw
{
	/// <summary>
	/// Patch:
	/// recipes
	/// </summary>

	//TODO: inject rjw recipes
	//[HarmonyPatch(typeof(HealthCardUtility), "GenerateSurgeryOption")]
	//internal static class PATCH_HealthCardUtility_recipes
	//{
	//	//private static FloatMenuOption GenerateSurgeryOption(Pawn pawn, Thing thingForMedBills, RecipeDef recipe, IEnumerable<ThingDef> missingIngredients, BodyPartRecord part = null)
	//	//public FloatMenuOption(string label, Action action, MenuOptionPriority priority = MenuOptionPriority.Default, Action mouseoverGuiAction = null, Thing revalidateClickTarget = null, float extraPartWidth = 0, Func<Rect, bool> extraPartOnGUI = null, WorldObject revalidateWorldClickTarget = null);
	//	//floatMenuOption = new FloatMenuOption(text, action, MenuOptionPriority.Default, null, null, 0f, null, null);
	//	[HarmonyPostfix]
	//	private static void Postfix(ref FloatMenuOption __result, ref Pawn pawn)
	//	{

	//		ModLog.Message("PATCH_HealthCardUtility_recipes");
	//		ModLog.Message("PATCH_HealthCardUtility_recipes list: " + __result);
	//		//foreach (FloatMenuOption recipe in recipeOptionsMaker)
	//		//	{
	//		//		ModLog.Message("PATCH_HealthCardUtility_recipes: " + recipe);
	//		//	}

	//		return;
	//	}
	//}

	//erm.. idk ?
	//[HarmonyPatch(typeof(HealthCardUtility), "GetTooltip")]
	//internal static class PATCH_HealthCardUtility_GetTooltip
	//{
	//	[HarmonyPostfix]
	//	private static void Postfix(Pawn pawn)
	//	{
	//		ModLog.Message("GetTooltip");
	//		//ModLog.Message("PATCH_HealthCardUtility_recipes list: " + floatMenuOption);
	//		//foreach (FloatMenuOption recipe in recipeOptionsMaker)
	//		//	{
	//		//		ModLog.Message("PATCH_HealthCardUtility_recipes: " + recipe);
	//		//	}
	//		return;
	//	}
	//}

	//TODO: make toggle/floatmenu to parts switching
	//[HarmonyPatch(typeof(HealthCardUtility), "EntryClicked")]
	//internal static class PATCH_HealthCardUtility_EntryClicked
	//{
	//	[HarmonyPostfix]
	//	private static void Postfix(Pawn pawn)
	//	{
	//		ModLog.Message("EntryClicked");
	//		//ModLog.Message("PATCH_HealthCardUtility_recipes list: " + floatMenuOption);
	//		//foreach (FloatMenuOption recipe in recipeOptionsMaker)
	//		//	{
	//		//		ModLog.Message("PATCH_HealthCardUtility_recipes: " + recipe);
	//		//	}
	//		return;
	//	}
	//}
}


code:
using System.Linq;
using Verse;
using System.Collections.Generic;
using HarmonyLib;
using RimWorld;

namespace rjw
{
	/// <summary>
	/// Patch ui for hero mode
	/// - disable pawn control for non owned hero
	/// - disable equipment management for non owned hero
	/// hardcore mode:
	/// - disable equipment management for non hero
	/// - disable pawn rmb menu for non hero
	/// - remove drafting widget for non hero
	/// </summary>

	//disable forced works(rmb workgivers)
	[HarmonyPatch(typeof(FloatMenuMakerMap), "CanTakeOrder")]
	[StaticConstructorOnStartup]
	static class disable_FloatMenuMakerMap
	{
		[HarmonyPostfix]
		static void NonHero_disable_controls(ref bool __result, Pawn pawn)
		{
			if (RJWSettings.RPG_hero_control)
			{
				if ((pawn.IsDesignatedHero() && !pawn.IsHeroOwner()))
				{
					__result = false;   //not hero owner, disable menu
					return;
				}

				if (!pawn.IsDesignatedHero() && RJWSettings.RPG_hero_control_HC)
				{
					if (pawn.Drafted && pawn.CanChangeDesignationPrisoner() && pawn.CanChangeDesignationColonist())
					{
						//allow control over drafted pawns, this is limited by below disable_Gizmos patch
					}
					else
					{
						__result = false; //not hero, disable menu
					}
				}
			}
		}
	}

	//TODO: disable equipment management
	/*
	//disable equipment management
	[HarmonyPatch(typeof(ITab_Pawn_Gear), "CanControl")]
	static class disable_equipment_management
	{
		[HarmonyPostfix]
		static bool this_is_postfix(ref bool __result, Pawn selPawnForGear)
		{
			Pawn pawn = selPawnForGear;

			if (RJWSettings.RPG_hero_control)
			{
				if ((pawn.IsDesignatedHero() && !pawn.IsHeroOwner()))	//not hero owner, disable drafting
				{
					__result = false;   //not hero owner, disable menu
				}
				else if (!pawn.IsDesignatedHero() && RJWSettings.RPG_hero_control_HC)   //not hero, disable drafting
				{
					if (false)
					{
						//add some filter for bots and stuff? if there is such stuff
						//so it can be drafted and controlled for fighting
					}
					else
					{
						__result = false; //not hero, disable menu
					}
				}
			}
			return true;
		}
	}
	*/

	//TODO: allow shared control over non colonists(droids, etc)?
	//disable command gizmos
	[HarmonyPatch(typeof(Pawn), "GetGizmos")]
	[StaticConstructorOnStartup]
	static class disable_Gizmos
	{
		[HarmonyPostfix]
		[HarmonyPriority(100)]
		static IEnumerable<Gizmo> NonHero_disable_gizmos(IEnumerable<Gizmo> __result, Pawn __instance)
		{

			Pawn pawn = __instance;
			string disablementReason = string.Empty;

			if (RJWSettings.RPG_hero_control)
			{
				if ((pawn.IsDesignatedHero() && !pawn.IsHeroOwner()))	//not hero owner, disable drafting
				{
					disablementReason = "ForHeroRefuse1Desc";
				}
				else if (!pawn.IsDesignatedHero() && RJWSettings.RPG_hero_control_HC)   //not hero, disable drafting
				{
					//no permission to change designation for NON prisoner hero/ other player
					if (pawn.CanChangeDesignationPrisoner() && pawn.CanChangeDesignationColonist()
							&& (pawn.kindDef.race.defName.Contains("AIRobot")
							|| (pawn.kindDef.race.defName.Contains("Droid") && !pawn.kindDef.race.defName.Contains("AndDroid"))
							|| pawn.kindDef.race.defName.Contains("RPP_Bot")
							))
					//if (false)
					{
						//add some filter for bots and stuff? if there is such stuff
						//so it can be drafted and controlled for fighting
					}
					else
					{
						disablementReason = "ForHeroRefuseHCDesc";
					}
				}
			}

			foreach (var gizmo in __result)
			{
				if ( disablementReason.NullOrEmpty() || (!(gizmo is Command)) )  //we do not filter out non-command Gizmos, such as shield bar or psychic entropy
				{
					yield return gizmo;
				}

				//ModLog.Message("Gizmo for " + xxx.get_pawnname(__instance) + " type: " + gizmo.GetType()+ ": " + gizmo);

				if (!disablementReason.NullOrEmpty())
				{
					if (gizmo is Verse.Command_VerbTarget) { //weapon icons
						gizmo.Disable(disablementReason.Translate());
						yield return gizmo;
					}
				}
				//all other command gizmos are dropped

			}
		}
	}
}


code:
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using RimWorld;
using Verse;
using UnityEngine;
using Multiplayer.API;

namespace rjw
{
	///
	/// Harmony patch to toggle the RJW designation box showing
	///
	[HarmonyPatch(typeof(PlaySettings), "DoPlaySettingsGlobalControls")]
	[StaticConstructorOnStartup]
	public static class RJW_corner_toggle
	{
		static readonly Texture2D icon = ContentFinder<Texture2D>.Get("UI/Commands/ComfortPrisoner_off");

		[HarmonyPostfix]
		public static void add_RJW_toggle(WidgetRow row, bool worldView)
		{
			if (worldView) return;
			if (!RJWSettings.show_RJW_designation_box) return;
			row.ToggleableIcon(ref RJWSettings.show_RJW_designation_box, icon, "RJW_designation_box_desc".Translate());
		}
	}

	///
	///Compact button group containing rjw designations on pawn
	///
	[HarmonyPatch(typeof(Pawn), "GetGizmos")]
	[StaticConstructorOnStartup]
	static class Rjw_buttons
	{
		[HarmonyPostfix]
		[HarmonyPriority(99)]
		static IEnumerable<Gizmo> add_designation_box(IEnumerable<Gizmo> __result, Pawn __instance)
		{
			foreach (var gizmo in __result)
			{
				yield return gizmo;
			}

			if (!RJWSettings.show_RJW_designation_box) yield break;
			if (!(__instance.Faction == Faction.OfPlayer || __instance.IsPrisonerOfColony)) yield break;
			//ModLog.Message("Harmony patch submit_button is called");
			var pawn = __instance;
			yield return new RJWdesignations(pawn);
		}
	}

	///
	///Submit gizmo
	///
	[HarmonyPatch(typeof(Pawn), "GetGizmos")]
	[StaticConstructorOnStartup]
	static class submit_button
	{
		[HarmonyPostfix]
		[HarmonyPriority(101)]
		static IEnumerable<Gizmo> add_button(IEnumerable<Gizmo> __result, Pawn __instance)
		{
			foreach (var gizmo in __result)
			{
				yield return gizmo;
			}

			//ModLog.Message("Harmony patch submit_button is called");
			var pawn = __instance;
			var enabled = RJWSettings.submit_button_enabled;

			if (enabled && pawn.IsColonistPlayerControlled && pawn.Drafted)
				if (pawn.CanChangeDesignationColonist())
					if (!(pawn.kindDef.race.defName.Contains("Droid") && !AndroidsCompatibility.IsAndroid(pawn)))
					{
						yield return new Command_Action
						{
							defaultLabel = "CommandSubmit".Translate(),
							icon = submit_icon,
							defaultDesc = "CommandSubmitDesc".Translate(),
							action = delegate
							{
								LayDownAndAccept(pawn);
							},
							hotKey = KeyBindingDefOf.Misc3
						};
					}

		}

		static Texture2D submit_icon = ContentFinder<Texture2D>.Get("UI/Commands/Submit", true);
		static HediffDef submit_hediff = HediffDef.Named("Hediff_Submitting");

		[SyncMethod]
		static void LayDownAndAccept(Pawn pawn)
		{
			//Log.Message("Submit button is pressed for " + pawn);
			pawn.health.AddHediff(submit_hediff);
		}
	}
}


code:
using System;
using RimWorld;
using Verse;
using HarmonyLib;

namespace rjw
{
	[HarmonyPatch(typeof(CompAbilityEffect_WordOfLove), "ValidateTarget")]
	internal static class PATCH_CompAbilityEffect_WordOfLove_ValidateTarget
	{
		[HarmonyPrefix]
		static bool GenderChecks(ref LocalTargetInfo target, LocalTargetInfo ___selectedTarget, ref bool __result)
		{
			Pawn pawn = ___selectedTarget.Pawn;
			Pawn pawn2 = target.Pawn;
			if (pawn != pawn2 && pawn != null && pawn2 != null)
			{
				__result = !xxx.is_asexual(pawn) && (xxx.is_bisexual(pawn) || xxx.is_pansexual(pawn) || (xxx.is_heterosexual(pawn) && pawn.gender != pawn2.gender) || (xxx.is_homosexual(pawn) && pawn.gender == pawn2.gender));
				if (__result == false)
				{ Messages.Message("AbilityCantApplyWrongAttractionGender".Translate(pawn, pawn2), pawn, MessageTypeDefOf.RejectInput, false); }
				return false;
			}
			return true;
		}
	}
}


code:
using System.Collections.Generic;
using RimWorld;
using Verse;

namespace rjw
{
	public class HediffCompProperties_SexPart : HediffCompProperties
	{
		public HediffCompProperties_SexPart()
		{
			compClass = typeof(HediffComp_SexPart);
		}
	}

	public class HediffCompProperties_Ovipositor : HediffCompProperties
	{

		// Currently unused in favour of searching parentDefs of every egg def, should refactor
		public List<HediffDef_InsectEgg> eggs;

		public IntRange eggIntervalTicks = new(GenDate.TicksPerDay, GenDate.TicksPerDay);

		public FloatRange eggCount = FloatRange.One;

		public HediffCompProperties_Ovipositor()
		{
			compClass = typeof(HediffComp_Ovipositor);
		}

		public override IEnumerable<string> ConfigErrors(HediffDef parentDef)
		{
			foreach (string err in base.ConfigErrors(parentDef))
			{
				yield return err;
			}
			// if (eggs.NullOrEmpty())
			// {
			// 	yield return "One or more <eggs> is required";
			// }
		}
	}

	public class HediffCompProperties_GrowsWithOwner : HediffCompProperties
	{
		public bool evenIfTransplanted;

		public HediffCompProperties_GrowsWithOwner()
		{
			compClass = typeof(HediffComp_GrowsWithOwner);
		}
	}
}


code:
using System.Collections.Generic;
using Verse;
using RimWorld;

namespace rjw
{
	public class HediffComp_GrowsWithOwner : HediffComp
	{
		public float lastOwnerSize = -1;

		public HediffCompProperties_GrowsWithOwner Props => (HediffCompProperties_GrowsWithOwner)props;

		public override void CompPostTick(ref float severityAdjustment)
		{
			//change pawn parts sizes for kids(?)
			//update size if pawn bodysize changed from last check
			if (Pawn.BodySize != lastOwnerSize)
			{
				if(parent.TryGetComp<HediffComp_SexPart>(out var partComp))
				{
					if (!partComp.isTransplant || Props.evenIfTransplanted)	//no changes for transplants
					{
						//get ~relative size variable from calculation()
						var relativeSize = partComp.baseSize;
						relativeSize /= partComp.originalOwnerSize;	//remove old lifestage bodysize mod
						relativeSize *= Pawn.BodySize;              //add new lifestage bodysize mod
						partComp.baseSize = relativeSize;           //save new size

						partComp.originalOwnerSize = Pawn.BodySize; //update owner size

						partComp.UpdateSeverity();			// update/reset part size
					}
					else
					{
						// otherwise, keep severity the same
						partComp.UpdateSeverity(parent.Severity);
					}
				}
				lastOwnerSize = Pawn.BodySize;
			}
		}

		public override void CompExposeData()
		{
			base.CompExposeData();
			Scribe_Values.Look(ref lastOwnerSize, "lastOwnerSize");
		}
	}
}


code:
using System.Collections.Generic;
using System.Linq;
using Multiplayer.API;
using RimWorld;
using Verse;

namespace rjw
{
	public class HediffComp_Ovipositor : HediffComp
	{
		public IntRange eggInterval;

		private int nextEggTick;

		private static HediffDef_InsectEgg UnknownEgg = DefDatabase<HediffDef_InsectEgg>.GetNamed("RJW_UnknownEgg");

		public HediffCompProperties_Ovipositor Props => props as HediffCompProperties_Ovipositor;

		public override void CompPostMake()
		{
			eggInterval = Props.eggIntervalTicks;
		}
		/// <summary>
		/// egg production ticks
		/// </summary>
		public override void CompPostTick(ref float severityAdjustment)
		{
			base.CompPostTick(ref severityAdjustment);

			var thisTick = Find.TickManager.TicksGame;

			if (thisTick >= nextEggTick)
			{
				var def = parent.def as HediffDef_SexPart;
				var pawn = parent.pawn;
				var isPlayerFaction = pawn.Faction?.IsPlayer ?? false; // Colonists/animals/mechs/mutants
				var onHomeMap = pawn.Map?.IsPlayerHome ?? false;

				if (!onHomeMap && !isPlayerFaction && !pawn.IsPrisonerOfColony && !pawn.IsSlaveOfColony)
				{
					return;
				}

				if (pawn.health.capacities.GetLevel(PawnCapacityDefOf.Moving) <= 0.5)
				{
					return;
				}

				float maxEggsSize = pawn.BodySize / 10f;
				if (xxx.has_quirk(pawn, "Incubator"))
				{
					maxEggsSize *= 2;
				}
				if (Genital_Helper.has_ovipositorF(pawn))
				{
					maxEggsSize *= 4;
				}

				float eggedSize = GetCombinedEggSize(pawn);

				if (RJWSettings.DevMode) ModLog.Message($"{xxx.get_pawnname(pawn)} filled with {eggedSize} out of max capacity of {maxEggsSize} eggs.");
				if (eggedSize < maxEggsSize)
				{
					HediffDef_InsectEgg egg;

					if (RJWSettings.DevMode) ModLog.Message($" Trying to find a {pawn.kindDef} egg");
					if (!TryGetEgg(pawn.kindDef.defName, out egg))
					{
						if (RJWSettings.DevMode) ModLog.Message($" No {pawn.kindDef} egg found, defaulting to UnknownEgg");
						egg = UnknownEgg;
					}

					if (RJWSettings.DevMode) ModLog.Message($"I choose you, {egg}!");

					var parentPart = parent.Part;
					if (parentPart != null)
					{
						AddEggs(egg, parentPart, Props.eggCount);
					}
				}

				// Reset for next egg.
				nextEggTick = RandomNextEggTick(thisTick);
			}
		}

		private static float GetCombinedEggSize(Pawn pawn)
		{
			float eggedSize = 0;
			List<Hediff_InsectEgg> ownEggs = new();
			pawn.health.hediffSet.GetHediffs(ref ownEggs);
			foreach (var ownEgg in ownEggs)
			{
				eggedSize += ownEgg.eggssize;
			}

			return eggedSize;
		}

		[SyncMethod]
		private int RandomNextEggTick(int fromTick)
		{
			return fromTick + eggInterval.RandomInRange;
		}

		[SyncMethod]
		private static bool TryGetEgg(string defname, out HediffDef_InsectEgg result)
		{
			var possibleEggs = PossibleEggs(defname);

			if(RJWSettings.DevMode) ModLog.Message($"TryGetEgg: Possible eggs for {defname} are {possibleEggs.Select(egg => egg.defName).ToCommaList()}");

			return possibleEggs.TryRandomElement(out result);
		}

		public static IEnumerable<HediffDef_InsectEgg> PossibleEggs(string defName)
		{
			if (defName == null)
			{
				return Enumerable.Empty<HediffDef_InsectEgg>();
			}
			var eggOptions = DefDatabase<HediffDef_InsectEgg>.AllDefs.Where(eggDef => eggDef.CanHaveParent(defName)).ToList();

			// Remove the unknown egg if we have other options
			if(eggOptions.Count > 1)
			{
				eggOptions = eggOptions.Where(eggDef => eggDef.defName != "RJW_UnknownEgg").ToList();
			}
			return eggOptions;
		}

		[SyncMethod]
		public void AddEggs(HediffDef_InsectEgg eggDef, BodyPartRecord bodyPart, FloatRange range)
		{
			float eggScore = range.RandomInRange + 0.5f;

			if (RJWSettings.DevMode)
			{
				ModLog.Message($"Added egg count: {eggScore}, int: {(int)eggScore}");
				ModLog.Message($"Min egg count: {range.min}, int: {(int)range.min}");
				ModLog.Message($"Max egg count: {range.max}, int: {(int)range.max}");
			}

			int eggsToAdd = (int) eggScore;
			for (int i = 0; i < eggsToAdd; i++)
			{
				var addedEgg = parent.pawn.health.AddHediff(eggDef, bodyPart) as Hediff_InsectEgg;
				addedEgg?.InitImplanter(parent.pawn);
			}
		}

		public override void CompExposeData()
		{
			base.CompExposeData();
			Scribe_Values.Look(ref nextEggTick, "nextEggTick");
			Scribe_Values.Look(ref eggInterval, "eggInterval", defaultValue: Props.eggIntervalTicks);
		}
	}
}


code:
using Verse;
using RimWorld;
using Multiplayer.API;
using System.Collections.Generic;
using System.Linq;

using ShowParts = rjw.RJWSettings.ShowParts;
using System.Text;

namespace rjw
{
	/// <summary>
	/// Comp for rjw hediff parts
	/// </summary>
	public class HediffComp_SexPart : HediffComp
	{
		public string SizeLabel => parent.CurStage?.label ?? "";

		public string originalOwnerRace = "";      //base race when part created
		public string previousOwner = "";            //base race when part created

		private float? forcedSize;
		public float baseSize;                              	// Absolute base size when part created, someday alter by operation
		public float originalOwnerSize = 0;                         // Body size of this part's first owner. Not used in calculations.
		public bool isTransplant = false;                   //transplanted from someone else

		// Whether this part could ever have been seen by the player based on mod settings and apparel.
		public bool discovered;

		private bool initialised;

		public new HediffDef_SexPart Def => (HediffDef_SexPart)base.Def;

		public HediffCompProperties_SexPart Props => (HediffCompProperties_SexPart)props;

		public float partFluidMultiplier;            // Multiplier on Milk/Ejaculation/Wetness
		private SexFluidDef fluidOverride;           // Cummies/milk - insectjelly/honey etc
		public float FluidAmount => GetFluidAmount();
		public float FluidMultiplier => GetFluidMultiplier();
		public SexFluidDef Fluid
		{
			get => fluidOverride ?? Def.fluid;
			set {
				if (value == Def.fluid)
				{
					fluidOverride = null;
				}
				else
				{
					fluidOverride = value;
				}
			}
		}
		public float GetFluidMultiplier()
		{
			return Def.GetFluidMultiplier(GetSeverity(), partFluidMultiplier, GetBodySize(), SexUtility.ScaleToHumanAge(Pawn));
		}

		public float GetFluidAmount()
		{
			return Def.GetFluidAmount(GetFluidMultiplier());
		}

		private List<string> DefaultPositionNames
		{
			get
			{
				List<string> result = Def.defaultBodyPartList ?? new();
				string singleDefaultPart = Def.defaultBodyPart;
				if (!singleDefaultPart.NullOrEmpty() && !result.Contains(singleDefaultPart))
				{
					result.Add(singleDefaultPart);
				}
				return result;
			}
		}

		public float Size
		{
			get => GetSize();
		}

		public bool HasForcedSize => forcedSize.HasValue;
		public float GetSize() {
			if (forcedSize.HasValue)
			{
				return forcedSize.Value;
			}
			return baseSize;
		}
		public float GetSeverity()
		{
			// Severity = baseSize / Pawn.BodySize;
			return GetSize() / Pawn.BodySize;
		}
		public void SetSeverity(float newSeverity, bool sync = true)
		{
			if(!forcedSize.HasValue)
			{
				baseSize = newSeverity * Pawn.BodySize;
			}
			if (sync)
			{
				SyncSeverity();
			}
		}

		public void SyncSeverity()
		{
			parent.Severity = GetSeverity();
		}
		public void UpdateSeverity(float newSeverity = -1f)
		{
			if(newSeverity < 0)
			{
				SyncSeverity();
			}
			else
			{
				SetSeverity(newSeverity);
			}
		}
		public void ForceSize(float size)
		{
			forcedSize = size;
			UpdateSeverity();
		}
		public void UnforceSize()
		{
			forcedSize = null;
			UpdateSeverity();
		}
		public float GetBodySize()
		{
			return Pawn.BodySize;
		}

		/// <summary>
		/// save data
		/// </summary>
		public override void CompExposeData()
		{
			base.CompExposeData();

			Scribe_Values.Look(ref baseSize, nameof(baseSize));
			Scribe_Values.Look(ref originalOwnerSize, nameof(originalOwnerSize));
			Scribe_Values.Look(ref originalOwnerRace, nameof(originalOwnerRace));
			Scribe_Values.Look(ref previousOwner, nameof(previousOwner));
			Scribe_Defs.Look(ref fluidOverride, nameof(fluidOverride));
			Scribe_Values.Look(ref partFluidMultiplier, nameof(partFluidMultiplier), defaultValue: Def.GetRandomPartFluidMultiplier());
			Scribe_Values.Look(ref isTransplant, nameof(isTransplant));
			Scribe_Values.Look(ref forcedSize, nameof(forcedSize));
			Scribe_Values.Look(ref discovered, nameof(discovered));
			Scribe_Values.Look(ref initialised, nameof(initialised), defaultValue: true);
		}

		public override void CompPostPostAdd(DamageInfo? dinfo)
		{
			base.CompPostPostAdd(dinfo);
			try//error at world gen
			{
				Pawn.GetRJWPawnData().genitals = new List<Hediff>();
				Pawn.GetRJWPawnData().breasts = new List<Hediff>();
				Pawn.GetRJWPawnData().anus = new List<Hediff>();
			}
			catch
			{
			}
			if (!initialised)
			{
				Init(reroll: false);
			}
		}

		public override void CompPostMake()
		{
			base.CompPostMake();
			// A null pawn is only possible in vanilla in verb-related debug outputs, but better safe than sorry.
			if (Pawn != null)
			{
				Init();
			}
		}

		public override void CompPostPostRemoved()
		{
			base.CompPostPostRemoved();
			try
			{
				Pawn.GetRJWPawnData().genitals = new List<Hediff>();
				Pawn.GetRJWPawnData().breasts = new List<Hediff>();
				Pawn.GetRJWPawnData().anus = new List<Hediff>();
			}
			catch
			{
			}
		}

		public override bool CompDisallowVisible()
		{
			if (base.CompDisallowVisible())
			{
				return true;
			}

			if (RJWSettings.ShowRjwParts == ShowParts.Hide)
			{
				return true;
			}

			if (discovered)
			{
				return false;
			}

			// Show if required by settings, or at game start if in dev mode
			if (RJWSettings.ShowRjwParts != ShowParts.Known || (Current.ProgramState != ProgramState.Playing && Prefs.DevMode))
			{
				discovered = true;
				return false;
			}

			// Show for hero and apparel-less pawns
			if (Pawn.IsDesignatedHero() && Pawn.IsHeroOwner() || Pawn.apparel == null)
			{
				discovered = true;
			}
			else
			{
				var partGroups = parent.Part.groups;
				if (!partGroups.Any(group => Pawn.apparel.BodyPartGroupIsCovered(group)))
				{
					discovered = true;
				}
			}
			return !discovered;
		}

		private static readonly StringBuilder sb = new();

		/// <summary>
		/// show part info in healthtab
		/// </summary>
		public override string CompTipStringExtra
		{
			get
			{
				if (!initialised)
				{
					ModLog.Warning(" " + xxx.get_pawnname(Pawn) + " part " + parent.def.defName + " is broken, resetting");
					Init();
					UpdateSeverity();
					updatepartposition();
				}
				// In theory the tip should be a condensed form and the longer form should be elsewhere, but where?
				sb.Clear();
				IEnumerable<string> tipLines = GetTipString(RJWSettings.ShowRjwParts == RJWSettings.ShowParts.Extended);
				foreach(string line in tipLines)
				{
					sb.AppendLine(line);
				}
				return sb.ToString();
			}
		}

		public IEnumerable<string> GetTipString(bool extended)
		{
			//ModLog.Message(" CompTipStringExtra " + xxx.get_pawnname(Pawn) + " " + parent.def.defName);
			// TODO: Part type should be a property in the extension.
			//StringBuilder defstringBuilder = new StringBuilder();
			if (parent.Part != null)
			{
				if (Def.genitalFamily == GenitalFamily.Breasts)
				{
					return GetBreastTip(extended);
				}
				else if (Def.genitalFamily == GenitalFamily.Penis)
				{
					return GetPenisTip(extended);
				}
				else if (Def.genitalFamily == GenitalFamily.Anus || Def.genitalFamily == GenitalFamily.Vagina)
				{
					return GetOrificeTip(extended);
				}
				else if (Def.genitalFamily == GenitalFamily.FemaleOvipositor || Def.genitalFamily == GenitalFamily.MaleOvipositor)
				{
					return GetEggTip();
				}
			}

			return Enumerable.Empty<string>();
		}

		private string _eggTip = null;
		IEnumerable<string> GetEggTip()
		{
			if (Def.genitalTags.Contains(GenitalTag.CanEgg))
			{
				if (_eggTip == null)
				{
					var possibleEggNames = HediffComp_Ovipositor.PossibleEggs(Pawn.kindDef?.defName)
						.SelectMany(eggDef => eggDef.childrenDefs)
						.Select(defName => DefDatabase<PawnKindDef>.GetNamedSilentFail(defName)?.LabelCap.ToString())
						.Where(label => label != null)
						.Distinct().ToList();

					_eggTip = possibleEggNames.ToCommaList();
				}
				yield return "RJW_PartInfo_eggs".Translate(_eggTip);
			}
		}

		IEnumerable<string> GetPenisTip(bool extended)
		{
			if (PartSizeCalculator.TryGetLength(parent, out float length))
			{
				yield return "RJW_PartInfo_length".Translate(length.ToString("F1"));
			}

			if (!extended)
			{
				yield break;
			}

			if (PartSizeCalculator.TryGetGirth(parent, out float girth))
			{
				yield return "RJW_PartInfo_girth".Translate(girth.ToString("F1"));
			}

			if (PartSizeCalculator.TryGetPenisWeight(parent, out float weight))
			{
				yield return "RJW_PartInfo_weight".Translate(weight.ToString("F1"));
			}

			foreach (var line in GetFluidTip())
			{
				yield return line;
			}

			foreach (var line in GetPropertiesTip())
			{
				yield return line;
			}
		}

		IEnumerable<string> GetOrificeTip(bool extended)
		{
			if (!extended)
			{
				yield break;
			}
			//defstringBuilder.Append(this.Def.description);
			//defstringBuilder.AppendLine("Cum: " + FluidType);
			//defstringBuilder.AppendLine("Ejaculation: " + (FluidAmmount * FluidModifier).ToString("F2") + "ml");

			// TODO: Should be scaled bigger than penis size either in xml or here.
			if (PartSizeCalculator.TryGetLength(parent, out float length))
			{
				yield return "RJW_PartInfo_maxLength".Translate(length.ToString("F1"));
			}

			if (PartSizeCalculator.TryGetGirth(parent, out float girth))
			{
				yield return "RJW_PartInfo_maxGirth".Translate(girth.ToString("F1"));
			}

			foreach (var line in GetFluidTip())
			{
				yield return line;
			}

			foreach (var line in GetPropertiesTip())
			{
				yield return line;
			}
		}

		IEnumerable<string> GetBreastTip(bool extended)
		{
			if(!extended)
			{
				foreach (var line in GetBraCupTip())
				{
					yield return line;
				}
				yield break;
			}

			if (PartSizeCalculator.TryGetBreastSize(parent, out BreastSize breastSize))
			{
				yield return "RJW_PartInfo_braSize".Translate(breastSize.GetBandSize(), breastSize.GetCupSize());
				yield return "RJW_PartInfo_weight".Translate(breastSize.weight.ToString("F3"));
			}

			foreach (var line in GetFluidTip())
			{
				yield return line;
			}

			foreach (var line in GetPropertiesTip())
			{
				yield return line;
			}
		}

		IEnumerable<string> GetBraCupTip()
		{
			if(GetSeverity() >= 0.02f)
			{
				if (PartSizeCalculator.TryGetBreastSize(parent, out BreastSize breastSize))
				{
					yield return "RJW_PartInfo_braCup".Translate(breastSize.GetCupSize());
				}
			}
		}

		IEnumerable<string> GetFluidTip()
		{
			if (FluidAmount != 0 && Fluid != null)
			{
				yield return "RJW_PartInfo_fluidTypeFluidAmount".Translate(
					Fluid.LabelCap,
					FluidAmount.ToString("F1"));
			}

			if (RJWSettings.DevMode)
			{
				yield return "RJW_PartInfo_fluidMultiplier".Translate($"{FluidMultiplier:F3} ({partFluidMultiplier:F3})");
			}
		}

		IEnumerable<string> GetPropertiesTip()
		{
			var showTransplant = isTransplant && parent is Hediff_NaturalSexPart;
			if (Def.partTags.Any() || showTransplant)
			{
				var tagList = Def.partTags.ToCommaList();
				if(showTransplant)
				{
					tagList = Def.partTags.Prepend("Transplant").ToCommaList();
				}
				yield return "RJW_PartInfo_properties".Translate(tagList);
			}
		}

		[SyncMethod]
		public void updatepartposition()
		{
			if (parent.def is not HediffDef_SexPart def)
			{
				return;
			}

			//Log.Message("0 " + (parent.Part == null));
			//Log.Message("1 " +parent.Part);
			//Log.Message("2 " +parent.Part.def);
			//Log.Message("3 " +parent.Part.def.defName);
			//Log.Message("4 " +partBase);
			//Log.Message("5 " +partBase.DefaultBodyPart);
			//ModLog.Message("pawn " + xxx.get_pawnname(Pawn) + ", part: " + parent.def.defName);
			if (parent.Part == null)
			{
				var bp = Pawn.RaceProps.body.AllParts.Find(x => DefaultPositionNames.Any(x.def.defName.Contains));
				if (bp != null)
				{
					ModLog.Warning(" " + xxx.get_pawnname(Pawn) + " part is null/wholebody? is in wrong BodyPart position, resetting to default: " + def.defaultBodyPart);
					parent.Part = bp;
					Pawn.GetRJWPawnData().genitals = new List<Hediff>();
					Pawn.GetRJWPawnData().breasts = new List<Hediff>();
					Pawn.GetRJWPawnData().anus = new List<Hediff>();
				}
				else
				{
					ModLog.Message(" " + xxx.get_pawnname(Pawn) + " default bodypart not found: " + def.defaultBodyPart + " -skip.");
				}
			}
			else if (def.defaultBodyPart != "" && !parent.Part.def.defName.Contains(def.defaultBodyPart))
			{
				//Log.Message("f");
				//Log.Message(partBase.DefaultBodyPartList.ToLineList());
				if (def.defaultBodyPartList.Any() && def.defaultBodyPartList.Any(parent.Part.def.defName.Contains))
				{
					return;
				}
				var bp = Pawn.RaceProps.body.AllParts.Find(x => x.def.defName.Contains(def.defaultBodyPart));
				if (bp != null)
				{
					ModLog.Warning(" " + xxx.get_pawnname(Pawn) + " part " + parent.def.defName + " is in wrong BodyPart position, resetting to default: " + def.defaultBodyPart);
					parent.Part = bp;
					Pawn.GetRJWPawnData().genitals = new List<Hediff>();
					Pawn.GetRJWPawnData().breasts = new List<Hediff>();
					Pawn.GetRJWPawnData().anus = new List<Hediff>();
				}
				else
				{
					ModLog.Message(" " + xxx.get_pawnname(Pawn) + " default bodypart not found: " + def.defaultBodyPart + " -skip.");
				}
				//if (pawn.IsColonist)
				//{
				//	Log.Message(xxx.get_pawnname(pawn) + " has broken hediffs, removing " + this.ToString());
				//	Log.Message(Part.ToString());
				//	Log.Message(bp.def.defName);
				//	Log.Message(partBase.DefaultBodyPart.ToString());
				//}
			}
		}

		/// <summary>
		/// fill comp data
		/// </summary>
		[SyncMethod]
		public void Init(Pawn pawn = null, bool reroll = true)
		{
			pawn ??= Pawn;

			if (originalOwnerSize == 0)
			{
				originalOwnerSize = pawn.BodySize;
			}

			if (reroll || baseSize == 0)
			{
				//var s = (parent is Hediff_NaturalSexPart) ? originalOwnerSize : Pawn.RaceProps.baseBodySize;
				baseSize = CalculateSize(pawn, originalOwnerSize);
			}

			if(reroll)
			{
				partFluidMultiplier = Def.GetRandomPartFluidMultiplier();
			}

			if (originalOwnerRace.NullOrEmpty())
			{
				originalOwnerRace = pawn?.kindDef.race.LabelCap ?? "RJW_PartInfo_unknownSpecies".Translate();
			}
			previousOwner = pawn?.LabelNoCount ?? "RJW_PartInfo_unknownOwner".Translate();

			UpdateSeverity();
			initialised = true;
		}

		[SyncMethod]
		public float CalculateSize(Pawn pawn, float bodySize)
		{
			float relativeSize;
			if (Def.genitalFamily == GenitalFamily.Breasts && pawn?.gender == Gender.Male && !Rand.Chance(TrapChance(pawn)))
			{
				relativeSize = .01f;
			}
			else
			{
				relativeSize = Def.GetRandomSize();
			}
			return relativeSize * bodySize;
		}

		private static float TrapChance(Pawn pawn)
		{
			if (RJWSettings.MaleTrap && !xxx.is_animal(pawn))
			{
				if (xxx.is_nympho(pawn))
				{
					return RJWSettings.futa_nymph_chance;
				}
				if (pawn.Faction != null && (int)pawn.Faction.def.techLevel < 5)
				{
					return RJWSettings.futa_natives_chance;
				}
				return RJWSettings.futa_spacers_chance;
			}
			return 0f;
		}
	}
}


code:
using Verse;
using RimWorld;

namespace rjw
{
	/// <summary>
	/// FeelingBroken raise/lower severity
	/// </summary>
	public class HediffCompProperties_FeelingBrokenSeverityReduce : HediffCompProperties
	{
		public HediffCompProperties_FeelingBrokenSeverityReduce()
		{
			this.compClass = typeof(HediffComp_FeelingBrokenSeverityReduce);
		}

		public SimpleCurve severityPerDayReduce;
	}
	class HediffComp_FeelingBrokenSeverityReduce : HediffComp_SeverityModifierBase
	{
		private HediffCompProperties_FeelingBrokenSeverityReduce Props
		{
			get
			{
				return (HediffCompProperties_FeelingBrokenSeverityReduce)this.props;
			}
		}
		public override void CompPostTick(ref float severityAdjustment)
		{
			base.CompPostTick(ref severityAdjustment);
			if (base.Pawn.IsHashIntervalTick(SeverityUpdateInterval))
			{
				float num = this.SeverityChangePerDay();
				num *= 0.00333333341f;
				if (xxx.has_traits(Pawn))
				{
					if (xxx.RoMIsActive)
						if (Pawn.story.traits.HasTrait(xxx.Succubus))
							num *= 4.0f;
					if (Pawn.story.traits.HasTrait(VanillaTraitDefOf.Tough))
					{
						num *= 2.0f;
					}
					if (Pawn.story.traits.HasTrait(VanillaTraitDefOf.Tough))
					{
						num *= 2.0f;
					}
					if (Pawn.story.traits.HasTrait(TraitDefOf.Wimp))
					{
						num *= 0.5f;
					}
					if (Pawn.story.traits.HasTrait(VanillaTraitDefOf.Nerves))
					{
						int td = Pawn.story.traits.DegreeOfTrait(VanillaTraitDefOf.Nerves);
						switch (td)
						{
							case -2:
								num *= 2.0f;
								break;
							case -1:
								num *= 1.5f;
								break;
							case 1:
								num *= 0.5f;
								break;
							case 2:
								num *= 0.25f;
								break;
						}
					}
				}
				severityAdjustment += num;
			}
		}
		public override float SeverityChangePerDay()
		{
			return this.Props.severityPerDayReduce.Evaluate(this.parent.ageTicks / GenDate.TicksPerDay);
		}
	}

	public class HediffCompProperties_FeelingBrokenSeverityIncrease : HediffCompProperties
	{
		public HediffCompProperties_FeelingBrokenSeverityIncrease()
		{
			this.compClass = typeof(HediffComp_FeelingBrokenSeverityIncrease);
		}

		public SimpleCurve severityPerDayIncrease;
	}
	class HediffComp_FeelingBrokenSeverityIncrease : AdvancedHediffComp
	{
		private HediffCompProperties_FeelingBrokenSeverityIncrease Props
		{
			get
			{
				return (HediffCompProperties_FeelingBrokenSeverityIncrease)this.props;
			}
		}
		public override void CompPostMerged(Hediff other)
		{
			float num = Props.severityPerDayIncrease.Evaluate(this.parent.ageTicks / GenDate.TicksPerDay);
			if (xxx.has_traits(Pawn))
			{
				if (xxx.RoMIsActive)
					if (Pawn.story.traits.HasTrait(xxx.Succubus))
						num *= 0.25f;

				if (Pawn.story.traits.HasTrait(VanillaTraitDefOf.Tough))
				{
					num *= 0.5f;
				}
				if (Pawn.story.traits.HasTrait(TraitDefOf.Wimp))
				{
					num *= 2.0f;
				}
				if (Pawn.story.traits.HasTrait(VanillaTraitDefOf.Nerves))
				{
					int td = Pawn.story.traits.DegreeOfTrait(VanillaTraitDefOf.Nerves);
					switch (td)
					{
						case -2:
							num *= 0.25f;
							break;
						case -1:
							num *= 0.5f;
							break;
						case 1:
							num *= 1.5f;
							break;
						case 2:
							num *= 2.0f;
							break;
					}
				}
			}
			other.Severity *= num;
		}
	}

	public class AdvancedHediffWithComps : HediffWithComps
	{
		public override bool TryMergeWith(Hediff other)
		{
			for (int i = 0; i < this.comps.Count; i++)
			{
				if(this.comps[i] is AdvancedHediffComp) ((AdvancedHediffComp)this.comps[i]).CompBeforeMerged(other);
			}
			return base.TryMergeWith(other);
		}
	}
	public class AdvancedHediffComp : HediffComp
	{
		public virtual void CompBeforeMerged(Hediff other)
		{
		}
	}
}


code:
using System.Linq;
using Verse;
using RimWorld;
using System.Text;
using Multiplayer.API;
using UnityEngine;
using System.Collections.Generic;
using rjw.Modules.Genitals.Enums;
using RimWorld.QuestGen;
using System;

namespace rjw
{
//TODO figure out how this thing works and move eggs to comps

	[StaticConstructorOnStartup]
	public class HediffDef_SexPart : HediffDef
	{
		public const float BaseFluidAmount = 20f;
		private static readonly FloatRange FluidAmountVariation = new(.75f, 1.25f);

		public SexFluidDef fluid;					//cummies/milk - insectjelly/honey etc
		public float fluidMultiplier = 1;           //amount of Milk/Ejaculation/Wetness
		public bool produceFluidOnOrgasm;
		public string defaultBodyPart = "";			//Bodypart to move this part to, after fucking up with pc or other mod
		public List<string> defaultBodyPartList;    //Bodypart list to move this part to, after fucking up with pc or other mod

		public float sizeMultiplier = 1f;

		public PartSizeConfigDef sizeProfile;
		public List<string> partTags = new();
		public GenitalFamily genitalFamily;
		public List<GenitalTag> genitalTags = new();

		public override IEnumerable<string> ConfigErrors()
		{
			foreach (string err in base.ConfigErrors())
			{
				yield return err;
			}
			if (sizeProfile == null && genitalFamily != GenitalFamily.Undefined)
			{
				yield return $"Missing required <{nameof(sizeProfile)}> field. Pick a default from "
				+ "<RJW Defs folder>/ConfigDefs/PartSizeConfigs.xml.";
			}
		}

		[SyncMethod]
		public float GetRandomPartFluidMultiplier()
		{
			return FluidAmountVariation.RandomInRange;
		}

		public float GetFluidAmount(float totalFluidMultiplier)
		{
			return BaseFluidAmount * totalFluidMultiplier;
		}

		public float GetFluidMultiplier(float partSeverity, float partFluidMultipler, float pawnBodySize, float pawnAge=40)
		{
			// should be ~0.5 - 2 for average human
			// biggst vanilla animals with biggest parts, ~8

			var m = this.fluidMultiplier; // Base multiplier for part (pussy/breats/etc)
			m *= Math.Min(50 / Math.Max(1, pawnAge), 1.0f); // Lower when 50+ years old
			m *= pawnBodySize; // Creature size. Usually Race baseBodySize since Severity is already bodySize scaled.

			// Part size. Increases for above Average, decreases for below Average
			// (0 -> 0.5), (0.5 -> 1), (1 -> 2), (2 -> 4)
			m *= Math.Max(1f, partSeverity * 2f); // Above average, uncapped
			m *= Math.Min(1f, partSeverity + 0.5f); // Below average, capped to 0.5

			m *= partFluidMultipler; // Permanent multiplier for part, includes race part multiplier.
			return m;
		}

		[SyncMethod]
		public float GetRandomSize()
		{
			float sample = Rand.Gaussian(0.5f, 0.125f);
			return Mathf.Clamp(sample, 0.01f, 1f) * sizeMultiplier;
		}

		public string GetStandardSizeLabel(float size)
		{
			return stages.FindLast(stage => stage.minSeverity <= size)?.label;
		}
	}
}


code:
using Verse;
using RimWorld;

namespace rjw
{
	public class Hediff_ArtificialSexPart : Hediff_AddedPart, ISexPartHediff
	{
		public override bool ShouldRemove => false;

		public HediffDef_SexPart Def => def as HediffDef_SexPart;

		public HediffWithComps AsHediff => this;

		public override float Severity {
			get => base.Severity;
			set
			{
				var prevSeverity = base.Severity;
				base.Severity = value;
				ISexPartHediffExtensions.DetectExternalSeverityChange(this, prevSeverity, value);
			}
		}

		public override string LabelBase => def.label;

		/// <summary>
		/// stack hediff in health tab?
		/// </summary>
		public override int UIGroupKey
		{
			get
			{
				return loadID;
			}
		}

		/// <summary>
		/// do not merge same rjw parts into one
		/// </summary>
		public override bool TryMergeWith(Hediff other)
		{
			return false;
		}
	}
}


code:
using RimWorld;
using System.Collections.Generic;
using System.Linq;
using Verse;

namespace rjw
{
	public class Cocoon : HediffWithComps
	{
		public int tickNext;

		public override void PostMake()
		{
			Severity = 1.0f;
			SetNextTick();
		}

		public override void ExposeData()
		{
			base.ExposeData();
			Scribe_Values.Look(ref tickNext, "tickNext", 1000, true);
		}

		public override void Tick()
		{
			if (Find.TickManager.TicksGame >= tickNext)
			{
				//Log.Message("Cocoon::Tick() " + base.xxx.get_pawnname(pawn));
				HealWounds();
				SatisfyHunger();
				SatisfyThirst();
				SetNextTick();
			}
		}

		public void HealWounds()
		{
			IEnumerable<Hediff> enumerable = from hd in pawn.health.hediffSet.hediffs
											 where !hd.IsTended() && hd.TendableNow()
											 select hd;
			if (enumerable != null)
			{
				foreach (Hediff item in enumerable)
				{
					HediffWithComps val = item as HediffWithComps;
					if (val != null)
						if (val.Bleeding)
						{
							//Log.Message("TrySealWounds " + xxx.get_pawnname(pawn) + ", Bleeding " + item.Label);
							//HediffComp_TendDuration val2 = HediffUtility.TryGetComp<HediffComp_TendDuration>(val);
							val.Heal(0.5f);
							//val2.tendQuality = 1f;
							//val2.tendTicksLeft = 10000;
							//pawn.health.Notify_HediffChanged(item);
						}
						// tend infections
						// tend lifeThreatening chronic
						else if ((!val.def.chronic && val.def.lethalSeverity > 0f) || (val.CurStage?.lifeThreatening ?? false))
						{
							//Log.Message("TryHeal " + xxx.get_pawnname(pawn) + ", infection(?) " + item.Label);
							HediffComp_TendDuration val2 = HediffUtility.TryGetComp<HediffComp_TendDuration>(val);
							val2.tendQuality = 1f;
							val2.tendTicksLeft = 10000;
							pawn.health.Notify_HediffChanged(item);
						}
				}
			}
		}

		public void SatisfyHunger()
		{
			Need_Food need = pawn.needs.TryGetNeed<Need_Food>();
			if (need == null)
			{
				return;
			}
			//pawn.PositionHeld.IsInPrisonCell(pawn.Map)
			//Log.Message("Cocoon::SatisfyHunger() " + xxx.get_pawnname(pawn) + " IsInPrisonCell " + pawn.PositionHeld.IsInPrisonCell(pawn.Map));
			//Log.Message("Cocoon::SatisfyHunger() " + xxx.get_pawnname(pawn) + " GetRoom " + pawn.PositionHeld.GetRoom(pawn.Map));
			//Log.Message("Cocoon::SatisfyHunger() " + xxx.get_pawnname(pawn) + " GetRoom " + pawn.PositionHeld.GetZone(pawn.Map));

			if (need.CurLevel < 0.15f)
			{
				//Log.Message("Cocoon::SatisfyHunger() " + xxx.get_pawnname(pawn) + " need to eat");
				float nutrition_amount = need.MaxLevel / 5f;
				pawn.needs.food.CurLevel += nutrition_amount;
			}
		}

		public void SatisfyThirst()
		{
			if (!xxx.DubsBadHygieneIsActive)
				return;

			Need need = pawn.needs.AllNeeds.Find(x => x.def == xxx.DBHThirst);
			if (need == null)
			{
				return;
			}

			if (need.CurLevel < 0.15f)
			{
				//Log.Message("Cocoon::SatisfyThirst() " + xxx.get_pawnname(pawn) + " need to drink");
				float nutrition_amount = need.MaxLevel / 5f;
				pawn.needs.TryGetNeed(need.def).CurLevel += nutrition_amount;
			}
		}

		public void SetNextTick()
		{
			//make actual tick every 16.6 sec
			tickNext = Find.TickManager.TicksGame + 1000;
			//Log.Message("Cocoon::SetNextTick() " + tickNext);
		}
	}
}


code:
using Verse;
using RimWorld;

namespace rjw
{
	public class Hediff_NaturalSexPart : HediffWithComps, ISexPartHediff
	{
		public override bool ShouldRemove => false;

		public HediffDef_SexPart Def => def as HediffDef_SexPart;

		public HediffWithComps AsHediff => this;

		public override float Severity {
			get => base.Severity;
			set
			{
				var prevSeverity = base.Severity;
				base.Severity = value;
				ISexPartHediffExtensions.DetectExternalSeverityChange(this, prevSeverity, value);
			}
		}

		public override string LabelBase
		{
			get
			{
				/*
				 * make patch to make/save capmods?
				if (CapMods.Count < 5)
				{
					PawnCapacityModifier pawnCapacityModifier = new PawnCapacityModifier();
					pawnCapacityModifier.capacity = PawnCapacityDefOf.Moving;
					pawnCapacityModifier.offset += 0.5f;
					CapMods.Add(pawnCapacityModifier);
				}
				*/

				//name/kind
				return def.label;
			}
		}

		/// <summary>
		/// stack hediff in health tab?
		/// </summary>
		public override int UIGroupKey
		{
			get
			{
				return loadID;
			}
		}

		/// <summary>
		/// do not merge same rjw parts into one
		/// </summary>
		public override bool TryMergeWith(Hediff other)
		{
			return false;
		}
	}
}


code:
using Verse;

//Hediff worker for pawns' "lay down and submit" button
namespace rjw
{
	public class Hediff_Submitting: HediffWithComps
	{
		public override bool ShouldRemove {
			get
			{
				Pawn daddy = pawn.CarriedBy;
				if (daddy != null && daddy.Faction == pawn.Faction)
				{
					return true;
				}
				else
					return base.ShouldRemove;
			}
		}
	}
}


code:
using Verse;
using UnityEngine;
using rjw.Modules.Shared.Extensions;

namespace rjw
{
	// Would be nice to call this just 'ISexPart', but ILewdablePart already exists and represents something similar but different.
	public interface ISexPartHediff
	{
		HediffDef_SexPart Def { get; }

		T GetComp<T>() where T : HediffComp;

		HediffWithComps AsHediff { get; }
	}

	public static class ISexPartHediffExtensions
	{
		public static HediffComp_SexPart GetPartComp(this ISexPartHediff partHediff) => partHediff.GetComp<HediffComp_SexPart>();

		public static Pawn GetOwner(this ISexPartHediff part) => part.AsHediff.pawn;

		public static void DetectExternalSeverityChange(HediffWithComps hediff, float prevValue, float newValue)
		{
			if(Find.TickManager.Paused && prevValue != newValue)
			{
				if(hediff.TryGetComp<HediffComp_SexPart>(out var comp))
				{
					if (newValue != comp.GetSeverity())
					{
						ModLog.Message($"DetectExternalSeverityChange: {hediff.LabelBaseCap} ({hediff.pawn.GetName()}), {prevValue} -> {newValue}.");
						comp.SetSeverity(newValue, sync: false);
					}
				}
			}
		}
	}
}


code:
using System.Collections.Generic;
using Verse;

namespace rjw
{
	public class PartAdder
	{
		public float chance = 0f;
		public string rjwPart;
		public List<string> bodyParts;
	}
}


code:
using Verse;
using RimWorld;
using System;
using System.Collections.Generic;
using System.Linq;

namespace rjw
{
	public static class PartSizeCalculator
	{
		public static bool TryGetLength(Hediff hediff, out float length)
		{
			if (!hediff.TryGetComp<HediffComp_SexPart>(out var comp))
			{
				length = 0;
				return false;
			}

			return Internal.TryGetLength(hediff.def, Internal.GetPartSeverity(hediff), Internal.GetBodySize(hediff), out length);
		}

		public static bool TryGetGirth(Hediff hediff, out float girth)
		{
			if (!hediff.TryGetComp<HediffComp_SexPart>(out var comp))
			{
				girth = 0;
				return false;
			}

			return Internal.TryGetGirth(hediff.def, Internal.GetPartSeverity(hediff), Internal.GetBodySize(hediff), out girth);
		}

		public static bool TryGetPenisWeight(Hediff hediff, out float weight)
		{
			if (!TryGetLength(hediff, out float length) ||
				!TryGetGirth(hediff, out float girth))
			{
				weight = 0f;
				return false;
			}

			var density = Curves.GetSizeConfig(hediff).density;
			if (density == null)
			{
				weight = 0f;
				return false;
			}

			var r = girth / (2.0 * Math.PI);
			var volume = r * r * Math.PI * length;

			weight = (float)(volume * density.Value / 1000f);
			return true;
		}

		public static bool TryGetBreastSize(Hediff hediff, out BreastSize breastSize)
		{
			if (!hediff.TryGetComp<HediffComp_SexPart>(out var comp))
			{
				breastSize = new BreastSize(0, 0, 1);
				return false;
			}

			return Internal.TryGetBreastSize(hediff.def, Internal.GetPartSeverity(hediff), Internal.GetBodySize(hediff), out breastSize);
		}

		public class Internal
		{
			public static float GetBodySize(Hediff hediff)
			{
				if (hediff.TryGetComp<HediffComp_SexPart>(out var comp))
				{
					return comp.GetBodySize();
				}
				return hediff.pawn.BodySize;
			}

			public static float GetPartSeverity(Hediff hediff)
			{
				if (hediff.TryGetComp<HediffComp_SexPart>(out var comp))
				{
					return comp.GetSeverity();
				}
				return hediff.Severity;
			}

			public static bool TryGetLength(HediffDef hediffDef, float severity, float bodySize, out float length)
			{
				var curve = Curves.LengthCurve(hediffDef);

				if(!Curves.EvaluateCurve(curve, severity, bodySize, out length)) {
					length = 0;
					return false;
				}

				return true;
			}

			public static bool TryGetGirth(HediffDef hediffDef, float severity, float bodySize, out float girth)
			{
				var curve = Curves.GirthCurve(hediffDef);

				if(!Curves.EvaluateCurve(curve, severity, bodySize, out girth)) {
					girth = 0;
					return false;
				}

				return true;
			}

			public static bool TryGetCupSize(HediffDef hediffDef, float severity, out float cupSize)
			{
				var curve = Curves.CupSizeCurve(hediffDef);

				if(!Curves.EvaluateCurve(curve, severity, 1.0f, out cupSize)) {
					cupSize = 0;
					return false;
				}

				return true;
			}

			public static bool TryGetBandSize(HediffDef hediffDef, float bodySize, out float bandSize)
			{
				var baseBandSize = BraSizeConfigDef.Instance.bandSizeBase;

				bandSize = (float)Math.Round(baseBandSize * bodySize / 2) * 2;

				return true;
			}

			public static bool TryGetBreastSize(HediffDef hediffDef, float severity, float bodySize, out BreastSize breastSize)
			{
				var density = Curves.GetSizeConfig(hediffDef).density;
				if (density == null)
				{
					density = 1.0f;
				}

				breastSize = new BreastSize(0, 0, 1);

				if(!TryGetCupSize(hediffDef, severity, out var cupSize))
				{
					return false;
				}

				if(!TryGetBandSize(hediffDef, bodySize, out var bandSize))
				{
					return false;
				}

				breastSize = new BreastSize(cupSize, bandSize, density.Value);
				return true;
			}
		}

		public static class Curves
		{
			public static SimpleCurve LengthCurve(HediffDef def)
			{
				return BuildCurve(def.stages, GetSizeConfig(def)?.lengths);
			}

			public static SimpleCurve GirthCurve(HediffDef def)
			{
				return BuildCurve(def.stages, GetSizeConfig(def)?.girths);
			}

			public static SimpleCurve CupSizeCurve(HediffDef def)
			{
				return BuildCurve(def.stages, GetSizeConfig(def)?.cupSizes);
			}

			public static SimpleCurve BuildCurve(List<HediffStage> stages, List<float> stageSizes)
			{
				if(stageSizes == null) return null;
				return new SimpleCurve(stages.Zip(stageSizes, (stage, stageSize) => new CurvePoint(stage.minSeverity, stageSize)));
			}

			public static bool EvaluateCurve(SimpleCurve curve, float input, float scale, out float measurement)
			{
				if (curve == null) {
					measurement = 0;
					return false;
				}
				measurement = curve.Evaluate(input) * scale;
				return true;
			}

			public static bool EvaluateInvertedCurve(SimpleCurve curve, float measurement, float scale, out float input)
			{
				if (curve == null) {
					input = 0;
					return false;
				}
				input = curve.EvaluateInverted(measurement/scale);
				return true;
			}

			public static PartSizeConfigDef GetSizeConfig(Hediff hediff)
			{
				return GetSizeConfig(hediff.def);
			}

			public static PartSizeConfigDef GetSizeConfig(HediffDef def)
			{
				return (def as HediffDef_SexPart)?.sizeProfile;
			}
		}

		public class Inversion
		{
			public static bool TryInvertLength(HediffDef hediffDef, float length, float bodySize, out float severity)
			{
				var curve = Curves.LengthCurve(hediffDef);

				if(!Curves.EvaluateInvertedCurve(curve, length, bodySize, out severity)) {
					severity = 0;
					return false;
				}

				return true;
			}

			public static bool TryInvertGirth(HediffDef hediffDef, float girth, float bodySize, out float severity)
			{
				var curve = Curves.GirthCurve(hediffDef);

				if(!Curves.EvaluateInvertedCurve(curve, girth, bodySize, out severity)) {
					severity = 0;
					return false;
				}

				return true;
			}

			public static bool TryInvertCupSize(HediffDef hediffDef, float cupSize, out float severity)
			{
				var curve = Curves.CupSizeCurve(hediffDef);

				if(!Curves.EvaluateInvertedCurve(curve, cupSize, 1.0f, out severity)) {
					severity = 0;
					return false;
				}

				return true;
			}

			public static bool TryInvertBreastWeight(HediffDef hediffDef, float weight, float bodySize, out float severity, out BreastSize breastSize)
			{
				var density = Curves.GetSizeConfig(hediffDef).density;
				if (density == null)
				{
					severity = 0f;
					breastSize = new BreastSize(0, 0, 1);
					return false;
				}

				var volume = weight / density.Value;
				return TryInvertBreastVolume(hediffDef, volume, bodySize, out severity, out breastSize);
			}

			public static bool TryInvertBreastVolume(HediffDef hediffDef, float volume, float bodySize, out float severity, out BreastSize breastSize)
			{
				severity = 0f;
				breastSize = new BreastSize(0, 0, 1);

				if(!Internal.TryGetBandSize(hediffDef, bodySize, out var bandSize))
				{
					return false;
				}

				var cupSize = BreastSize.CalculateCupSize(volume, bandSize);

				if(!TryInvertCupSize(hediffDef, cupSize, out severity))
				{
					return false;
				}

				var density = Curves.GetSizeConfig(hediffDef).density ?? 1.0f;

				breastSize = new BreastSize(cupSize, bandSize, density);

				return true;
			}
		}

		public class Modify
		{
			public static bool TryModifyLength(Hediff hediff, Func<float, float> lengthModifier, out float newSeverity)
			{
				if(!TryGetLength(hediff, out var length))
				{
					newSeverity = 0;
					return false;
				}

				return Inversion.TryInvertLength(hediff.def, lengthModifier(length), Internal.GetBodySize(hediff), out newSeverity);
			}

			public static bool TryModifyGirth(Hediff hediff, Func<float, float> girthModifier, out float newSeverity)
			{
				if(!TryGetGirth(hediff, out var girth))
				{
					newSeverity = 0;
					return false;
				}

				return Inversion.TryInvertGirth(hediff.def, girthModifier(girth), Internal.GetBodySize(hediff), out newSeverity);
			}

			public static bool TryModifyCupSize(Hediff hediff, Func<float, float> cupSizeModifier, out float newSeverity)
			{
				if(!Internal.TryGetCupSize(hediff.def, Internal.GetPartSeverity(hediff), out var cupSize))
				{
					newSeverity = 0;
					return false;
				}

				return Inversion.TryInvertCupSize(hediff.def, cupSizeModifier(cupSize), out newSeverity);
			}

			public static bool TryModifyBreastVolume(Hediff hediff, Func<float, float> volumeModifier, out float newSeverity)
			{
				if(!TryGetBreastSize(hediff, out var breastSize))
				{
					newSeverity = 0;
					return false;
				}

				return Inversion.TryInvertBreastVolume(hediff.def, volumeModifier(breastSize.volume), Internal.GetBodySize(hediff), out newSeverity, out _);
			}

			public static bool TryModifyBreastWeight(Hediff hediff, Func<float, float> weightModifier, out float newSeverity)
			{
				if(!TryGetBreastSize(hediff, out var breastSize))
				{
					newSeverity = 0;
					return false;
				}

				return Inversion.TryInvertBreastWeight(hediff.def, weightModifier(breastSize.weight), Internal.GetBodySize(hediff), out newSeverity, out _);
			}
		}
	}
}


code:
using Verse;
using RimWorld;
using System.Collections.Generic;

namespace rjw
{
	public class PartSizeConfigDef : Def
	{
			public bool bodysizescale = false; // rescales parts sizes based on bodysize of initial owner race

			/// <summary>
			/// Human standard would be 1.0. Null for no weight display.
			/// </summary>
			public float? density = null;

			public List<float> lengths;
			public List<float> girths;
			public List<float> cupSizes;
	}
}


code:
using Verse;
using RimWorld;
using System.Collections.Generic;
using System.Linq;
using rjw.Modules.Interactions.Enums;
using rjw.Modules.Interactions.Objects.Parts;

namespace rjw
{
	public class SexFluidDef : Def
	{
		public ThingDef filth;

		public ThingDef consumable;

		public float consumableFluidRatio = 1;

		public bool ingestThroughAnyOrifice;

		public bool alwaysProduceFilth;

		public float baseNutrition;

		public float baseNutritionCost = 1;

		public bool quenchesThirst = true;

		public List<string> tags = new List<string>();

		public List<SexFluidIngestionDoer> ingestionDoers;

		public override IEnumerable<string> ConfigErrors()
		{
			foreach (string err in base.ConfigErrors())
			{
				yield return err;
			}
			if (filth != null && filth.filth == null)
			{
				yield return $"{nameof(filth)} must be a ThingDef with a <filth> field.";
			}
			if (consumable != null && baseNutrition != 0)
			{
				yield return $"{nameof(consumable)} is set, so {nameof(baseNutrition)} is meaningless.";
			}
		}

		public override void ResolveReferences()
		{
			base.ResolveReferences();

			if (consumable != null)
			{
				descriptionHyperlinks ??= new();
				descriptionHyperlinks.AddDistinct(consumable);
			}
		}

	}
}


code:
using Verse;

namespace rjw
{
	public abstract class SexFluidIngestionDoer
	{
		public abstract void Ingested(Pawn pawn, SexFluidDef fluid, float amount, ISexPartHediff fromPart, ISexPartHediff toPart = null);
	}
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using Verse;

namespace rjw
{
	public class InteractionExtension : DefModExtension
	{
		/// <summary>
		/// </summary>
		public string RMBLabel = ""; // rmb menu
		public string rjwSextype = ""; // xxx.rjwSextype

		public List<string> rulepack_defs = new List<string>(); //rulepack(s) for this interaction
	}
}