code:
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using Verse;
using rjw.Modules.Testing;
using LudeonTK;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		public static bool is_sex_part(Hediff hed) => hed is ISexPartHediff;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		public static bool is_anus(Hediff hed)
		{
			if (hed.def is not HediffDef_SexPart def) return false;
			return def.genitalFamily == GenitalFamily.Anus;
		}

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

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

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

		public static bool is_vagina(Hediff hed)
		{
			if (hed.def is not HediffDef_SexPart def)
			{
				return false;
			}
			return is_vagina_family(def.genitalFamily);
		}

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

			return parts.Any(is_penis);
		}

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

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

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

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

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

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

		public static bool is_penis(Hediff hed)
		{
			if (hed.def is not HediffDef_SexPart def) return false;
			return is_penis_family(def.genitalFamily);
		}

		public static bool is_fertile_penis(Hediff hed)
		{
			if (hed.def is not HediffDef_SexPart def) return false;
			if (!is_penis_family(def.genitalFamily)) return false;

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

		public static bool is_infertile_penis(Hediff hed)
		{
			if (hed.def is not HediffDef_SexPart def) return false;
			if (!is_penis_family(def.genitalFamily)) return false;

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

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

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

				// Matches hemipenis.
				var props = (hed.def as HediffDef_SexPart)?.tags;
				if (!props.NullOrEmpty() && props.Contains("Multiple"))
				{
					return true;
				}

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

			return false;
		}

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

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

		}

		public static bool is_male_ovipositor(Hediff hed)
		{
			if (hed.def is not HediffDef_SexPart def) return false;
			return def.genitalFamily == GenitalFamily.MaleOvipositor;
		}

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

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

		public static bool is_female_ovipositor(Hediff hed)
		{
			if (hed.def is not HediffDef_SexPart def) return false;
			return def.genitalFamily == GenitalFamily.FemaleOvipositor;
		}

		/// <summary>
		/// Can do autofellatio if penis is huge or bigger
		/// </summary>
		/// <param name="pawn"></param>
		/// <returns></returns>
		public static bool can_do_autofelatio(Pawn pawn, List<Hediff> parts = null)
		{
			parts ??= pawn.GetGenitalsList();
			if (parts.NullOrEmpty()) return false;

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

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

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

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

			var ovipositors = parts.Select(part => part.TryGetComp<HediffComp_Ovipositor>()).Where(p => p != null);
			foreach (var ovi in ovipositors)
			{
				//if (hed.minEggAmount)
				if (minEggsProduced < 0)
				{
					minEggsProduced = ovi.Props.eggCount.min;
				}
			}
			return (int)minEggsProduced;
		}

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

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

			var ovipositorComps = parts.Select(part => part.TryGetComp<HediffComp_Ovipositor>()).Where(p => p != null);
			foreach (var ovi in ovipositorComps)
			{
				if (maxEggsProduced < 0)
				{
					maxEggsProduced = ovi.Props.eggCount.max;
				}
			}
			return (int)maxEggsProduced;
		}

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

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

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

			DebugTables.MakeTablesDialog(dataSources, table);

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

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

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

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

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

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

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

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

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

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

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

}


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

namespace rjw
{
	public static 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;
		}

		public static bool IsNaturalSexPart(this HediffDef def)
		{
			return def is HediffDef_SexPart && typeof(Hediff_NaturalSexPart).IsAssignableFrom(def.hediffClass);
		}

		public static bool IsArtificialSexPart(this HediffDef def)
		{
			return def is HediffDef_SexPart && typeof(Hediff_ArtificialSexPart).IsAssignableFrom(def.hediffClass);
		}
	}
}

license: mit


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

using static rjw.PreceptDefOf;

namespace rjw
{
	public static class IdeoHelper
	{
		public static readonly List<PreceptDef> freeLovePrecepts;

		public static readonly List<PreceptDef> nonPolyPrecepts;

		public static readonly List<PreceptDef> prudePrecepts;

		public static bool ClassicMode => !ModsConfig.IdeologyActive || Find.IdeoManager.classicMode;

		static IdeoHelper()
		{
			if (ModsConfig.IdeologyActive)
			{
				freeLovePrecepts = new() { Lovin_Free, Lovin_FreeApproved };
				nonPolyPrecepts = new() {
					Lovin_SpouseOnly_Mild,
					Lovin_SpouseOnly_Moderate,
					Lovin_SpouseOnly_Strict
				};
				prudePrecepts = new() { Lovin_Prohibited, Lovin_Horrible };

			}
			else
			{
				freeLovePrecepts = new() { Lovin_Free };
			}
		}

		/// <summary>
		/// Check whether pawn has an ideo with a free love precept.
		/// </summary>
		/// <param name="pawn">The pawn whose ideo is being checked</param>
		/// <param name="allowClassic">
		/// 	Whether the hidden 'Lovin: Free' precept included in classic and non-DLC ideos should be respected.<br />
		/// 	In other words, whether we are strong enough to accept canon or cling fearfully to our silly 21st century mores.
		/// </param>
		/// <returns>True if etc etc, false otherwise</returns>
		public static bool IsIdeologicallyPoly(this Pawn pawn, bool allowClassic = true)
		{
			if (pawn.Ideo == null)
			{
				return false;
			}
			if (ClassicMode)
			{
				return allowClassic;
			}

			var lovinPrecept = GetLovinPreceptDef(pawn.Ideo);
			if (lovinPrecept == null)
			{
				return false;
			}
			return freeLovePrecepts.Contains(lovinPrecept);
		}

		public static bool IsIdeologicallyPrudish(this Pawn pawn)
		{
			if (pawn.Ideo == null || ClassicMode)
			{
				return false;
			}

			var lovinPrecept = GetLovinPreceptDef(pawn.Ideo);
			if (lovinPrecept == null)
			{
				return false;
			}

			return prudePrecepts.Contains(lovinPrecept);
		}

		private static PreceptDef GetLovinPreceptDef(Ideo ideo)
		{
			return ideo.PreceptsListForReading.FirstOrDefault(p => p.def.issue == IssueDefOf.Lovin)?.def;
		}
	}
}


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

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

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

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

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

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

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

			part = Genital_Helper.generic_breasts;

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

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

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

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

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

			part = Genital_Helper.generic_anus;

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

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


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

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

			return comp;
		}

	}
}


code:
using Verse;
using Verse.AI;

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

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

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


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

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


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

namespace rjw
{
	class RacePartDef_Helper
	{
		/// <summary>
		/// Returns true if a partAdders was chosen (even if that part is "no part").
		/// </summary>
		[SyncMethod]
		public static bool TryRacePartDef_partAdders(Pawn pawn)
		{
			//RaceGroupDef_Helper.TryGetRaceGroupDef(pawn.kindDef, out var raceGroupDef);
			if (!RaceGroupDef_Helper.TryGetRaceGroupDef(pawn.kindDef, out var raceGroupDef))
				return false;
			if (raceGroupDef.partAdders.NullOrEmpty())
				return false;

			List<BodyPartRecord> pawnParts = pawn.RaceProps?.body?.AllParts;
			pawnParts = pawnParts.FindAll(record => !pawn.health.hediffSet.PartIsMissing(record));

			foreach (var Adder in raceGroupDef.partAdders)
			{
				if (Adder.chance <= Rand.Value)
					continue;

				var racePartDef = DefDatabase<RacePartDef>.GetNamedSilentFail(Adder.rjwPart);

				if (racePartDef == null)
				{
					//No part found
					continue;
				}

				if (!racePartDef.TryGetHediffDef(out var rjwPartDef))
				{
					//No part found
					continue;
				}

				pawnParts = pawnParts.FindAll(record => Adder.bodyParts.Contains(record.def.defName));

				foreach (BodyPartRecord record in pawnParts)
				{
					var hediff = RacePartDef_Helper.MakePart(rjwPartDef, pawn, record, racePartDef);
					pawn.health.AddHediff(hediff, record);
				}
			}
			return true;
		}

		/// <summary>
		/// Returns true if a race part was chosen (even if that part is "no part").
		/// </summary>
		[SyncMethod]
		public static bool TryChooseRacePartDef(RaceGroupDef raceGroupDef, SexPartType sexPartType, out RacePartDef racePartDef)
		{
			var partNames = raceGroupDef.GetRacePartDefNames(sexPartType);
			if (partNames == null)
			{
				// Missing list, so nothing was chosen.
				racePartDef = null;
				return false;
			}
			else if (!partNames.Any())
			{
				// Empty list, so "no part" was chosen.
				racePartDef = RacePartDef.None;
				return true;
			}

			var chances = raceGroupDef.GetChances(sexPartType);
			var hasChances = chances != null && chances.Count() > 0;

			if (hasChances && chances.Count() != partNames.Count())
			{
				// No need for this to be runtime, should probably be a config error in RaceGroupDef.
				ModLog.Error($"RaceGroupDef named {raceGroupDef.defName} has {partNames.Count()} parts but {chances.Count()} chances for {sexPartType}.");
				racePartDef = null;
				return false;
			}

			string partName;
			if (hasChances)
			{
				var indexes = partNames.Select((x, i) => i);
				partName = partNames[indexes.RandomElementByWeight(i => chances[i])];
			}
			else
			{
				partName = partNames.RandomElement();
			}

			racePartDef = DefDatabase<RacePartDef>.GetNamedSilentFail(partName);
			if (racePartDef == null)
			{
				ModLog.Error($"Could not find a RacePartDef named {partName} referenced by RaceGroupDef named {raceGroupDef.defName}.");
				return false;
			}
			else
			{
				return true;
			}
		}

		[SyncMethod]
		public static Hediff MakePart(HediffDef hediffDef, Pawn pawn, BodyPartRecord bodyPartRecord, RacePartDef racePartDef)
		{
			var hediff = HediffMaker.MakeHediff(hediffDef, pawn, bodyPartRecord);
			var compHediff = hediff.TryGetComp<HediffComp_SexPart>();
			if (compHediff != null)
			{
				//Log.Message("RacePartDef_Helper::PartMaker init comps " + hediff.Label);
				compHediff.Init(pawn);
				if (racePartDef.FluidDef != null)
				{
					compHediff.Fluid = racePartDef.FluidDef;
				}
				if (racePartDef.severityCurve != null && racePartDef.severityCurve.Any())
				{
					// If the part was initialized with min size then leave it,
					//  otherwise reroll with our curve. (For male nipples)
					if(compHediff.GetSeverity() != 0.01f)
					{
						compHediff.SetSeverity(racePartDef.severityCurve.Evaluate(Rand.Value));
					}
				}
				if (racePartDef.fluidModifier.HasValue)
				{
					compHediff.partFluidMultiplier *= racePartDef.fluidModifier.Value;
				}
				compHediff.UpdateSeverity();
			}

			return hediff;
		}

		/// <summary>
		/// Generates and logs RacePartDef xml shells for each RJW hediff so they can be manually saved as a def file and referenced by RaceGroupDefs.
		/// In theory this could be done automatically at run time. But then we also might want to add real configuration
		/// to the shells so for now just checking in the shells.
		/// </summary>
		public static void GeneratePartShells()
		{
			var defs = DefDatabase<HediffDef_SexPart>.AllDefs.OrderBy(def => def.defName);
			var template = "\t<rjw.RacePartDef>\n\t\t<defName>{0}</defName>\n\t\t<hediffName>{0}</hediffName>\n\t</rjw.RacePartDef>";
			var strings = defs.Select(def => string.Format(template, def.defName));
			ModLog.Message(" RacePartDef shells:\n" + string.Join("\n", strings));
		}
	}
}


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

namespace rjw
{
	public class SexPartAdder
	{
		/// <summary>
		/// return true if going to set penis,
		/// return false for vagina
		/// </summary>
		public static bool IsAddingPenis(Pawn pawn, Gender gender)
		{
			return (pawn.gender == Gender.Male) ? (gender != Gender.Female) : (gender == Gender.Male);
		}

		/// <summary>
		/// generate part hediff
		/// </summary>
		public static Hediff MakePart(HediffDef def, Pawn pawn, BodyPartRecord bpr)
		{
			//Log.Message("SexPartAdder::PartMaker ( " + xxx.get_pawnname(pawn) + " ) " + def.defName);
			Hediff hd = HediffMaker.MakeHediff(def, pawn, bpr);
			//Log.Message("SexPartAdder::PartMaker ( " + xxx.get_pawnname(pawn) + " ) " + hd.def.defName);
			HediffComp_SexPart compHediff = hd.TryGetComp<HediffComp_SexPart>();
			if (compHediff != null)
			{
				//Log.Message("SexPartAdder::PartMaker init comps " + hd.Label);
				compHediff.Init(pawn);
				compHediff.UpdateSeverity();
			}
			return hd;
		}

		/// <summary>
		/// operation - move part data from thing to hediff
		/// </summary>
		public static Hediff recipePartAdder(RecipeDef recipe, Pawn pawn, BodyPartRecord part, List<Thing> ingredients)
		{
			Hediff hd = HediffMaker.MakeHediff(recipe.addsHediff, pawn, part);
			Thing thing = ingredients.Find(x => x.def.defName == recipe.addsHediff.defName);

			CompThingBodyPart thingComp = thing.TryGetComp<rjw.CompThingBodyPart>();
			HediffComp_SexPart hediffComp = hd.TryGetComp<rjw.HediffComp_SexPart>();

			if (hediffComp != null && thingComp != null)
			{
				if (thingComp.HasSize())
				{
					if (thingComp.fluid != null && thingComp.fluid != hediffComp.Def.fluid)
					{
						hediffComp.Fluid = thingComp.fluid;
					}
					hediffComp.partFluidMultiplier = thingComp.partFluidMultiplier ?? 1.0f;
					hediffComp.originalOwnerSize = thingComp.originalOwnerSize;
					hediffComp.originalOwnerRace = thingComp.originalOwnerRace;
					hediffComp.previousOwner = thingComp.previousOwner;
					hediffComp.isTransplant = true;
					hediffComp.SetSeverity(thingComp.CalculateSize(hediffComp.GetBodySize()));
				}

				else  //not initialised things, drop pods, trader?  //TODO: figure out how to populate rjw parts at gen
					hd = MakePart(hd.def, pawn, part);
			}

			return hd;
		}

		/// <summary>
		/// operation - move part data from hediff to thing
		/// </summary>
		public static Thing recipePartRemover(Hediff hd)
		{
			Thing thing = null;

			if (hd.def.spawnThingOnRemoved != null)
			{
				thing = ThingMaker.MakeThing(hd.def.spawnThingOnRemoved);

				CompThingBodyPart thingComp = thing.TryGetComp<rjw.CompThingBodyPart>();
				HediffComp_SexPart hediffComp = hd.TryGetComp<rjw.HediffComp_SexPart>();

				if (thingComp != null && hediffComp != null)
				{
					thingComp.InitFromComp(hediffComp);
				}
			}
			return thing;
		}

		[SyncMethod]
		public static void add_genitals(Pawn pawn, Pawn parent = null, Gender gender = Gender.None)
		{
			//--Log.Message("Genital_Helper::add_genitals( " + xxx.get_pawnname(pawn) + " ) called");
			BodyPartRecord partBPR = Genital_Helper.get_genitalsBPR(pawn);
			var parts = pawn.GetGenitalsList();

			//--Log.Message("Genital_Helper::add_genitals( " + xxx.get_pawnname(pawn) + " ) - checking genitals");
			if (partBPR == null)
			{
				//--ModLog.Message(" add_genitals( " + xxx.get_pawnname(pawn) + " ) doesn't have a genitals");
				return;
			}
			else if (pawn.health.hediffSet.PartIsMissing(partBPR))
			{
				//--ModLog.Message(" add_genitals( " + xxx.get_pawnname(pawn) + " ) had a genital but was removed.");
				return;
			}
			if (Genital_Helper.has_genitals(pawn, parts) && gender == Gender.None)//allow to add gender specific genitals(futa)
			{
				//--ModLog.Message(" add_genitals( " + xxx.get_pawnname(pawn) + " ) already has genitals");
				return;
			}

			HediffDef part;

			// maybe add some check based on bodysize of pawn for genitals size limit
			//Log.Message("Genital_Helper::add_genitals( " + pawn.RaceProps.baseBodySize + " ) - 1");
			//Log.Message("Genital_Helper::add_genitals( " + pawn.kindDef.race.size. + " ) - 2");

			part = (IsAddingPenis(pawn, gender)) ? Genital_Helper.generic_penis : Genital_Helper.generic_vagina;

			if (Genital_Helper.has_vagina(pawn, parts) && part == Genital_Helper.generic_vagina)
			{
				//--ModLog.Message(" add_genitals( " + xxx.get_pawnname(pawn) + " ) already has vagina");
				return;
			}
			if (Genital_Helper.has_male_bits(pawn, parts) && part == Genital_Helper.generic_penis)
			{
				//--ModLog.Message(" add_genitals( " + xxx.get_pawnname(pawn) + " ) already has penis");
				return;
			}

			//override race genitals
			if (part == Genital_Helper.generic_vagina && pawn.TryAddRacePart(SexPartType.FemaleGenital))
			{
				return;
			}
			if (part == Genital_Helper.generic_penis && pawn.TryAddRacePart(SexPartType.MaleGenital))
			{
				return;
			}
			LegacySexPartAdder.AddGenitals(pawn, parent, gender, partBPR, part);
		}

		public static void add_breasts(Pawn pawn, Pawn parent = null, Gender gender = Gender.None)
		{
			//--ModLog.Message(" add_breasts( " + xxx.get_pawnname(pawn) + " ) called");
			BodyPartRecord partBPR = Genital_Helper.get_breastsBPR(pawn);

			if (partBPR == null)
			{
				//--ModLog.Message(" add_breasts( " + xxx.get_pawnname(pawn) + " ) - pawn doesn't have a breasts");
				return;
			}
			else if (pawn.health.hediffSet.PartIsMissing(partBPR))
			{
				//--ModLog.Message(" add_breasts( " + xxx.get_pawnname(pawn) + " ) had breasts but were removed.");
				return;
			}
			if (pawn.GetBreastList().Count > 0)
			//if (Genital_Helper.has_breasts(pawn))
			{
				//--ModLog.Message(" add_breasts( " + xxx.get_pawnname(pawn) + " ) - pawn already has breasts");
				return;
			}

			//TODO: figure out how to add (flat) breasts to males
			//Check for (flat) breasts to males done in RacepartDef_Helper.cs when severity curve checked
			var racePartType = (pawn.gender == Gender.Female || gender == Gender.Female)
				? SexPartType.FemaleBreast
				: SexPartType.MaleBreast;
			if (pawn.TryAddRacePart(racePartType))
			{
				return;
			}

			LegacySexPartAdder.AddBreasts(pawn, partBPR, parent);
		}

		public static void add_anus(Pawn pawn, Pawn parent = null)
		{
			BodyPartRecord partBPR = Genital_Helper.get_anusBPR(pawn);

			if (partBPR == null)
			{
				//--ModLog.Message(" add_anus( " + xxx.get_pawnname(pawn) + " ) doesn't have an anus");
				return;
			}
			else if (pawn.health.hediffSet.PartIsMissing(partBPR))
			{
				//--ModLog.Message(" add_anus( " + xxx.get_pawnname(pawn) + " ) had an anus but was removed.");
				return;
			}
			if (pawn.GetAnusList().Count > 0)
			{
				//--ModLog.Message(" add_anus( " + xxx.get_pawnname(pawn) + " ) already has an anus");
				return;
			}

			if (pawn.TryAddRacePart(SexPartType.Anus))
			{
				return;
			}

			LegacySexPartAdder.AddAnus(pawn, partBPR, parent);
		}
	}
}