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

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

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

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

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

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

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

			part = Genital_Helper.generic_breasts;

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

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

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

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

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

			part = Genital_Helper.generic_anus;

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

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


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

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

			return comp;
		}

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

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

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

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

		//	eggThingDef.comps.Add(compPropHatcher);

		//	FertileEggPawnKinds.Add(pawnKindDef, eggThingDef);

		//	return eggThingDef;
		//}

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

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

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

		//	UnfertileEggPawnKinds.Add(pawnKindDef, eggThingDef);

		//	return eggThingDef;
		//}

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

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

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

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

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

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

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

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

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

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

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

		//	return copiedEgg;
		//}

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

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

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

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

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

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

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

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

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

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

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

		//	return copiedEgg;
		//}
	}
}

repo_name: aelina/rjw

license: mit


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

			if (raceGroupDef.HasUdder && sexPartType == SexPartType.FemaleBreast && !pawn.health.hediffSet.HasHediff(Genital_Helper.udder_breasts))
			{
				var bp = Genital_Helper.get_uddersBPR(pawn);
				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;
		}
	}
}


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<CompHediffBodyPart>();
			if (compHediff != null)
			{
				compHediff.initComp(pawn);
				if (racePartDef.fluidType != null)
				{
					compHediff.FluidType = racePartDef.fluidType;
				}
				if (racePartDef.fluidModifier != null)
				{
					compHediff.FluidModifier = racePartDef.fluidModifier.Value;
				}
				if (racePartDef.severityCurve != null && racePartDef.severityCurve.Any())
				{
					// Size math is in flux right now, but the idea is that for an individual pawn
					// the size chosen in the RaceGroupDef is the size reported in the UI regardless of pawn's BodySize.

					// Check for males to only get nipple sized (flat) breasts
					// Trap calculation is done in initComp method above, by this point only pawns with SizeBase of 0.01
					// should be those that failed the Trap calc, are male, or are very unlucky on their breast size roll
					// Check for is male at the end because its possible (though very unlikely) for a female to roll a 0.01, and
					// the severity curve for that race's breasts may actually have it as impossible for a female to be that small
					if (compHediff.SizeBase == 0.01f && hediff.def.defName.ToLower().Contains("breast") && pawn.gender == Gender.Male)
					{
						compHediff.updatesize();
					}
					else
					{
						var severity = racePartDef.severityCurve.Evaluate(Rand.Value);
						compHediff.SizeBase = severity;
						compHediff.EffSize = severity;
						compHediff.updatesize(severity);
					}
				}
				else
				{
					compHediff.updatesize();
				}
			}

			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_PartBase>.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;

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);
			CompHediffBodyPart compHediff = hd.TryGetComp<CompHediffBodyPart>();
			if (compHediff != null)
			{
				//Log.Message("SexPartAdder::PartMaker init comps");
				compHediff.initComp(pawn);
				compHediff.updatesize();
			}
			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 CompThing = thing.TryGetComp<rjw.CompThingBodyPart>();
			CompHediffBodyPart CompHediff = hd.TryGetComp<rjw.CompHediffBodyPart>();

			if (CompHediff != null && CompThing != null)
				if (CompThing.SizeBase > 0)
				{
					CompHediff.FluidType = CompThing.FluidType;
					CompHediff.FluidAmmount = CompThing.FluidAmmount;
					CompHediff.FluidModifier = CompThing.FluidModifier;
					CompHediff.SizeBase = CompThing.SizeBase;
					CompHediff.SizeOwner = CompThing.SizeOwner;
					CompHediff.RaceOwner = CompThing.RaceOwner;
					CompHediff.PreviousOwner = CompThing.PreviousOwner;
					CompHediff.Eggs = CompThing.Eggs;
					CompHediff.Transplanted = true;
					CompHediff.updatesize();
				}
				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 = ThingMaker.MakeThing(hd.def.spawnThingOnRemoved);

			CompThingBodyPart CompThing = thing.TryGetComp<rjw.CompThingBodyPart>();
			CompHediffBodyPart CompHediff = hd.TryGetComp<rjw.CompHediffBodyPart>();

			if (CompThing != null && CompHediff != null)
			{
					CompThing.FluidType = CompHediff.FluidType;
					CompThing.FluidAmmount = CompHediff.FluidAmmount;
					CompThing.FluidModifier = CompHediff.FluidModifier;
					CompThing.Size = CompHediff.Size;
					CompThing.SizeBase = CompHediff.SizeBase;
					CompThing.SizeOwner = CompHediff.SizeOwner;
					CompThing.RaceOwner = CompHediff.RaceOwner;
					CompThing.PreviousOwner = xxx.get_pawnname(CompHediff.Pawn);
					CompThing.Eggs = CompHediff.Eggs;
			}
			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_penis_fertile(pawn, parts) || Genital_Helper.has_penis_infertile(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);
		}
	}
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using UnityEngine;
using Verse;
using Verse.AI;
using Verse.Sound;
using HarmonyLib;
using Multiplayer.API;
using rjw.Modules.Interactions.Contexts;
using rjw.Modules.Interactions.Implementation;
using rjw.Modules.Interactions;
using rjw.Modules.Interactions.Objects;
using rjw.Modules.Interactions.Enums;

namespace rjw
{
	public class SexUtility
	{
		private const float base_sat_per_fuck = 0.40f;
		private const float base_sat_per_quirk = 0.20f;

		public static readonly InteractionDef AnimalSexChat = DefDatabase<InteractionDef>.GetNamed("AnimalSexChat");

		private static readonly ThingDef cum = ThingDef.Named("FilthCum");
		private static readonly ThingDef girlcum = ThingDef.Named("FilthGirlCum");

		public static readonly List<InteractionDef> SexInterractions = DefDatabase<InteractionDef>.AllDefsListForReading.Where(x => x.HasModExtension<InteractionExtension>()).ToList();

		// Alert checker that is called from several jobs. Checks the pawn relation, and whether it should sound alert.
		// notification in top left corner
		// rape attempt
		public static void RapeTargetAlert(Pawn rapist, Pawn target)
		{
			if (target.IsDesignatedComfort() && rapist.jobs.curDriver.GetType() == typeof(JobDriver_RapeComfortPawn))
				if (!RJWPreferenceSettings.ShowForCP)
					return;
			if (target.IsDesignatedComfort() && rapist.jobs.curDriver.GetType() == typeof(JobDriver_Breeding))
				if (target.IsDesignatedBreeding())
					if (!RJWPreferenceSettings.ShowForBreeding)
						return;

			bool silent = false;
			PawnRelationDef relation = rapist.GetMostImportantRelation(target);
			string rapeverb = "rape";

			if (xxx.is_mechanoid(rapist)) rapeverb = "assault";
			else if (xxx.is_animal(rapist) || xxx.is_animal(target)) rapeverb = "breed";

			// TODO: Need to write a cherker method for family relations. Would be useful for other things than just this, such as incest settings/quirk.

			string message = (xxx.get_pawnname(rapist) + " is trying to " + rapeverb + " " + xxx.get_pawnname(target));
			message += relation == null ? "." : (", " + rapist.Possessive() + " " + relation.GetGenderSpecificLabel(target) + ".");

			switch (RJWPreferenceSettings.rape_attempt_alert)
			{
				case RJWPreferenceSettings.RapeAlert.Enabled:
					break;
				case RJWPreferenceSettings.RapeAlert.Humanlikes:
					if (!xxx.is_human(target))
						return;
					break;
				case RJWPreferenceSettings.RapeAlert.Colonists:
					if (!target.IsColonist)
						return;
					break;
				case RJWPreferenceSettings.RapeAlert.Silent:
					silent = true;
					break;
				default:
					return;
			}

			if (!silent)
			{
				Messages.Message(message, rapist, MessageTypeDefOf.NegativeEvent);
			}
			else
			{
				Messages.Message(message, rapist, MessageTypeDefOf.SilentInput);
			}
		}

		// Alert checker that is called from several jobs.
		// notification in top left corner
		// rape started
		public static void BeeingRapedAlert(Pawn rapist, Pawn target)
		{
			if (target.IsDesignatedComfort() && rapist.jobs.curDriver.GetType() == typeof(JobDriver_RapeComfortPawn))
				if (!RJWPreferenceSettings.ShowForCP)
					return;
			if (target.IsDesignatedComfort() && rapist.jobs.curDriver.GetType() == typeof(JobDriver_Breeding))
				if (target.IsDesignatedBreeding())
					if (!RJWPreferenceSettings.ShowForBreeding)
						return;

			bool silent = false;

			switch (RJWPreferenceSettings.rape_alert)
			{
				case RJWPreferenceSettings.RapeAlert.Enabled:
					break;
				case RJWPreferenceSettings.RapeAlert.Humanlikes:
					if (!xxx.is_human(target))
						return;
					break;
				case RJWPreferenceSettings.RapeAlert.Colonists:
					if (!target.IsColonist)
						return;
					break;
				case RJWPreferenceSettings.RapeAlert.Silent:
					silent = true;
					break;
				default:
					return;
			}

			if (!silent)
			{
				Messages.Message(xxx.get_pawnname(target) + " is getting raped.", target, MessageTypeDefOf.NegativeEvent);
			}
			else
			{
				Messages.Message(xxx.get_pawnname(target) + " is getting raped.", target, MessageTypeDefOf.SilentInput);
			}
		}

		// Quick method that return a body part by name. Used for checking if a pawn has a specific body part, etc.
		public static BodyPartRecord GetPawnBodyPart(Pawn pawn, string bodyPart)
		{
			return pawn.RaceProps.body.AllParts.Find(x => x.def == DefDatabase<BodyPartDef>.GetNamed(bodyPart));
		}

		public static void CumFilthGenerator(Pawn pawn)
		{
			if (pawn == null) return;
			if (pawn.Dead) return;
			if (xxx.is_slime(pawn)) return;
			if (!RJWSettings.cum_filth) return;

			// Larger creatures, larger messes.
			float pawn_cum = Math.Min(80 / ScaleToHumanAge(pawn), 2.0f) * pawn.BodySize;

			// Increased output if the pawn has the Messy quirk.
			if (xxx.has_quirk(pawn, "Messy"))
				pawn_cum *= 2.0f;

			var parts = pawn.GetGenitalsList();

			if (Genital_Helper.has_vagina(pawn, parts))
				FilthMaker.TryMakeFilth(pawn.PositionHeld, pawn.MapHeld, girlcum, pawn.LabelIndefinite(), (int)Math.Max(pawn_cum/2, 1.0f));

			if (Genital_Helper.has_penis_fertile(pawn, parts))
				FilthMaker.TryMakeFilth(pawn.PositionHeld, pawn.MapHeld, cum, pawn.LabelIndefinite(), (int)Math.Max(pawn_cum, 1.0f));
		}

		// The pawn may or may not clean up the mess after fapping.
		[SyncMethod]
		public static bool ConsiderCleaning(Pawn fapper)
		{
			if (!RJWSettings.cum_filth) return false;
			if (!xxx.has_traits(fapper) || fapper.story == null) return false;
			if (fapper.WorkTagIsDisabled(WorkTags.Cleaning)) return false;

			float do_cleaning = 0.5f; // 50%

			if (!fapper.PositionHeld.Roofed(fapper.Map))
				do_cleaning -= 0.25f; // Less likely to clean if outdoors.

			if (xxx.CTIsActive && fapper.story.traits.HasTrait(xxx.RCT_NeatFreak))
				do_cleaning += 1.00f;

			if (xxx.has_quirk(fapper, "Messy"))
				do_cleaning -= 0.75f;

			switch (fapper.needs?.rest?.CurCategory)
			{
				case RestCategory.Exhausted:
					do_cleaning -= 0.5f;
					break;
				case RestCategory.VeryTired:
					do_cleaning -= 0.3f;
					break;
				case RestCategory.Tired:
					do_cleaning -= 0.1f;
					break;
				case RestCategory.Rested:
					do_cleaning += 0.3f;
					break;
			}

			if (fapper.story.traits.DegreeOfTrait(TraitDefOf.NaturalMood) == -2) // Depressive
				do_cleaning -= 0.3f;
			if (fapper.story.traits.DegreeOfTrait(TraitDefOf.Industriousness) == 2) // Industrious
				do_cleaning += 1.0f;
			else if (fapper.story.traits.DegreeOfTrait(TraitDefOf.Industriousness) == 1) // Hard worker
				do_cleaning += 0.5f;
			else if (fapper.story.traits.DegreeOfTrait(TraitDefOf.Industriousness) == -1) // Lazy
				do_cleaning -= 0.5f;
			else if (fapper.story.traits.DegreeOfTrait(TraitDefOf.Industriousness) == -2) // Slothful
				do_cleaning -= 1.0f;

			if (xxx.is_ascetic(fapper))
				do_cleaning += 0.2f;

			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			return Rand.Chance(do_cleaning);
		}

		/// Handles after-sex trait and thought gain, and fluid creation. Initiator of the act (whore, rapist, female zoophile, etc) should be first.
		[SyncMethod]
		public static void Aftersex(SexProps props)
		{
			if (props.sexType == xxx.rjwSextype.Masturbation)
			{
				AfterMasturbation(props);
				return;
			}

			bool bothInMap = false;

			if (!props.partner.Dead)
				bothInMap = props.pawn.Map != null && props.partner.Map != null; //Added by Hoge. false when called this function for despawned pawn: using for background rape like a kidnappee

			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			if (bothInMap)
			{
				//Catch-all timer increase, for ensuring that pawns don't get stuck repeating jobs.
				if (!props.isCoreLovin)
				{
					props.pawn.rotationTracker.Face(props.partner.DrawPos);
					props.pawn.rotationTracker.FaceCell(props.partner.Position);
				}
				if (!props.partner.Dead)
				{
					if (!props.isCoreLovin)
					{
						props.partner.rotationTracker.Face(props.pawn.DrawPos);
						props.partner.rotationTracker.FaceCell(props.pawn.Position);
					}
					if (RJWSettings.sounds_enabled)
					{
						if (props.isRape)
						{
							if (Rand.Value > 0.30f)
								LifeStageUtility.PlayNearestLifestageSound(props.partner, (ls) => ls.soundAngry, 1.2f);
							else
								LifeStageUtility.PlayNearestLifestageSound(props.partner, (ls) => ls.soundCall, 1.2f);

							props.pawn.Drawer.Notify_MeleeAttackOn(props.partner);
							props.partner.stances.StaggerFor(Rand.Range(10, 300));
						}
						else
							LifeStageUtility.PlayNearestLifestageSound(props.partner, (ls) => ls.soundCall);
					}
					if (props.sexType == xxx.rjwSextype.Vaginal || props.sexType == xxx.rjwSextype.DoublePenetration)
						if (xxx.is_Virgin(props.partner))
						{
							//TODO: bind virginity to parts of pawn
							/*
							string thingdef_penis_name = Genital_Helper.get_penis_all(pawn)?.def.defName ?? "";
							ThingDef thingdef_penis = null;

							Log.Message("SexUtility::thingdef_penis_name " + thingdef_penis_name);
							Log.Message("SexUtility::thingdef_penis 1 " + thingdef_penis);

							if (thingdef_penis_name != "")
								thingdef_penis = (from x in DefDatabase<ThingDef>.AllDefs where x.defName == thingdef_penis_name select x).RandomElement();
							Log.Message("SexUtility::thingdef_penis 2 " + thingdef_penis);

							partner.TakeDamage(new DamageInfo(DamageDefOf.Stab, 1, 999, -1.0f, null, xxx.genitals, thingdef_penis));
							*/
						}
				}

				if (RJWSettings.sounds_enabled && props.isCoreLovin)
					SoundDef.Named("Cum").PlayOneShot(!props.partner.Dead
						? new TargetInfo(props.partner.Position, props.pawn.Map)
						: new TargetInfo(props.pawn.Position, props.pawn.Map));

				if (props.isRape)
				{
					if (Rand.Value > 0.30f)
						LifeStageUtility.PlayNearestLifestageSound(props.pawn, (ls) => ls.soundAngry, 1.2f);
					else
						LifeStageUtility.PlayNearestLifestageSound(props.pawn, (ls) => ls.soundCall, 1.2f);
				}
				else
					LifeStageUtility.PlayNearestLifestageSound(props.pawn, (ls) => ls.soundCall);
			}

			if (props.usedCondom)
			{
				if (CondomUtility.UsedCondom != null)
					GenSpawn.Spawn(CondomUtility.UsedCondom, props.pawn.Position, props.pawn.Map);
				CondomUtility.useCondom(props.pawn);
				CondomUtility.useCondom(props.partner);
			}
			else
			{
				if (props.isCoreLovin) // non core handled by jobdriver
				{
					//apply cum to floor:
					CumFilthGenerator(props.pawn);
					CumFilthGenerator(props.partner);

					PregnancyHelper.impregnate(props);
					TransferNutrition(props);
				}
			}

			if (props.isRape && !props.partner.Dead)
				AfterSexUtility.processBrokenPawn(props.partner);

			//Satisfy(pawn, partner, sextype, rape);

			//TODO: below is fucked up, unfuck it someday
			AfterSexUtility.UpdateRecords(props);
			CheckTraitGain(props);
		}

		// Solo acts.
		public static void AfterMasturbation(SexProps props)
		{
			IncreaseTicksToNextLovin(props.pawn);

			//apply cum to floor:
			CumFilthGenerator(props.pawn);

			AfterSexUtility.UpdateRecords(props);

			// No traits from solo. Enable if some are edded. (Voyerism?)
			//check_trait_gain(pawn);
		}

		// Scales alien lifespan to human age.
		// Some aliens have broken lifespans, that can be manually corrected here.
		public static int ScaleToHumanAge(Pawn pawn, int humanLifespan = 80)
		{
			float pawnAge = pawn.ageTracker.AgeBiologicalYearsFloat;
			float pawnLifespan = pawn.RaceProps.lifeExpectancy;

			if (pawn.def.defName == "Human") return (int)pawnAge; // Human, no need to scale anything.

			// Xen races, all broken and need a fix.
			if (pawn.def.defName.ContainsAny("Alien_Sergal", "Alien_SergalNME", "Alien_Xenn", "Alien_Racc", "Alien_Ferrex", "Alien_Wolvx", "Alien_Frijjid", "Alien_Fennex") && pawnLifespan >= 2000f)
			{
				pawnAge = Math.Min(pawnAge, 80f); // Clamp to 80.
				pawnLifespan = 80f;
			}
			if (pawn.def.defName.ContainsAny("Alien_Gnoll", "Alien_StripedGnoll") && pawnLifespan >= 2000f)
			{
				pawnAge = Math.Min(pawnAge, 60f); // Clamp to 60.
				pawnLifespan = 60f; // Mature faster than humans.
			}

			// Immortal races that mature at similar rate to humans.
			if (pawn.def.defName.ContainsAny("LF_Dragonia", "LotRE_ElfStandardRace", "Alien_Crystalloid", "Alien_CrystalValkyrie"))
			{
				pawnAge = Math.Min(pawnAge, 40f); // Clamp to 40 - never grow 'old'.
				pawnLifespan = 80f;
			}

			float age_scaling = humanLifespan / pawnLifespan;
			float scaled_age = pawnAge * age_scaling;

			if (scaled_age < 1)
				scaled_age = 1;

			return (int)scaled_age;
		}

		// Used in complex impregnation calculation. Pawns/animals with similar parts have better compatibility.
		public static float BodySimilarity(Pawn pawn, Pawn partner)
		{
			float size_adjustment = Mathf.Lerp(0.3f, 1.05f, 1.0f - Math.Abs(pawn.BodySize - partner.BodySize));

			//ModLog.Message(" Size adjustment: " + size_adjustment);

			List<BodyPartDef> pawn_partlist = new List<BodyPartDef> { };
			List<BodyPartDef> pawn_mismatched = new List<BodyPartDef> { };
			List<BodyPartDef> partner_mismatched = new List<BodyPartDef> { };

			//ModLog.Message("Checking compatibility for " + xxx.get_pawnname(pawn) + " and " + xxx.get_pawnname(partner));
			bool pawnHasHands = pawn.health.hediffSet.GetNotMissingParts().Any(part => part.IsInGroup(BodyPartGroupDefOf.RightHand) || part.IsInGroup(BodyPartGroupDefOf.LeftHand));

			foreach (BodyPartRecord part in pawn.RaceProps.body.AllParts)
			{
				pawn_partlist.Add(part.def);
			}
			float pawn_count = pawn_partlist.Count();

			foreach (BodyPartRecord part in partner.RaceProps.body.AllParts)
			{
				partner_mismatched.Add(part.def);
			}
			float partner_count = partner_mismatched.Count();

			foreach (BodyPartDef part in pawn_partlist)
			{
				if (partner_mismatched.Contains(part))
				{
					pawn_mismatched.Add(part);
					partner_mismatched.Remove(part);
				}
			}

			float pawn_mismatch = pawn_mismatched.Count() / pawn_count;
			float partner_mismatch = partner_mismatched.Count() / partner_count;

			//ModLog.Message("Body type similarity for " + xxx.get_pawnname(pawn) + " and " + xxx.get_pawnname(partner) + ": " + Math.Round(((pawn_mismatch + partner_mismatch) * 50) * size_adjustment, 1) + "%");

			return ((pawn_mismatch + partner_mismatch) / 2) * size_adjustment;
		}

		public static void SatisfyPersonal(SexProps props, float satisfaction = 0.4f)
		{
			Pawn pawn = props.pawn;
			Pawn partner = props.partner;
			//--Log.Message("xxx::satisfy( " + pawn_name + ", " + partner_name + ", " + violent + "," + isCoreLovin + " ) - modifying partner satisfaction");
			var sex_need = pawn?.needs?.TryGetNeed<Need_Sex>();
			if (sex_need == null) return;

			float Trait_Satisfaction = 1f;
			float Quirk_Satisfaction = 1f;
			float Stat_Satisfaction = 0f;
			float Circumstances_Satisfaction = 0f; // violence/broken

			float joysatisfaction = satisfaction;

			// Bonus satisfaction from traits
			if (pawn != null && partner != null)
			{
				if (xxx.is_animal(partner) && xxx.is_zoophile(pawn))
				{
					Trait_Satisfaction += 0.5f;
				}
				if (partner.Dead && xxx.is_necrophiliac(pawn))
				{
					Trait_Satisfaction += 0.5f;
				}
			}

			// Calculate bonus satisfaction from quirks
			var quirkCount = Quirk.CountSatisfiedQuirks(props);
			Quirk_Satisfaction += quirkCount * base_sat_per_quirk;

			// Apply sex satisfaction stat (min 0.1 default 1) as a modifier to total satisfaction
			Stat_Satisfaction += Math.Max(xxx.get_sex_satisfaction(pawn), 0.1f);

			// Apply extra multiplier for special/violence circumstances
			Circumstances_Satisfaction += get_satisfaction_circumstance_multiplier(props);

			satisfaction *= Trait_Satisfaction * Quirk_Satisfaction * Stat_Satisfaction;

			joysatisfaction = satisfaction;
			satisfaction *= Circumstances_Satisfaction;
			if (!RJWSettings.Disable_RecreationDrain)
				joysatisfaction *= (Circumstances_Satisfaction - 1);
			else
				joysatisfaction *=  Circumstances_Satisfaction;

			//Log.Message("SatisfyPersonal( " + pawn + ", " + satisfaction + " ) - setting pawn sexneed");

			sex_need.CurLevel += satisfaction;

			if (quirkCount > 0)
			{
				Quirk.AddThought(pawn);
			}

			var joy_need = pawn.needs.TryGetNeed<Need_Joy>();
			if (joy_need == null) return;

			joysatisfaction *= joysatisfaction > 0 ? 0.5f: 1f;              // convert half of positive satisfaction to joy
			//Log.Message("SatisfyPersonal( " + pawn + ", " + joysatisfaction + " ) - setting pawn joyneed");
			pawn.needs.joy.CurLevel += joysatisfaction;

			//add Ahegao
			//sex&joy > 95%
			//sex&broken

			var lev = sex_need.CurLevel;
			if (lev >= sex_need.thresh_ahegao() &&
				(joy_need.CurInstantLevelPercentage > sex_need.thresh_ahegao()) ||
				(AfterSexUtility.BodyIsBroken(pawn) && pawn.health.hediffSet.GetFirstHediffOfDef(xxx.feelingBroken)?.CurStageIndex >= 3))
			{
					var thoughtDef = DefDatabase<ThoughtDef>.GetNamed("RJW_Ahegao");
					pawn.needs.mood.thoughts.memories.TryGainMemory(thoughtDef);
			}
		}

		private static void CheckTraitGain(SexProps props)
		{
			CheckTraitGainInitiator(props);
			CheckTraitGainPartner(props);
		}
		private static void CheckTraitGainInitiator(SexProps props)
		{
			Pawn pawn = props.pawn;
			Pawn partner = props.partner;
			if (!xxx.has_traits(pawn) || pawn.records.GetValue(xxx.CountOfSex) <= 10) return;

			if (RJWSettings.AddTrait_Necrophiliac && !xxx.is_necrophiliac(pawn) && partner.Dead && pawn.records.GetValue(xxx.CountOfSexWithCorpse) > 0.5 * pawn.records.GetValue(xxx.CountOfSex))
			{
				pawn.story.traits.GainTrait(new Trait(xxx.necrophiliac));
				//Log.Message(xxx.get_pawnname(necro) + " aftersex, not necro, adding necro trait");
			}
			if (RJWSettings.AddTrait_Rapist && !xxx.is_rapist(pawn) && !xxx.is_masochist(pawn) && props.isRape && pawn.records.GetValue(xxx.CountOfRapedHumanlikes) > 0.12 * pawn.records.GetValue(xxx.CountOfSex))
			{
				var chance = 0.5f;
				if (xxx.is_kind(pawn)) chance -= 0.25f;
				if (xxx.is_prude(pawn)) chance -= 0.25f;
				if (xxx.is_zoophile(pawn)) chance -= 0.25f; // Less interested in raping humanlikes.
				if (xxx.is_ascetic(pawn)) chance -= 0.2f;
				if (xxx.is_bloodlust(pawn)) chance += 0.2f;
				if (xxx.is_psychopath(pawn)) chance += 0.25f;

				CheckTraitGainInitiatorRNG(pawn, chance);
			}
			if (RJWSettings.AddTrait_Zoophiliac && !xxx.is_zoophile(pawn) && xxx.is_animal(partner)
				&& (pawn.records.GetValue(xxx.CountOfSexWithAnimals) + pawn.records.GetValue(xxx.CountOfSexWithInsects) > 0.5 * pawn.records.GetValue(xxx.CountOfSex)))
			{
				pawn.story.traits.GainTrait(new Trait(xxx.zoophile));
				pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDef(xxx.got_bred);
				pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDef(xxx.got_anal_bred);
				pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDef(xxx.got_groped);
				pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDef(xxx.got_licked);
				//--Log.Message(xxx.get_pawnname(pawn) + " aftersex, not zoo, adding zoo trait");
			}
			if (RJWSettings.AddTrait_Nymphomaniac && !xxx.is_nympho(pawn))
			{
				if (pawn.health.hediffSet.HasHediff(HediffDef.Named("HumpShroomAddiction")))
				{
					pawn.story.traits.GainTrait(new Trait(xxx.nymphomaniac));
					//Log.Message(xxx.get_pawnname(pawn) + " is HumpShroomAddicted, not nymphomaniac, adding nymphomaniac trait");
				}
			}
		}

		[SyncMethod]
		private static void CheckTraitGainInitiatorRNG(Pawn pawn, float chance)
		{
			if (Rand.Chance(chance))
			{
				pawn.story.traits.GainTrait(new Trait(xxx.rapist));
				//--Log.Message(xxx.get_pawnname(pawn) + " aftersex, not rapist, adding rapist trait");
			}
		}

		[SyncMethod]
		private static void CheckTraitGainPartner(SexProps props)
		{
			Pawn pawn = props.partner;
			Pawn partner = props.pawn;
			if (!xxx.has_traits(pawn) || pawn.records.GetValue(xxx.CountOfSex) <= 10) return;

			if (RJWSettings.AddTrait_Zoophiliac && !xxx.is_zoophile(pawn) && xxx.is_animal(partner)
				&& (pawn.records.GetValue(xxx.CountOfSexWithAnimals) + pawn.records.GetValue(xxx.CountOfSexWithInsects) > 0.5 * pawn.records.GetValue(xxx.CountOfSex)))
			{
				pawn.story.traits.GainTrait(new Trait(xxx.zoophile));
				pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDef(xxx.got_bred);
				pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDef(xxx.got_anal_bred);
				pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDef(xxx.got_groped);
				pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDef(xxx.got_licked);
				//--Log.Message(xxx.get_pawnname(pawn) + " aftersex, not zoo, adding zoo trait");
			}
			if (RJWSettings.AddTrait_Nymphomaniac && !xxx.is_nympho(pawn))
			{
				if (pawn.health.hediffSet.HasHediff(HediffDef.Named("HumpShroomAddiction")))
				{
					pawn.story.traits.GainTrait(new Trait(xxx.nymphomaniac));
					//Log.Message(xxx.get_pawnname(pawn) + " is HumpShroomAddicted, not nymphomaniac, adding nymphomaniac trait");
				}
			}
		}

		// Checks if enough time has passed from previous lovin'.
		public static bool ReadyForLovin(Pawn pawn)
		{
			return Find.TickManager.TicksGame > pawn.mindState.canLovinTick;
		}

		// Checks if enough time has passed from previous search for a hookup.
		// Checks if hookups allowed during working hours, exlcuding nymphs
		public static bool ReadyForHookup(Pawn pawn)
		{
			if (!xxx.is_nympho(pawn) && RJWHookupSettings.NoHookupsDuringWorkHours && ((pawn.timetable != null) ? pawn.timetable.CurrentAssignment : TimeAssignmentDefOf.Anything) == TimeAssignmentDefOf.Work) return false;
			return Find.TickManager.TicksGame > CompRJW.Comp(pawn).NextHookupTick;
		}

		private static void IncreaseTicksToNextLovin(Pawn pawn)
		{
			if (pawn == null || pawn.Dead) return;
			int currentTime = Find.TickManager.TicksGame;
			if (pawn.mindState.canLovinTick <= currentTime)
				pawn.mindState.canLovinTick = currentTime + GenerateMinTicksToNextLovin(pawn);
		}

		[SyncMethod]
		public static int GenerateMinTicksToNextLovin(Pawn pawn)
		{
			if (DebugSettings.alwaysDoLovin) return 100;
			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());

			float tick = 1.0f;

			// Nymphs automatically get the tick increase from the trait influence on sex drive.
			if (xxx.is_animal(pawn))
			{
				//var mateMtbHours = pawn.RaceProps.mateMtbHours / 24 * GenDate.TicksPerDay;
				//if (mateMtbHours > 0)
				//	interval = mateMtbHours
				if (RJWSettings.Animal_mating_cooldown == 0)
					tick = 0.75f;
				else
					return RJWSettings.Animal_mating_cooldown * 2500;
			}
			else if (xxx.is_prude(pawn))
				tick = 1.5f;

			if (pawn.Has(Quirk.Vigorous))
				tick *= 0.8f;

			float sex_drive = xxx.get_sex_drive(pawn);
			if (sex_drive <= 0.05f)
				sex_drive = 0.05f;

			float interval = AgeConfigDef.Instance.lovinIntervalHoursByAge.Evaluate(ScaleToHumanAge(pawn));
			float rinterval = Math.Max(0.5f, Rand.Gaussian(interval, 0.3f));
			return (int)(tick * rinterval * (2500.0f / sex_drive));
		}

		public static void IncreaseTicksToNextHookup(Pawn pawn)
		{
			if (pawn == null || pawn.Dead)
				return;

			// There are 2500 ticks per rimworld hour. Sleeping an hour between checks seems like a good start.
			// We could get fancier and weight it by sex drive and stuff, but would people even notice?
			const int TicksBetweenHookups = 2500;

			int currentTime = Find.TickManager.TicksGame;
			CompRJW.Comp(pawn).NextHookupTick = currentTime + TicksBetweenHookups;
		}

		//
		// Determines the sex type and handles the log output.
		// "Pawn" should be initiator of the act (rapist, whore, etc), "Partner" should be the target.
		//
		public static void ProcessSex(SexProps props)
		{
			//Log.Message("usedCondom=" + usedCondom);
			if (props.pawn == null || props.partner == null)
			{
				if (props.pawn == null)
					ModLog.Error("[SexUtility] ERROR: pawn is null.");
				if (props.partner == null)
					ModLog.Error("[SexUtility] ERROR: partner is null.");
				return;
			}

			IncreaseTicksToNextLovin(props.pawn);
			IncreaseTicksToNextLovin(props.partner);

			Aftersex(props);

			AfterSexUtility.think_about_sex(props);
		}

		[SyncMethod]
		public static SexProps SelectSextype(Pawn pawn, Pawn partner, bool rape, bool whoring)
		{
			var SP = new SexProps();
			SP.pawn = pawn;
			SP.partner = partner;
			SP.isRape = rape;
			SP.isWhoring = whoring;

			//Caufendra's magic is happening here
			InteractionInputs inputs = new InteractionInputs()
			{
				Initiator = pawn,
				Partner = partner,
				IsRape = rape,
				IsWhoring = whoring
			};

			//this should be added as a static readonly but ... since the class is so big,
			//it's probably best not to overload the static constructor
			ILewdInteractionService lewdInteractionService = LewdInteractionService.Instance;

			InteractionOutputs outputs = lewdInteractionService.GenerateInteraction(inputs);

			SP.sexType = outputs.Generated.RjwSexType;
			SP.rulePack = outputs.Generated.RulePack.defName;
			SP.dictionaryKey = outputs.Generated.InteractionDef.Interaction;

			return SP;
		}

		public static void LogSextype(Pawn giving, Pawn receiving, string rulepack, InteractionDef dictionaryKey)
		{
			List<RulePackDef> extraSentencePacks = new List<RulePackDef>();
			if (!rulepack.NullOrEmpty())
				extraSentencePacks.Add(RulePackDef.Named(rulepack));
			LogSextype(giving, receiving, extraSentencePacks, dictionaryKey);
		}

		public static void LogSextype(Pawn giving, Pawn receiving, List<RulePackDef> extraSentencePacks, InteractionDef dictionaryKey)
		{
			if (extraSentencePacks.NullOrEmpty())
			{
				extraSentencePacks = new List<RulePackDef>();
				string extraSentenceRulePack = SexRulePackGet(dictionaryKey);
				if (!extraSentenceRulePack.NullOrEmpty())
					extraSentencePacks.Add(RulePackDef.Named(extraSentenceRulePack));
			}
			PlayLogEntry_Interaction playLogEntry = new PlayLogEntry_Interaction(dictionaryKey, giving, receiving, extraSentencePacks);
			Find.PlayLog.Add(playLogEntry);
		}

		[SyncMethod]
		public static string SexRulePackGet(InteractionDef dictionaryKey)
		{
			var extension = Modules.Interactions.Helpers.InteractionHelper.GetWithExtension(dictionaryKey).Extension;
			string extraSentenceRulePack = "";
			if (!extension.rulepack_defs.NullOrEmpty())
			{
				extraSentenceRulePack = extension.rulepack_defs.RandomElement();
			}

			try
			{
				if (RulePackDef.Named(extraSentenceRulePack) != null)
				{
				}
			}
			catch
			{
				ModLog.Warning("RulePackDef " + extraSentenceRulePack + " for " + dictionaryKey + " not found");
				extraSentenceRulePack = "";
			}
			return extraSentenceRulePack;
		}

		public static xxx.rjwSextype rjwSextypeGet(InteractionDef dictionaryKey)
		{
			var extension = Modules.Interactions.Helpers.InteractionHelper.GetWithExtension(dictionaryKey).Extension;
			var sextype = xxx.rjwSextype.None;
			if (!extension.rjwSextype.NullOrEmpty())
				sextype = ParseHelper.FromString<xxx.rjwSextype>(extension.rjwSextype);

			if (RJWSettings.DevMode) ModLog.Message("rjwSextypeGet:dictionaryKey " + dictionaryKey + " sextype " + sextype);
			return sextype;
		}

		[SyncMethod]
		public static void Sex_Beatings(SexProps props)
		{
			Pawn pawn = props.pawn;
			Pawn partner = props.partner;
			if ((xxx.is_animal(pawn) && xxx.is_animal(partner)))
				return;

			//dont remember what it does, probably manhunter stuff or not? disable and wait reports
			//if (!xxx.is_human(pawn))
			//	return;

			//If a pawn is incapable of violence/has low melee, they most likely won't beat their partner
			if (pawn.skills?.GetSkill(SkillDefOf.Melee).Level < 1)
				return;

			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			float rand_value = Rand.Value;
			//float rand_value = RJW_Multiplayer.RJW_MP_RAND();
			float victim_pain = partner.health.hediffSet.PainTotal;
			// bloodlust makes the aggressor more likely to hit the prisoner
			float beating_chance = xxx.config.base_chance_to_hit_prisoner * (xxx.is_bloodlust(pawn) ? 1.5f : 1.0f);
			// psychopath makes the aggressor more likely to hit the prisoner past the significant_pain_threshold
			float beating_threshold = xxx.is_psychopath(pawn) ? xxx.config.extreme_pain_threshold : pawn.HostileTo(partner) ? xxx.config.significant_pain_threshold : xxx.config.minor_pain_threshold;

			//--Log.Message("roll_to_hit:  rand = " + rand_value + ", beating_chance = " + beating_chance + ", victim_pain = " + victim_pain + ", beating_threshold = " + beating_threshold);
			if ((victim_pain < beating_threshold && rand_value < beating_chance) || (rand_value < (beating_chance / 2) && xxx.is_bloodlust(pawn)))
			{
				Sex_Beatings_Dohit(pawn, partner, props.isRapist);
			}
		}

		public static void Sex_Beatings_Dohit(Pawn pawn, Pawn partner, bool isRape = false)
		{
			//--Log.Message("   done told her twice already...");
			if (InteractionUtility.TryGetRandomVerbForSocialFight(pawn, out Verb v))
			{
				//Log.Message("   v. : " + v);
				//Log.Message("   v.GetDamageDef : " + v.GetDamageDef());
				//Log.Message("   v.v.tool - " + v.tool.label);
				//Log.Message("   v.v.tool.power base - " + v.tool.power);
				var orgpower = v.tool.power;
				//in case something goes wrong
				try
				{
					//Log.Message("   v.v.tool.power base - " + v.tool.power);
					if (RJWSettings.gentle_rape_beating || !isRape)
					{
						v.tool.power = 0;
						//partner.stances.stunner.StunFor(600, pawn);
					}
					//Log.Message("   v.v.tool.power mod - " + v.tool.power);
					var guilty = true;
					if (!pawn.guilt.IsGuilty)
					{
						guilty = false;
					}
					pawn.meleeVerbs.TryMeleeAttack(partner, v);

					if (pawn.guilt.IsGuilty && !guilty)
						pawn.guilt.Notify_Guilty(0);
				}
				catch
				{ }
				v.tool.power = orgpower;
				//Log.Message("   v.v.tool.power reset - " + v.tool.power);
			}
		}

		// Overrides the current clothing. Defaults to nude, with option to keep headgear on.
		public static void DrawNude(Pawn pawn, bool keep_hat_on = false)
		{
			if (!xxx.is_human(pawn)) return;
			if (pawn.Map != Find.CurrentMap) return;
			if (RJWPreferenceSettings.sex_wear == RJWPreferenceSettings.Clothing.Clothed) return;

			//undress
			pawn.Drawer.renderer.graphics.ClearCache();
			pawn.Drawer.renderer.graphics.apparelGraphics.Clear();
			//pawn.Drawer.renderer.graphics.ResolveApparelGraphics();
			//Log.Message("DrawNude: " + pawn.Name);

			//add "clothes"
			foreach (Apparel current in pawn.apparel.WornApparel.Where(x
				=> x.def is bondage_gear_def
				|| (!x.def.thingCategories.NullOrEmpty() && x.def.thingCategories.Any(x=> x.defName.ToLower().ContainsAny("vibrator", "piercing", "strapon")))
				|| RJWPreferenceSettings.sex_wear == RJWPreferenceSettings.Clothing.Headgear
				|| keep_hat_on
				&& (!x.def.apparel.bodyPartGroups.NullOrEmpty() && (x.def.apparel.bodyPartGroups.Contains(BodyPartGroupDefOf.FullHead)
					|| x.def.apparel.bodyPartGroups.Contains(BodyPartGroupDefOf.UpperHead)))))
			{
				ApparelGraphicRecord item;
				if (ApparelGraphicRecordGetter.TryGetGraphicApparel(current, pawn.story.bodyType, out item))
				{
					pawn.Drawer.renderer.graphics.apparelGraphics.Add(item);
				}
			}
			GlobalTextureAtlasManager.TryMarkPawnFrameSetDirty(pawn);
		}

		public static void reduce_rest(Pawn pawn, float x = 1f)
		{
			if (pawn.Has(Quirk.Vigorous)) x -= x/2;

			Need_Rest need_rest = pawn.needs.TryGetNeed<Need_Rest>();
			if (need_rest == null)
				return;

			need_rest.CurLevel -= need_rest.RestFallPerTick * x;
		}
		public static void OffsetPsyfocus(Pawn pawn, float x = 0)//0-1
		{
			if (ModsConfig.RoyaltyActive)
			{
				//pawn.psychicEntropy.Notify_Meditated();
				if (pawn.HasPsylink)
				{
					pawn.psychicEntropy.OffsetPsyfocusDirectly(x);
				}
			}
		}

		//Takes the nutrition away from the one penetrating(probably) and injects it to the one on the receiving end
		//As with everything in the mod, this could be greatly extended, current focus though is to prevent starvation of those caught in a huge horde of rappers (that may happen with some mods)
		public static void TransferNutrition(SexProps props)
		{
			//Log.Message("xxx::TransferNutrition:: " + xxx.get_pawnname(pawn) + " => " + xxx.get_pawnname(partner));
			if (props.partner?.needs == null)
			{
				//Log.Message("xxx::TransferNutrition() failed due to lack of transfer equipment or pawn ");
				return;
			}
			if (props.pawn?.needs == null)
			{
				//Log.Message("xxx::TransferNutrition() failed due to lack of transfer equipment or pawn ");
				return;
			}

			//transfer nutrition from presumanbly "giver" to partner
			if (props.sexType == xxx.rjwSextype.Sixtynine ||
				props.sexType == xxx.rjwSextype.Oral ||
				props.sexType == xxx.rjwSextype.Cunnilingus ||
				props.sexType == xxx.rjwSextype.Fellatio)
			{
				Pawn pawn = props.pawn;
				Pawn partner = props.partner;
				Pawn giver, receiver;
				List<Hediff> pawnparts = pawn.GetGenitalsList();
				List<Hediff> partnerparts = partner.GetGenitalsList();
				bool t = false;

				if (props.sexType == xxx.rjwSextype.Sixtynine || Genital_Helper.has_penis_fertile(pawn, pawnparts) || Genital_Helper.has_ovipositorF(pawn, pawnparts))
				{
					giver = pawn;
					receiver = partner;

					Need_Food need = giver.needs?.TryGetNeed<Need_Food>();
					if (need == null)
					{
						//Log.Message("TransferNutrition() " + xxx.get_pawnname(giver) + " doesn't track nutrition in itself, probably shouldn't feed the others");
						return;
					}
					float nutrition_amount = Math.Min(need.MaxLevel / 15f, need.CurLevel); //body size is taken into account implicitly by need.MaxLevel
					giver.needs.food.CurLevel = need.CurLevel - nutrition_amount;
					//Log.Message("TransferNutrition() " + xxx.get_pawnname(giver) + " sent " + nutrition_amount + " of nutrition");

					if (receiver.needs?.TryGetNeed<Need_Food>() != null)
					{
						//Log.Message("xxx::TransferNutrition() " +  xxx.get_pawnname(receiver) + " can receive");
						receiver.needs.food.CurLevel += nutrition_amount;
					}
					TransferThirst(giver, receiver);
					t = true;
				}

				if (props.sexType == xxx.rjwSextype.Sixtynine || (t == false && props.isCoreLovin && (Genital_Helper.has_penis_fertile(partner, partnerparts) || Genital_Helper.has_ovipositorF(partner, partnerparts))))
				{
					giver = partner;
					receiver = pawn;

					Need_Food need = giver.needs?.TryGetNeed<Need_Food>();
					if (need == null)
					{
						//Log.Message("TransferNutrition() " + xxx.get_pawnname(giver) + " doesn't track nutrition in itself, probably shouldn't feed the others");
						return;
					}
					float nutrition_amount = Math.Min(need.MaxLevel / 15f, need.CurLevel); //body size is taken into account implicitly by need.MaxLevel
					giver.needs.food.CurLevel = need.CurLevel - nutrition_amount;
					//Log.Message("TransferNutrition() " + xxx.get_pawnname(giver) + " sent " + nutrition_amount + " of nutrition");

					if (receiver.needs?.TryGetNeed<Need_Food>() != null)
					{
						//Log.Message("TransferNutrition() " +  xxx.get_pawnname(receiver) + " can receive");
						receiver.needs.food.CurLevel += nutrition_amount;
					}
					TransferThirst(giver, receiver);
				}
			}
			TransferNutritionSucc(props);
		}

		public static void TransferThirst(Pawn pawn, Pawn receiver)
		{
			if (xxx.DubsBadHygieneIsActive)
			{
				Need DBHThirst = receiver.needs?.AllNeeds.Find(x => x.def == xxx.DBHThirst);
				if (DBHThirst != null)
				{
					//Log.Message("TransferThirst() " + xxx.get_pawnname(receiver) + " decreasing thirst");
					receiver.needs.TryGetNeed(DBHThirst.def).CurLevel += 0.1f;
				}
			}
		}

		public static void TransferNutritionSucc(SexProps props)
		{
			//succubus mana and rest regen stuff
			if (props.sexType == xxx.rjwSextype.Oral ||
				props.sexType == xxx.rjwSextype.Cunnilingus ||
				props.sexType == xxx.rjwSextype.Fellatio ||
				props.sexType == xxx.rjwSextype.Vaginal ||
				props.sexType == xxx.rjwSextype.Anal ||
				props.sexType == xxx.rjwSextype.DoublePenetration)
			{
				if (xxx.has_traits(props.partner))
				{
					bool gainrest = false;
					if (xxx.RoMIsActive && (props.partner.story.traits.HasTrait(xxx.Succubus) || props.partner.story.traits.HasTrait(xxx.Warlock)))
					{
						Need TM_Mana = props.partner?.needs?.AllNeeds.Find(x => x.def == xxx.TM_Mana);
						if (TM_Mana != null)
						{
							//Log.Message("TransferNutritionSucc() " + xxx.get_pawnname(partner) + " increase mana");
							props.partner.needs.TryGetNeed(TM_Mana.def).CurLevel += 0.1f;
						}
						gainrest = true;
					}

					if (xxx.NightmareIncarnationIsActive)
					{
						foreach (var x in props.partner.AllComps?.Where(x => x.props?.ToString() == "NightmareIncarnation.CompProperties_SuccubusRace"))
						{
							Need NI_Need_Mana = props.partner?.needs?.AllNeeds.Find(x => x.def == xxx.NI_Need_Mana);
							if (NI_Need_Mana != null)
							{
								//Log.Message("TransferNutritionSucc() " + xxx.get_pawnname(partner) + " increase mana");
								props.partner.needs.TryGetNeed(NI_Need_Mana.def).CurLevel += 0.1f;
							}
							gainrest = true;
							break;
						}
					}

					if (gainrest)
					{
						Need_Rest need = props.pawn.needs.TryGetNeed<Need_Rest>();
						if (need != null)
						{
							//Log.Message("TransferNutritionSucc() " + xxx.get_pawnname(partner) + " increase rest");
							props.partner.needs.TryGetNeed(need.def).CurLevel += 0.25f;
							//Log.Message("TransferNutritionSucc() " + xxx.get_pawnname(pawn) + " decrease rest");
							props.pawn.needs.TryGetNeed(need.def).CurLevel -= 0.25f;
						}
					}
				}

				if (xxx.has_traits(props.pawn))
				{
					bool gainrest = false;
					if (xxx.RoMIsActive && (props.pawn.story.traits.HasTrait(xxx.Succubus) || props.pawn.story.traits.HasTrait(xxx.Warlock)))
					{
						Need TM_Mana = props.pawn?.needs?.AllNeeds.Find(x => x.def == xxx.TM_Mana);
						if (TM_Mana != null)
						{
							//Log.Message("TransferNutritionSucc() " + xxx.get_pawnname(pawn) + " increase mana");
							props.pawn.needs.TryGetNeed(TM_Mana.def).CurLevel += 0.1f;
						}
					}

					if (xxx.NightmareIncarnationIsActive)
					{
						foreach (var x in props.pawn.AllComps?.Where(x => x.props?.ToString() == "NightmareIncarnation.CompProperties_SuccubusRace"))
						{
							Need NI_Need_Mana = props.pawn?.needs?.AllNeeds.Find(x => x.def == xxx.NI_Need_Mana);
							if (NI_Need_Mana != null)
							{
								//Log.Message("TransferNutritionSucc() " + xxx.get_pawnname(partner) + " increase mana");
								props.pawn.needs.TryGetNeed(NI_Need_Mana.def).CurLevel += 0.1f;
							}
							gainrest = true;
							break;
						}
					}

					if (gainrest)
					{
						Need_Rest need = props.partner.needs.TryGetNeed<Need_Rest>();
						if (need != null)
						{
							//Log.Message("TransferNutritionSucc() " + xxx.get_pawnname(pawn) + " increase rest");
							props.pawn.needs.TryGetNeed(need.def).CurLevel += 0.25f;
							//Log.Message("TransferNutritionSucc() " + xxx.get_pawnname(partner) + " decrease rest");
							props.partner.needs.TryGetNeed(need.def).CurLevel -= 0.25f;
						}
					}
				}
			}
		}

		public static float get_broken_consciousness_debuff(Pawn pawn)
		{
			if (pawn == null)
			{
				return 1.0f;
			}

			try
			{
				Hediff broken = pawn.health.hediffSet.GetFirstHediffOfDef(xxx.feelingBroken);
				foreach (PawnCapacityModifier capMod in broken.CapMods)
				{
					if (capMod.capacity == PawnCapacityDefOf.Consciousness)
					{
						if (RJWSettings.DevMode) ModLog.Message("Broken Pawn Consciousness factor: " + capMod.postFactor);
						return capMod.postFactor;
					}
				}

				// fallback
				return 1.0f;
			}
			catch (NullReferenceException)
			{
				//Log.Warning(e.ToString());
				return 1f;
			}
		}

		public static float get_satisfaction_circumstance_multiplier(SexProps props)
		{
			// Get a multiplier for satisfaction that should apply on top of the sex satisfaction stat
			// This is mostly for traits that only affect satisfaction in some circumstances

			Pawn pawn = props.pawn;
			Boolean isViolentSex = props.isRape;

			float multiplier = 1.0f;
			if (xxx.is_human(pawn))
			{
				// Negate consciousness debuffs for broken pawns (counters the sex satisfaction score being affected by low consciousness)
				multiplier = (1.0f / get_broken_consciousness_debuff(pawn));

				// Multiplier bonus for violent traits and violent sex
				if (!props.isReceiver)
				{
					// Rapists/Bloodlusts get 50% more satisfaction from violent encounters, and less from non-violent
					if (isViolentSex)
					{
						if (xxx.is_rapist(pawn) || xxx.is_bloodlust(pawn) || xxx.is_psychopath(pawn))
							multiplier += 0.5f;
						else
							multiplier -= 0.2f;
					}
					else
					{
						if (xxx.is_rapist(pawn) || xxx.is_bloodlust(pawn))
							multiplier -= 0.2f;
						else
							multiplier += 0.2f;
					}
				}
				else if (props.isReceiver)
				{
					// Masochists get 50% more satisfaction from receiving violent sex and 20% less from normal sex
					if (isViolentSex)
					{
						if (xxx.is_masochist(pawn))
							multiplier += 0.5f;
						else
							multiplier -= 0.2f;
					}
					else
					{
						if (xxx.is_masochist(pawn))
							multiplier -= 0.2f;
						else
							multiplier += 0.2f;
					}

					// Multiplier for broken pawns
					if (isViolentSex && AfterSexUtility.BodyIsBroken(pawn))
					{
						// Add bonus satisfaction based on stage
						switch (pawn.health.hediffSet.GetFirstHediffOfDef(xxx.feelingBroken).CurStageIndex)
						{
							case 0:
								break;

							case 1:
								// 50% bonus for stage 2
								//multiplier += 0.5f;
								break;

							case 2:
								// 100% bonus for stage 2
								multiplier += 1.0f;
								break;
						}
					}
				}
				//ModLog.Message("Sex satisfaction multiplier: " + multiplier);
			}
			return multiplier;
		}
	}
}


code:
using Multiplayer.API;
using Verse;

namespace rjw
{
	public static class Sexualizer
	{
		static void SexualizeSingleGenderPawn(Pawn pawn)
		{
			// Single gender is futa without the female gender change.
			SexPartAdder.add_genitals(pawn, null, Gender.Male);
			SexPartAdder.add_genitals(pawn, null, Gender.Female);
			SexPartAdder.add_breasts(pawn, null, Gender.Female);
			SexPartAdder.add_anus(pawn, null);
		}

		static void SexualizeGenderlessPawn(Pawn pawn)
		{
			if (RJWSettings.GenderlessAsFuta && !xxx.is_mechanoid(pawn) && (pawn.RaceProps.Animal || pawn.RaceProps.Humanlike))
			{
				if (RJWSettings.DevMode)
					ModLog.Message(" SexualizeGenderlessPawn() - genderless pawn, treating Genderless pawn As Futa" + xxx.get_pawnname(pawn));
				//set gender to female for futas
				pawn.gender = Gender.Female;
				SexPartAdder.add_genitals(pawn, null, Gender.Male);
				SexPartAdder.add_genitals(pawn, null, Gender.Female);
				SexPartAdder.add_breasts(pawn, null, Gender.Female);
				SexPartAdder.add_anus(pawn, null);
			}
			else
			{
				if (RJWSettings.DevMode)
					ModLog.Message(" SexualizeGenderlessPawn() - unable to sexualize genderless pawn " + xxx.get_pawnname(pawn) + " gender: " + pawn.gender);
			}
		}

		[SyncMethod]
		static void SexualizeGenderedPawn(Pawn pawn)
		{
			//apply normal gender
			SexPartAdder.add_genitals(pawn, null, pawn.gender);

			//apply futa gender
			//if (pawn.gender == Gender.Female) // changing male to futa will break pawn generation due to relations

			if (pawn.Faction != null && !xxx.is_animal(pawn)) //null faction throws error
			{
				//ModLog.Message(" SexualizeGenderedPawn( " + xxx.get_pawnname(pawn) + " ) techLevel: " + (int)pawn.Faction.def.techLevel);
				//ModLog.Message(" SexualizeGenderedPawn( " + xxx.get_pawnname(pawn) + " ) techLevel: " + pawn.Faction.Name);

				//natives/spacer futa
				float chance = (int)pawn.Faction.def.techLevel < 5 ? RJWSettings.futa_natives_chance : RJWSettings.futa_spacers_chance;
				//nymph futa gender
				chance = xxx.is_nympho(pawn) ? RJWSettings.futa_nymph_chance : chance;

				// ModLog.Message($"SexualizeGenderedPawn {chance} from {RJWSettings.futa_nymph_chance} {RJWSettings.futa_natives_chance} {RJWSettings.futa_spacers_chance}");
				if (Rand.Chance(chance))
				{
					//make futa
					if (pawn.gender == Gender.Female && RJWSettings.FemaleFuta)
						SexPartAdder.add_genitals(pawn, null, Gender.Male);
					//make trap
					else if (pawn.gender == Gender.Male && RJWSettings.MaleTrap)
						SexPartAdder.add_breasts(pawn, null, Gender.Female);
				}
			}
			SexPartAdder.add_breasts(pawn, null, pawn.gender);
			SexPartAdder.add_anus(pawn, null);
		}

		[SyncMethod]
		public static void sexualize_pawn(Pawn pawn)
		{
			//ModLog.Message(" sexualize_pawn( " + xxx.get_pawnname(pawn) + " ) called");
			if (pawn == null)
			{
				return;
			}

			//override pawn
			if (RacePartDef_Helper.TryRacePartDef_partAdders(pawn))
				return;

			if (RaceGroupDef_Helper.TryGetRaceGroupDef(pawn, out var raceGroupDef) &&
				raceGroupDef.hasSingleGender)
			{
				if (RJWSettings.DevMode)
					ModLog.Message($"sexualize_pawn() - sexualizing single gender pawn {xxx.get_pawnname(pawn)}  race: {raceGroupDef.defName}");
				SexualizeSingleGenderPawn(pawn);
			}
			else if (pawn.RaceProps.hasGenders)
			{
				SexualizeGenderedPawn(pawn);
			}
			else
			{
				if (Current.ProgramState == ProgramState.Playing) // DO NOT run at world generation, throws error in generating relationship stuff
				{
					SexualizeGenderlessPawn(pawn);
					return;
				}
			}

			if (!pawn.Dead)
			{
				//Add ticks at generation, so pawns don't instantly start lovin' when generated (esp. on scenario start).
				//if (xxx.RoMIsActive && pawn.health.hediffSet.HasHediff(HediffDef.Named("TM_ShapeshiftHD"))) // Rimworld of Magic's polymorph/shapeshift
				//	pawn.mindState.canLovinTick = Find.TickManager.TicksGame + (int)(SexUtility.GenerateMinTicksToNextLovin(pawn) * Rand.Range(0.01f, 0.05f));
				if (!xxx.is_insect(pawn))
					pawn.mindState.canLovinTick = Find.TickManager.TicksGame + (int)(SexUtility.GenerateMinTicksToNextLovin(pawn) * Rand.Range(0.1f, 1.0f));
				else
					pawn.mindState.canLovinTick = Find.TickManager.TicksGame + (int)(SexUtility.GenerateMinTicksToNextLovin(pawn) * Rand.Range(0.01f, 0.2f));

				//ModLog.Message(" sexualize_pawn( " + xxx.get_pawnname(pawn) + " ) add sexneed");
				if (pawn.RaceProps.Humanlike)
				{
					var sex_need = pawn.needs.TryGetNeed<Need_Sex>();
					if (pawn.Faction != null && !(pawn.Faction?.IsPlayer ?? false) && sex_need != null)
					{
						sex_need.CurLevel = Rand.Range(0.01f, 0.75f);
					}
				}
			}
		}
	}
}


code:
using System;
using System.Diagnostics;
using UnityEngine;
using Verse;

namespace rjw
{
	public static class Logger
	{
		private static readonly LogMessageQueue messageQueueRJW = new LogMessageQueue();
		public static void Message(string text)
		{
			bool DevModeEnabled = RJWSettings.DevMode;
			if (!DevModeEnabled) return;
			UnityEngine.Debug.Log(text);
			messageQueueRJW.Enqueue(new LogMessage(LogMessageType.Message, text, StackTraceUtility.ExtractStackTrace()));
		}
		public static void Warning(string text)
		{
			bool DevModeEnabled = RJWSettings.DevMode;
			if (!DevModeEnabled) return;
			UnityEngine.Debug.Log(text);
			messageQueueRJW.Enqueue(new LogMessage(LogMessageType.Warning, text, StackTraceUtility.ExtractStackTrace()));
		}
		public static void Error(string text)
		{
			bool DevModeEnabled = RJWSettings.DevMode;
			if (!DevModeEnabled) return;
			UnityEngine.Debug.Log(text);
			messageQueueRJW.Enqueue(new LogMessage(LogMessageType.Error, text, StackTraceUtility.ExtractStackTrace()));
		}

		public static TimeSpan Time(Action action)
		{
			Stopwatch stopwatch = Stopwatch.StartNew();
			action();
			stopwatch.Stop();
			return stopwatch.Elapsed;
		}
	}
}


code:
using Verse;

namespace rjw
{
	public class MapCom_Injector : MapComponent
	{
		public bool injected_designator = false;

		public bool triggered_after_load = false;

		public MapCom_Injector(Map m) : base(m)
		{
		}

		public override void MapComponentUpdate()
		{
		}

		public override void MapComponentTick()
		{
		}

		public override void MapComponentOnGUI()
		{
			var currently_visible = Find.CurrentMap == map;

			if ((!injected_designator) && currently_visible)
			{
				//Find.ReverseDesignatorDatabase.AllDesignators.Add(new Designator_ComfortPrisoner());
				//Find.ReverseDesignatorDatabase.AllDesignators.Add(new Designator_Breed());
				injected_designator = true;
			}
			else if (injected_designator && (!currently_visible))
				injected_designator = false;
		}

		public override void ExposeData()
		{
		}
	}
}


code:
using System;
using Verse;

namespace rjw
{
	public sealed class MiscTranslationDef : Def
	{
		public Type targetClass;
		public string stringA = null;
		public string stringB = null;
		public string stringC = null;

		private void Assert(bool check, string errorMessage)
		{
			if (!check)
			{
				ModLog.Error($"Invalid data in MiscTranslationDef {defName}: {errorMessage}");
			}
		}

		public override void PostLoad()
		{
			Assert(targetClass != null, "targetClass field must be set");
		}

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