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

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

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

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

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

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

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

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

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

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

			var partBPR = Genital_Helper.get_genitalsBPR(pawn);
			var parts = Genital_Helper.get_PartsHediffList(pawn, partBPR);

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

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

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

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

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

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

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

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

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

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

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

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

			if (targets.Any())
			{
				var partBPR = Genital_Helper.get_genitalsBPR(pawn);
				var parts = Genital_Helper.get_PartsHediffList(pawn, partBPR);

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

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

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

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

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

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

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

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

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

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

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

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

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


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

namespace rjw
{
	/// <summary>
	/// Helper for sex with (humanlikes)
	/// </summary>
	public class CasualSex_Helper
	{
		// 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 && (xxx.is_laying_down_alone(x) || xxx.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 (xxx.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 (!xxx.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)
					);
		}
	}
}

repo_name: ElToro/rjw

license: mit


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

namespace rjw
{
	public class CondomUtility
	{
		public static readonly RecordDef CountOfCondomsUsed = DefDatabase<RecordDef>.GetNamed("CountOfCondomsUsed");
		public static readonly ThoughtDef SexWithCondom = DefDatabase<ThoughtDef>.GetNamed("SexWithCondom");
		public static readonly ThingDef Condom = DefDatabase<ThingDef>.GetNamedSilentFail("Condom");
		public static readonly ThingDef UsedCondom = DefDatabase<ThingDef>.GetNamedSilentFail("UsedCondom");

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

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

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


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

//This one is helper lib for handling all the trans surgery.
namespace rjw
{
	public static class GenderHelper
	{
		public enum Sex { male, female, trap, futa, none }//there is Verse,Gender but it is shit
		//These would probably be better packed in some enumerable structure, so that functions below weren't if-trees, but I don't know C#, sry.
		public static HediffDef was_boy = HediffDef.Named("hediff_was_boy");
		public static HediffDef was_girl = HediffDef.Named("hediff_was_girl");
		public static HediffDef was_futa = HediffDef.Named("hediff_was_futa");
		public static HediffDef was_trap = HediffDef.Named("hediff_was_trap");

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

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

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

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

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

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

		public static Sex GetSex(Pawn pawn)
		{
			var partBPR = Genital_Helper.get_genitalsBPR(pawn);
			var parts = Genital_Helper.get_PartsHediffList(pawn, partBPR);

			bool has_vagina = Genital_Helper.has_vagina(pawn, parts);
			bool has_penis = Genital_Helper.has_penis_fertile(pawn, parts);
			bool has_penis_infertile = Genital_Helper.has_penis_infertile(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 && !has_penis_infertile)
				res = Sex.female;
			else if (has_vagina && (has_penis || has_penis_infertile))
				res = Sex.futa;
			else if ((has_penis || has_penis_infertile) && has_breasts && !has_male_breasts)
				res = Sex.trap;
			else if (has_penis || has_penis_infertile) //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;
		}
		/*
		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.Error(" 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 Verse;

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");

		public static BodyPartRecord get_genitalsBPR(Pawn pawn)
		{
			//--Log.Message("Genital_Helper::get_genitals( " + xxx.get_pawnname(pawn) + " ) called");
			BodyPartRecord Part = pawn?.RaceProps.body.AllParts.Find(bpr => bpr.def.defName == "Genitals");

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

		public static BodyPartRecord get_breastsBPR(Pawn pawn)
		{
			//--ModLog.Message(" get_breasts( " + xxx.get_pawnname(pawn) + " ) called");
			BodyPartRecord Part = pawn?.RaceProps.body.AllParts.Find(bpr => bpr.def.defName == "Chest");

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

		public static BodyPartRecord get_mouthBPR(Pawn pawn)
		{
			//--ModLog.Message(" get_mouth( " + xxx.get_pawnname(pawn) + " ) called");
			BodyPartRecord Part = pawn?.RaceProps.body.AllParts.Find(bpr => bpr.def.defName.ToLower().ContainsAny("mouth", "teeth", "jaw", "beak"));

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

		public static BodyPartRecord get_stomachBPR(Pawn pawn)
		{
			//--ModLog.Message(" get_stomach( " + xxx.get_pawnname(pawn) + " ) called");
			BodyPartRecord Part = pawn?.RaceProps.body.AllParts.Find(bpr => bpr.def.defName.ToLower().ContainsAny("stomach"));

			if (Part == null)
			{
				BodyPartRecord TorsoPart = pawn?.RaceProps.body.AllParts.Find(bpr => bpr.def.defName.ToLower().ContainsAny("torso"));
				//--ModLog.Message(" get_stomach( " + xxx.get_pawnname(pawn) + " ) - Part is null, trying to get torso...");
				if (TorsoPart == null)
				{
					//--ModLog.Message(" get_stomach( " + xxx.get_pawnname(pawn) + " ) - Part is null, no torso.");
					return null;
				}
				return TorsoPart;
			}
			return Part;
		}

		public static BodyPartRecord get_tailBPR(Pawn pawn)
		{
			//should probably make scale race check or something
			//--ModLog.Message(" get_tail( " + xxx.get_pawnname(pawn) + " ) called");
			BodyPartRecord Part = pawn?.RaceProps.body.AllParts.Find(bpr => bpr.def.defName.ToLower().ContainsAny("tail"));

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

		public static BodyPartRecord get_anusBPR(Pawn pawn)
		{
			//--ModLog.Message(" get_anus( " + xxx.get_pawnname(pawn) + " ) called");
			BodyPartRecord Part = pawn?.RaceProps.body.AllParts.Find(bpr => bpr.def.defName == "Anus");

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

		public static bool breasts_blocked(Pawn pawn)
		{
			if (pawn.apparel != null)
			{
				foreach (var app in pawn.apparel.WornApparel)
				{
					if ((app.def is bondage_gear_def gear_def) && (gear_def.blocks_breasts))
						return true;
				}
			}
			return false;
		}

		public static bool anus_blocked(Pawn pawn)
		{
			if (pawn.apparel != null)
			{
				foreach (var app in pawn.apparel.WornApparel)
				{
					if ((app.def is bondage_gear_def gear_def) && (gear_def.blocks_anus))
						return true;
				}
			}
			return false;
		}

		public static bool genitals_blocked(Pawn pawn)
		{
			if (pawn.apparel != null)
			{
				foreach (var app in pawn.apparel.WornApparel)
				{
					if ((app.def is bondage_gear_def gear_def) && (gear_def.blocks_penis || gear_def.blocks_vagina))
						return true;
				}
			}
			return false;
		}

		public static bool hands_blocked(Pawn pawn)
		{
			if (pawn.apparel != null)
			{
				foreach (var app in pawn.apparel.WornApparel)
				{
					if ((app.def is bondage_gear_def gear_def) && (gear_def.blocks_hands))
						return true;
				}
			}
			return false;
		}

		public static bool penis_blocked(Pawn pawn)
		{
			if (pawn.apparel != null)
			{
				foreach (var app in pawn.apparel.WornApparel)
				{
					if ((app.def is bondage_gear_def gear_def) && (gear_def.blocks_penis))
						return true;
				}
			}
			return false;
		}

		public static bool oral_blocked(Pawn pawn)
		{
			if (pawn.apparel != null)
			{
				foreach (var app in pawn.apparel.WornApparel)
				{
					if ((app.def is bondage_gear_def gear_def) && (gear_def.blocks_oral))
						return true;
				}
			}
			return false;
		}

		public static bool vagina_blocked(Pawn pawn)
		{
			if (pawn.apparel != null)
			{
				foreach (var app in pawn.apparel.WornApparel)
				{
					if ((app.def is bondage_gear_def gear_def) && (gear_def.blocks_vagina))
						return true;
				}
			}
			return false;
		}

		public static List<Hediff> get_PartsHediffList(Pawn pawn, BodyPartRecord Part)
		{
			if (Part is null)
				return null;

			return pawn.health.hediffSet.hediffs.FindAll((Hediff hed) =>
					hed.Part == Part &&
					(hed is Hediff_PartBaseNatural || hed is Hediff_PartBaseArtifical));
		}

		public static bool has_genitals(Pawn pawn, List<Hediff> parts = null)
		{
			if (parts == null)
			{
				parts = get_PartsHediffList(pawn, get_genitalsBPR(pawn));
			}
			if (parts.NullOrEmpty())
				return false;

			return true;
		}

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

			return parts.Any((Hediff hed) =>
					!hed.def.defName.ToLower().Contains("featureless"));
		}

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

			return parts.Any((Hediff hed) =>
					(((hed is Hediff_PartBaseNatural) &&
					(hed as Hediff_PartBaseNatural).CurStageIndex == 0)
					||
					((hed is Hediff_PartBaseArtifical) &&
					(hed as Hediff_PartBaseArtifical).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)
		{
			if (parts == null)
			{
				parts = get_PartsHediffList(pawn, get_breastsBPR(pawn));
			}
			if (parts.NullOrEmpty())
				return false;

			return parts.Any((Hediff hed) =>
					(((hed is Hediff_PartBaseNatural) &&
					(hed as Hediff_PartBaseNatural).CurStageIndex > 1)
					||
					((hed is Hediff_PartBaseArtifical) &&
					(hed as Hediff_PartBaseArtifical).CurStageIndex > 1)
					));
		}

		public static bool has_tail(Pawn pawn)
		{
			BodyPartRecord Part = get_tailBPR(pawn);
			if (Part is null)
				return false;

			return !pawn.health.hediffSet.hediffs.Any((Hediff hed) =>
					(hed.Part == Part) && (hed is Hediff_MissingPart));
		}

		public static bool has_mouth(Pawn pawn)
		{
			BodyPartRecord Part = get_mouthBPR(pawn);
			if (Part is null)
				return false;

			return !pawn.health.hediffSet.hediffs.Any((Hediff hed) =>
					(hed.Part == Part) && (hed is Hediff_MissingPart));
		}

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

			return parts.Any((Hediff hed) =>
					hed.def.defName.ToLower().Contains("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)
		{
			if (parts == null)
			{
				parts = get_PartsHediffList(pawn, get_genitalsBPR(pawn));
			}
			if (parts.NullOrEmpty())
				return false;

			return parts.Any((Hediff hed) =>
					(hed.def.defName.ToLower().Contains("vagina") || hed.def.defName.ToLower().Contains("ovipositorf")));
		}

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

			return parts.Any((Hediff hed) =>
					(hed.def.defName.ToLower().Contains("penis") || hed.def.defName.ToLower().Contains("ovipositorm")));
		}
		public static bool has_penis_infertile(Pawn pawn, List<Hediff> parts = null)
		{
			if (parts == null)
			{
				parts = get_PartsHediffList(pawn, get_genitalsBPR(pawn));
			}
			if (parts.NullOrEmpty())
				return false;

			return parts.Any((Hediff hed) =>
					(hed.def.defName.ToLower().Contains("pegdick") || (hed.def.defName.ToLower().Contains("tentacle") && !hed.def.defName.ToLower().Contains("penis"))));
		}

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

			int count = 0;
			foreach(Hediff hed in parts)
			{
				if (hed.def.defName.ToLower().Contains("hemipenis"))
					return true;
				if (hed.def.defName.ToLower().Contains("penis") ||
					hed.def.defName.ToLower().Contains("pegdick") ||
					hed.def.defName.ToLower().Contains("ovipositorm") ||
					hed.def.defName.ToLower().Contains("tentacle")
					)
					count += 1;
			}

			return count > 1;
		}

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

			return parts.Any((Hediff hed) =>
					hed.def.defName.ToLower().Contains("ovipositorm"));
		}

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

			return parts.Any((Hediff hed) =>
					hed.def.defName.ToLower().Contains("ovipositorf"));

		}
		/// <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)
		{
			if (parts == null)
			{
				parts = get_PartsHediffList(pawn, get_genitalsBPR(pawn));
			}
			if (parts.NullOrEmpty())
				return false;

			return parts.Any((Hediff hed) =>
					(((hed is Hediff_PartBaseNatural) &&
					(hed as Hediff_PartBaseNatural).CurStageIndex > 3)
					||
					((hed is Hediff_PartBaseArtifical) &&
					(hed as Hediff_PartBaseArtifical).CurStageIndex > 3)) &&
					(!hed.def.defName.ToLower().Contains("vagina")) &&
					(
					hed.def.defName.ToLower().Contains("penis") ||
					hed.def.defName.ToLower().Contains("pegdick") ||
					hed.def.defName.ToLower().Contains("ovipositorm") ||
					hed.def.defName.ToLower().Contains("tentacle")
					));
		}

		/// <summary>
		/// Count only fertile penises
		/// </summary>
		/// <param name="pawn"></param>
		/// <returns></returns>
		public static bool is_futa(Pawn pawn, List<Hediff> parts = null)
		{
			if (parts == null)
			{
				parts = get_PartsHediffList(pawn, get_genitalsBPR(pawn));
			}
			if (parts.NullOrEmpty())
				return false;

			return (Genital_Helper.has_penis_fertile(pawn, parts) && has_vagina(pawn, parts));
		}
	}
}


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"))
					{
						pawn.health.AddHediff(Genital_Helper.featureless_chest, bpr);
						return;
					}
					else if (racename.ContainsAny("avali", "khess"))
					{
						return;
					}
					else if (racename.Contains("droid"))
					{
						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)
						{
							var bp = pawn.RaceProps.body.AllParts.Find(x => x.def.defName == "Torso");
							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"))
				{
					bpr = pawn.RaceProps.body.AllParts.Find(x => x.def.defName == "Torso");
					part = Genital_Helper.udder_breasts;
				}
				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 (racename.Contains("droid"))
			{
				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 (racename.Contains("droid"))
			{
				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;

			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 System.Collections.Generic;
using System.Linq;
using Verse;

namespace rjw
{
	class RaceGroupDef_Helper
	{
		/// <summary>
		/// Cache for TryGetRaceGroupDef.
		/// </summary>
		static readonly IDictionary<PawnKindDef, RaceGroupDef> RaceGroupByPawnKind = new Dictionary<PawnKindDef, RaceGroupDef>();

		public static bool TryGetRaceGroupDef(PawnKindDef pawnKindDef, out RaceGroupDef raceGroupDef)
		{
			if (RaceGroupByPawnKind.TryGetValue(pawnKindDef, out raceGroupDef))
			{
				return raceGroupDef != null;
			}
			else
			{
				raceGroupDef = GetRaceGroupDefInternal(pawnKindDef);
				RaceGroupByPawnKind.Add(pawnKindDef, raceGroupDef);
				return raceGroupDef != null;
			}
		}
		public static bool TryGetRaceGroupDef(Pawn pawn, out RaceGroupDef raceGroupDef)
		{
			return TryGetRaceGroupDef(pawn.kindDef, out raceGroupDef);
		}

		/// <summary>
		/// Returns the best match RaceGroupDef for the given pawn, or null if none is found.
		/// </summary>
		static RaceGroupDef GetRaceGroupDefInternal(Pawn pawn)
		{
			return GetRaceGroupDefInternal(pawn.kindDef);
		}
		static RaceGroupDef GetRaceGroupDefInternal(PawnKindDef kindDef)
		{
			var raceName = kindDef.race.defName;
			var pawnKindName = kindDef.defName;
			var groups = DefDatabase<RaceGroupDef>.AllDefs;

			var kindMatches = groups.Where(group => group.pawnKindNames?.Contains(pawnKindName) ?? false).ToList();
			var raceMatches = groups.Where(group => group.raceNames?.Contains(raceName) ?? false).ToList();
			var count = kindMatches.Count() + raceMatches.Count();
			if (count == 0)
			{
				//ModLog.Message($"Pawn named '{pawn.Name}' matched no RaceGroupDef. If you want to create a matching RaceGroupDef you can use the raceName '{raceName}' or the pawnKindName '{pawnKindName}'.");
				return null;
			}
			else if (count == 1)
			{
				// ModLog.Message($"Pawn named '{pawn.Name}' matched 1 RaceGroupDef.");
				return kindMatches.Concat(raceMatches).Single();
			}
			else
			{
				// ModLog.Message($"Pawn named '{pawn.Name}' matched {count} RaceGroupDefs.");

				// If there are multiple RaceGroupDef matches, choose one of them.
				// First prefer defs NOT defined in rjw.
				// Then prefer a match by kind over a match by race.
				return kindMatches.FirstOrDefault(match => !IsThisMod(match))
					?? raceMatches.FirstOrDefault(match => !IsThisMod(match))
					?? kindMatches.FirstOrDefault()
					?? raceMatches.FirstOrDefault();
			}
		}

		static bool IsThisMod(Def def)
		{
			var rjwContent = LoadedModManager.RunningMods.Single(pack => pack.Name == "RimJobWorld");
			return rjwContent.AllDefs.Contains(def);
		}

		/// <summary>
		/// Returns true if a race part was chosen (even if that part is "no part").
		/// </summary>
		public static bool TryAddRacePart(Pawn pawn, SexPartType sexPartType)
		{
			if (!TryGetRaceGroupDef(pawn, out var raceGroupDef))
			{
				// No race, so nothing was chosen.
				return false;
			}

			if (!RacePartDef_Helper.TryChooseRacePartDef(raceGroupDef, sexPartType, out var racePartDef))
			{
				// Failed to find a part, so nothing was chosen.
				return false;
			}

			if (racePartDef.IsNone)
			{
				// "no part" was explicitly chosen.
				return true;
			}

			if (raceGroupDef.HasUdder && sexPartType == SexPartType.FemaleBreast && !pawn.health.hediffSet.HasHediff(Genital_Helper.udder_breasts))
			{
				var bp = pawn.RaceProps.body.AllParts.Find(x => x.def.defName == "Torso");
				pawn.health.AddHediff(SexPartAdder.MakePart(Genital_Helper.udder_breasts, pawn, bp), bp);
			}

			var target = sexPartType.GetBodyPartDef();
			var bodyPartRecord = pawn.RaceProps.body.AllParts.Find(bpr => bpr.def == target);
			if (!racePartDef.TryGetHediffDef(out var hediffDef))
			{
				// Failed to find hediffDef.
				return false;
			}

			var hediff = RacePartDef_Helper.MakePart(hediffDef, pawn, bodyPartRecord, racePartDef);
			pawn.health.AddHediff(hediff, bodyPartRecord);
			// A part was chosen and added.
			return true;
		}
	}
}