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
	{
		// List of jobs that can be interrupted by quickies.
		public static readonly List<JobDef> quickieAllowedJobs = new List<JobDef> { null, JobDefOf.Wait_Wander, JobDefOf.GotoWander, JobDefOf.Clean, JobDefOf.ClearSnow,
			JobDefOf.CutPlant, JobDefOf.HaulToCell, JobDefOf.Deconstruct, JobDefOf.LayDown, JobDefOf.Research, JobDefOf.SmoothFloor, JobDefOf.SmoothWall,
			JobDefOf.SocialRelax, JobDefOf.StandAndBeSociallyActive, JobDefOf.RemoveApparel, JobDefOf.Strip, JobDefOf.Wait, JobDefOf.FillFermentingBarrel,
			JobDefOf.Sow, JobDefOf.Shear, JobDefOf.DeliverFood, JobDefOf.Hunt, JobDefOf.Mine, JobDefOf.RearmTurret, JobDefOf.RemoveFloor, JobDefOf.RemoveRoof,
			JobDefOf.Repair, JobDefOf.TakeBeerOutOfFermentingBarrel, JobDefOf.Uninstall, JobDefOf.Meditate, JobDefOf.OperateDeepDrill,
			JobDefOf.OperateScanner, JobDefOf.Reign, JobDefOf.Slaughter, xxx.Masturbate};

		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;

			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 (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 ||
							!CasualSex_Helper.quickieAllowedJobs.Contains(target.jobs.curJob.def)
							))
						{
							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 ||
					!CasualSex_Helper.quickieAllowedJobs.Contains(targetPawn.jobs.curJob.def)
					))
				{
					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 == RoomRoleDefOf.Laboratory || room.Role == RoomRoleDefOf.RecRoom
					|| room.Role == RoomRoleDefOf.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));
			}
		}
	}
}

repo_name: PhaetonYT/rjw

license: mit


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 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;
		}
	}
}


code:
using Multiplayer.API;
using RimWorld;
using Verse;

namespace rjw
{
	/// <summary>
	/// Old, hardcoded part choosing code. Used as a fallback if no RaceGroupDef is found.
	/// </summary>
	public static class LegacySexPartAdder
	{
		static bool privates_gender(Pawn pawn, Gender gender)
		{
			return SexPartAdder.IsAddingPenis(pawn, gender);
		}

		[SyncMethod]
		public static double GenderTechLevelCheck(Pawn pawn)
		{
			bool lowtechlevel = true;

			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			double value = Rand.Value;

			if (pawn?.Faction != null)
				lowtechlevel = (int)pawn.Faction.def.techLevel < 5;
			else if (pawn == null)
				lowtechlevel = false;

			//--save savages from inventing hydraulic and bionic genitals
			while (lowtechlevel && value >= 0.90)
			{
				value = Rand.Value;
			}
			return value;
		}

		public static void AddBreasts(Pawn pawn, BodyPartRecord bpr, Pawn parent)
		{
			var temppawn = parent ?? pawn;
			HediffDef part;
			double value = GenderTechLevelCheck(pawn);
			string racename = temppawn.kindDef.race.defName.ToLower();

			part = Genital_Helper.generic_breasts;

			if (xxx.is_mechanoid(pawn))
			{
				return;
			}
			if (xxx.is_insect(temppawn))
			{
				// this will probably need override in case there are humanoid insect race
				//--ModLog.Message(" add_breasts( " + xxx.get_pawnname(pawn) + " ) - is insect,doesn't need breasts");
				return;
			}
			//alien races - MoreMonstergirls
			else if (racename.Contains("slime"))
			{
				//slimes are always females, and idc what anyone else say!
				part = Genital_Helper.slime_breasts;
			}
			else
			{
				if (pawn.RaceProps.Humanlike)
				{
					//alien races - ChjDroid, ChjAndroid
					if (racename.ContainsAny("mantis", "rockman", "slug", "zoltan", "engie", "sergal", "cutebold", "dodo", "owl", "parrot",
						"penguin", "cassowary", "chicken", "vulture"))
					{
						if (!pawn.health.hediffSet.HasHediff(Genital_Helper.featureless_chest))
							pawn.health.AddHediff(Genital_Helper.featureless_chest, bpr);

						return;
					}
					else if (racename.ContainsAny("avali", "khess"))
					{
						return;
					}
					else if (AndroidsCompatibility.IsAndroid(pawn))
					{
						if (pawn.story.GetBackstory(BackstorySlot.Childhood) != null)
						{
							if (pawn.story.Childhood.untranslatedTitleShort.ToLower().Contains("bishojo"))
								part = Genital_Helper.bionic_breasts;
							else if (pawn.story.Childhood.untranslatedTitleShort.ToLower().Contains("pleasure"))
								part = Genital_Helper.bionic_breasts;
							else if (pawn.story.Childhood.untranslatedTitleShort.ToLower().Contains("idol"))
								part = Genital_Helper.bionic_breasts;
							else if (pawn.story.Childhood.untranslatedTitleShort.ToLower().Contains("social"))
								part = Genital_Helper.hydraulic_breasts;
							else if (pawn.story.Childhood.untranslatedTitleShort.ToLower().Contains("substitute"))
								part = Genital_Helper.average_breasts;
							else if (pawn.story.GetBackstory(BackstorySlot.Adulthood) != null)
							{
								if (pawn.story.Adulthood.untranslatedTitleShort.ToLower().Contains("courtesan"))
									part = Genital_Helper.bionic_breasts;
								else if (pawn.story.Adulthood.untranslatedTitleShort.ToLower().Contains("social"))
									part = Genital_Helper.hydraulic_breasts;
							}
							else
								return;
						}
						else if (pawn.story.GetBackstory(BackstorySlot.Adulthood) != null)
						{
							if (pawn.story.Adulthood.untranslatedTitleShort.ToLower().Contains("courtesan"))
								part = Genital_Helper.bionic_breasts;
							else if (pawn.story.Adulthood.untranslatedTitleShort.ToLower().Contains("social"))
								part = Genital_Helper.hydraulic_breasts;
						}
						if (part == Genital_Helper.generic_breasts)
							return;
					}
					//alien races - MoreMonstergirls
					else if (racename.Contains("cowgirl"))
					{
						part = Genital_Helper.average_breasts;
						if (value < 0.50 && !pawn.health.hediffSet.HasHediff(Genital_Helper.udder_breasts) && pawn.gender != Gender.Male)
						{
							var bp = Genital_Helper.get_uddersBPR(pawn);
							pawn.health.AddHediff(SexPartAdder.MakePart(Genital_Helper.udder_breasts, pawn, bp), bp);
						}
					}
					else
					{
						if (value < 0.90 || (pawn.ageTracker.AgeBiologicalYears < 2))
							part = Genital_Helper.average_breasts;
						else if (value < 0.95)
							part = Genital_Helper.hydraulic_breasts;
						else
							part = Genital_Helper.bionic_breasts;
					}
				}
				else if (racename.ContainsAny("mammoth", "elasmotherium", "chalicotherium", "megaloceros", "sivatherium", "deinotherium",
					"aurochs", "zygolophodon", "uintatherium", "gazelle", "ffalo", "boomalope", "cow", "miltank", "elk", "reek", "nerf",
					"bantha", "tauntaun", "caribou", "deer", "ibex", "dromedary", "alpaca", "llama", "goat", "moose", "horse", "donkey", "yak"))
				{
					if (!pawn.health.hediffSet.HasHediff(Genital_Helper.udder_breasts) && pawn.gender != Gender.Male)
					{
						bpr = Genital_Helper.get_uddersBPR(pawn);
						part = Genital_Helper.udder_breasts;
					}
					else
						return;
				}
				else if (racename.ContainsAny("cassowary", "emu", "dinornis", "ostrich", "turkey", "chicken", "duck", "murkroW", "bustard", "palaeeudyptes",
					"goose", "tukiri", "porg", "yi", "kiwi", "penguin", "quail", "ptarmigan", "doduo", "flamingo", "plup", "empoleon", "meadow ave") && !racename.ContainsAny("duck-billed"))
				{
					return;  // Separate list for birds, to make it easier to add cloaca at some later date.
				}   // Other breastless creatures.
				else if (racename.ContainsAny("titanis", "titanoboa", "guan", "tortoise", "turt", "aerofleet", "quinkana", "megalochelys",
					"purussaurus", "cobra", "dewback", "rancor", "frog", "onyx", "flommel", "lapras", "aron", "chinchou",
					"squirtle", "wartortle", "blastoise", "totodile", "croconaw", "feraligatr", "litwick", "pumpkaboo", "shuppet", "haunter",
					"gastly", "oddish", "hoppip", "tropius", "budew", "roselia", "bellsprout", "drifloon", "chikorita", "bayleef", "meganium",
					"char", "drago", "dratini", "saur", "tyrannus", "carnotaurus", "baryonyx", "minmi", "diplodocus", "phodon", "indominus",
					"raptor", "caihong", "coelophysis", "cephale", "compsognathus", "mimus", "troodon", "dactyl", "tanystropheus", "geosternbergia",
					"deino", "suchus", "dracorex", "cephalus", "trodon", "quetzalcoatlus", "pteranodon", "antarctopelta", "stygimoloch", "rhabdodon",
					"rhamphorhynchus", "ceratops", "ceratus", "zalmoxes", "mochlodon", "gigantophis", "crab", "pulmonoscorpius", "manipulator",
					"meganeura", "euphoberia", "holcorobeus", "protosolpuga", "barbslinger", "blizzarisk", "frostmite", "devourer", "hyperweaver",
					"macrelcana", "acklay", "elemental", "megalania", "gecko", "gator", "komodo", "scolipede", "shuckle", "combee", "shedinja",
					"caterpie", "wurmple", "lockjaw", "needlepost", "needleroll", "squid", "slug", "gila", "pleura"))
				{
					return;
				}
				pawn.health.AddHediff(SexPartAdder.MakePart(part, pawn, bpr), bpr);
			}
		}

		public static void AddGenitals(Pawn pawn, Pawn parent, Gender gender, BodyPartRecord bpr, HediffDef part)
		{
			var temppawn = parent ?? pawn;
			double value = GenderTechLevelCheck(pawn);
			string racename = temppawn.kindDef.race.defName.ToLower();

			//Log.Message("Genital_Helper::add_genitals( " + xxx.get_pawnname(pawn));
			//Log.Message("Genital_Helper::add_genitals( " + pawn.kindDef.race.defName);
			//Log.Message("Genital_Helper::is male( " + privates_gender(pawn, gender));
			//Log.Message("Genital_Helper::is male1( " + pawn.gender);
			//Log.Message("Genital_Helper::is male2( " + gender);
			if (xxx.is_mechanoid(pawn))
			{
				return;
			}
			//insects
			else if (xxx.is_insect(temppawn)
				 || racename.Contains("apini")
				 || racename.Contains("mantodean")
				 || racename.Contains("insect")
				 || racename.Contains("bug"))
			{
				part = (privates_gender(pawn, gender)) ? Genital_Helper.ovipositorM : Genital_Helper.ovipositorF;
				//override for Better infestations, since queen is male at creation
				if (racename.Contains("Queen"))
					part = Genital_Helper.ovipositorF;
			}
			//space cats pawns
			else if ((racename.Contains("orassan") || racename.Contains("neko")) && !racename.ContainsAny("akaneko"))
			{
				if ((value < 0.70) || (pawn.ageTracker.AgeBiologicalYears < 2) || !pawn.RaceProps.Humanlike)
					part = (privates_gender(pawn, gender)) ? Genital_Helper.feline_penis : Genital_Helper.feline_vagina;
				else if (value < 0.90)
					part = (privates_gender(pawn, gender)) ? Genital_Helper.hydraulic_penis : Genital_Helper.hydraulic_vagina;
				else
					part = (privates_gender(pawn, gender)) ? Genital_Helper.bionic_penis : Genital_Helper.bionic_vagina;
			}
			//space dog pawns
			else if (racename.Contains("fennex")
				 || racename.Contains("xenn")
				 || racename.Contains("leeani")
				 || racename.Contains("ferian")
				 || racename.Contains("callistan"))
			{
				if ((value < 0.70) || (pawn.ageTracker.AgeBiologicalYears < 2) || !pawn.RaceProps.Humanlike)
					part = (privates_gender(pawn, gender)) ? Genital_Helper.canine_penis : Genital_Helper.canine_vagina;
				else if (value < 0.90)
					part = (privates_gender(pawn, gender)) ? Genital_Helper.hydraulic_penis : Genital_Helper.hydraulic_vagina;
				else
					part = (privates_gender(pawn, gender)) ? Genital_Helper.bionic_penis : Genital_Helper.bionic_vagina;
			}
			//space horse pawns
			else if (racename.Contains("equium"))
			{
				if ((value < 0.70) || (pawn.ageTracker.AgeBiologicalYears < 2) || !pawn.RaceProps.Humanlike)
					part = (privates_gender(pawn, gender)) ? Genital_Helper.equine_penis : Genital_Helper.equine_vagina;
				else if (value < 0.90)
					part = (privates_gender(pawn, gender)) ? Genital_Helper.hydraulic_penis : Genital_Helper.hydraulic_vagina;
				else
					part = (privates_gender(pawn, gender)) ? Genital_Helper.bionic_penis : Genital_Helper.bionic_vagina;
			}
			//space raccoon pawns
			else if (racename.Contains("racc") && !racename.Contains("raccoon"))
			{
				if ((value < 0.70) || (pawn.ageTracker.AgeBiologicalYears < 2) || !pawn.RaceProps.Humanlike)
					part = (privates_gender(pawn, gender)) ? Genital_Helper.raccoon_penis : Genital_Helper.generic_vagina;
				else if (value < 0.90)
					part = (privates_gender(pawn, gender)) ? Genital_Helper.hydraulic_penis : Genital_Helper.hydraulic_vagina;
				else
					part = (privates_gender(pawn, gender)) ? Genital_Helper.bionic_penis : Genital_Helper.bionic_vagina;
			}
			//alien races - ChjDroid, ChjAndroid
			else if (AndroidsCompatibility.IsAndroid(pawn))
			{
				if (pawn.story.GetBackstory(BackstorySlot.Childhood) != null)
				{
					if (pawn.story.Childhood.untranslatedTitleShort.ToLower().Contains("bishojo"))
						part = (privates_gender(pawn, gender)) ? Genital_Helper.bionic_penis : Genital_Helper.bionic_vagina;
					else if (pawn.story.Childhood.untranslatedTitleShort.ToLower().Contains("pleasure"))
						part = (privates_gender(pawn, gender)) ? Genital_Helper.bionic_penis : Genital_Helper.bionic_vagina;
					else if (pawn.story.Childhood.untranslatedTitleShort.ToLower().Contains("idol"))
						part = (privates_gender(pawn, gender)) ? Genital_Helper.bionic_penis : Genital_Helper.bionic_vagina;
					else if (pawn.story.Childhood.untranslatedTitleShort.ToLower().Contains("social"))
						part = (privates_gender(pawn, gender)) ? Genital_Helper.hydraulic_penis : Genital_Helper.hydraulic_vagina;
					else if (pawn.story.Childhood.untranslatedTitleShort.ToLower().Contains("substitute"))
						part = (privates_gender(pawn, gender)) ? Genital_Helper.average_penis : Genital_Helper.average_vagina;
					else if (pawn.story.GetBackstory(BackstorySlot.Adulthood) != null)
					{
						if (pawn.story.Adulthood.untranslatedTitleShort.ToLower().Contains("courtesan"))
							part = (privates_gender(pawn, gender)) ? Genital_Helper.bionic_penis : Genital_Helper.bionic_vagina;
						else if (pawn.story.Adulthood.untranslatedTitleShort.ToLower().Contains("social"))
							part = (privates_gender(pawn, gender)) ? Genital_Helper.hydraulic_penis : Genital_Helper.hydraulic_vagina;
					}
					else
						return;
				}
				else if (pawn.story.GetBackstory(BackstorySlot.Adulthood) != null)
				{
					if (pawn.story.Adulthood.untranslatedTitleShort.ToLower().Contains("courtesan"))
						part = (privates_gender(pawn, gender)) ? Genital_Helper.bionic_penis : Genital_Helper.bionic_vagina;
					else if (pawn.story.Adulthood.untranslatedTitleShort.ToLower().Contains("social"))
						part = (privates_gender(pawn, gender)) ? Genital_Helper.hydraulic_penis : Genital_Helper.hydraulic_vagina;
				}
				if (part == Genital_Helper.generic_penis || part == Genital_Helper.generic_vagina)
					return;
			}
			//animal cats
			else if (racename.ContainsAny("cat", "cougar", "lion", "leopard", "cheetah", "panther", "tiger", "lynx", "smilodon", "akaneko"))
			{
				part = (privates_gender(pawn, gender)) ? Genital_Helper.feline_penis : Genital_Helper.feline_vagina;
			}
			//animal canine/dogs
			else if (racename.ContainsAny("husky", "warg", "terrier", "collie", "hound", "retriever", "mastiff", "wolf", "fox",
				"vulptex", "dachshund", "schnauzer", "corgi", "pug", "doberman", "chowchow", "borzoi", "saintbernard", "newfoundland",
				"poodle", "dog", "coyote"))
			{
				part = (privates_gender(pawn, gender)) ? Genital_Helper.canine_penis : Genital_Helper.canine_vagina;
			}
			//animal horse - MoreMonstergirls
			else if (racename.ContainsAny("horse", "centaur", "zebra", "donkey", "dryad"))
			{
				part = (privates_gender(pawn, gender)) ? Genital_Helper.equine_penis : Genital_Helper.equine_vagina;
			}
			//animal raccoon
			else if (racename.Contains("racc"))
			{
				part = (privates_gender(pawn, gender)) ? Genital_Helper.raccoon_penis : Genital_Helper.generic_vagina;
			}
			//animal crocodilian (alligator, crocodile, etc)
			else if (racename.ContainsAny("alligator", "crocodile", "caiman", "totodile", "croconaw", "feraligatr", "quinkana", "purussaurus", "kaprosuchus", "sarcosuchus"))
			{
				part = (privates_gender(pawn, gender)) ? Genital_Helper.crocodilian_penis : Genital_Helper.generic_vagina;
			}
			//hemipenes - mostly reptiles and snakes
			else if (racename.ContainsAny("guana", "cobra", "gecko", "snake", "boa", "quinkana", "megalania", "gila", "gigantophis", "komodo", "basilisk", "thorny", "onix", "lizard", "slither") && !racename.ContainsAny("boar"))
			{
				part = (privates_gender(pawn, gender)) ? Genital_Helper.hemipenis : Genital_Helper.generic_vagina;
			}
			//animal dragon - MoreMonstergirls
			else if (racename.ContainsAny("dragon", "thrumbo", "drake", "charizard", "saurus"))
			{
				part = (privates_gender(pawn, gender)) ? Genital_Helper.dragon_penis : Genital_Helper.dragon_vagina;
			}
			//animal slime - MoreMonstergirls
			else if (racename.Contains("slime"))
			{
				// slime always futa
				pawn.health.AddHediff(SexPartAdder.MakePart(privates_gender(pawn, gender) ? Genital_Helper.slime_penis : Genital_Helper.slime_vagina, pawn, bpr), bpr);
				pawn.health.AddHediff(SexPartAdder.MakePart(privates_gender(pawn, gender) ? Genital_Helper.slime_vagina : Genital_Helper.slime_penis, pawn, bpr), bpr);
				return;
			}
			//animal demons - MoreMonstergirls
			else if (racename.Contains("impmother") || racename.Contains("demon"))
			{
				// 25% futa
				pawn.health.AddHediff(SexPartAdder.MakePart(privates_gender(pawn, gender) ? Genital_Helper.demon_penis : Genital_Helper.demon_vagina, pawn, bpr), bpr);
				if (Rand.Value < 0.25f)
					pawn.health.AddHediff(SexPartAdder.MakePart(privates_gender(pawn, gender) ? Genital_Helper.demon_penis : Genital_Helper.demonT_penis, pawn, bpr), bpr);
				return;
			}
			//animal demons - MoreMonstergirls
			else if (racename.Contains("baphomet"))
			{
				if (Rand.Value < 0.50f)
					pawn.health.AddHediff(SexPartAdder.MakePart(privates_gender(pawn, gender) ? Genital_Helper.demon_penis : Genital_Helper.demon_vagina, pawn, bpr), bpr);
				else
					pawn.health.AddHediff(SexPartAdder.MakePart(privates_gender(pawn, gender) ? Genital_Helper.equine_penis : Genital_Helper.demon_vagina, pawn, bpr), bpr);
				return;
			}
			else if (pawn.RaceProps.Humanlike)
			{
				//--Log.Message("Genital_Helper::add_genitals( " + xxx.get_pawnname(pawn) + " ) - race is humanlike");
				if (value < 0.90 || (pawn.ageTracker.AgeBiologicalYears < 2))
					part = (privates_gender(pawn, gender)) ? Genital_Helper.average_penis : Genital_Helper.average_vagina;
				else if (value < 0.95)
					part = (privates_gender(pawn, gender)) ? Genital_Helper.hydraulic_penis : Genital_Helper.hydraulic_vagina;
				else
					part = (privates_gender(pawn, gender)) ? Genital_Helper.bionic_penis : Genital_Helper.bionic_vagina;
			}
			//--Log.Message("Genital_Helper::add_genitals final ( " + xxx.get_pawnname(pawn) + " ) " + part.defName);
			var hd = SexPartAdder.MakePart(part, pawn, bpr);
			//Log.Message("Genital_Helper::add_genitals final ( " + xxx.get_pawnname(pawn) + " ) " + hd.def.defName + " sev " + hd.Severity + " bpr " + BPR.def.defName);
			pawn.health.AddHediff(hd, bpr);
			//Log.Message("Genital_Helper::add_genitals final ( " + xxx.get_pawnname(pawn) + " ) " + pawn.health.hediffSet.HasHediff(hd.def));
		}

		public static void AddAnus(Pawn pawn, BodyPartRecord bpr, Pawn parent)
		{
			var temppawn = parent ?? pawn;
			HediffDef part;
			double value = GenderTechLevelCheck(pawn);
			string racename = temppawn.kindDef.race.defName.ToLower();

			part = Genital_Helper.generic_anus;

			if (xxx.is_mechanoid(pawn))
			{
				return;
			}
			else if (xxx.is_insect(temppawn))
			{
				part = Genital_Helper.insect_anus;
			}
			//alien races - ChjDroid, ChjAndroid
			else if (AndroidsCompatibility.IsAndroid(pawn))
			{
				if (pawn.story.GetBackstory(BackstorySlot.Childhood) != null)
				{
					if (pawn.story.Childhood.untranslatedTitleShort.ToLower().Contains("bishojo"))
						part = Genital_Helper.bionic_anus;
					else if (pawn.story.Childhood.untranslatedTitleShort.ToLower().Contains("pleasure"))
						part = Genital_Helper.bionic_anus;
					else if (pawn.story.Childhood.untranslatedTitleShort.ToLower().Contains("idol"))
						part = Genital_Helper.bionic_anus;
					else if (pawn.story.Childhood.untranslatedTitleShort.ToLower().Contains("social"))
						part = Genital_Helper.hydraulic_anus;
					else if (pawn.story.Childhood.untranslatedTitleShort.ToLower().Contains("substitute"))
						part = Genital_Helper.average_anus;
					else if (pawn.story.GetBackstory(BackstorySlot.Adulthood) != null)
					{
						if (pawn.story.Adulthood.untranslatedTitleShort.ToLower().Contains("courtesan"))
							part = Genital_Helper.bionic_anus;
						else if (pawn.story.Adulthood.untranslatedTitleShort.ToLower().Contains("social"))
							part = Genital_Helper.hydraulic_anus;
					}
				}
				else if (pawn.story.GetBackstory(BackstorySlot.Adulthood) != null)
				{
					if (pawn.story.Adulthood.untranslatedTitleShort.ToLower().Contains("courtesan"))
						part = Genital_Helper.bionic_anus;
					else if (pawn.story.Adulthood.untranslatedTitleShort.ToLower().Contains("social"))
						part = Genital_Helper.hydraulic_anus;
				}
				if (part == Genital_Helper.generic_anus)
					return;
			}
			else if (racename.Contains("slime"))
			{
				part = Genital_Helper.slime_anus;
			}
			//animal demons - MoreMonstergirls
			else if (racename.Contains("impmother") || racename.Contains("baphomet") || racename.Contains("demon"))
			{
				part = Genital_Helper.demon_anus;
			}
			else if (pawn.RaceProps.Humanlike)
			{
				if (value < 0.90 || (pawn.ageTracker.AgeBiologicalYears < 2))
					part = Genital_Helper.average_anus;
				else if (value < 0.95)
					part = Genital_Helper.hydraulic_anus;
				else
					part = Genital_Helper.bionic_anus;
			}

			pawn.health.AddHediff(SexPartAdder.MakePart(part, pawn, bpr), bpr);
		}
	}
}


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

namespace rjw
{
	class OviHelper
	{
		static readonly IDictionary<PawnKindDef, ThingDef> FertileEggPawnKinds = new Dictionary<PawnKindDef, ThingDef>();
		static readonly IDictionary<PawnKindDef, ThingDef> UnfertileEggPawnKinds = new Dictionary<PawnKindDef, ThingDef>();
		public static CompProperties_EggLayer GenerateEggLayerProperties(PawnKindDef pawnKindDef, RaceGroupDef raceGroupDef)
		{
			CompProperties_EggLayer comp = new CompProperties_EggLayer();
			comp.eggFertilizedDef = DefDatabase<ThingDef>.GetNamed(raceGroupDef.eggFertilizedDef);
			comp.eggUnfertilizedDef = DefDatabase<ThingDef>.GetNamed(raceGroupDef.eggUnfertilizedDef);
			comp.eggProgressUnfertilizedMax = raceGroupDef.eggProgressUnfertilizedMax;
			comp.eggLayIntervalDays = raceGroupDef.eggLayIntervalDays;
			//comp.eggCountRange = raceGroupDef.eggCountRange;
			//comp.eggFertilizationCountMax = raceGroupDef.eggFertilizationCountMax;
			//comp.eggLayFemaleOnly = raceGroupDef.eggLayFemaleOnly;

			return comp;
		}

		//private static ThingDef createFertilizedDef(PawnKindDef pawnKindDef, RaceGroupDef raceGroupDef)
		//{
		//	ThingDef eggThingDef;
		//	if (FertileEggPawnKinds.TryGetValue(pawnKindDef, out eggThingDef))
		//	{
		//		return eggThingDef;
		//	}

		//	eggThingDef = EggFertBase();
		//	eggThingDef.defName = eggThingDef.defName + "_" + pawnKindDef.race.defName;

		//	eggThingDef.label = eggThingDef.label.Replace("PAWNKIND", pawnKindDef.race.label);
		//	eggThingDef.description = eggThingDef.label.Replace("PAWNKIND", pawnKindDef.race.label);

		//	CompRJWProperties_Hatcher compPropHatcher = new CompRJWProperties_Hatcher();
		//	compPropHatcher.hatcherDaystoHatch = 3.5f; // This should probably be some sort of race property
		//	compPropHatcher.hatcherPawn = pawnKindDef;

		//	eggThingDef.comps.Add(compPropHatcher);

		//	FertileEggPawnKinds.Add(pawnKindDef, eggThingDef);

		//	return eggThingDef;
		//}

		//private static ThingDef createUnfertilizedDef(PawnKindDef pawnKindDef, RaceGroupDef raceGroupDef)
		//{
		//	ThingDef eggThingDef;
		//	if (UnfertileEggPawnKinds.TryGetValue(pawnKindDef, out eggThingDef))
		//	{
		//		return eggThingDef;
		//	}

		//	eggThingDef = EggUnfertBase();
		//	eggThingDef.defName = eggThingDef.defName + "_" + pawnKindDef.race.defName;

		//	eggThingDef.label = eggThingDef.label.Replace("PAWNKIND", pawnKindDef.race.label);
		//	eggThingDef.description = eggThingDef.label.Replace("PAWNKIND", pawnKindDef.race.label);

		//	UnfertileEggPawnKinds.Add(pawnKindDef, eggThingDef);

		//	return eggThingDef;
		//}

		//private static ThingDef EggFertBase()
		//{
		//	ThingDef baseEgg = ThingDef.Named("RjwFertEggPawn");

		//	// There's no easy way to deep-copy a ThingDef, so here we go...
		//	ThingDef copiedEgg = new ThingDef();

		//	copiedEgg.thingClass = baseEgg.thingClass;
		//	copiedEgg.category = baseEgg.category;
		//	copiedEgg.resourceReadoutPriority = baseEgg.resourceReadoutPriority;
		//	copiedEgg.useHitPoints = baseEgg.useHitPoints;
		//	copiedEgg.altitudeLayer = baseEgg.altitudeLayer;
		//	copiedEgg.stackLimit = baseEgg.stackLimit;
		//	copiedEgg.alwaysHaulable = baseEgg.alwaysHaulable;
		//	copiedEgg.drawGUIOverlay = baseEgg.drawGUIOverlay;
		//	copiedEgg.rotatable = baseEgg.rotatable;
		//	copiedEgg.pathCost = baseEgg.pathCost;

		//	copiedEgg.defName = baseEgg.defName;
		//	copiedEgg.label = baseEgg.label;
		//	copiedEgg.description = baseEgg.description;
		//	copiedEgg.selectable = baseEgg.selectable;
		//	copiedEgg.tickerType = baseEgg.tickerType;
		//	copiedEgg.healthAffectsPrice = baseEgg.healthAffectsPrice;

		//	copiedEgg.graphicData = new GraphicData();
		//	copiedEgg.graphicData.texPath = baseEgg.graphicData.texPath;
		//	copiedEgg.graphicData.color = baseEgg.graphicData.color;
		//	copiedEgg.graphicData.graphicClass = baseEgg.graphicData.graphicClass;

		//	copiedEgg.ingestible = new IngestibleProperties();
		//	copiedEgg.ingestible.parent = baseEgg.ingestible.parent;
		//	copiedEgg.ingestible.canAutoSelectAsFoodForCaravan = baseEgg.ingestible.canAutoSelectAsFoodForCaravan;
		//	copiedEgg.ingestible.drugCategory = baseEgg.ingestible.drugCategory;
		//	copiedEgg.ingestible.optimalityOffsetFeedingAnimals = baseEgg.ingestible.optimalityOffsetFeedingAnimals;
		//	copiedEgg.ingestible.optimalityOffsetHumanlikes = baseEgg.ingestible.optimalityOffsetHumanlikes;
		//	copiedEgg.ingestible.nurseable = baseEgg.ingestible.nurseable;
		//	copiedEgg.ingestible.preferability = baseEgg.ingestible.preferability;
		//	copiedEgg.ingestible.sourceDef = baseEgg.ingestible.sourceDef;
		//	copiedEgg.ingestible.joyKind = baseEgg.ingestible.joyKind;
		//	copiedEgg.ingestible.joy = baseEgg.ingestible.joy;
		//	copiedEgg.ingestible.foodType = baseEgg.ingestible.foodType;
		//	copiedEgg.ingestible.ingestHoldOffsetStanding = baseEgg.ingestible.ingestHoldOffsetStanding;
		//	copiedEgg.ingestible.ingestReportStringEat = baseEgg.ingestible.ingestReportStringEat;
		//	copiedEgg.ingestible.ingestHoldUsesTable = baseEgg.ingestible.ingestHoldUsesTable;
		//	copiedEgg.ingestible.ingestCommandString = baseEgg.ingestible.ingestCommandString;
		//	copiedEgg.ingestible.maxNumToIngestAtOnce = baseEgg.ingestible.maxNumToIngestAtOnce;
		//	copiedEgg.ingestible.outcomeDoers = baseEgg.ingestible.outcomeDoers;
		//	copiedEgg.ingestible.baseIngestTicks = baseEgg.ingestible.baseIngestTicks;
		//	copiedEgg.ingestible.ingestReportString = baseEgg.ingestible.ingestReportString;
		//	copiedEgg.ingestible.useEatingSpeedStat = baseEgg.ingestible.useEatingSpeedStat;
		//	copiedEgg.ingestible.tasteThought = baseEgg.ingestible.tasteThought;
		//	copiedEgg.ingestible.chairSearchRadius = baseEgg.ingestible.chairSearchRadius;
		//	copiedEgg.ingestible.specialThoughtAsIngredient = baseEgg.ingestible.specialThoughtAsIngredient;
		//	copiedEgg.ingestible.ingestEffect = baseEgg.ingestible.ingestEffect;
		//	copiedEgg.ingestible.ingestEffectEat = baseEgg.ingestible.ingestEffectEat;
		//	copiedEgg.ingestible.ingestSound = baseEgg.ingestible.ingestSound;
		//	copiedEgg.ingestible.specialThoughtDirect = baseEgg.ingestible.specialThoughtDirect;

		//	copiedEgg.thingCategories = new List<ThingCategoryDef>
		//	{
		//		ThingCategoryDef.Named("EggsFertilized")
		//	};

		//	copiedEgg.statBases = new List<StatModifier>();
		//	foreach (StatModifier statMod in baseEgg.statBases)
		//	{
		//		StatModifier copiedMod = new StatModifier();
		//		copiedMod.stat = statMod.stat;
		//		copiedMod.value = statMod.value;

		//		copiedEgg.statBases.Add(copiedMod);
		//	}

		//	copiedEgg.comps = new List<CompProperties>();
		//	foreach (CompProperties compProp in baseEgg.comps)
		//	{
		//		if (compProp is CompProperties_TemperatureRuinable)
		//		{
		//			CompProperties_TemperatureRuinable baseTempRuin = compProp as CompProperties_TemperatureRuinable;
		//			CompProperties_TemperatureRuinable newTempRuin = new CompProperties_TemperatureRuinable
		//			{
		//				minSafeTemperature = baseTempRuin.minSafeTemperature,
		//				maxSafeTemperature = baseTempRuin.maxSafeTemperature,
		//				progressPerDegreePerTick = baseTempRuin.progressPerDegreePerTick
		//			};
		//			copiedEgg.comps.Add(newTempRuin);
		//		}

		//		if (compProp is CompProperties_Rottable)
		//		{
		//			CompProperties_Rottable baseRot = compProp as CompProperties_Rottable;
		//			CompProperties_Rottable newRot = new CompProperties_Rottable
		//			{
		//				daysToRotStart = baseRot.daysToRotStart,
		//				rotDestroys = baseRot.rotDestroys,
		//				disableIfHatcher = baseRot.disableIfHatcher
		//			};
		//			copiedEgg.comps.Add(newRot);
		//		}
		//	}

		//	return copiedEgg;
		//}

		//private static ThingDef EggUnfertBase()
		//{
		//	ThingDef baseEgg = ThingDef.Named("RjwUnfertEggPawn");

		//	// There's no easy way to deep-copy a ThingDef, so here we go...
		//	ThingDef copiedEgg = new ThingDef();

		//	copiedEgg.thingClass = baseEgg.thingClass;
		//	copiedEgg.category = baseEgg.category;
		//	copiedEgg.resourceReadoutPriority = baseEgg.resourceReadoutPriority;
		//	copiedEgg.useHitPoints = baseEgg.useHitPoints;
		//	copiedEgg.altitudeLayer = baseEgg.altitudeLayer;
		//	copiedEgg.stackLimit = baseEgg.stackLimit;
		//	copiedEgg.alwaysHaulable = baseEgg.alwaysHaulable;
		//	copiedEgg.drawGUIOverlay = baseEgg.drawGUIOverlay;
		//	copiedEgg.rotatable = baseEgg.rotatable;
		//	copiedEgg.pathCost = baseEgg.pathCost;

		//	copiedEgg.defName = baseEgg.defName;
		//	copiedEgg.label = baseEgg.label;
		//	copiedEgg.description = baseEgg.description;
		//	copiedEgg.selectable = baseEgg.selectable;
		//	copiedEgg.tickerType = baseEgg.tickerType;
		//	copiedEgg.healthAffectsPrice = baseEgg.healthAffectsPrice;

		//	copiedEgg.graphicData = new GraphicData();
		//	copiedEgg.graphicData.texPath = baseEgg.graphicData.texPath;
		//	copiedEgg.graphicData.color = baseEgg.graphicData.color;
		//	copiedEgg.graphicData.graphicClass = baseEgg.graphicData.graphicClass;

		//	copiedEgg.ingestible = new IngestibleProperties();
		//	copiedEgg.ingestible.parent = baseEgg.ingestible.parent;
		//	copiedEgg.ingestible.canAutoSelectAsFoodForCaravan = baseEgg.ingestible.canAutoSelectAsFoodForCaravan;
		//	copiedEgg.ingestible.drugCategory = baseEgg.ingestible.drugCategory;
		//	copiedEgg.ingestible.optimalityOffsetFeedingAnimals = baseEgg.ingestible.optimalityOffsetFeedingAnimals;
		//	copiedEgg.ingestible.optimalityOffsetHumanlikes = baseEgg.ingestible.optimalityOffsetHumanlikes;
		//	copiedEgg.ingestible.nurseable = baseEgg.ingestible.nurseable;
		//	copiedEgg.ingestible.preferability = baseEgg.ingestible.preferability;
		//	copiedEgg.ingestible.sourceDef = baseEgg.ingestible.sourceDef;
		//	copiedEgg.ingestible.joyKind = baseEgg.ingestible.joyKind;
		//	copiedEgg.ingestible.joy = baseEgg.ingestible.joy;
		//	copiedEgg.ingestible.foodType = baseEgg.ingestible.foodType;
		//	copiedEgg.ingestible.ingestHoldOffsetStanding = baseEgg.ingestible.ingestHoldOffsetStanding;
		//	copiedEgg.ingestible.ingestReportStringEat = baseEgg.ingestible.ingestReportStringEat;
		//	copiedEgg.ingestible.ingestHoldUsesTable = baseEgg.ingestible.ingestHoldUsesTable;
		//	copiedEgg.ingestible.ingestCommandString = baseEgg.ingestible.ingestCommandString;
		//	copiedEgg.ingestible.maxNumToIngestAtOnce = baseEgg.ingestible.maxNumToIngestAtOnce;
		//	copiedEgg.ingestible.outcomeDoers = baseEgg.ingestible.outcomeDoers;
		//	copiedEgg.ingestible.baseIngestTicks = baseEgg.ingestible.baseIngestTicks;
		//	copiedEgg.ingestible.ingestReportString = baseEgg.ingestible.ingestReportString;
		//	copiedEgg.ingestible.useEatingSpeedStat = baseEgg.ingestible.useEatingSpeedStat;
		//	copiedEgg.ingestible.tasteThought = baseEgg.ingestible.tasteThought;
		//	copiedEgg.ingestible.chairSearchRadius = baseEgg.ingestible.chairSearchRadius;
		//	copiedEgg.ingestible.specialThoughtAsIngredient = baseEgg.ingestible.specialThoughtAsIngredient;
		//	copiedEgg.ingestible.ingestEffect = baseEgg.ingestible.ingestEffect;
		//	copiedEgg.ingestible.ingestEffectEat = baseEgg.ingestible.ingestEffectEat;
		//	copiedEgg.ingestible.ingestSound = baseEgg.ingestible.ingestSound;
		//	copiedEgg.ingestible.specialThoughtDirect = baseEgg.ingestible.specialThoughtDirect;

		//	copiedEgg.thingCategories = new List<ThingCategoryDef>
		//	{
		//		ThingCategoryDef.Named("EggsUnfertilized")
		//	};

		//	copiedEgg.statBases = new List<StatModifier>();
		//	foreach (StatModifier statMod in baseEgg.statBases)
		//	{
		//		StatModifier copiedMod = new StatModifier();
		//		copiedMod.stat = statMod.stat;
		//		copiedMod.value = statMod.value;

		//		copiedEgg.statBases.Add(copiedMod);
		//	}

		//	copiedEgg.comps = new List<CompProperties>();
		//	foreach (CompProperties compProp in baseEgg.comps)
		//	{
		//		if (compProp is CompProperties_TemperatureRuinable)
		//		{
		//			CompProperties_TemperatureRuinable baseTempRuin = compProp as CompProperties_TemperatureRuinable;
		//			CompProperties_TemperatureRuinable newTempRuin = new CompProperties_TemperatureRuinable
		//			{
		//				minSafeTemperature = baseTempRuin.minSafeTemperature,
		//				maxSafeTemperature = baseTempRuin.maxSafeTemperature,
		//				progressPerDegreePerTick = baseTempRuin.progressPerDegreePerTick
		//			};
		//			copiedEgg.comps.Add(newTempRuin);
		//		}

		//		if (compProp is CompProperties_Rottable)
		//		{
		//			CompProperties_Rottable baseRot = compProp as CompProperties_Rottable;
		//			CompProperties_Rottable newRot = new CompProperties_Rottable
		//			{
		//				daysToRotStart = baseRot.daysToRotStart,
		//				rotDestroys = baseRot.rotDestroys,
		//				disableIfHatcher = baseRot.disableIfHatcher
		//			};
		//			copiedEgg.comps.Add(newRot);
		//		}
		//	}

		//	return copiedEgg;
		//}
	}
}


code:
using Verse;
using Verse.AI;

namespace rjw
{
	public static class Pather_Utility
	{
		public static bool cells_to_target_casual(Pawn pawn, IntVec3 Position)
		{
			//less taxing, ignores walls
			return pawn.Position.DistanceTo(Position) < RJWSettings.maxDistanceCellsCasual;
		}

		public static bool cells_to_target_rape(Pawn pawn, IntVec3 Position)
		{
			//less taxing, ignores walls
			return pawn.Position.DistanceTo(Position) < RJWSettings.maxDistanceCellsRape;
		}

		public static bool can_path_to_target(Pawn pawn, IntVec3 Position)
		{
			//more taxing, using real pathing
			bool canit = true;
			if (RJWSettings.maxDistancePathCost > 0)
			{
				PawnPath pawnPath = pawn.Map.pathFinder.FindPath(pawn.Position, Position, pawn);
				if (pawnPath.TotalCost > RJWSettings.maxDistancePathCost)
					canit = false;// too far
				pawnPath.Dispose();
			}
			return canit;
		}
	}
}