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

namespace rjw
{
	public static class AfterSexUtility
	{
		//aftersex thoughts
		public static readonly ThoughtDef got_raped = DefDatabase<ThoughtDef>.GetNamed("GotRaped");
		public static readonly ThoughtDef got_anal_raped = DefDatabase<ThoughtDef>.GetNamed("GotAnalRaped");
		public static readonly ThoughtDef got_anal_raped_byfemale = DefDatabase<ThoughtDef>.GetNamed("GotAnalRapedByFemale");
		public static readonly ThoughtDef got_raped_unconscious = DefDatabase<ThoughtDef>.GetNamed("GotRapedUnconscious");
		public static readonly ThoughtDef masochist_got_raped_unconscious = DefDatabase<ThoughtDef>.GetNamed("MasochistGotRapedUnconscious");

		public static readonly ThoughtDef got_bred = DefDatabase<ThoughtDef>.GetNamed("GotBredByAnimal");
		public static readonly ThoughtDef got_anal_bred = DefDatabase<ThoughtDef>.GetNamed("GotAnalBredByAnimal");
		public static readonly ThoughtDef got_licked = DefDatabase<ThoughtDef>.GetNamed("GotLickedByAnimal");
		public static readonly ThoughtDef got_groped = DefDatabase<ThoughtDef>.GetNamed("GotGropedByAnimal");

		public static readonly ThoughtDef masochist_got_raped = DefDatabase<ThoughtDef>.GetNamed("MasochistGotRaped");
		public static readonly ThoughtDef masochist_got_anal_raped = DefDatabase<ThoughtDef>.GetNamed("MasochistGotAnalRaped");
		public static readonly ThoughtDef masochist_got_anal_raped_byfemale = DefDatabase<ThoughtDef>.GetNamed("MasochistGotAnalRapedByFemale");
		public static readonly ThoughtDef masochist_got_bred = DefDatabase<ThoughtDef>.GetNamed("MasochistGotBredByAnimal");
		public static readonly ThoughtDef masochist_got_anal_bred = DefDatabase<ThoughtDef>.GetNamed("MasochistGotAnalBredByAnimal");
		public static readonly ThoughtDef masochist_got_licked = DefDatabase<ThoughtDef>.GetNamed("MasochistGotLickedByAnimal");
		public static readonly ThoughtDef masochist_got_groped = DefDatabase<ThoughtDef>.GetNamed("MasochistGotGropedByAnimal");
		public static readonly ThoughtDef allowed_animal_to_breed = DefDatabase<ThoughtDef>.GetNamed("AllowedAnimalToBreed");
		public static readonly ThoughtDef allowed_animal_to_lick = DefDatabase<ThoughtDef>.GetNamed("AllowedAnimalToLick");
		public static readonly ThoughtDef allowed_animal_to_grope = DefDatabase<ThoughtDef>.GetNamed("AllowedAnimalToGrope");
		public static readonly ThoughtDef zoophile_got_bred = DefDatabase<ThoughtDef>.GetNamed("ZoophileGotBredByAnimal");
		public static readonly ThoughtDef zoophile_got_anal_bred = DefDatabase<ThoughtDef>.GetNamed("ZoophileGotAnalBredByAnimal");
		public static readonly ThoughtDef zoophile_got_licked = DefDatabase<ThoughtDef>.GetNamed("ZoophileGotLickedByAnimal");
		public static readonly ThoughtDef zoophile_got_groped = DefDatabase<ThoughtDef>.GetNamed("ZoophileGotGropedByAnimal");
		public static readonly ThoughtDef hate_my_rapist = DefDatabase<ThoughtDef>.GetNamed("HateMyRapist");
		public static readonly ThoughtDef kinda_like_my_rapist = DefDatabase<ThoughtDef>.GetNamed("KindaLikeMyRapist");
		public static readonly ThoughtDef allowed_me_to_get_raped = DefDatabase<ThoughtDef>.GetNamed("AllowedMeToGetRaped");
		public static readonly ThoughtDef stole_some_lovin = DefDatabase<ThoughtDef>.GetNamed("StoleSomeLovin");
		public static readonly ThoughtDef bloodlust_stole_some_lovin = DefDatabase<ThoughtDef>.GetNamed("BloodlustStoleSomeLovin");
		public static readonly ThoughtDef violated_corpse = DefDatabase<ThoughtDef>.GetNamed("ViolatedCorpse");
		public static readonly ThoughtDef gave_virginity = DefDatabase<ThoughtDef>.GetNamed("FortunateGaveVirginity");
		public static readonly ThoughtDef lost_virginity = DefDatabase<ThoughtDef>.GetNamed("UnfortunateLostVirginity");
		public static readonly ThoughtDef took_virginity = DefDatabase<ThoughtDef>.GetNamed("TookVirginity");

		//violent - mark true when pawn rape partner
		//Note: violent is not reliable, since either pawn could be the rapist. Check jobdrivers instead, they're still active since this is called before ending the job.
		public static void think_about_sex(SexProps props)
		{
			Pawn receiving = props.partner;//TODO: replace this shit at some point when interaction has reversed roles
			think_about_sex(props.pawn, props.partner, receiving == props.pawn, props, props.isWhoring);
			think_about_sex(props.partner, props.pawn, receiving == props.partner, props, false);
		}
		public static void think_about_sex(Pawn pawn, Pawn partner, bool isReceiving, SexProps props, bool whoring = false)
		{
			// Partner should never be null, but just in case something gets changed elsewhere..
			if (partner == null)
			{
				ModLog.Error("xxx::think-after_sex( ERROR: " + xxx.get_pawnname(pawn) + " has no partner. This should not be called from solo acts. Sextype: " + props.sexType);
				return;
			}

			// Both pawns are now checked individually, instead of giving thoughts to the partner.
			//Can just return if the currently checked pawn is dead or can't have thoughts, which simplifies the checks.
			if (pawn.Dead || !xxx.is_human(pawn))
				return;

			bool masochist = xxx.is_masochist(pawn);
			bool zoophile = xxx.is_zoophile(pawn);
			bool guilty = props.canBeGuilty;
			if (HasLoveEnhancer(pawn, partner))
			{
				masochist = true;
				zoophile = true;
			}
			if (masochist)
			{
				if (RJWSettings.rape_beating)
					pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDefWhereOtherPawnIs(ThoughtDefOf.HarmedMe, partner);
			}
			if (!masochist)
			{
				//TODO: someday rough sex?
				//foreach (DirectPawnRelation relation in pawn.relations.DirectRelations)
				//{
				//	if (relation.otherPawn == partner)
				//		if (relation.def == PawnRelationDefOf.Lover ||
				//			relation.def == PawnRelationDefOf.Fiance ||
				//			relation.def == PawnRelationDefOf.Spouse)
				//		{
				//			masochist = true;
				//			break;
				//		}
				//}
				//if (pawn.IsPrisoner || masochist || is_slave(pawn) || pawn.IsDesignatedComfort())
				if (pawn.IsPrisoner || xxx.is_slave(pawn) || pawn.IsDesignatedComfort())
				{
					guilty = false;
				}
			}
			else
				guilty = false;

			ThoughtDef thoughtgain = null;
			//unconscious pawns has no thoughts
			//and if they has sex, its probably rape, since they have no choice
			if (!pawn.health.capacities.CanBeAwake)
			{
				thoughtgain = xxx.is_masochist(pawn) ? masochist_got_raped_unconscious : got_raped_unconscious;
				pawn.needs.mood.thoughts.memories.TryGainMemory(thoughtgain);
				return;
			}

			// TODO: refactor all these repeated arguments to be a `ref struct`.
			// This would be a breaking change, though.

			thoughtgain = think_about_sex_Bestiality(pawn, partner, isReceiving, props, whoring, masochist, zoophile, guilty);

			if (thoughtgain == null)
				thoughtgain = think_about_sex_Rapist(pawn, partner, isReceiving, props, whoring, masochist, zoophile, guilty);

			if (thoughtgain == null)
				thoughtgain = think_about_sex_Victim(pawn, partner, isReceiving, props, whoring, masochist, zoophile, guilty);

			if (thoughtgain == null)
				thoughtgain = think_about_sex_Consensual(pawn, partner, isReceiving, props, whoring, masochist, zoophile, guilty);

			if (thoughtgain != null)
			{
				var newMemory = FinalizeThought(thoughtgain, pawn, partner, isReceiving, props, whoring, masochist, zoophile, guilty);
				pawn.needs.mood?.thoughts.memories.TryGainMemory(newMemory, partner);
			}
		}

		public static Thought_Memory FinalizeThought(
			ThoughtDef thoughtgain,
			Pawn pawn,
			Pawn partner,
			bool isReceiving,
			SexProps props,
			bool whoring,
			bool masochist,
			bool zoophile,
			bool guilty
		  )
		{
			var newMemory = (Thought_Memory)ThoughtMaker.MakeThought(thoughtgain);

			// Apply mood boost for vanilla lovin' from love enhancer.
			if (thoughtgain == ThoughtDefOf.GotSomeLovin && HasLoveEnhancer(pawn, partner))
				newMemory.moodPowerFactor = 1.5f;

			return newMemory;
		}

		public static ThoughtDef think_about_sex_Bestiality(
			Pawn pawn,
			Pawn partner,
			bool isReceiving,
			SexProps props,
			bool whoring,
			bool masochist,
			bool zoophile,
			bool guilty
			)
		{

			//ModLog.Message("xxx::think-after_sex( ERROR: 3");
			// Thoughts for animal-on-colonist.
			ThoughtDef thoughtDef = null;
			if (xxx.is_animal(partner) && isReceiving)
			{
				if (!zoophile && !props.isRape)
				{
					//ModLog.Message("xxx::think-after_sex( ERROR: 3.1");
					if (props.sexType == xxx.rjwSextype.Oral)
						thoughtDef = (allowed_animal_to_lick);
					else if (props.sexType == xxx.rjwSextype.Anal || props.sexType == xxx.rjwSextype.Vaginal)
						thoughtDef = (allowed_animal_to_breed);
					else //Other rarely seen sex types, such as fingering (by primates, monster girls, etc)
						thoughtDef = (allowed_animal_to_grope);
				}
				else
				{
					//ModLog.Message("xxx::think-after_sex( ERROR: 3.2");
					if (!zoophile)
					{
						if (props.sexType == xxx.rjwSextype.Oral)
							thoughtDef = (masochist ? masochist_got_licked : got_licked);
						else if (props.sexType == xxx.rjwSextype.Vaginal)
							thoughtDef = (masochist ? masochist_got_bred : got_bred);
						else if (props.sexType == xxx.rjwSextype.Anal)
							thoughtDef = (masochist ? masochist_got_anal_bred : got_anal_bred);
						else //Other types
							thoughtDef = (masochist ? masochist_got_groped : got_groped);
					}
					else
					{
						if (props.sexType == xxx.rjwSextype.Oral)
							thoughtDef = (zoophile_got_licked);
						else if (props.sexType == xxx.rjwSextype.Vaginal)
							thoughtDef = (zoophile_got_bred);
						else if (props.sexType == xxx.rjwSextype.Anal)
							thoughtDef = (zoophile_got_anal_bred);
						else //Other types
							thoughtDef = (zoophile_got_groped);
					}
				}
				think_about_sex_Bestiality_TameAttempt(pawn, partner, isReceiving, props, whoring, masochist, zoophile, guilty);
			}
			return thoughtDef;
		}

		public static void think_about_sex_Bestiality_TameAttempt(
			Pawn pawn,
			Pawn partner,
			bool isReceiving,
			SexProps props,
			bool whoring,
			bool masochist,
			bool zoophile,
			bool guilty
			)
		{
			if (pawn.CurJob != null)
				if (!(pawn.Downed && partner.Downed))
					if (!partner.Dead && zoophile && (pawn.CurJob.def != xxx.gettin_raped) && partner.Faction == null && pawn.Faction == Faction.OfPlayer)
					{
						InteractionDef intDef = !partner.AnimalOrWildMan() ? InteractionDefOf.RecruitAttempt : InteractionDefOf.TameAttempt;
						pawn.interactions.TryInteractWith(partner, intDef);
					}
		}

		public static ThoughtDef think_about_sex_Rapist(
			Pawn pawn,
			Pawn partner,
			bool isReceiving,
			SexProps props,
			bool whoring,
			bool masochist,
			bool zoophile,
			bool guilty
			)
		{
			ThoughtDef thoughtDef = null;
			if (partner.Dead || (partner.CurJob != null && partner.CurJob.def == xxx.gettin_raped) || pawn.jobs?.curDriver is JobDriver_Rape)
			{
				thoughtDef = (xxx.is_rapist(pawn) || xxx.is_bloodlust(pawn) || xxx.is_psychopath(pawn)) ? bloodlust_stole_some_lovin : stole_some_lovin;

				if ((xxx.is_necrophiliac(pawn) || xxx.is_psychopath(pawn)) && partner.Dead)
				{
					thoughtDef = (violated_corpse);
				}
			}
			return thoughtDef;
		}

		public static ThoughtDef think_about_sex_Victim(
			Pawn pawn,
			Pawn partner,
			bool isReceiving,
			SexProps props,
			bool whoring,
			bool masochist,
			bool zoophile,
			bool guilty
			)
		{
			ThoughtDef thoughtDef = null;
			if (pawn.CurJob != null && pawn.CurJob.def == xxx.gettin_raped || partner.jobs?.curDriver is JobDriver_Rape)
			{
				if (xxx.is_human(partner))
				{
					if (props.sexType == xxx.rjwSextype.Anal)
					{
						if (xxx.is_female(partner))
						{
							thoughtDef = masochist ? masochist_got_anal_raped_byfemale : got_anal_raped_byfemale;
						}
						else
						{
							thoughtDef = masochist ? masochist_got_anal_raped : got_anal_raped;
						}
					}
					else
					{
						thoughtDef = masochist ? masochist_got_raped : got_raped;
					}

					ThoughtDef pawn_thought_about_rapist = masochist ? kinda_like_my_rapist : hate_my_rapist;
					pawn.needs.mood.thoughts.memories.TryGainMemory(pawn_thought_about_rapist, partner);
					if (guilty)
						partner.guilt.Notify_Guilty(GenDate.TicksPerDay);

				}

				think_about_sex_Victim_Blame(pawn, partner, isReceiving, props, whoring, masochist, zoophile, guilty);
			}
			return thoughtDef;
		}
		public static void think_about_sex_Victim_Blame(
			Pawn pawn,
			Pawn partner,
			bool isReceiving,
			SexProps props,
			bool whoring,
			bool masochist,
			bool zoophile,
			bool guilty
			)
		{
			if (pawn.Faction != null && pawn.Map != null && !masochist && !(xxx.is_animal(partner) && zoophile))
			{
				foreach (Pawn bystander in pawn.Map.mapPawns.SpawnedPawnsInFaction(pawn.Faction).Where(x => !xxx.is_animal(x) && x != pawn && x != partner && !x.Downed && !x.Suspended))
				{
					// dont see through walls, dont see whole map, only 15 cells around
					if (pawn.CanSee(bystander) && pawn.Position.DistanceTo(bystander.Position) < 15)
					{
						pawn.needs.mood.thoughts.memories.TryGainMemory(allowed_me_to_get_raped, bystander);
					}
				}
			}
		}

		public static ThoughtDef think_about_sex_Consensual(
			Pawn pawn,
			Pawn partner,
			bool isReceiving,
			SexProps props,
			bool whoring,
			bool masochist,
			bool zoophile,
			bool guilty
			)
		{
			ThoughtDef thoughtDef = null;
			if (xxx.is_human(partner))
			{
				if (!props.isCoreLovin && !whoring)
				{
					// human partner and not part of rape or necrophilia. add the vanilla GotSomeLovin thought
					thoughtDef = ThoughtDefOf.GotSomeLovin;
				}
			}
			return thoughtDef;
		}

		public static bool HasLoveEnhancer(Pawn pawn) =>
			pawn.health.hediffSet.hediffs.Any((Hediff h) => h.def == HediffDefOf.LoveEnhancer);

		public static bool HasLoveEnhancer(Pawn pawn, Pawn partner) =>
			HasLoveEnhancer(pawn) || HasLoveEnhancer(partner);

		public static void UpdateRecords(SexProps props)
		{
			UpdateRecordsInitiator(props);
			if (props.sexType != xxx.rjwSextype.Masturbation)
				UpdateRecordsPartner(props);
		}

		private static void UpdateRecordsInitiator(SexProps props)
		{

			Pawn pawn = props.pawn;
			Pawn partner = props.partner;

			if (props.sexType == xxx.rjwSextype.Masturbation)
			{
				pawn.records.Increment(xxx.CountOfFappin);
				return;
			}

			//bool isVirginSex = xxx.is_Virgin(pawn); //need copy value before count increase.
			//ThoughtDef currentThought = null;

			pawn.records.Increment(xxx.CountOfSex);
			//bool masochist = xxx.is_masochist(pawn);
			//bool zoophile = xxx.is_zoophile(pawn);
			//if (HasLoveEnhancer(pawn, partner))
			//{
			//	masochist = true;
			//	zoophile = true;
			//}

			if (props.partner.Dead)
			{
				pawn.records.Increment(xxx.CountOfSexWithCorpse);
			}
			if (!props.isRape)
			{
				if (xxx.is_human(partner))
				{
					pawn.records.Increment(xxx.CountOfSexWithHumanlikes);
					//currentThought = isLoveSex ? xxx.gave_virginity : null;
				}
				else if (xxx.is_insect(partner))
				{
					pawn.records.Increment(xxx.CountOfSexWithInsects);
				}
				else if (xxx.is_animal(partner))
				{
					pawn.records.Increment(xxx.CountOfSexWithAnimals);
					//currentThought = zoophile ? xxx.gave_virginity : null;
				}
				else
				{
					pawn.records.Increment(xxx.CountOfSexWithOthers);
				}
			}
			else
			{
				//if (!pawnIsRaper)
				//{
				//	currentThought = masochist ? xxx.gave_virginity : xxx.lost_virginity;
				//}

				if (xxx.is_human(partner))
				{
					pawn.records.Increment(xxx.CountOfRapedHumanlikes);
					//if (pawnIsRaper && (xxx.is_rapist(pawn) || xxx.is_bloodlust(pawn)))
					//	currentThought = xxx.gave_virginity;
				}
				else if (xxx.is_insect(partner))
				{
					pawn.records.Increment(xxx.CountOfSexWithInsects);
					pawn.records.Increment(xxx.CountOfRapedInsects);
				}
				else if (xxx.is_animal(partner))
				{
					pawn.records.Increment(xxx.CountOfSexWithAnimals);
					pawn.records.Increment(xxx.CountOfRapedAnimals);
					//if (zoophile) currentThought = xxx.gave_virginity;
				}
				else
				{
					pawn.records.Increment(xxx.CountOfSexWithOthers);
					pawn.records.Increment(xxx.CountOfRapedOthers);
				}
			}

			//if (isVirginSex) //&& (sextype == rjwSextype.Vaginal || sextype == rjwSextype.DoublePenetration))
			//{
			//	Log.Message(xxx.get_pawnname(pawn) + " | " + xxx.get_pawnname(partner) + " | " + currentThought);
			//	Log.Message("1");
			//	if (!is_animal(partner))//passive
			//	{
			//		if (currentThought != null)
			//			partner.needs.mood.thoughts.memories.TryGainMemory(currentThought);
			//	}
			//	Log.Message("2");
			//	if (!is_animal(pawn))//active
			//	{
			//		currentThought = took_virginity;
			//		pawn.needs.mood.thoughts.memories.TryGainMemory(currentThought);
			//	}
			//}
		}

		private static void UpdateRecordsPartner(SexProps props)
		{

			if (props.partner == null || props.partner.Dead) // masturbation or corpse fucking
				return;

			Pawn pawn = props.partner;
			Pawn partner = props.pawn;

			//bool isVirginSex = xxx.is_Virgin(pawn); //need copy value before count increase.
			//ThoughtDef currentThought = null;

			pawn.records.Increment(xxx.CountOfSex);
			//bool masochist = xxx.is_masochist(pawn);
			//bool zoophile = xxx.is_zoophile(pawn);
			//if (HasLoveEnhancer(pawn, partner))
			//{
			//	masochist = true;
			//	zoophile = true;
			//}

			if (!props.isRape)
			{
				if (xxx.is_human(partner))
				{
					pawn.records.Increment(xxx.CountOfSexWithHumanlikes);
					//currentThought = isLoveSex ? xxx.gave_virginity : null;
				}
				else if (xxx.is_insect(partner))
				{
					pawn.records.Increment(xxx.CountOfSexWithInsects);
				}
				else if (xxx.is_animal(partner))
				{
					pawn.records.Increment(xxx.CountOfSexWithAnimals);
					//currentThought = zoophile ? xxx.gave_virginity : null;
				}
				else
				{
					pawn.records.Increment(xxx.CountOfSexWithOthers);
				}
			}
			else
			{
				//if (!pawnIsRaper)
				//{
				//	currentThought = masochist ? xxx.gave_virginity : xxx.lost_virginity;
				//}

				if (xxx.is_human(partner))
				{
					pawn.records.Increment(xxx.CountOfBeenRapedByHumanlikes);
					//if (pawnIsRaper && (xxx.is_rapist(pawn) || xxx.is_bloodlust(pawn)))
					//	currentThought = xxx.gave_virginity;
				}
				else if (xxx.is_insect(partner))
				{
					pawn.records.Increment(xxx.CountOfSexWithInsects);
					pawn.records.Increment(xxx.CountOfBeenRapedByInsects);
				}
				else if (xxx.is_animal(partner))
				{
					pawn.records.Increment(xxx.CountOfSexWithAnimals);
					pawn.records.Increment(xxx.CountOfBeenRapedByAnimals);
					//if (zoophile) currentThought = xxx.gave_virginity;
				}
				else
				{
					pawn.records.Increment(xxx.CountOfSexWithOthers);
					pawn.records.Increment(xxx.CountOfBeenRapedByOthers);
				}
			}

			//if (isVirginSex) //&& (sextype == rjwSextype.Vaginal || sextype == rjwSextype.DoublePenetration))
			//{
			//	Log.Message(xxx.get_pawnname(pawn) + " | " + xxx.get_pawnname(partner) + " | " + currentThought);
			//	Log.Message("1");
			//	if (!is_animal(partner))//passive
			//	{
			//		if (currentThought != null)
			//			partner.needs.mood.thoughts.memories.TryGainMemory(currentThought);
			//	}
			//	Log.Message("2");
			//	if (!is_animal(pawn))//active
			//	{
			//		currentThought = took_virginity;
			//		pawn.needs.mood.thoughts.memories.TryGainMemory(currentThought);
			//	}
			//}
		}

		//============↓======Section of processing the broken body system===============↓=============
		public static bool BodyIsBroken(Pawn pawn)
		{
			return pawn.health.hediffSet.HasHediff(xxx.feelingBroken);
		}

		[SyncMethod]
		public static bool BadlyBroken(Pawn pawn, out Trait addedTrait)
		{
			addedTrait = null;
			if (!BodyIsBroken(pawn))
			{
				return false;
			}

			int stage = pawn.health.hediffSet.GetFirstHediffOfDef(xxx.feelingBroken).CurStageIndex;
			if (stage >= 3)
			{
				//when broken make character masochist
				//todo remove/replace social/needs dubuffs
				if (RJWSettings.AddTrait_Masocist && !xxx.is_masochist(pawn))
				{
					var chance = 0.05f;
					if (Rand.Chance(chance))
					{
						if (xxx.is_rapist(pawn))
						{
							pawn.story.traits.allTraits.Remove(pawn.story.traits.GetTrait(xxx.rapist));
							//Log.Message(xxx.get_pawnname(pawn) + " BadlyBroken, switch rapist -> masochist");
						}
						addedTrait = new Trait(xxx.masochist);
						pawn.story.traits.GainTrait(addedTrait);
						//Log.Message(xxx.get_pawnname(pawn) + " BadlyBroken, not masochist, adding masochist trait");

						pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDef(got_raped);
						pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDef(got_licked);
						pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDef(hate_my_rapist);
						pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDef(allowed_me_to_get_raped);
						pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDef(got_anal_raped);
						pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDef(got_anal_raped_byfemale);
					}
				}
				if (pawn.IsPrisonerOfColony)
				{
					pawn.guest.resistance = Mathf.Max(pawn.guest.resistance - 1f, 0f);
					//Log.Message(xxx.get_pawnname(pawn) + " BadlyBroken, reduce prisoner resistance");
				}
			}
			return stage > 1;
		}

		//add variant for eggs?
		public static void processBrokenPawn(Pawn pawn, List<Trait> addedTraits)
		{
			// Called after rape/breed
			if (pawn is null)
				return;

			if (xxx.is_human(pawn) && !pawn.Dead && pawn.records != null)
			{
				if (xxx.has_traits(pawn))
				{
					if (xxx.is_slime(pawn))
						return;

					if (!BodyIsBroken(pawn))
						pawn.health.AddHediff(xxx.feelingBroken);
					else
					{
						float brokenSeverityGain = xxx.feelingBroken.initialSeverity;
						int feelingBrokenStage = pawn.health.hediffSet.GetFirstHediffOfDef(xxx.feelingBroken).CurStageIndex;

						if (xxx.RoMIsActive)
							if (pawn.story.traits.HasTrait(xxx.Succubus))
								brokenSeverityGain *= 0.25f;

						if (pawn.story.traits.HasTrait(VanillaTraitDefOf.Tough))
						{
							brokenSeverityGain *= 0.5f;
						}
						if (pawn.story.traits.HasTrait(TraitDefOf.Wimp))
						{
							brokenSeverityGain *= 2.0f;
						}
						if (pawn.story.traits.HasTrait(VanillaTraitDefOf.Nerves))
						{
							int currentNervesDegree = pawn.story.traits.DegreeOfTrait(VanillaTraitDefOf.Nerves);

							switch (currentNervesDegree)
							{
								case -2:
									brokenSeverityGain *= 2.0f;
									break;
								case -1:
									brokenSeverityGain *= 1.5f;
									break;
								case 1:
									brokenSeverityGain *= 0.5f;
									break;
								case 2:
									brokenSeverityGain *= 0.25f;
									break;
							}

							if (RJWSettings.AddTrait_Nerves)
							{
								int newNervesDegree = 0;
								if (feelingBrokenStage >= 3 && currentNervesDegree > -2)
								{
									newNervesDegree = -2;
								}
								else if (feelingBrokenStage >= 2 && currentNervesDegree > -1)
								{
									newNervesDegree = -1;
								}

								if (newNervesDegree < 0)
								{
									pawn.story.traits.RemoveTrait(pawn.story.traits.GetTrait(VanillaTraitDefOf.Nerves));
									Trait newNerves = new Trait(VanillaTraitDefOf.Nerves, newNervesDegree);
									pawn.story.traits.GainTrait(newNerves);
									addedTraits.Add(newNerves);
								}
							}
						}
						else if (RJWSettings.AddTrait_Nerves && feelingBrokenStage > 1)
						{
							pawn.story.traits.GainTrait(new Trait(VanillaTraitDefOf.Nerves, -1));
							Trait nervous = new Trait(VanillaTraitDefOf.Nerves, -1);
							pawn.story.traits.GainTrait(nervous);
							addedTraits.Add(nervous);

						}
						pawn.health.hediffSet.GetFirstHediffOfDef(xxx.feelingBroken).Severity += brokenSeverityGain;
					}

					BadlyBroken(pawn, out Trait newMasochistTrait);
					if (newMasochistTrait != null)
					{
						addedTraits.Add(newMasochistTrait);
					}
				}
			}
		}

		//============↑======Section of processing the broken body system===============↑=============
	}
}


code:
using Verse;
using Verse.AI;
using System.Linq;
using RimWorld;
using System;

namespace rjw
{
	public static class Bed_Utility
	{
		public static bool bed_has_at_least_two_occupants(Building_Bed bed)
		{
			return bed.CurOccupants.Count() >= 2;
		}

		public static bool in_same_bed(Pawn pawn, Pawn partner)
		{
			if (pawn.InBed() && partner.InBed())
			{
				if (pawn.CurrentBed() == partner.CurrentBed())
					return true;
			}
			return false;
		}

		public static bool is_laying_down_alone(Pawn pawn)
		{
			if (pawn == null || !pawn.InBed()) return false;

			if (pawn.CurrentBed() != null)
				return !bed_has_at_least_two_occupants(pawn.CurrentBed());
			return true;
		}

		public static IntVec3 SleepPosOfAssignedPawn(this Building_Bed bed, Pawn pawn)
		{
			int slotIndex = 0;

			if (bed.OwnersForReading.Contains(pawn))
			{
				for (byte i = 0; i < bed.OwnersForReading.Count; i++)
				{
					if (bed.OwnersForReading[i] == pawn)
					{
						slotIndex = i;
					}
				}
			}
			else
			{
				// get random position
				slotIndex = GenTicks.TicksGame % bed.SleepingSlotsCount;
			}

			return bed.GetSleepingSlotPos(slotIndex);
		}
		public static void FailOnBedNoLongerUsable(this Toil toil, TargetIndex bedIndex, Building_Bed bed)
		{
			if (toil == null)
			{
				throw new ArgumentNullException(nameof(toil));
			}

			toil.FailOnDespawnedOrNull(bedIndex);
			toil.FailOn(bed.IsBurning);
			toil.FailOn(() => HealthAIUtility.ShouldSeekMedicalRestUrgent(toil.actor));
			toil.FailOn(() => toil.actor.IsColonist && !toil.actor.CurJob.ignoreForbidden && !toil.actor.Downed && bed.IsForbidden(toil.actor));
		}
	}
}

license: mit


code:
//#define TESTMODE // Uncomment to enable logging.

using Verse;
using Verse.AI;
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using System.Diagnostics;
using Multiplayer.API;

namespace rjw
{
	/// <summary>
	/// Helper for sex with animals
	/// </summary>
	public class BreederHelper
	{
		public const int max_animals_at_once = 1; // lets not forget that the purpose is procreation, not sodomy

		[Conditional("TESTMODE")]
		private static void DebugText(string msg)
		{
			ModLog.Message(msg);
		}

		//animal try to find best designated pawn to breed
		[SyncMethod]
		public static Pawn find_designated_breeder(Pawn pawn, Map m)
		{
			if (!DesignatorsData.rjwBreeding.Any())
				return null;

			DebugText("BreederHelper::find_designated_breeder( " + xxx.get_pawnname(pawn) + " ) called");

			float min_fuckability = 0.10f;							// Don't rape pawns with <10% fuckability
			float avg_fuckability = 0f;								// Average targets fuckability, choose target higher than that
			var valid_targets = new Dictionary<Pawn, float>();		// Valid pawns and their fuckability
			Pawn chosentarget = null;                               // Final target pawn

			DebugText(" breeder.Faction( " + pawn.Faction + " )");
			if (pawn.Faction == null)// HostileTo causes error on fresh colony(w/o name)
				return null;

			DebugText(" DesignatorsData.rjwBreeding.Count( " + DesignatorsData.rjwBreeding.Count() + " )");
			IEnumerable<Pawn> targets = DesignatorsData.rjwBreeding.Where(x
				=> x != pawn
				&& xxx.is_not_dying(x)
				&& xxx.can_get_raped(x)
				&& !x.IsForbidden(pawn)
				&& !x.Suspended
				&& !x.HostileTo(pawn)
				&& !(x.IsPregnant() && xxx.is_animal(x))
				&& pawn.CanReserveAndReach(x, PathEndMode.Touch, Danger.Some, max_animals_at_once)
				&& ((RJWSettings.bestiality_enabled && xxx.is_human(x)) || (RJWSettings.animal_on_animal_enabled && xxx.is_animal(x)))
				);

			DebugText(" initial targets( " + targets.Count() + " )");

			var parts = pawn.GetGenitalsList();

			foreach (Pawn target in targets)
			{
				DebugText(" Checking target " + target.kindDef.race.defName.ToLower());
				if (!Pather_Utility.cells_to_target_rape(pawn, target.Position))
					continue;// too far

				var fuc = SexAppraiser.would_fuck(pawn, target, invert_opinion: true, ignore_gender: (Genital_Helper.has_male_bits(pawn, parts) || xxx.is_insect(pawn)));
				DebugText(" target score( " + xxx.get_pawnname(pawn) + " -> " + xxx.get_pawnname(target) + " (" + fuc.ToString() + " / " + min_fuckability.ToString() + ")");

				if (fuc > min_fuckability)
					if (Pather_Utility.can_path_to_target(pawn, target.Position))
						valid_targets.Add(target, fuc);
			}

			DebugText(" valid_targets( " + valid_targets.Count() + " )");

			if (valid_targets.Any())
			{
				//avg_fuckability = valid_targets.Average(x => x.Value);

				// choose pawns to fuck with above average fuckability
				var valid_targetsFilteredAnimals = valid_targets.Where(x => x.Value >= avg_fuckability);

				if (valid_targetsFilteredAnimals.Any())
					chosentarget = valid_targetsFilteredAnimals.RandomElement().Key;
			}

			DebugText(" chosentarget( " + xxx.get_pawnname(chosentarget) + " )");
			return chosentarget;
		}

		//animal or human try to find animals to breed (non designation)
		//public static Pawn find_breeder_animal(Pawn pawn, Map m, bool SameRace = true)
		[SyncMethod]
		public static Pawn find_breeder_animal(Pawn pawn, Map m)
		{
			DebugText("BreederHelper::find_breeder_animal( " + xxx.get_pawnname(pawn) + " ) called");

			float min_fuckability = 0.10f;							// Don't rape pawns with <10% fuckability
			float avg_fuckability = 0f;								// Average targets fuckability, choose target higher than that
			var valid_targets = new Dictionary<Pawn, float>();		// Valid pawns and their fuckability
			Pawn chosentarget = null;                               // Final target pawn

			DebugText(" m.mapPawns.AllPawnsSpawned.Count( " + m.mapPawns.AllPawnsSpawned.Count() + " )");
			//Pruning initial pawn list.
			IEnumerable<Pawn> targets = m.mapPawns.AllPawnsSpawned.Where(x
				=> x != pawn
				&& xxx.is_animal(x)
				&& xxx.can_get_raped(x)
				&& !x.IsForbidden(pawn)
				&& !x.Suspended
				&& !x.HostileTo(pawn)
				&& pawn.CanReserveAndReach(x, PathEndMode.Touch, Danger.Some, max_animals_at_once)
				&& ((RJWSettings.bestiality_enabled && xxx.is_human(pawn)) || (RJWSettings.animal_on_animal_enabled && xxx.is_animal(pawn)))
				//&& SameRace ? pawn.kindDef.race == x.kindDef.race : true
				);

			DebugText(" initial targets( " + targets.Count() + " )");

			if (targets.Any())
			{
				var parts = pawn.GetGenitalsList();

				//filter pawns for female, who can fuck her
				//not sure if faction check should be but w/e
				if (!Genital_Helper.has_male_bits(pawn, parts) && (Genital_Helper.has_vagina(pawn, parts) || Genital_Helper.has_anus(pawn)))
				{
					targets = targets.Where(x => xxx.can_fuck(x) && x.Faction == pawn.Faction);
				}

				//for humans, animals dont have need - always = 3f
				//if not horny, seek only targets with safe temp
				if (xxx.need_some_sex(pawn) < 3.0f)
				{
					targets = targets.Where(x => pawn.CanReach(x, PathEndMode.Touch, Danger.None));
				}

				//Used for interspecies animal-on-animal.
				//Animals will only go for targets they can see.
				if (xxx.is_animal(pawn))
				{
					targets = targets.Where(x => pawn.CanSee(x) && pawn.def.defName != x.def.defName);
				}
				else
				{
					// Pickier about the targets if the pawn has no prior experience.
					if (pawn.records.GetValue(xxx.CountOfSexWithAnimals) < 3 && !xxx.is_zoophile(pawn))
					{
						min_fuckability *= 2f;
					}

					if (xxx.is_frustrated(pawn))
					{   // Less picky when frustrated...
						min_fuckability *= 0.6f;
					}
					else if (!xxx.is_hornyorfrustrated(pawn))
					{   // ...and far more picky when satisfied.
						min_fuckability *= 2.5f;
					}
				}
			}

			if (!targets.Any())
			{
				DebugText(" valid_targets( " + targets.Count() + " )");
				return null; //None found.
			}

			foreach (Pawn target in targets)
			{
				DebugText(" Checking target " + target.kindDef.race.defName.ToLower());

				if (!Pather_Utility.cells_to_target_rape(pawn, target.Position))
					continue;// too far

				float fuc = SexAppraiser.would_fuck_animal(pawn, target); // 0.0 to ~3.0, orientation checks etc.
				DebugText(" target score( " + xxx.get_pawnname(pawn) + " -> " + xxx.get_pawnname(target) + " (" + fuc.ToString() + " / " + min_fuckability.ToString() + ")");

				if (fuc > min_fuckability)
					if (Pather_Utility.can_path_to_target(pawn, target.Position))
						valid_targets.Add(target, fuc);
			}

			DebugText(" valid_targets( " + valid_targets.Count() + " )");
			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			if (valid_targets.Any())
			{
				avg_fuckability = valid_targets.Average(x => x.Value);

				// choose pawns to fuck with above average fuckability
				var valid_targetsFilteredAnimals = valid_targets.Where(x => x.Value >= avg_fuckability);

				if (valid_targetsFilteredAnimals.Any())
					chosentarget = valid_targetsFilteredAnimals.RandomElement().Key;
			}

			DebugText(" chosentarget( " + xxx.get_pawnname(chosentarget) + " )");
			return chosentarget;
		}
	}
}


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

namespace rjw
{
	/// <summary>
	/// Helper for sex with (humanlikes)
	/// </summary>
	public class CasualSex_Helper
	{
		public static readonly HashSet<string> quickieAllowedJobs = new HashSet<string>(DefDatabase<StringListDef>.GetNamed("QuickieInterruptibleJobs").strings);

		public static bool CanHaveSex(Pawn target)
		{
			return xxx.can_fuck(target) || xxx.can_be_fucked(target);
		}

		[SyncMethod]
		public static bool roll_to_skip(Pawn pawn, Pawn target, out float fuckability)
		{
			fuckability = SexAppraiser.would_fuck(pawn, target); // 0.0 to 1.0
			if (fuckability < RJWHookupSettings.MinimumFuckabilityToHookup)
			{
				if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" roll_to_skip(I, {xxx.get_pawnname(pawn)} won't fuck {xxx.get_pawnname(target)}), ({fuckability})");
				return false;
			}

			float reciprocity = xxx.is_animal(target) ? 1.0f : SexAppraiser.would_fuck(target, pawn);
			if (reciprocity < RJWHookupSettings.MinimumFuckabilityToHookup)
			{
				if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" roll_to_skip({xxx.get_pawnname(target)} won't fuck me, {xxx.get_pawnname(pawn)}), ({reciprocity})");
				return false;
			}

			float chance_to_skip = 0.9f - 0.7f * fuckability;
			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			return Rand.Value < chance_to_skip;
		}

		[SyncMethod]
		public static Pawn find_partner(Pawn pawn, Map map, bool bedsex = false)
		{
			string pawnName = xxx.get_pawnname(pawn);
			if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({pawnName}): starting.");

			bool pawnIsNympho = xxx.is_nympho(pawn);
			bool pawnCanPickAnyone = RJWSettings.WildMode;// || (pawnIsNympho && RJWHookupSettings.NymphosCanPickAnyone);
			bool pawnCanPickAnimals = (pawnCanPickAnyone || xxx.is_zoophile(pawn)) && RJWSettings.bestiality_enabled;
			bool pawnCanPickMechanoids = (pawnCanPickAnyone || xxx.is_mechophile(pawn));

			if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({pawnName}): nympho:{pawnIsNympho}, ignores rules:{pawnCanPickAnyone}, zoo:{pawnCanPickAnimals}");

			if (!RJWHookupSettings.ColonistsCanHookup && pawn.IsFreeColonist && !pawnCanPickAnyone)
			{
				if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({pawnName}): is a colonist and colonist hookups are disabled in mod settings");
				return null;
			}

			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());

			// Check AllPawns, not just colonists, to include guests.
			List<Pawn> targets = map.mapPawns.AllPawns.Where(x
				=> x != pawn
				&& ((!bedsex && !x.InBed()) || (bedsex && (Bed_Utility.is_laying_down_alone(x) || Bed_Utility.in_same_bed(x, pawn))))
				&& !x.IsForbidden(pawn)
				&& xxx.IsTargetPawnOkay(x)
				&& CanHaveSex(x)
				&& x.Map == pawn.Map
				&& !x.HostileTo(pawn)
				//&& ((pawnCanPickAnimals && xxx.is_animal(x)) || !xxx.is_animal(x))
				&& !xxx.is_animal(x)
				).ToList();

			if (!targets.Any())
			{
				if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({pawnName}): no eligible targets");
				return null;
			}

			// HippieMode means: everyone is allowed to have sex with everyone else ("free love")
			//	- no prioritization of partners
			//	- not necessary to be horny
			//	- never cheating
			// -> just pick a partner

			if (!RJWSettings.HippieMode)
			{
				if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({pawnName}): considering {targets.Count} targets");

				// find lover/partner on same map
				List<Pawn> partners = targets.Where(x
					=> pawn.relations.DirectRelationExists(PawnRelationDefOf.Lover, x)
					|| pawn.relations.DirectRelationExists(PawnRelationDefOf.Fiance, x)
					|| pawn.relations.DirectRelationExists(PawnRelationDefOf.Spouse, x)
					).ToList();

				if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({pawnName}): considering {partners.Count} partners");

				if(pawnCanPickMechanoids)
				{
					partners.AddRange(targets.Where(x => x.IsColonyMechPlayerControlled));
				}

				if (partners.Any())
				{
					partners.Shuffle(); //Randomize order.
					foreach (Pawn target in partners)
					{
						if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({pawnName}): checking lover {xxx.get_pawnname(target)}");

						//interruptible jobs
						if (!bedsex && target.jobs?.curJob != null &&
							(target.jobs.curJob.playerForced ||
							!quickieAllowedJobs.Contains(target.jobs.curJob.def.defName)
							))
						{
							if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_pawn_to_fuck({pawnName}): lover has important job ({target.jobs.curJob.def}), skipping");
							continue;
						}
						if (Pather_Utility.cells_to_target_casual(pawn, target.Position)
							&& pawn.CanReserveAndReach(target, PathEndMode.OnCell, Danger.Some, 1, 0)
							&& target.CanReserve(pawn, 1, 0)
							&& SexAppraiser.would_fuck(pawn, target) > 0.1f
							&& SexAppraiser.would_fuck(target, pawn) > 0.1f)
						{
							if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({pawnName}): banging lover {xxx.get_pawnname(target)}");
							return target;
						}
					}
				}

				// No lovers around... see if the pawn fancies a hookup.  Nymphos and frustrated pawns always do!
				if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({pawnName}): no partners available.  checking canHookup");
				bool canHookup = pawnIsNympho || pawnCanPickAnyone || xxx.is_frustrated(pawn) || (xxx.is_horny(pawn) && Rand.Value < RJWHookupSettings.HookupChanceForNonNymphos);
				if (!canHookup)
				{
					if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({pawnName}): no hookup today");
					return null;
				}

				// No cheating from casual hookups... would probably make colony relationship management too annoying
				if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({pawnName}): checking canHookupWithoutCheating");
				bool hookupWouldBeCheating = xxx.HasNonPolyPartner(pawn);
				if (hookupWouldBeCheating)
				{
					if (RJWHookupSettings.NymphosCanCheat && pawnIsNympho && xxx.is_frustrated(pawn))
					{
						if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({pawnName}): I'm a nympho and I'm so frustrated that I'm going to cheat");
						// No return here so they continue searching for hookup
					}
					else if (pawn.health.hediffSet.HasHediff(HediffDef.Named("AlcoholHigh")))
					{
						if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({pawnName}): I want to bang and im too drunk to care if its cheating");
					}
					else
					{
						if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({pawnName}): I want to bang but that's cheating");
						return null;
					}
				}
			}
			Pawn best_fuckee = FindBestPartner(pawn, targets, pawnCanPickAnyone, pawnIsNympho);
			return best_fuckee;
		}

		/// <summary> Checks all of our potential partners to see if anyone's eligible, returning the most attractive and convenient one. </summary>
		public static Pawn FindBestPartner(Pawn pawn, List<Pawn> targets, bool pawnCanPickAnyone, bool pawnIsNympho)
		{
			string pawnName = xxx.get_pawnname(pawn);

			Pawn best_fuckee = null;
			float best_fuckability_score = 0;

			foreach (Pawn targetPawn in targets)
			{
				if (targetPawn.relations == null) // probably droids or who knows what modded abomination
					continue;

				if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" FindBestPartner({pawnName}): checking hookup {xxx.get_pawnname(targetPawn)}");

				// Check to see if the mod settings for hookups allow this pairing
				if (!pawnCanPickAnyone && !HookupAllowedViaSettings(pawn, targetPawn))
					continue;

				//interruptible jobs
				if (targetPawn.jobs?.curJob != null &&
					(targetPawn.jobs.curJob.playerForced ||
					!quickieAllowedJobs.Contains(targetPawn.jobs.curJob.def.defName)
					))
				{
					if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" FindBestPartner({pawnName}): targetPawn has important job ({targetPawn.jobs.curJob.def}, playerForced: {targetPawn.jobs.curJob.playerForced}), skipping");
					continue;
				}

				// Check for homewrecking (banging a pawn who's in a relationship)
				if (!xxx.is_animal(targetPawn) &&
					!RJWSettings.HippieMode &&
					xxx.HasNonPolyPartner(targetPawn))
				{
					if (RJWHookupSettings.NymphosCanHomewreck && pawnIsNympho && xxx.is_frustrated(pawn))
					{
						// Hookup allowed... rip colony mood
					}
					else if (RJWHookupSettings.NymphosCanHomewreckReverse && xxx.is_nympho(targetPawn) && xxx.is_frustrated(targetPawn))
					{
						// Hookup allowed... rip colony mood
					}
					else if (!pawn.health.hediffSet.HasHediff(HediffDef.Named("AlcoholHigh")))
					{
						if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" FindBestPartner({pawnName}): not hooking up with {xxx.get_pawnname(targetPawn)} to avoid homewrecking");
						continue;
					}
				}

				// If the pawn has had sex recently and isn't horny right now, skip them.
				if (!SexUtility.ReadyForLovin(targetPawn) && !xxx.is_hornyorfrustrated(targetPawn))
				{
					if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" FindBestPartner({pawnName}): hookup {xxx.get_pawnname(targetPawn)} isn't ready for lovin'");
					continue;
				}

				if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" FindBestPartner({pawnName}): hookup {xxx.get_pawnname(targetPawn)} is sufficiently single");

				if (!xxx.is_animal(targetPawn))
				{
					float relations = pawn.relations.OpinionOf(targetPawn);
					if (relations < RJWHookupSettings.MinimumRelationshipToHookup)
					{
						if (!(relations > 0 && xxx.is_nympho(pawn)))
						{
							if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" FindBestPartner({pawnName}): hookup {xxx.get_pawnname(targetPawn)}, i don't like them:({relations})");
							continue;
						}
					}

					relations = targetPawn.relations.OpinionOf(pawn);
					if (relations < RJWHookupSettings.MinimumRelationshipToHookup)
					{
						if (!(relations > 0 && xxx.is_nympho(targetPawn)))
						{
							if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" FindBestPartner({pawnName}): hookup {xxx.get_pawnname(targetPawn)}, don't like me:({relations})");
							continue;
						}
					}

					float attraction = pawn.relations.SecondaryRomanceChanceFactor(targetPawn);
					if (attraction < RJWHookupSettings.MinimumAttractivenessToHookup)
					{
						if (!(attraction > 0 && xxx.is_nympho(pawn)))
						{
							if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" FindBestPartner({pawnName}): hookup {xxx.get_pawnname(targetPawn)}, i don't find them attractive:({attraction})");
							continue;
						}
					}
					attraction = targetPawn.relations.SecondaryRomanceChanceFactor(pawn);
					if (attraction < RJWHookupSettings.MinimumAttractivenessToHookup)
					{
						if (!(attraction > 0 && xxx.is_nympho(targetPawn)))
						{
							if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" FindBestPartner({pawnName}): hookup {xxx.get_pawnname(targetPawn)}, doesn't find me attractive:({attraction})");
							continue;
						}
					}
				}

				// Check to see if the two pawns are willing to bang, and if so remember how much attractive we find them
				float fuckability = 0f;
				if (pawn.CanReserveAndReach(targetPawn, PathEndMode.OnCell, Danger.Some, 1, 0) &&
					targetPawn.CanReserve(pawn, 1, 0) &&
					roll_to_skip(pawn, targetPawn, out fuckability)) // do NOT check pawnIgnoresRules here - these checks, particularly roll_to_skip, are critical
				{
					float dis = pawn.Position.DistanceTo(targetPawn.Position);

					if (dis <= 4)
					{
						// Right next to me (in my bed)?  You'll do.
						if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" FindBestPartner({pawnName}): hookup {xxx.get_pawnname(targetPawn)} is right next to me. we'll bang, ok?");
						best_fuckability_score = 1.0e6f;
						best_fuckee = targetPawn;
						break;
					}
					else if (dis > RJWSettings.maxDistanceCellsCasual)
					{
						// too far
						if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" FindBestPartner({pawnName}): hookup {xxx.get_pawnname(targetPawn)} is too far... distance:{dis} max:{RJWSettings.maxDistanceCellsCasual}");
						continue;
					}
					else
					{
						// scaling fuckability by distance may give us more varied results and give the less attractive folks a chance
						float fuckability_score = fuckability / GenMath.Sqrt(GenMath.Sqrt(dis));
						if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" FindBestPartner({pawnName}): hookup {xxx.get_pawnname(targetPawn)} is totally bangable.  attraction: {fuckability}, score:{fuckability_score}");

						if (fuckability_score > best_fuckability_score)
						{
							best_fuckee = targetPawn;
							best_fuckability_score = fuckability_score;
						}
					}
				}
			}

			if (best_fuckee == null)
			{
				if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" FindBestPartner({pawnName}): couldn't find anyone to bang");
			}
			else
			{
				if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" FindBestPartner({pawnName}): found rando {xxx.get_pawnname(best_fuckee)} with score {best_fuckability_score}");
			}

			return best_fuckee;
		}

		/// <summary> Checks to see if the mod settings allow the two pawns to hookup. </summary>
		public static bool HookupAllowedViaSettings(Pawn pawn, Pawn targetPawn)
		{
			// Can prisoners hook up?
			if (pawn.IsPrisonerOfColony || pawn.IsPrisoner || xxx.is_slave(pawn))
			{
				if (!RJWHookupSettings.PrisonersCanHookupWithNonPrisoner && !(targetPawn.IsPrisonerOfColony || targetPawn.IsPrisoner || xxx.is_slave(targetPawn)))
				{
					if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({xxx.get_pawnname(pawn)}): not hooking up with {xxx.get_pawnname(targetPawn)} due to mod setting PrisonersCanHookupWithNonPrisoner");
					return false;
				}

				if (!RJWHookupSettings.PrisonersCanHookupWithPrisoner && (targetPawn.IsPrisonerOfColony || targetPawn.IsPrisoner || xxx.is_slave(targetPawn)))
				{
					if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({xxx.get_pawnname(pawn)}): not hooking up with {xxx.get_pawnname(targetPawn)} due to mod setting PrisonersCanHookupWithPrisoner");
					return false;
				}
			}
			else
			{
				// Can non prisoners hook up with prisoners?
				if (!RJWHookupSettings.CanHookupWithPrisoner && (targetPawn.IsPrisonerOfColony || targetPawn.IsPrisoner || xxx.is_slave(targetPawn)))
				{
					if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({xxx.get_pawnname(pawn)}): not hooking up with {xxx.get_pawnname(targetPawn)} due to mod setting CanHookupWithPrisoner");
					return false;
				}
			}

			// Can slave hook up?
			//if (xxx.is_slave(pawn))
			//{
			//	if (!RJWHookupSettings.SlaveCanHookup)
			//	{
			//		if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({xxx.get_pawnname(pawn)}): not hooking up with {xxx.get_pawnname(targetPawn)} due to mod setting SlaveCanHookup");
			//		return false;
			//	}
			//}

			// Can colonist hook up with visitors?
			if (pawn.IsFreeColonist && !xxx.is_slave(pawn))
			{
				if (!RJWHookupSettings.ColonistsCanHookupWithVisitor && targetPawn.Faction != Faction.OfPlayer && !targetPawn.IsPrisonerOfColony)
				{
					if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({xxx.get_pawnname(pawn)}): not hooking up with {xxx.get_pawnname(targetPawn)} due to mod setting ColonistsCanHookupWithVisitor");
					return false;
				}
			}

			// Can visitors hook up?
			if (pawn.Faction != Faction.OfPlayer && !pawn.IsPrisonerOfColony)
			{
				// visitors vs colonist
				if (!RJWHookupSettings.VisitorsCanHookupWithColonists && targetPawn.IsColonist)
				{
					if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({xxx.get_pawnname(pawn)}): not hooking up with {xxx.get_pawnname(targetPawn)} due to mod setting VisitorsCanHookupWithColonists");
					return false;
				}

				// visitors vs visitors
				if (!RJWHookupSettings.VisitorsCanHookupWithVisitors && targetPawn.Faction != Faction.OfPlayer)
				{
					if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" find_partner({xxx.get_pawnname(pawn)}): not hooking up with {xxx.get_pawnname(targetPawn)} due to mod setting VisitorsCanHookupWithVisitors");
					return false;
				}
			}

			// TODO: Not sure if this handles all the pawn-on-animal cases.

			return true;
		}

		[SyncMethod]
		public static IntVec3 FindSexLocation(Pawn pawn, Pawn partner = null)
		{
			IntVec3 position = pawn.Position;
			int bestPosition = -100;
			IntVec3 cell = pawn.Position;
			int maxDistance = 40;

			FloatRange temperature = pawn.ComfortableTemperatureRange();
			List<Pawn> all_pawns = pawn.Map.mapPawns.AllPawnsSpawned.Where(x
				=> x.Position.DistanceTo(pawn.Position) < 100
				&& xxx.is_human(x)
				&& x != pawn
				&& x != partner
				).ToList();

			bool is_somnophile = xxx.has_quirk(pawn, "Somnophile");
			bool is_exhibitionist = xxx.has_quirk(pawn, "Exhibitionist");

			//ModLog.Message(" Pawn is " + xxx.get_pawnname(pawn) + ", current cell is " + cell);

			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			List<IntVec3> random_cells = new List<IntVec3>();
			Dictionary<int, int> random_cells_rooms_doors = new Dictionary<int, int>();
			for (int loop = 0; loop < 50; ++loop)
			{
				random_cells.Add(position + IntVec3.FromVector3(Vector3Utility.HorizontalVectorFromAngle(Rand.Range(0, 360)) * Rand.RangeInclusive(1, maxDistance)));
			}

			random_cells = random_cells.Where(x
				=> x.Standable(pawn.Map)
				&& x.InAllowedArea(pawn)
				&& x.GetDangerFor(pawn, pawn.Map) != Danger.Deadly
				&& !x.ContainsTrap(pawn.Map)
				&& !x.ContainsStaticFire(pawn.Map)
				).Distinct().ToList();

			//ModLog.Message(" Found " + random_cells.Count + " valid cells.");

			foreach (var rcell in random_cells)
			{
				var rooom = rcell.GetRoom(pawn.Map);
				if (!random_cells_rooms_doors.ContainsKey(rooom.ID) && !rooom.PsychologicallyOutdoors)
					random_cells_rooms_doors.Add(rooom.ID, rooom.ContainedAndAdjacentThings.Count(
						x => x.def.IsDoor
						&& x.def.holdsRoof && x.def.blockLight));   //dubs hyg toilets stall doors(false,false)?
			}

			foreach (IntVec3 random_cell in random_cells)
			{
				if (!Pather_Utility.cells_to_target_casual(pawn, random_cell))
					continue;// too far

				int score = 0;
				Room room = random_cell.GetRoom(pawn.Map);

				bool might_be_seen = MightBeSeen(all_pawns, random_cell, pawn, partner);

				if (is_exhibitionist)
				{
					if (might_be_seen)
						score += 5;
					else
						score -= 10;
				}
				else
				{
					if (might_be_seen)
						score -= 30;
				}
				if (partner == null && is_somnophile) // Fap while Watching someone sleep. Not creepy at all!
				{
					if (all_pawns.Any(x
						=> !x.Awake()
						&& x.Position.DistanceTo(random_cell) < 6
						&& GenSight.LineOfSight(random_cell, x.Position, pawn.Map)
						))
						score += 50;
				}

				if (random_cell.GetTemperature(pawn.Map) > temperature.min && random_cell.GetTemperature(pawn.Map) < temperature.max)
					score += 20;
				else
					score -= 20;
				if (random_cell.Roofed(pawn.Map))
					score += 5;
				if (random_cell.HasEatSurface(pawn.Map))
					score += 5; // Hide in vegetation.
				if (random_cell.GetDangerFor(pawn, pawn.Map) == Danger.Some)
					score -= 25;
				else if (random_cell.GetDangerFor(pawn, pawn.Map) == Danger.None)
					score += 5;
				if (random_cell.GetTerrain(pawn.Map) == TerrainDefOf.WaterShallow ||
					random_cell.GetTerrain(pawn.Map) == TerrainDefOf.WaterMovingShallow ||
					random_cell.GetTerrain(pawn.Map) == TerrainDefOf.WaterOceanShallow)
					score -= 20;

				if (random_cell.GetThingList(pawn.Map).Any(x => x.def.IsWithinCategory(ThingCategoryDefOf.Corpses)))
					if (xxx.is_necrophiliac(pawn))
						score += 20;
					else
						score -= 20;
				if (random_cell.GetThingList(pawn.Map).Any(x => x.def.IsWithinCategory(ThingCategoryDefOf.Foods)))
					score -= 10;

				if (room == pawn.Position.GetRoom(pawn.MapHeld))
					score -= 10;
				if (room.PsychologicallyOutdoors)
					score += 5;
				if (room.IsHuge)
					score -= 5;
				if (room.ContainedBeds.Any())
				{
					score += 5;
					if (room.ContainedBeds.Any(x => x.Position == random_cell))
						score += 5;
				}
				if (!room.Owners.Any())
					score += 10;
				else if (room.Owners.Contains(pawn))
					score += 20;
				if (room.IsDoorway && !is_exhibitionist)
					score -= 100;
				if (room.Role == RoomRoleDefOf.Bedroom)
					score += 10;
				else if (room.Role == RoomRoleDefOf.Barracks || room.Role == RoomRoleDefOf.PrisonBarracks || room.Role == RoomRoleDefOf.PrisonCell
					|| room.Role == VanillaRoomRoleDefOf.Laboratory || room.Role == VanillaRoomRoleDefOf.RecRoom
					|| room.Role == VanillaRoomRoleDefOf.DiningRoom || room.Role == RoomRoleDefOf.Hospital
					)
					if (is_exhibitionist)
						score += 10;
					else
						score -= 5;
				if (room.GetStat(RoomStatDefOf.Cleanliness) < 0.01f)
					score -= 5;
				if (room.GetStat(RoomStatDefOf.GraveVisitingJoyGainFactor) > 0.1f)
					if (xxx.is_necrophiliac(pawn))
					{
						score += 5;
					}
					else
					{
						score -= 5;
					}

				var doors = random_cells_rooms_doors.TryGetValue(room.ID);
				if (doors > 1)
					if (is_exhibitionist)
					{
						score += 2 * doors;
					}
					else
					{
						score -= 5 * doors;
					}

				if (score <= bestPosition) continue;

				bestPosition = score;
				cell = random_cell;
			}

			return cell;

			//ModLog.Message(" Best cell is " + cell);
		}

		public static bool MightBeSeen(List<Pawn> otherPawns, IntVec3 cell, Pawn pawn, Pawn partner = null)
		{
			return otherPawns.Any(x
					=> x != partner
					&& x.Awake()
					&& x.Position.DistanceTo(cell) < 50
					&& GenSight.LineOfSight(x.Position, cell, pawn.Map)
					);
		}
	}
}


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

namespace rjw
{
	//TODO: this needs rewrite to account reverse and group sex
	public class CondomUtility
	{
		public static readonly RecordDef CountOfCondomsUsed = DefDatabase<RecordDef>.GetNamed("CountOfCondomsUsed");
		public static readonly ThoughtDef SexWithCondom = DefDatabase<ThoughtDef>.GetNamed("SexWithCondom");
		public static readonly ThingDef Condom = DefDatabase<ThingDef>.GetNamedSilentFail("Condom");
		public static readonly ThingDef UsedCondom = DefDatabase<ThingDef>.GetNamedSilentFail("UsedCondom");

		/// <summary>
		/// Returns whether it was able to remove one condom from the pawn
		/// </summary>
		/// <param name="pawn"></param>
		/// <returns></returns>
		public static bool TryUseCondom(Pawn pawn)
		{
			if (Condom == null) return false;
			if (!xxx.is_human(pawn)) return false;
			if (xxx.has_quirk(pawn, "ImpregnationFetish")) return false;
			List<Thing> pawn_condoms = pawn.inventory.innerContainer.ToList().FindAll(obj => obj.def == Condom);
			if (pawn_condoms.Any())
			{
				var stack = pawn_condoms.Pop();
				stack.stackCount--;
				if (stack.stackCount <= 0)
				{
					stack.Destroy();
				}
				return true;
			}
			return false;
		}

		/// <summary>
		/// Applies the effects of having used a condom.
		/// </summary>
		/// <param name="pawn"></param>
		public static void useCondom(Pawn pawn)
		{
			if (Condom == null) return;
			if (!xxx.is_human(pawn)) return;
			pawn.records.Increment(CountOfCondomsUsed);
			pawn.needs.mood.thoughts.memories.TryGainMemory(SexWithCondom);
		}

		/// <summary>
		/// Try to get condom near sex location
		/// </summary>
		/// <param name="pawn"></param>
		public static void GetCondomFromRoom(Pawn pawn)
		{
			if (Condom == null) return;
			if (!xxx.is_human(pawn)) return;
			if (xxx.has_quirk(pawn, "ImpregnationFetish")) return;
			List<Thing> condoms_in_room = pawn.GetRoom().ContainedAndAdjacentThings.FindAll(obj => obj.def == Condom && pawn.Position.DistanceTo(obj.Position) < 10);
			//List<Thing> condoms_in_room = pawn.ownership.OwnedRoom?.ContainedAndAdjacentThings.FindAll(obj => obj.def == Condom);
			if (condoms_in_room.Any())
			{
				pawn.inventory.innerContainer.TryAdd(condoms_in_room.Pop().SplitOff(1));
			}
		}
	}
}


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

//This one is helper lib for handling all the trans surgery.
namespace rjw
{
	public static class GenderHelper
	{
		public enum Sex { male, female, trap, futa, none }//there is Verse,Gender but it is shit

		//These would probably be better packed in some enumerable structure, so that functions below weren't if-trees, but I don't know C#, sry.
		public static HediffDef was_boy = HediffDef.Named("hediff_was_boy");
		public static HediffDef was_girl = HediffDef.Named("hediff_was_girl");
		public static HediffDef was_futa = HediffDef.Named("hediff_was_futa");
		public static HediffDef was_trap = HediffDef.Named("hediff_was_trap");

		static List<HediffDef> old_sex_list = new List<HediffDef> { was_boy, was_girl, was_futa, was_trap };
		static Dictionary<Sex, HediffDef> sex_to_old_sex = new Dictionary<Sex, HediffDef>() {
			{Sex.male, was_boy },{Sex.female, was_girl},{Sex.trap, was_trap},{Sex.futa, was_futa}
		};
		static Dictionary<HediffDef, Sex> old_sex_to_sex = sex_to_old_sex.ToDictionary(x => x.Value, x => x.Key);

		public static HediffDef m2t = HediffDef.Named("hediff_male2trap");
		public static HediffDef m2f = HediffDef.Named("hediff_male2female");
		public static HediffDef m2h = HediffDef.Named("hediff_male2futa");

		public static HediffDef f2t = HediffDef.Named("hediff_female2trap");
		public static HediffDef f2m = HediffDef.Named("hediff_female2male");
		public static HediffDef f2h = HediffDef.Named("hediff_female2futa");

		public static HediffDef h2t = HediffDef.Named("hediff_futa2trap");
		public static HediffDef h2m = HediffDef.Named("hediff_futa2male");
		public static HediffDef h2f = HediffDef.Named("hediff_futa2female");

		public static HediffDef t2h = HediffDef.Named("hediff_trap2futa");
		public static HediffDef t2m = HediffDef.Named("hediff_trap2male");
		public static HediffDef t2f = HediffDef.Named("hediff_trap2female");

		static List<HediffDef> SexChangeThoughts = new List<HediffDef> { m2t, m2f, m2h, f2t, f2m, f2h, h2t, h2m, h2f };

		public static Sex GetSex(Pawn pawn)
		{
			var parts = pawn.GetGenitalsList();

			bool has_vagina = Genital_Helper.has_vagina(pawn, parts);
			bool has_penis = Genital_Helper.has_male_bits(pawn, parts);
			bool has_breasts = Genital_Helper.has_breasts(pawn);
			bool has_male_breasts = Genital_Helper.has_male_breasts(pawn);
			//BodyType? bt = pawn.story?.bodyType;
			//if (bt != null)
			//	bt = BodyType.Undefined;

			Sex res;
			if (has_vagina && !has_penis)
				res = Sex.female;
			else if (has_vagina && has_penis)
				res = Sex.futa;
			else if (has_penis && has_breasts && !has_male_breasts)
				res = Sex.trap;
			else if (has_penis) //probably should change this later
				res = Sex.male;
			else if (pawn.gender == Gender.Male)
				res = Sex.male;
			else if (pawn.gender == Gender.Female)
				res = Sex.female;
			else
				res = Sex.none;
			return res;
		}

		/// <summary>
		/// Checks to see if two sexes can do hetero stuff.
		/// </summary>
		public static bool CanBeHetero(Sex pawnSex, Sex partnerSex) => (pawnSex, partnerSex) switch
		{
			// Get un-gendered out of the possibilities.
			(Sex.none, _) or (_, Sex.none) => false,
			// Futa can be straight towards anything remaining.
			(Sex.futa, _) or (_, Sex.futa) => true,
			// Female vs male.
			(Sex.female, Sex.male or Sex.trap) => true,
			// Male vs female.
			(Sex.male or Sex.trap, Sex.female) => true,
			// Otherwise, nah.
			_ => false
		};

		/// <summary>
		/// <para>Checks to see if two pawns can do hetero stuff.</para>
		/// <para>If you are also going to check `CanBeHomo`, get the sexes first.</para>
		/// </summary>
		public static bool CanBeHetero(Pawn pawn, Pawn partner) =>
			CanBeHetero(GetSex(pawn), GetSex(partner));

		/// <summary>
		/// Checks to see if two sexes can do homo stuff.
		/// </summary>
		public static bool CanBeHomo(Sex pawnSex, Sex partnerSex) => (pawnSex, partnerSex) switch
		{
			// Get un-gendered out of the possibilities.
			(Sex.none, _) or (_, Sex.none) => false,
			// Futa can be gay towards anything remaining.
			(Sex.futa, _) or (_, Sex.futa) => true,
			// Male vs male.
			(Sex.male or Sex.trap, Sex.male or Sex.trap) => true,
			// Female vs female.
			(Sex.female, Sex.female) => true,
			// Otherwise, nah.
			_ => false
		};

		/// <summary>
		/// <para>Checks to see if two pawns can do homo stuff.</para>
		/// <para>If you are also going to check `CanBeHetero`, get the sexes first.</para>
		/// </summary>
		public static bool CanBeHomo(Pawn pawn, Pawn partner) =>
			CanBeHomo(GetSex(pawn), GetSex(partner));

		/*
		public static HediffDef GetReactionHediff(Sex before, Sex after)
		{
			if (before == after)
				return null;

			if (before == Sex.male)
				return (after == Sex.female) ? m2f : m2t;
			else if (before == Sex.female)
			{
				if (after == Sex.male)
					return f2m;
				else if (after == Sex.trap)
					return f2t;
				else if (after == Sex.futa)
					return f2h;
				else
					return null;
			}
			else if (before == Sex.futa && (after == Sex.female || after == Sex.none))
				return h2f;
			else//trap to anything, futa to trap; probably won't even be reached ever
				return null;
		}
		*/

		//TODO: fix reactions
		public static HediffDef GetReactionHediff(Sex before, Sex after)
		{
			if (before == after)
				return null;
			else if (before == Sex.male)
			{
				if (after == Sex.female)
					return m2f;
				else if (after == Sex.trap)
					return m2t;
				else if (after == Sex.futa)
					return m2h;
				else
					return null;
			}
			else if (before == Sex.female)
			{
				if (after == Sex.male)
					return f2m;
				else if (after == Sex.trap)
					return f2t;
				else if (after == Sex.futa)
					return f2h;
				else
					return null;
			}
			else if (before == Sex.futa)
			{
				if (after == Sex.male)
					return h2m;
				else if (after == Sex.female)
					return h2f;
				else if (after == Sex.trap)
					return h2t;
				else
					return null;
			}
			else if (before == Sex.trap)
			{
				if (after == Sex.male)
					return t2m;
				else if (after == Sex.female)
					return t2f;
				else if (after == Sex.futa)
					return t2h;
				else
					return null;
			}
			else//unicorns?
				return null;
		}

		public static bool WasThisBefore(Pawn pawn, Sex after)
		{
			Hediff was = null;

			switch (after)
			{
				case Sex.male:
					was = pawn.health.hediffSet.GetFirstHediffOfDef(was_boy);
					break;
				case Sex.female:
					was = pawn.health.hediffSet.GetFirstHediffOfDef(was_girl);
					break;
				case Sex.trap:
					was = pawn.health.hediffSet.GetFirstHediffOfDef(was_trap);
					break;
				case Sex.futa:
					was = pawn.health.hediffSet.GetFirstHediffOfDef(was_futa);
					break;
			}
			return (was != null) ? true : false;
		}

		//Get one of the sexes that were on this pawn before
		public static Sex GetOriginalSex(Pawn pawn)
		{
			foreach (var os in old_sex_list)
			{
				if (pawn.health.hediffSet.GetFirstHediffOfDef(os) != null)
					return old_sex_to_sex[os];
			}
			return Sex.none;//it shouldnt reach here though
		}

		public static Hediff IsInDenial(Pawn pawn)
		{
			Hediff res = null;
			foreach (var h in SexChangeThoughts)
			{
				res = pawn.health.hediffSet.GetFirstHediffOfDef(h);
				if (res != null)
					break;
			}
			return res;
		}

		//roll how much gender fluid the pawn is.
		//In ideal world this would actually take into account from where to where transition is moving and so on.
		//Same applies to the thought hediffs themselves, but we get what we can get now
		static float RollSexChangeSeverity(Pawn pawn)
		{
			float res = 1;
			if (xxx.is_bisexual(pawn))
				res *= 0.5f;
			if (pawn.story != null && (pawn.story.bodyType == BodyTypeDefOf.Thin || pawn.story.bodyType == BodyTypeDefOf.Fat))
				res *= 0.8f;
			if (!pawn.ageTracker.CurLifeStage.reproductive)
				res *= 0.2f;
			else
				res *= AgeConfigDef.Instance.rigidityByAge.Evaluate(SexUtility.ScaleToHumanAge(pawn));

			return res;
		}

		//Quick hack to check if hediff is adding happiness
		static bool is_happy(this Hediff thought)
		{
			return thought.CurStageIndex == 0;
		}
		static void make_happy(this Hediff thought)
		{
			if (thought.Severity > 0.24f)
				thought.Severity = 0.24f;//this is currently max severity for hediff, that is associated with positive mood
		}
		static void mix_thoughts(this Hediff newer, Hediff older)
		{
			newer.Severity = (newer.Severity + older.Severity) / 2f;
		}

		static void GiveThought(Pawn pawn, HediffDef thought, bool happy = false, Hediff old_thought = null)
		{
			pawn.health.AddHediff(thought);
			var new_thought = pawn.health.hediffSet.GetFirstHediffOfDef(thought);
			if (happy)
			{
				new_thought.make_happy();
				return;
			}
			new_thought.Severity = RollSexChangeSeverity(pawn);
			if (old_thought != null)
			{
				new_thought.Severity = (new_thought.Severity + old_thought.Severity) / 2f;
			}
		}

		/// <summary>
		/// Executes action and then changes sex if necessary.
		/// </summary>
		public static void ChangeSex(Pawn pawn, Action action)
		{
			var before = GetSex(pawn);
			action();
			var after = GetSex(pawn);

			try
			{
				ChangeSex(pawn, before, after);
			}
			catch
			{
				ModLog.Error("ChangeSex error: " + xxx.get_pawnname(pawn));
			}
		}

		public static void ChangeSex(Pawn pawn, Sex oldsex, Sex newsex)
		{
			if (CompRJW.Comp(pawn) == null)
			{
				ModLog.Warning(" ChangeSex error: CompRJW.Comp(pawn) should not be null");
				return;
			}

			//Log.Message("ChangeSex 1" + oldsex);
			//Log.Message("ChangeSex 2" + newsex);
			// Wakeup pawn sexuality if it has none before
			if (!(CompRJW.Comp(pawn).orientation == Orientation.Asexual || CompRJW.Comp(pawn).orientation == Orientation.None))
				if (oldsex == newsex)
					return;

			//update ingame genders
			if (newsex == Sex.none)
				return;
			else if (newsex == Sex.male || newsex == Sex.trap)
				pawn.gender = Gender.Male;
			else
				pawn.gender = Gender.Female;

			// Sexualize pawn after installation of parts if it was "not interested in".
			if (oldsex == Sex.none || CompRJW.Comp(pawn).orientation == Orientation.Asexual || CompRJW.Comp(pawn).orientation == Orientation.None)
				if (pawn.kindDef.race.defName.ToLower().Contains("droid") && !AndroidsCompatibility.IsAndroid(pawn))
				{
					//basic droids,they don't care
					return;
				}
				else
				{
					CompRJW.Comp(pawn).Sexualize(pawn, true);
				}

			var old_thought = IsInDenial(pawn);
			var react = GetReactionHediff(oldsex, newsex);

			if (old_thought == null || old_thought.is_happy())//pawn either liked it or got used already
			{
				//Log.Message("ChangeSex 1 old_thought" + old_thought);
				//Log.Message("ChangeSex 1 react" + react);
				if (react != null)
				{
					// IsDesignatedHero() crash world gen when adding rjw artificial tech hediffs to royalty, assume they are happy with their implants
					try
					{
						GiveThought(pawn, react, pawn.IsDesignatedHero());//give unhappy, if not hero}
					}
					catch
					{
						ModLog.Warning(" ChangeSex error " + xxx.get_pawnname(pawn) + " faction " + pawn.Faction.Name + ". Probably tried to change sex at world gen for royalty implant, skipping");
						GiveThought(pawn, react, happy: true);
					}
				}
				if (old_thought != null)
					pawn.health.RemoveHediff(old_thought);

				//add tracking hediff
				pawn.health.AddHediff(sex_to_old_sex[oldsex]);
			}
			else//pawn was unhappy
			{
				if (WasThisBefore(pawn, newsex))//pawn is happy to be previous self
				{
					GiveThought(pawn, react, happy: true);
					pawn.health.RemoveHediff(old_thought);
				}
				else//pawn is still unhappy mix the unhappiness from two
				{
					react = GetReactionHediff(GetOriginalSex(pawn), newsex);//check reaction from original sex
					if (react != null)
					{
						GiveThought(pawn, react, old_thought: old_thought);
						pawn.health.RemoveHediff(old_thought);
					}
					//else pawn keeps old unhappy thought
				}
			}
		}
	}
}


code:
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using Verse;
using rjw.Modules.Testing;
using LudeonTK;
using GenitalFamily = rjw.Modules.Interactions.Enums.GenitalFamily;
using GenitalTag = rjw.Modules.Interactions.Enums.GenitalTag;
using GenitalPartExtension = rjw.Modules.Interactions.DefModExtensions.GenitalPartExtension;

namespace rjw
{
	public static class Genital_Helper
	{
		public static HediffDef generic_anus = HediffDef.Named("GenericAnus");
		public static HediffDef generic_penis = HediffDef.Named("GenericPenis");
		public static HediffDef generic_vagina = HediffDef.Named("GenericVagina");
		public static HediffDef generic_breasts = HediffDef.Named("GenericBreasts");

		public static HediffDef average_penis = HediffDef.Named("Penis");
		public static HediffDef hydraulic_penis = HediffDef.Named("HydraulicPenis");
		public static HediffDef bionic_penis = HediffDef.Named("BionicPenis");
		public static HediffDef archotech_penis = HediffDef.Named("ArchotechPenis");

		public static HediffDef average_vagina = HediffDef.Named("Vagina");
		public static HediffDef hydraulic_vagina = HediffDef.Named("HydraulicVagina");
		public static HediffDef bionic_vagina = HediffDef.Named("BionicVagina");
		public static HediffDef archotech_vagina = HediffDef.Named("ArchotechVagina");

		public static HediffDef average_breasts = HediffDef.Named("Breasts");
		public static HediffDef hydraulic_breasts = HediffDef.Named("HydraulicBreasts");
		public static HediffDef bionic_breasts = HediffDef.Named("BionicBreasts");
		public static HediffDef archotech_breasts = HediffDef.Named("ArchotechBreasts");
		public static HediffDef featureless_chest = HediffDef.Named("FeaturelessChest");
		public static HediffDef udder_breasts = HediffDef.Named("UdderBreasts");

		public static HediffDef average_anus = HediffDef.Named("Anus");
		public static HediffDef hydraulic_anus = HediffDef.Named("HydraulicAnus");
		public static HediffDef bionic_anus = HediffDef.Named("BionicAnus");
		public static HediffDef archotech_anus = HediffDef.Named("ArchotechAnus");

		public static HediffDef peg_penis = HediffDef.Named("PegDick");

		public static HediffDef insect_anus = HediffDef.Named("InsectAnus");
		public static HediffDef ovipositorM = HediffDef.Named("OvipositorM");
		public static HediffDef ovipositorF = HediffDef.Named("OvipositorF");

		public static HediffDef demonT_penis = HediffDef.Named("DemonTentaclePenis");
		public static HediffDef demon_penis = HediffDef.Named("DemonPenis");
		public static HediffDef demon_vagina = HediffDef.Named("DemonVagina");
		public static HediffDef demon_anus = HediffDef.Named("DemonAnus");

		public static HediffDef slime_breasts = HediffDef.Named("SlimeBreasts");
		public static HediffDef slime_penis = HediffDef.Named("SlimeTentacles");
		public static HediffDef slime_vagina = HediffDef.Named("SlimeVagina");
		public static HediffDef slime_anus = HediffDef.Named("SlimeAnus");

		public static HediffDef feline_penis = HediffDef.Named("CatPenis");
		public static HediffDef feline_vagina = HediffDef.Named("CatVagina");

		public static HediffDef canine_penis = HediffDef.Named("DogPenis");
		public static HediffDef canine_vagina = HediffDef.Named("DogVagina");

		public static HediffDef equine_penis = HediffDef.Named("HorsePenis");
		public static HediffDef equine_vagina = HediffDef.Named("HorseVagina");

		public static HediffDef dragon_penis = HediffDef.Named("DragonPenis");
		public static HediffDef dragon_vagina = HediffDef.Named("DragonVagina");

		public static HediffDef raccoon_penis = HediffDef.Named("RaccoonPenis");

		public static HediffDef hemipenis = HediffDef.Named("HemiPenis");

		public static HediffDef crocodilian_penis = HediffDef.Named("CrocodilianPenis");

		// I'm not a big fan of looking for defs through string matching, but
		// I suppose it handles the odd rebelious race mod.

		public static readonly BodyPartTagDef BodyPartTagDefEatingSource = DefDatabase<BodyPartTagDef>.GetNamed("EatingSource");
		public static readonly BodyPartTagDef BodyPartTagDefMetabolismSource = DefDatabase<BodyPartTagDef>.GetNamed("MetabolismSource");
		public static readonly BodyPartTagDef BodyPartTagDefTongue = DefDatabase<BodyPartTagDef>.GetNamed("Tongue");

		private static readonly HashSet<BodyPartDef> mouthBPs =
			DefDatabase<BodyPartDef>.AllDefsListForReading
				.Where((def) => def.defName.ToLower().ContainsAny("mouth", "teeth", "jaw", "beak") || def.tags.Contains(BodyPartTagDefEatingSource))
				.ToHashSet();

		private static readonly HashSet<BodyPartDef> tongueBPs =
			DefDatabase<BodyPartDef>.AllDefsListForReading
				.Where((def) => def.defName.ToLower().Contains("tongue") || def.tags.Contains(BodyPartTagDefTongue))
				.ToHashSet();

		private static readonly HashSet<BodyPartDef> stomachBPs =
			DefDatabase<BodyPartDef>.AllDefsListForReading
				.Where((def) => def.defName.ToLower().Contains("stomach") || def.tags.Contains(BodyPartTagDefMetabolismSource))
				.ToHashSet();

		private static readonly HashSet<BodyPartDef> torsoBPs =
			DefDatabase<BodyPartDef>.AllDefsListForReading
				.Where((def) => def.defName.ToLower().Contains("torso"))
				.ToHashSet();

		private static readonly HashSet<BodyPartDef> tailBPs =
			DefDatabase<BodyPartDef>.AllDefsListForReading
				.Where((def) => def.defName.ToLower().Contains("tail"))
				.ToHashSet();

		private static readonly HashSet<BodyPartDef> bodyBPs =
			DefDatabase<BodyPartDef>.AllDefsListForReading
				.Where((def) => def.defName.ToLower().Contains("body"))
				.ToHashSet();

		private static readonly BodyPartTagDef bodyPartTagDef_tongue =
			DefDatabase<BodyPartTagDef>.GetNamed("Tongue");

		// These BPRs are added by RJW.  They must have some sex part attached
		// to count as being present.

		public static BodyPartRecord get_genitalsBPR(Pawn pawn)
		{
			//--Log.Message("Genital_Helper::get_genitals( " + xxx.get_pawnname(pawn) + " ) called");
			var bpr = pawn?.RaceProps.body.AllParts.Find(bpr => (bpr.def == xxx.genitalsDef));
			if (bpr is not null) return bpr;

			//--ModLog.Message(" get_genitals( " + xxx.get_pawnname(pawn) + " ) - Part is null");
			return null;
		}

		public static BodyPartRecord get_anusBPR(Pawn pawn)
		{
			//--ModLog.Message(" get_anus( " + xxx.get_pawnname(pawn) + " ) called");
			var bpr = pawn?.RaceProps.body.AllParts.Find(bpr => bpr.def == xxx.anusDef);
			if (bpr is not null) return bpr;

			//--ModLog.Message(" get_anus( " + xxx.get_pawnname(pawn) + " ) - Part is null");
			return null;
		}

		public static BodyPartRecord get_breastsBPR(Pawn pawn)
		{
			//--ModLog.Message(" get_breasts( " + xxx.get_pawnname(pawn) + " ) called");
			var bpr = pawn?.RaceProps.body.AllParts.Find(bpr => bpr.def == xxx.breastsDef);
			if (bpr is not null) return bpr;

			//--ModLog.Message(" get_breasts( " + xxx.get_pawnname(pawn) + " ) - Part is null");
			return null;
		}

		public static BodyPartRecord get_uddersBPR(Pawn pawn)
		{
			//--ModLog.Message(" get_udder( " + xxx.get_pawnname(pawn) + " ) called");
			var bpr = pawn?.RaceProps.body.AllParts.Find(bpr => bpr.def == xxx.flankDef);
			if (bpr is not null) return bpr;

			//--ModLog.Message(" get_udders( " + xxx.get_pawnname(pawn) + " ) - Part is null");
			return null;
		}

		// These BPRs are either pre-existing or added by other mods.  They are considered
		// present as long as they have no `Hediff_PartRemoved` on them.  Use the `has_bpr`
		// method to check that they're not chopped off, blown off, smashed off, or whatever
		// other kinds of terrible things can befall a poor pawn.

		public static BodyPartRecord get_mouthBPR(Pawn pawn)
		{
			//--ModLog.Message(" get_mouth( " + xxx.get_pawnname(pawn) + " ) called");
			var bpr = pawn?.RaceProps.body.AllParts.Find((bpr) => mouthBPs.Contains(bpr.def));
			if (bpr is not null) return bpr;

			//--ModLog.Message(" get_mouth( " + xxx.get_pawnname(pawn) + " ) - Part is null");
			return null;
		}

		public static BodyPartRecord get_tongueBPR(Pawn pawn)
		{
			//--ModLog.Message(" get_tongue( " + xxx.get_pawnname(pawn) + " ) called");
			if (pawn?.RaceProps.body.AllParts is { } allParts)
			{
				foreach (var bpr in allParts)
				{
					if (tongueBPs.Contains(bpr.def)) return bpr;
					if (bpr.def.tags.Contains(bodyPartTagDef_tongue)) return bpr;
				}
			}

			//--ModLog.Message(" get_tongue( " + xxx.get_pawnname(pawn) + " ) - Part is null");
			return null;
		}

		public static BodyPartRecord get_stomachBPR(Pawn pawn)
		{
			//--ModLog.Message(" get_stomach( " + xxx.get_pawnname(pawn) + " ) called");
			if (pawn?.RaceProps.body.AllParts is { } allParts)
			{
				var bpr = allParts.Find((bpr) => stomachBPs.Contains(bpr.def));
				if (bpr is not null) return bpr;

				//--ModLog.Message(" get_stomach( " + xxx.get_pawnname(pawn) + " ) - Part is null, trying to get torso...");
				bpr = allParts.Find((bpr) => torsoBPs.Contains(bpr.def));
				if (bpr is not null) return bpr;
			}

			//--ModLog.Message(" get_stomach( " + xxx.get_pawnname(pawn) + " ) - Part is null, no stomach or torso.");
			return null;
		}

		public static BodyPartRecord get_tailBPR(Pawn pawn)
		{
			//should probably make scale race check or something
			//--ModLog.Message(" get_tail( " + xxx.get_pawnname(pawn) + " ) called");
			var bpr = pawn?.RaceProps.body.AllParts.Find(bpr => tailBPs.Contains(bpr.def));
			if (bpr is not null) return bpr;

			//--ModLog.Message(" get_tail( " + xxx.get_pawnname(pawn) + " ) - Part is null");
			return null;
		}

		public static BodyPartRecord get_torsoBPR(Pawn pawn)
		{
			//--ModLog.Message(" get_torsoBPR( " + xxx.get_pawnname(pawn) + " ) called");
			if (pawn?.RaceProps.body.AllParts is { } allParts)
			{
				var bpr = allParts.Find((bpr) => torsoBPs.Contains(bpr.def));
				if (bpr is not null) return bpr;

				//--ModLog.Message(" get_torsoBPR( " + xxx.get_pawnname(pawn) + " ) - Part is null, trying to get body...");
				bpr = allParts.Find((bpr) => bodyBPs.Contains(bpr.def));
				if (bpr is not null) return bpr;
			}

			//--ModLog.Message(" get_torsoBPR( " + xxx.get_pawnname(pawn) + " ) - Part is null, no torso or body");
			return null;
		}

		public static bool breasts_blocked(Pawn pawn)
		{
			if (pawn.apparel?.WornApparel is not { } wornApparel) return false;
			return wornApparel.ToBondageGear().Any((def) => def.blocks_breasts);
		}

		public static bool udder_blocked(Pawn pawn)
		{
			if (pawn.apparel?.WornApparel is not { } wornApparel) return false;
			return wornApparel.ToBondageGear().Any((def) => def.blocks_udder);
		}

		public static bool anus_blocked(Pawn pawn)
		{
			if (pawn.apparel?.WornApparel is not { } wornApparel) return false;
			return wornApparel.ToBondageGear().Any((def) => def.blocks_anus);
		}

		public static bool genitals_blocked(Pawn pawn)
		{
			if (pawn.apparel?.WornApparel is not { } wornApparel) return false;
			return wornApparel.ToBondageGear().Any((def) => def.blocks_penis || def.blocks_vagina);
		}

		public static bool hands_blocked(Pawn pawn)
		{
			if (pawn.apparel?.WornApparel is not { } wornApparel) return false;
			return wornApparel.ToBondageGear().Any((def) => def.blocks_hands);
		}

		public static bool penis_blocked(Pawn pawn)
		{
			if (pawn.apparel?.WornApparel is not { } wornApparel) return false;
			return wornApparel.ToBondageGear().Any((def) => def.blocks_penis);
		}

		public static bool oral_blocked(Pawn pawn)
		{
			if (pawn.apparel?.WornApparel is not { } wornApparel) return false;
			return wornApparel.ToBondageGear().Any((def) => def.blocks_oral);
		}

		public static bool vagina_blocked(Pawn pawn)
		{
			if (pawn.apparel?.WornApparel is not { } wornApparel) return false;
			return wornApparel.ToBondageGear().Any((def) => def.blocks_vagina);
		}

		private static IEnumerable<bondage_gear_def> ToBondageGear(this List<Apparel> wornApparel) =>
			wornApparel.Select((app) => app.def).OfType<bondage_gear_def>();

		public static bool is_sex_part(Hediff hed) =>
			hed is Hediff_PartBaseNatural || hed is Hediff_PartBaseArtifical;

		public static List<Hediff> get_PartsHediffList(Pawn pawn, BodyPartRecord Part) =>
			pawn.health.hediffSet.hediffs.FindAll((hed) => is_sex_part(hed) && hed.Part == Part);

		public static List<Hediff> get_AllPartsHediffList(Pawn pawn) =>
			pawn.health.hediffSet.hediffs.FindAll(is_sex_part);

		public static bool has_genitals(Pawn pawn, List<Hediff> parts = null)
		{
			parts ??= pawn.GetGenitalsList();
			return !parts.NullOrEmpty();
		}

		public static bool has_breasts(Pawn pawn, List<Hediff> parts = null)
		{
			parts ??= pawn.GetBreastList();
			if (parts.NullOrEmpty()) return false;

			return parts.Any((hed) => hed.def != featureless_chest);
		}

		public static bool has_udder(Pawn pawn, List<Hediff> parts = null)
		{
			parts ??= pawn.GetUdderList();
			return !parts.NullOrEmpty();
		}

		public static bool has_male_breasts(Pawn pawn, List<Hediff> parts = null)
		{
			parts ??= pawn.GetBreastList();
			if (parts.NullOrEmpty()) return false;

			return parts.Any((hed) => is_sex_part(hed) && hed.CurStageIndex == 0);
		}

		/// <summary>
		/// Can do breastjob if breasts are average or bigger
		/// </summary>
		/// <param name="pawn"></param>
		/// <returns></returns>
		public static bool can_do_breastjob(Pawn pawn, List<Hediff> parts = null)
		{
			parts ??= pawn.GetBreastList();
			if (parts.NullOrEmpty()) return false;

			return parts.Any((hed) => is_sex_part(hed) && hed.CurStageIndex > 1);
		}

		public static bool has_tail(Pawn pawn) =>
			has_bpr(pawn, get_tailBPR(pawn));

		public static bool has_mouth(Pawn pawn) =>
			has_bpr(pawn, get_mouthBPR(pawn));

		public static bool has_tongue(Pawn pawn) =>
			has_bpr(pawn, get_tongueBPR(pawn));

		/// <summary>
		/// <para>Checks to see if a body part is actually attached.</para>
		/// <para>This applies to BPRs that are not added by RJW.  For those,
		/// we count the part as missing if they do not have an accompanying
		/// sex-part.</para>
		/// </summary>
		/// <param name="pawn">The pawn to check.</param>
		/// <param name="bpr">The `BodyPartRecord` indicating the body part.</param>
		/// <returns>Whether the part is missing.</returns>
		public static bool has_bpr(Pawn pawn, BodyPartRecord bpr)
		{
			if (bpr is null) return false;
			return !pawn.health.hediffSet.hediffs
				.Where((hed) => hed.Part == bpr)
				.Any((hed) => hed is Hediff_MissingPart);
		}

		public static bool has_anus(Pawn pawn, List<Hediff> parts = null)
		{
			parts ??= pawn.GetAnusList();
			if (parts.NullOrEmpty()) return false;

			for (int i = 0; i < parts.Count; i++)
				if (is_anus(parts[i]))
					return true;
			return false;
		}

		public static bool is_anus(Hediff hed)
		{
			if (!GenitalPartExtension.TryGet(hed, out var ext)) return false;
			return ext.family == GenitalFamily.Anus;
		}

		/// <summary>
		/// Insertable, this is both vagina and ovipositorf
		/// </summary>
		/// <param name="pawn"></param>
		/// <param name="parts"></param>
		/// <returns></returns>
		public static bool has_vagina(Pawn pawn, List<Hediff> parts = null)
		{
			parts ??= pawn.GetGenitalsList();
			if (parts.NullOrEmpty()) return false;

			for (int i = 0; i < parts.Count; i++)
				if (is_vagina(parts[i]))
					return true;
			return false;
		}

		private static bool is_vagina_family(GenitalFamily family) => family switch
		{
			GenitalFamily.Vagina => true,
			GenitalFamily.FemaleOvipositor => true,
			_ => false
		};

		public static bool is_vagina(Hediff hed) =>
			GenitalPartExtension.TryGet(hed, out var ext) && is_vagina_family(ext.family);

		/// <summary>
		/// <para>Checks to see if a pawn has a penis, whether fertile or not.</para>
		/// <para>This includes male ovipositors, but not female ovipositors (that is
		/// regarded more like a vagina).</para>
		/// </summary>
		/// <param name="pawn">The pawn to inspect.</param>
		/// <param name="parts">The pre-obtained parts, if available.</param>
		public static bool has_male_bits(Pawn pawn, List<Hediff> parts = null)
		{
			parts ??= pawn.GetGenitalsList();
			if (parts.NullOrEmpty()) return false;

			return parts.Any(is_penis);
		}

		public static bool has_penis_fertile(Pawn pawn, List<Hediff> parts = null)
		{
			parts ??= pawn.GetGenitalsList();
			if (parts.NullOrEmpty()) return false;

			for (int i = 0; i < parts.Count; i++)
				if (is_fertile_penis(parts[i]))
					return true;
			return false;
		}

		public static bool has_penis_infertile(Pawn pawn, List<Hediff> parts = null)
		{
			parts ??= pawn.GetGenitalsList();
			if (parts.NullOrEmpty()) return false;

			for (int i = 0; i < parts.Count; i++)
				if (is_infertile_penis(parts[i]))
					return true;
			return false;
		}

		private static bool is_penis_family(GenitalFamily family) => family switch
		{
			GenitalFamily.Penis => true,
			GenitalFamily.MaleOvipositor => true,
			_ => false
		};

		private static bool is_fertile_tag(GenitalTag tag) => tag switch
		{
			GenitalTag.CanFertilize => true,
			GenitalTag.CanFertilizeEgg => true,
			_ => false
		};

		public static bool is_penis(Hediff hed) =>
			GenitalPartExtension.TryGet(hed, out var ext) && is_penis_family(ext.family);

		public static bool is_fertile_penis(Hediff hed)
		{
			if (!GenitalPartExtension.TryGet(hed, out var ext)) return false;
			if (!is_penis_family(ext.family)) return false;

			for (int i = 0; i < ext.tags.Count; i++)
				if (is_fertile_tag(ext.tags[i]))
					return true;
			return false;
		}

		public static bool is_infertile_penis(Hediff hed)
		{
			if (!GenitalPartExtension.TryGet(hed, out var ext)) return false;
			if (!is_penis_family(ext.family)) return false;

			for (int i = 0; i < ext.tags.Count; i++)
				if (is_fertile_tag(ext.tags[i]))
					return false;
			return true;
		}

		public static bool has_multipenis(Pawn pawn, List<Hediff> parts = null)
		{
			parts ??= pawn.GetGenitalsList();
			if (parts.NullOrEmpty()) return false;

			int count = 0;
			foreach (Hediff hed in parts)
			{
				if (!is_penis(hed)) continue;

				// Matches hemipenis.
				if (PartProps.TryGetProps(hed, out var props))
					if (props.Contains("Multiple"))
						return true;

				count += 1;
				if (count > 1) return true;
			}

			return false;
		}

		public static bool has_ovipositorM(Pawn pawn, List<Hediff> parts = null)
		{
			parts ??= pawn.GetGenitalsList();
			if (parts.NullOrEmpty()) return false;

			for (int i = 0; i < parts.Count; i++)
				if (is_male_ovipositor(parts[i]))
					return true;
			return false;

			static bool is_male_ovipositor(Hediff hed)
			{
				if (!GenitalPartExtension.TryGet(hed, out var ext)) return false;
				return ext.family == GenitalFamily.MaleOvipositor;
			}
		}

		public static bool has_ovipositorF(Pawn pawn, List<Hediff> parts = null)
		{
			parts ??= pawn.GetGenitalsList();
			if (parts.NullOrEmpty()) return false;

			for (int i = 0; i < parts.Count; i++)
				if (is_female_ovipositor(parts[i]))
					return true;
			return false;

			static bool is_female_ovipositor(Hediff hed)
			{
				if (!GenitalPartExtension.TryGet(hed, out var ext)) return false;
				return ext.family == GenitalFamily.FemaleOvipositor;
			}
		}
		/// <summary>
		/// Can do autofellatio if penis is huge or bigger
		/// </summary>
		/// <param name="pawn"></param>
		/// <returns></returns>
		public static bool can_do_autofelatio(Pawn pawn, List<Hediff> parts = null)
		{
			parts ??= pawn.GetGenitalsList();
			if (parts.NullOrEmpty()) return false;

			return parts.Any((hed) => is_penis(hed) && hed.CurStageIndex > 3);
		}

		/// <summary>
		/// Count only fertile penises
		/// </summary>
		/// <param name="pawn"></param>
		/// <returns></returns>
		public static bool is_futa(Pawn pawn, List<Hediff> parts = null)
		{
			parts ??= pawn.GetGenitalsList();
			return has_vagina(pawn, parts) && has_penis_fertile(pawn, parts);
		}

		public static int min_EggsProduced(Pawn pawn, List<Hediff> parts = null)
		{
			parts ??= pawn.GetGenitalsList();
			// No fitting parts - return 0
			if (parts.NullOrEmpty()) return 0;

			// set min to 1 in case it is not defined in the xml file
			int minEggsProduced = 1;

			var eggParts = parts.Select(part => part.def).OfType<HediffDef_PartBase>();
			foreach (HediffDef_PartBase hed in eggParts)
			{
				//if (hed.minEggAmount)
				if (minEggsProduced > 0)
				{
					minEggsProduced = hed.minEggsProduced;
				}
			}

			return minEggsProduced;
		}

		public static int max_EggsProduced(Pawn pawn, List<Hediff> parts = null)
		{
			parts ??= pawn.GetGenitalsList();
			// No fitting parts - return 0
			if (parts.NullOrEmpty()) return 0;

			// set min to 1 in case it is not defined in the xml file
			int maxEggsProduced = 1;

			var eggParts = parts.Select(part => part.def).OfType<HediffDef_PartBase>();
			foreach (HediffDef_PartBase hed in eggParts)
			{
				//if (hed.minEggAmount)
				if (maxEggsProduced > 0)
				{
					maxEggsProduced = hed.maxEggsProduced;
				}
			}

			return maxEggsProduced;
		}

		/// <summary>
		/// <para>This utility appears in the "Output" tab of the debug menu.</para>
		/// <para>It generates every kind of pawn in the game and runs all these methods
		/// against them.  You can use this to validate changes to these utilities.</para>
		/// <para>Just run this before your changes, then after, and use some kind of diff
		/// utility to see if anything is ...different.</para>
		/// <para>Be aware that sometimes pawns can just generate in an imperfect way,
		/// like an iguana whose tail is currently severed off will change how `has_tail`
		/// might generate its output.</para>
		/// </summary>
		[DebugOutput("RJW", onlyWhenPlaying = true)]
		public static void GenitalsOfAllPawnKinds()
		{
			var artificialParts = DefDatabase<RecipeDef>.AllDefsListForReading
				.Where((r) => r.workerClass == typeof(Recipe_InstallGenitals))
				.Where((r) => typeof(Hediff_PartBaseArtifical).IsAssignableFrom(r.addsHediff.hediffClass))
				.ToArray();

			var dataSources = DefDatabase<PawnKindDef>.AllDefsListForReading
				.Where((d) => d.race != null)
				.OrderBy((d) => d.defName)
				.SelectMany(CreatePawns)
				.ToArray();

			var table = new TableDataGetter<(Pawn pawn, string label)>[]
			{
				new("kind", (d) => d.pawn.kindDef.defName),
				new("sex", (d) => GenderHelper.GetSex(d.pawn).ToString().CapitalizeFirst()),
				new("gender", (d) => d.pawn.gender),
				new("label", (d) => d.label),
				new("genitalsBPR", (d) => get_genitalsBPR(d.pawn)?.def.defName),
				new("breastsBPR", (d) => get_breastsBPR(d.pawn)?.def.defName),
				new("uddersBPR", (d) => get_uddersBPR(d.pawn)?.def.defName),
				new("mouthBPR", (d) => get_mouthBPR(d.pawn)?.def.defName),
				new("tongueBPR", (d) => get_tongueBPR(d.pawn)?.def.defName),
				new("stomachBPR", (d) => get_stomachBPR(d.pawn)?.def.defName),
				new("tailBPR", (d) => get_tailBPR(d.pawn)?.def.defName),
				new("anusBPR", (d) => get_anusBPR(d.pawn)?.def.defName),
				new("torsoBPR", (d) => get_torsoBPR(d.pawn)?.def.defName),
				new("has_genitals", (d) => has_genitals(d.pawn)),
				new("has_breasts", (d) => has_breasts(d.pawn)),
				new("has_udder", (d) => has_udder(d.pawn)),
				new("has_male_breasts", (d) => has_male_breasts(d.pawn)),
				new("can_do_breastjob", (d) => can_do_breastjob(d.pawn)),
				new("has_tail", (d) => has_tail(d.pawn)),
				new("has_mouth", (d) => has_mouth(d.pawn)),
				new("has_tongue", (d) => has_tongue(d.pawn)),
				new("has_anus", (d) => has_anus(d.pawn)),
				new("has_vagina", (d) => has_vagina(d.pawn)),
				new("has_male_bits", (d) => has_male_bits(d.pawn)),
				new("has_penis_fertile", (d) => has_penis_fertile(d.pawn)),
				new("has_penis_infertile", (d) => has_penis_infertile(d.pawn)),
				new("has_multipenis", (d) => has_multipenis(d.pawn)),
				new("has_ovipositorM", (d) => has_ovipositorM(d.pawn)),
				new("has_ovipositorF", (d) => has_ovipositorF(d.pawn)),
				new("is_futa", (d) => is_futa(d.pawn)),
				new("min_EggsProduced", (d) => min_EggsProduced(d.pawn)),
				new("max_EggsProduced", (d) => max_EggsProduced(d.pawn))
			};

			DebugTables.MakeTablesDialog(dataSources, table);

			IEnumerable<PawnGenerationRequest> CreateRequests(PawnKindDef def)
			{
				if (!def.RaceProps.hasGenders)
				{
					yield return new PawnGenerationRequest(kind: def)
						.RequestDefaults();
				}
				else
				{
					yield return new PawnGenerationRequest(kind: def, fixedGender: Gender.Male)
						.RequestDefaults();

					yield return new PawnGenerationRequest(kind: def, fixedGender: Gender.Female)
						.RequestDefaults();
				}
			}

			IEnumerable<(Pawn pawn, string label)> CreatePawns(PawnKindDef def)
			{
				foreach (var request in CreateRequests(def))
					if (TestHelper.GenerateSeededPawn(request) is { } pawn)
						yield return (pawn, "natural");

				// Only going to test artificial parts and complex sexes on colonists.
				if (def.defName != "Colonist") yield break;

				// Producing fertile and infertile futa.
				foreach (var request in CreateRequests(def))
				{
					if (request.FixedGender == Gender.Female)
					{
						if (TestHelper.GenerateSeededPawn(request) is { } fertileFemale)
							if (TestHelper.MakeIntoFuta(fertileFemale))
								yield return (fertileFemale, "as fertile female futa");

						if (TestHelper.GenerateSeededPawn(request) is { } infertileFemale)
							if (TestHelper.MakeIntoFuta(infertileFemale, true))
								yield return (infertileFemale, "as infertile female futa");
					}
					else if (request.FixedGender == Gender.Male)
					{
						if (TestHelper.GenerateSeededPawn(request) is { } fertileMale)
							if (TestHelper.MakeIntoFuta(fertileMale))
								yield return (fertileMale, "as fertile male futa");

						if (TestHelper.GenerateSeededPawn(request) is { } infertileMale)
							if (TestHelper.MakeIntoFuta(infertileMale))
								yield return (infertileMale, "as infertile male futa");
					}
				}

				// Producing a trap.
				foreach (var request in CreateRequests(def))
				{
					if (request.FixedGender != Gender.Male) continue;

					if (TestHelper.GenerateSeededPawn(request) is { } malePawn)
						if (TestHelper.MakeIntoTrap(malePawn))
							yield return (malePawn, "as trap");
				}

				// Testing artifical parts.
				foreach (var recipe in artificialParts)
				{
					foreach (var request in CreateRequests(def))
					{
						var pawn = TestHelper.GenerateSeededPawn(request);
						if (pawn is null) continue;
						if (!TestHelper.ApplyPartToPawn(pawn, recipe)) continue;

						yield return (pawn, $"has {recipe.addsHediff.defName}");
					}
				}
			}
		}
	}

}


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

namespace rjw
{
	public class HediffHelper
	{
		/// <summary>
		/// Creates hediff with custom hediff class
		/// </summary>
		/// <typeparam name="T">hediff class. Should be class defined by hediff or subclass of that class</typeparam>
		/// <param name="def">hediffDef</param>
		/// <param name="pawn">owner of newly created hediff</param>
		/// <param name="partRecord">bodypart heduff assigned to</param>
		/// <returns>newly created hediff</returns>
		public static T MakeHediff<T>(HediffDef def, Pawn pawn, BodyPartRecord partRecord = null) where T : Hediff
		{
			if (!def.hediffClass.IsAssignableFrom(typeof(T)))
			{
				throw new InvalidOperationException($"trying to create hediff with incompatible class: {typeof(T).Name} is not a {def.hediffClass.Name} or its subclass");
			}

			T hediff = (T)Activator.CreateInstance(typeof(T));
			hediff.def = def;
			hediff.pawn = pawn;
			hediff.Part = partRecord;
			hediff.loadID = Find.UniqueIDsManager.GetNextHediffID();
			hediff.PostMake();

			return hediff;
		}

		/// <summary>
		/// Fill existing hediff class with data
		/// </summary>
		/// <typeparam name="T">hediff class. Should be class defined by hediff or subclass of that class</typeparam>
		/// <param name="def">hediffDef</param>
		/// <param name="hediff">empty hediff instance</Hediff>
		/// <param name="pawn">owner of newly created hediff</param>
		/// <param name="partRecord">bodypart heduff assigned to</param>
		/// <returns>newly created hediff</returns>
		public static T FillHediff<T>(HediffDef def, T hediff, Pawn pawn, BodyPartRecord partRecord = null) where T : Hediff
		{
			if (!def.hediffClass.IsAssignableFrom(typeof(T)))
			{
				throw new InvalidOperationException($"trying to create hediff with incompatible class: {typeof(T).Name} is not a {def.hediffClass.Name} or its subclass");
			}

			hediff.def = def;
			hediff.pawn = pawn;
			hediff.Part = partRecord;
			hediff.loadID = Find.UniqueIDsManager.GetNextHediffID();
			hediff.PostMake();

			return hediff;
		}
	}
}