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

namespace rjw
{
	public class JobGiver_DoQuickie : ThinkNode_JobGiver
	{
		/// <summary> Checks all of our potential partners to see if anyone's eligible, returning the most attractive and convenient one. </summary>
		protected override Job TryGiveJob(Pawn pawn)
		{
			if (!RJWHookupSettings.HookupsEnabled || !RJWHookupSettings.QuickHookupsEnabled)
				return null;

			if (pawn.Drafted)
				return null;

			if (!SexUtility.ReadyForHookup(pawn))
				return null;

			// We increase the time right away to prevent the fairly expensive check from happening too frequently
			SexUtility.IncreaseTicksToNextHookup(pawn);

			// If the pawn is a whore, or recently had sex, skip the job unless they're really horny
			if (!xxx.is_frustrated(pawn) && (xxx.is_whore(pawn) || !SexUtility.ReadyForLovin(pawn)))
				return null;

			// This check attempts to keep groups leaving the map, like guests or traders, from turning around to hook up
			if (pawn.mindState?.duty?.def == DutyDefOf.TravelOrLeave)
			{
				// TODO: Some guest pawns keep the TravelOrLeave duty the whole time, I think the ones assigned to guard the pack animals.
				// That's probably ok, though it wasn't the intention.
				if (RJWSettings.DebugLogJoinInBed) ModLog.Message($" Quickie.TryGiveJob:({xxx.get_pawnname(pawn)}): has TravelOrLeave, no time for lovin!");
				return null;
			}

			if (pawn.CurJob == null)
			{
				//--Log.Message("   checking pawn and abilities");
				if (CasualSex_Helper.CanHaveSex(pawn))
				{
					//--Log.Message("   finding partner");
					Pawn partner = CasualSex_Helper.find_partner(pawn, pawn.Map, false);

					//--Log.Message("   checking partner");
					if (partner == null)
						return null;

					// Interrupt current job.
					if (pawn.CurJob != null && pawn.jobs.curDriver != null)
						pawn.jobs.curDriver.EndJobWith(JobCondition.InterruptForced);

					//--Log.Message("   returning job");
					return JobMaker.MakeJob(xxx.quick_sex, partner);
				}
			}

			return null;
		}
	}
}


code:
using RimWorld;
using rjw.Modules.Shared.Extensions;
using rjw.Modules.Shared.Logs;
using Verse;
using Verse.AI;

namespace rjw
{
	public class JobGiver_JoinInBed : ThinkNode_JobGiver
	{
		private readonly static ILog _log = LogManager.GetLogger<JobGiver_JoinInBed>();

		protected override Job TryGiveJob(Pawn pawn)
		{
			if (!RJWHookupSettings.HookupsEnabled)
			{
				_log.Debug("Hookups disabled");
				return null;
			}

			if (pawn.Drafted)
				return null;

			if (!SexUtility.ReadyForHookup(pawn))
				return null;

			string name = pawn.GetName();

			_log.Debug($"{name} was ready");

			// We increase the time right away to prevent the fairly expensive check from happening too frequently
			SexUtility.IncreaseTicksToNextHookup(pawn);

			// If the pawn is a whore, or recently had sex, skip the job unless they're really horny
			if (!xxx.is_frustrated(pawn) && (xxx.is_whore(pawn) || !SexUtility.ReadyForLovin(pawn)))
			{
				_log.Debug($"{name} but wasn't ready");
				return null;
			}

			// This check attempts to keep groups leaving the map, like guests or traders, from turning around to hook up
			if (pawn.mindState?.duty?.def == DutyDefOf.TravelOrLeave)
			{
				// TODO: Some guest pawns keep the TravelOrLeave duty the whole time, I think the ones assigned to guard the pack animals.
				// That's probably ok, though it wasn't the intention.
				if (RJWSettings.DebugLogJoinInBed) _log.Debug($"JoinInBed.TryGiveJob:({xxx.get_pawnname(pawn)}): has TravelOrLeave, no time for lovin!");
				return null;
			}

			if (pawn.CurJob == null || pawn.CurJob.def == JobDefOf.LayDown)
			{
				//--Log.Message("   checking pawn and abilities");
				if (CasualSex_Helper.CanHaveSex(pawn))
				{
					//--Log.Message("   finding partner");
					Pawn partner = CasualSex_Helper.find_partner(pawn, pawn.Map, true);

					//--Log.Message("   checking partner");
					if (partner == null)
					{
						_log.Debug($"{name} didn't find a partner");
						return null;
					}

					// Can never be null, since find checks for bed.
					Building_Bed bed = partner.CurrentBed();

					// Interrupt current job.
					if (pawn.CurJob != null && pawn.jobs.curDriver != null)
						pawn.jobs.curDriver.EndJobWith(JobCondition.InterruptForced);

					//--Log.Message("   returning job");
					return JobMaker.MakeJob(xxx.casual_sex, partner, bed);
				}
			}

			return null;
		}
	}
}

repo_name: ElToro/rjw

license: mit


code:
using Verse;
using Verse.AI;

namespace rjw
{
	/// <summary>
	/// Attempts to give a lay egg job to an eligible humanoid.
	/// </summary>
	public class JobGiver_LayEgg : RimWorld.JobGiver_LayEgg
	{

	}
}


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

namespace rjw
{
	public class JobGiver_Masturbate : ThinkNode_JobGiver
	{
		protected override Job TryGiveJob(Pawn pawn)
		{
			//--ModLog.Message(" JobGiver_Masturbate::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) called");

			if (pawn.Drafted) return null;

			if (!xxx.can_masturbate(pawn)) return null;

			// Whores only fap if frustrated, unless imprisoned.
			if ((SexUtility.ReadyForLovin(pawn) && (!xxx.is_whore(pawn) || pawn.IsPrisoner || xxx.is_slave(pawn))) || xxx.is_frustrated(pawn))
			{
				if (RJWPreferenceSettings.FapInBed && pawn.jobs.curDriver is JobDriver_LayDown)
				{
					Building_Bed bed = ((JobDriver_LayDown)pawn.jobs.curDriver).Bed;

					if (bed != null)
					{
						if ((xxx.is_frustrated(pawn) || xxx.has_quirk(pawn, "Exhibitionist")) || bed.GetRoom().Role == RoomRoleDefOf.Bedroom || bed.GetRoom().Role == RoomRoleDefOf.PrisonCell)
							return JobMaker.MakeJob(xxx.Masturbate, pawn, bed, bed.Position);
					}
				}
				else if (RJWPreferenceSettings.FapEverywhere && (xxx.is_frustrated(pawn) || xxx.has_quirk(pawn, "Exhibitionist")))
				{
					var spot = CasualSex_Helper.FindSexLocation(pawn);
					if (spot == null)
						return null;
					return JobMaker.MakeJob(xxx.Masturbate, pawn, null, spot);
				}
			}
			return null;
		}
	}
}


code:
using Verse;
using Verse.AI;

namespace rjw
{
	/// <summary>
	/// Attempts to give a mating job with a Bond to an eligible animal.
	/// </summary>
	public class JobGiver_MateBonded : ThinkNode_JobGiver
	{
		protected override Job TryGiveJob(Pawn animal)
		{
			if (!SexUtility.ReadyForLovin(animal))
				return null;

			if (xxx.is_healthy(animal) && CasualSex_Helper.CanHaveSex(animal))
			{
				//search for desiganted target to sex
				Pawn designated_target = CasualSex_Helper.Find_bond(animal, animal.Map);
				if (designated_target != null)
				{
					if (!xxx.is_satisfied(designated_target))
					return JobMaker.MakeJob(xxx.animalMate, designated_target);
				}
			}
			return null;
		}
	}
}


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

namespace rjw
{
	public class JobGiver_RandomRape : ThinkNode_JobGiver
	{
		[SyncMethod]
		public Pawn find_victim(Pawn pawn, Map m)
		{
			float min_fuckability = 0.10f;							// Don't rape pawns with <10% fuckability
			float avg_fuckability = 0f;                             // Average targets fuckability, choose target higher than that
			var valid_targets = new Dictionary<Pawn, float>();      // Valid pawns and their fuckability
			Pawn chosentarget = null;                               // Final target pawn

			// could be prisoner, colonist, or non-hostile outsider
			IEnumerable<Pawn> targets = m.mapPawns.AllPawnsSpawned.Where(x
				=> x != pawn
				&& xxx.is_not_dying(x)
				&& xxx.can_get_raped(x)
				&& !x.Suspended
				&& !x.Drafted
				&& !x.IsForbidden(pawn)
				&& pawn.CanReserveAndReach(x, PathEndMode.Touch, Danger.Some, xxx.max_rapists_per_prisoner, 0)
				&& !x.HostileTo(pawn)
				);

			//Zoo rape Animal
			if (xxx.is_zoophile(pawn) && RJWSettings.bestiality_enabled)
			{
				foreach (Pawn target in targets.Where(x => xxx.is_animal(x)))
				{
					if (!Pather_Utility.cells_to_target_rape(pawn, target.Position))
						continue;// too far

					float fuc = SexAppraiser.would_fuck(pawn, target, true, true);

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

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

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

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

					return chosentarget;
				}
			}

			valid_targets = new Dictionary<Pawn, float>();
			// rape Humanlike
			foreach (Pawn target in targets.Where(x => !xxx.is_animal(x)))
			{
				if (!Pather_Utility.cells_to_target_rape(pawn, target.Position))
					continue;// too far

				float fuc = SexAppraiser.would_fuck(pawn, target, true, true);

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

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

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

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

			return chosentarget;
		}

		protected override Job TryGiveJob(Pawn pawn)
		{
			//ModLog.Message(" JobGiver_RandomRape::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) called");
			if (!xxx.can_rape(pawn)) return null;

			if (pawn.health.hediffSet.HasHediff(HediffDef.Named("Hediff_RapeEnemyCD"))) return null;
			pawn.health.AddHediff(HediffDef.Named("Hediff_RapeEnemyCD"), null, null, null);

			Pawn victim = find_victim(pawn, pawn.Map);
			if (victim == null) return null;

			//ModLog.Message(" JobGiver_RandomRape::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) - found victim " + xxx.get_pawnname(victim));
			return JobMaker.MakeJob(xxx.RapeRandom, victim);
		}
	}
}


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

namespace rjw
{
	/// <summary>
	/// Pawn try to find enemy to rape.
	/// </summary>
	public class JobGiver_RapeEnemy : ThinkNode_JobGiver
	{
		protected override Job TryGiveJob(Pawn pawn)
		{
			if (RJWSettings.DebugRape) ModLog.Message(" JobGiver_RapeEnemy::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) called0");
			//ModLog.Message(" JobGiver_RapeEnemy::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) 0 " + SexUtility.ReadyForLovin(pawn));
			//ModLog.Message(" JobGiver_RapeEnemy::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) 1 " + (xxx.need_some_sex(pawn) <= 1f));
			//ModLog.Message(" JobGiver_RapeEnemy::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) 2 " + !(SexUtility.ReadyForLovin(pawn) || xxx.need_some_sex(pawn) <= 1f));
			//ModLog.Message(" JobGiver_RapeEnemy::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) 1 " + Find.TickManager.TicksGame);
			//ModLog.Message(" JobGiver_RapeEnemy::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) 2 " + pawn.mindState.canLovinTick);

			if (pawn.Drafted) return null;

			if (pawn.health.hediffSet.HasHediff(HediffDef.Named("Hediff_RapeEnemyCD")) || !pawn.health.capacities.CanBeAwake || !(SexUtility.ReadyForLovin(pawn) || xxx.need_some_sex(pawn) <= 1f))
			//if (pawn.health.hediffSet.HasHediff(HediffDef.Named("Hediff_RapeEnemyCD")) || !pawn.health.capacities.CanBeAwake || (SexUtility.ReadyForLovin(pawn) || xxx.is_human(pawn) ? xxx.need_some_sex(pawn) <= 1f : false))
				return null;

			if (!xxx.can_rape(pawn)) return null;
			if (RJWSettings.DebugRape) ModLog.Message(" JobGiver_RapeEnemy::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) can rape");

			JobDef_RapeEnemy rapeEnemyJobDef = null;
			int? highestPriority = null;
			foreach (JobDef_RapeEnemy job in DefDatabase<JobDef_RapeEnemy>.AllDefs)
			{
				if (job.CanUseThisJobForPawn(pawn))
				{
					if (highestPriority == null)
					{
						rapeEnemyJobDef = job;
						highestPriority = job.priority;
					}
					else if (job.priority > highestPriority)
					{
						rapeEnemyJobDef = job;
						highestPriority = job.priority;
					}
				}
			}

			if (rapeEnemyJobDef == null)
			{
				if (RJWSettings.DebugRape) ModLog.Message(" JobGiver_RapeEnemy::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) no valid rapeEnemyJobDef found");
				return null;
			}

			if (RJWSettings.DebugRape) ModLog.Message(" JobGiver_RapeEnemy::ChoosedJobDef( " + xxx.get_pawnname(pawn) + " ) - " + rapeEnemyJobDef?.ToString() + " choosen");
			Pawn victim = rapeEnemyJobDef?.FindVictim(pawn, pawn.Map);

			if (RJWSettings.DebugRape) ModLog.Message(" JobGiver_RapeEnemy::FoundVictim( " + xxx.get_pawnname(victim) + " )");

			//prevents 10 job stacks error, no idea whats the prob with JobDriver_Rape
			//if (victim != null)
			pawn.health.AddHediff(HediffDef.Named("Hediff_RapeEnemyCD"), null, null, null);

			return victim != null ? JobMaker.MakeJob(rapeEnemyJobDef, victim) : null;
			/*
			else
			{
				if (RJWSettings.DebugRape) ModLog.Message("" + this.GetType().ToString() + "::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) - unable to find victim");
				pawn.mindState.canLovinTick = Find.TickManager.TicksGame + Rand.Range(75, 150);
			}
			*/
			//else {  if (RJWSettings.DebugRape) ModLog.Message(" JobGiver_RapeEnemy::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) - too fast to play next"); }
		}
	}
}


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

namespace rjw
{
	public class JobGiver_ViolateCorpse : ThinkNode_JobGiver
	{
		[SyncMethod]
		public static Corpse find_corpse(Pawn pawn, Map m)
		{
			float min_fuckability = 0.10f;                            // Don't rape pawns with <10% fuckability
			float avg_fuckability = 0f;                               // Average targets fuckability, choose target higher than that
			var valid_targets = new Dictionary<Corpse, float>();      // Valid pawns and their fuckability
			Corpse chosentarget = null;                               // Final target pawn

			IEnumerable<Thing> targets = m.spawnedThings.Where(x
				=> x is Corpse
				&& pawn.CanReserveAndReach(x, PathEndMode.OnCell, Danger.Some)
				&& !x.IsForbidden(pawn)
				);

			foreach (Corpse target in targets)
			{
				if (!Pather_Utility.cells_to_target_rape(pawn, target.Position))
					continue;// too far

				// Filter out rotters if not necrophile.
				if (!xxx.is_necrophiliac(pawn) && target.CurRotDrawMode != RotDrawMode.Fresh)
					continue;

				float fuc = SexAppraiser.would_fuck(pawn, target, false, false);

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

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

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

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

			return chosentarget;
		}

		protected override Job TryGiveJob(Pawn pawn)
		{
			// Most checks are done in ThinkNode_ConditionalNecro.

			// filter out necro for nymphs
			if (!RJWSettings.necrophilia_enabled) return null;

			if (pawn.Drafted) return null;

			//--ModLog.Message(" JobGiver_ViolateCorpse::TryGiveJob for ( " + xxx.get_pawnname(pawn) + " )");
			if (SexUtility.ReadyForLovin(pawn) || xxx.is_hornyorfrustrated(pawn))
			{
				//--ModLog.Message(" JobGiver_ViolateCorpse::TryGiveJob, can love ");
				if (!xxx.can_rape(pawn)) return null;

				var target = find_corpse(pawn, pawn.Map);
				//--ModLog.Message(" JobGiver_ViolateCorpse::TryGiveJob - target is " + (target == null ? "NULL" : "Found"));
				if (target != null)
				{
					return JobMaker.MakeJob(xxx.RapeCorpse, target);
				}
				// Ticks should only be increased after successful sex.
			}

			return null;
		}
	}
}


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

namespace rjw
{
	public class MentalState_RandomRape : SexualMentalState
	{

		public override void PostStart(string reason)
		{
			base.PostStart(reason);
			this.pawn.mindState.canLovinTick = -1;
		}
		public override bool ForceHostileTo(Thing t)
		{
			/*
			//planning random raper hostile to other colonists. but now, injured raper wont rape.
			if ((this.pawn.jobs != null) &&
				(this.pawn.jobs.curDriver != null) &&
				(this.pawn.jobs.curDriver as JobDriver_Rape != null))
			{
				return true;
			}*/
			return false;
		}

		public override bool ForceHostileTo(Faction f)
		{
			/*
			if ((this.pawn.jobs != null) &&
				(this.pawn.jobs.curDriver != null) &&
				(this.pawn.jobs.curDriver as JobDriver_Rape != null))
			{
				return true;
			}*/
			return false;
		}

		public override RandomSocialMode SocialModeMax()
		{
			return RandomSocialMode.Off;
		}
	}
}


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

namespace rjw
{
	public class SexualMentalState : MentalState
	{
		public override void MentalStateTick()
		{
			if (this.pawn.IsHashIntervalTick(150))
			{
				if (xxx.is_satisfied(pawn))
					if (!(pawn.jobs?.curDriver is JobDriver_Sex))
					{
						this.RecoverFromState();
						return;
					}
			}
			base.MentalStateTick();
		}
	}

	public class SexualMentalStateWorker : MentalStateWorker
	{
		public override bool StateCanOccur(Pawn pawn)
		{
			if (base.StateCanOccur(pawn))
			{
				return xxx.is_human(pawn) && xxx.can_rape(pawn) && xxx.is_hornyorfrustrated(pawn);
			}
			else
			{
				return false;
			}
		}
	}

	public class SexualMentalBreakWorker : MentalBreakWorker
	{
		public override float CommonalityFor(Pawn pawn, bool moodCaused = false)
		{
			if (xxx.is_human(pawn))
			{
				var need_sex = pawn.needs.TryGetNeed<Need_Sex>();
				if (need_sex != null)
					return base.CommonalityFor(pawn) * (def as SexualMentalBreakDef).commonalityMultiplierBySexNeed.Evaluate(need_sex.CurLevelPercentage * 100f);
				else
					return 0;
			}
			else
			{
				return 0;
			}
		}
	}
	public class SexualMentalStateDef : MentalStateDef
	{
	}
	public class SexualMentalBreakDef : MentalBreakDef
	{
		public SimpleCurve commonalityMultiplierBySexNeed;
	}
}


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

namespace rjw
{
	/// <summary>
	/// Helper class for ChJees's Androids mod.
	/// </summary>
	[StaticConstructorOnStartup]
	public static class AndroidsCompatibility
	{
		public static Type androidCompatType;
		public static readonly string typeName = "Androids.SexualizeAndroidRJW";
		private static bool foundType;

		static AndroidsCompatibility()
		{
			try
			{
				androidCompatType = Type.GetType(typeName);
				foundType = true;
				//Log.Message("Found Type: Androids.SexualizeAndroidRJW");
			}
			catch
			{
				foundType = false;
				//Log.Message("Did NOT find Type: Androids.SexualizeAndroidRJW");
			}
		}

		/*private static bool TestPredicate(DefModExtension extension)
		{
			if (extension == null)
				return false;

			Log.Message($"Predicate: {extension} : {extension.GetType()?.FullName}");
			return extension.GetType().FullName == typeName;
		}*/

		public static bool IsAndroid(ThingDef def)
		{
			if (def == null || !foundType)
			{
				return false;
			}

			return def.modExtensions != null && def.modExtensions.Any(extension => extension.GetType().FullName == typeName);
		}

		public static bool IsAndroid(Thing thing)
		{
			return IsAndroid(thing.def);
		}

		public static bool AndroidPenisFertility(Pawn pawn)
		{
			//androids only fertile with archotech parts
			BodyPartRecord Part = Genital_Helper.get_genitalsBPR(pawn);
			return (pawn.health.hediffSet.hediffs.Any((Hediff hed) =>
				(hed.Part == Part) &&
				(hed.def == Genital_Helper.archotech_penis)
				));
		}

		public static bool AndroidVaginaFertility(Pawn pawn)
		{
			//androids only fertile with archotech parts
			BodyPartRecord Part = Genital_Helper.get_genitalsBPR(pawn);
			return (pawn.health.hediffSet.hediffs.Any((Hediff hed) =>
				(hed.Part == Part) &&
				(hed.def == Genital_Helper.archotech_vagina)
				));
		}

	}
}


code:
using System;
using System.Text;
using System.Linq;
using UnityEngine;
using Verse;
using RimWorld;
using RimWorld.Planet;

namespace rjw.Modules.Benchmark
{
	// RimWorld only instantiates WorldComponents that exist, so if benchmarking
	// is not enabled, this class does not exist and never gets added to the game.
	// So, it wastes no resources at all.
#if BENCHMARK
	public class BenchmarkComponent : WorldComponent
	{
		const int loggingInterval = GenDate.TicksPerHour;
		const long maxNano = 1000L;
		const long maxMicro = maxNano * maxNano;
		const long maxMilli = maxNano * maxNano * maxNano;

		public BenchmarkComponent(World world) : base(world) { }

		public override void WorldComponentTick()
		{
			if (Find.TickManager.TicksGame % loggingInterval != 0) return;

			foreach (var record in BenchmarkCore.GetSamples().OrderBy((s) => s.methodName))
				LogResults(record);
		}

		private void LogResults(BenchyRecord record)
		{
			// Sort it so we can make some assumptions.
			record.samples.InsertionSort((l, r) => l.CompareTo(r));

			var methodName = record.methodName;
			var totalCalls = record.totalCalls;
			var length = record.samples.Length;
			var totalRuntime = record.samples.Sum();
			var averageNanoSecs = (float)record.samples.Average();
			var minNanoSecs = record.samples.First();
			var maxNanoSecs = record.samples.Last();
			(var loAvg, var hiAvg) = GetSplitPercentile(record.samples);

			StringBuilder sb = new();
			sb.Append("Benchy<").Append(methodName).Append(">(");
			sb.Append("avg: ").Append(ToUnitString(averageNanoSecs)).Append(", ");
			sb.Append("lo90%: ").Append(ToUnitString(loAvg)).Append(", ");
			sb.Append("hi10%: ").Append(ToUnitString(hiAvg)).Append(", ");
			sb.Append("min: ").Append(ToUnitString(minNanoSecs)).Append(", ");
			sb.Append("max: ").Append(ToUnitString(maxNanoSecs)).Append(")");
			sb.AppendLine().Append("  Total calls: ")
				.Append(totalCalls);
			sb.AppendLine().Append("  Total of last ").Append(length).Append(" samples: ")
				.Append(ToUnitString(totalRuntime));

			ModLog.Message(sb.ToString());
		}

		private (float, float) GetSplitPercentile(long[] samples)
		{
			var hiRange = Math.Max(Mathf.CeilToInt(0.1f * samples.Length), 1);
			var loRange = Math.Max(samples.Length - hiRange, 1);
			var hiAvg = (float)samples.TakeLast(hiRange).Average();
			var loAvg = (float)samples.Take(loRange).Average();
			return (loAvg, hiAvg);
		}

		private string ToUnitString(long nanoSecs) => nanoSecs switch
		{
			< maxNano => $"{nanoSecs}ns",
			_ => ToUnitString((float)nanoSecs)
		};

		private string ToUnitString(float nanoSecs) => nanoSecs switch
		{
			< maxNano => $"{nanoSecs:F3}ns",
			< maxMicro => $"{nanoSecs / maxNano:F3}µs",
			< maxMilli => $"{nanoSecs / maxMicro:F3}ms",
			_ => $"{nanoSecs / maxMilli:F3}s"
		};

	}
#endif
}


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

namespace rjw.Modules.Benchmark
{
#if BENCHMARK
	/// <summary>
	/// <para>BENCHMARKING ENABLED!</para>
	/// <para>A disposable ref-struct that represents a running benchmark.</para>
	/// <para>You must enable the `BENCHMARK` compiler constant to enable this.</para>
	/// <example>
	/// <para>To benchmark a method, the simplest way is to place a `using` expression
	/// at the start of the method.  The unused variable is required by this form of
	/// `using`.</para>
	/// <code>
	/// // Pull in the benchy type at the top of the C# file.
	/// using Benchy = rjw.Modules.Benchmark.Benchy;
	///
	/// void MethodToTest()
	/// {
	///   using var _benchy = Benchy.Watch(nameof(MethodToTest));
	///   DoNormalMethodThings();
	/// }
	/// </code>
	/// <para>When the method returns, the ref-struct is disposed, which records the
	/// run time of the function.</para>
	/// <para>A `using` block also works, if you prefer that.  Then you don't even need
	/// to define a variable, if that bugs you.</para>
	/// </example>
	/// </summary>
	public ref struct Benchy
	{
		private string methodName;
		private int warmupCalls;
		private readonly Stopwatch stopwatch;

		private Benchy(string methodName, int warmupCalls)
		{
			this.methodName = methodName;
			this.warmupCalls = warmupCalls;

			stopwatch = new Stopwatch();
			stopwatch.Start();
		}

		public void Dispose()
		{
			stopwatch.Stop();
			BenchmarkCore.AddResult(methodName, warmupCalls, stopwatch.ElapsedTicks);
		}

		/// <summary>
		/// <para>BENCHMARKING ENABLED!</para>
		/// <para>Begins timing a method from this point until disposed.</para>
		/// <para>If your method is called rarely, set `warmupCalls` lower, as needed.</para>
		/// </summary>
		/// <param name="methodName">The name of the method.</param>
		/// <param name="warmupCalls">How many calls to skip before taking samples.</param>
		/// <returns>A benchy instance, representing a benchmark context.</returns>
		public static Benchy Watch(string methodName, int warmupCalls = 10) =>
			new(methodName, warmupCalls);
	}

	public struct BenchyRecord
	{
		/// <summary>
		/// The name of the method benchmarked.
		/// </summary>
		public string methodName;
		/// <summary>
		/// <para>The total number of calls seen thus far.</para>
		/// <para>Note: the number of samples may not match this value.</para>
		/// </summary>
		public long totalCalls;
		/// <summary>
		/// The recorded sample times, in integer nanoseconds.
		/// </summary>
		public long[] samples;

		public BenchyRecord(string methodName, long totalCalls, long[] samples)
		{
			this.methodName = methodName;
			this.totalCalls = totalCalls;
			this.samples = samples;
		}
	}

	public static class BenchmarkCore
	{
		const int MAX_SAMPLES = 1000;
		static readonly long nanoSecsPerTick = 1000L * 1000L * 1000L / Stopwatch.Frequency;

		/// <summary>
		/// <para>Stores the samples of the benchmarked methods.</para>
		/// <para>The results are stored in integer nanoseconds.</para>
		/// </summary>
		static readonly Dictionary<string, Queue<long>> samples = new();

		/// <summary>
		/// <para>Stores the number of times each method was called.</para>
		/// </summary>
		static readonly Dictionary<string, long> callCounts = new();

		public static void AddResult(string methodName, int warmupCalls, long ticks)
		{
			var totalCalls = callCounts.GetWithFallback(methodName, 0L) + 1L;
			callCounts.SetOrAdd(methodName, totalCalls);

			// Let a few calls go without logging.  This warms up the jitter.
			if (totalCalls <= warmupCalls) return;

			// The call was too fast to really get a good read.  Use a minimum.
			ticks = ticks == 0L ? 1L : ticks;

			var resultStore = samples.TryGetValue(methodName, out var q) ? q : new();

			// Drop samples if we have too many.
			while (resultStore.Count >= MAX_SAMPLES)
				resultStore.Dequeue();

			resultStore.Enqueue(ticks * nanoSecsPerTick);

			samples.SetOrAdd(methodName, resultStore);
		}

		/// <summary>
		/// <para>Yields the current results of benchmarks.</para>
		/// <para>The `samples` are copied arrays, so are safe to manipulate.</para>
		/// </summary>
		/// <returns>An enumerable of benchmark records.</returns>
		public static IEnumerable<BenchyRecord> GetSamples()
		{
			foreach (var kvp in samples)
			{
				// Only bother if we have at least one sample.
				if (kvp.Value.Count == 0) continue;
				var totalCalls = callCounts.GetWithFallback(kvp.Key, 0L);
				yield return new BenchyRecord(kvp.Key, totalCalls, kvp.Value.ToArray());
			}
		}

	}
#else
	/// <summary>
	/// <para>BENCHMARKING DISABLED!</para>
	/// <para>Dummy ref-struct used when benchmarking is disabled.</para>
	/// <para>You must enable the `BENCHMARK` compiler constant to enable this.</para>
	/// <para>This allows for leaving `Benchy.Watch()` around the code with minimal
	/// effects on performance.</para>
	/// <para>But, you should remove them when you no longer intend to benchmark.</para>
	/// </summary>
	public ref struct Benchy
	{
		private Benchy(string methodName) { }
		public void Dispose() { }
		/// <summary>
		/// <para>BENCHMARKING DISABLED!</para>
		/// <para>Does nothing useful without the `BENCHMARK` compiler constant defined.</para>
		/// </summary>
		/// <param name="methodName">The name of the method.</param>
		/// <param name="warmupCalls">How many calls to skip before taking samples.</param>
		/// <returns>A new dummy instance that does nothing.</returns>
		public static Benchy Watch(string methodName, int warmupCalls = 10) =>
			new(methodName);
	}
#endif
}


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

// Adds options to the right-click menu for bondage gear to equip the gear on prisoners/downed pawns
namespace rjw
{
	public class CompBondageGear : CompUsable
	{
		public override IEnumerable<FloatMenuOption> CompFloatMenuOptions(Pawn pawn)
		{
			if ((pawn.Map != null) && (pawn.Map == Find.CurrentMap))// && (pawn.Map.mapPawns.PrisonersOfColonyCount > 0)
			{
				if (!pawn.CanReserve(parent))
					yield return new FloatMenuOption(FloatMenuOptionLabel(pawn) + " on (" + "Reserved".Translate() + ")", null, MenuOptionPriority.DisabledOption);
				else if (pawn.CanReach(parent, PathEndMode.Touch, Danger.Some))
					foreach (Pawn other in pawn.Map.mapPawns.AllPawns)
						if ((other != pawn) && other.Spawned && (other.Downed || other.IsPrisonerOfColony || xxx.is_slave(other)))
							yield return this.make_option(FloatMenuOptionLabel(pawn) + " on " + xxx.get_pawnname(other), pawn, other, (other.IsPrisonerOfColony || xxx.is_slave(other)) ? WorkTypeDefOf.Warden : null);
			}
		}
	}
}


code:
using RimWorld;
using Verse;

namespace rjw
{
	public class CompGetBondageGear : CompUseEffect
	{
		public override float OrderPriority
		{
			get
			{
				return -79;
			}
		}

		public override void DoEffect(Pawn p)
		{
			base.DoEffect(p);
			var app = parent as Apparel;
			if ((p.apparel != null) && (app != null))
				p.apparel.Wear(app);
		}
	}
}


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

namespace rjw
{
	public class CompHoloCryptoStamped : ThingComp
	{
		public string name;
		public string key;

		[SyncMethod]
		public string random_hex_byte()
		{
			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			var rv = Rand.RangeInclusive(0x00, 0xFF);
			var padding = (rv < 0x10) ? "0" : "";
			return padding + rv.ToString("X");
		}

		public override void Initialize(CompProperties pro)
		{
			name = NameGenerator.GenerateName(RulePackDef.Named("EngravedName"));
			key = "";
			for (int i = 0; i < 16; ++i)
				key += random_hex_byte();
		}

		public override void PostExposeData()
		{
			base.PostExposeData();
			Scribe_Values.Look<string>(ref name, "engraved_name");
			Scribe_Values.Look<string>(ref key, "cryptostamp");
		}

		public override bool AllowStackWith(Thing t)
		{
			return false;
		}

		public override string CompInspectStringExtra()
		{
			var inspect_engraving = "Engraved with the name \"" + name + "\"";
			var inspect_key = "Cryptostamp: " + key;
			return base.CompInspectStringExtra() + inspect_engraving + "\n" + inspect_key;
		}

		public override string TransformLabel(string lab)
		{
			return lab + " \"" + name + "\"";
		}

		public bool matches(CompHoloCryptoStamped other)
		{
			return String.Equals(key, other.key);
		}

		public void copy_stamp_from(CompHoloCryptoStamped other)
		{
			name = other.name;
			key = other.key;
		}
	}

	public class CompProperties_HoloCryptoStamped : CompProperties
	{
		public CompProperties_HoloCryptoStamped()
		{
			compClass = typeof(CompHoloCryptoStamped);
		}
	}
}


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

// Adds unlock options to right-click menu for holokeys.
namespace rjw
{
	public class CompStampedApparelKey : CompUsable
	{
		protected string make_label(Pawn pawn, Pawn other)
		{
			return FloatMenuOptionLabel(pawn) + " on " + ((other == null) ? "self" : xxx.get_pawnname(other));
		}

		public override IEnumerable<FloatMenuOption> CompFloatMenuOptions(Pawn pawn)
		{
			if (!pawn.CanReserve(parent))
				yield return new FloatMenuOption(FloatMenuOptionLabel(pawn) + " (" + "Reserved".Translate() + ")", null, MenuOptionPriority.DisabledOption);
			else if (pawn.CanReach(parent, PathEndMode.Touch, Danger.Some))
			{
				// Option for the pawn to use the key on themself
				if (!pawn.is_wearing_locked_apparel())
					yield return new FloatMenuOption("Not wearing locked apparel", null, MenuOptionPriority.DisabledOption);
				else
					yield return this.make_option(make_label(pawn, null), pawn, null, null);

				if ((pawn.Map != null) && (pawn.Map == Find.CurrentMap))
				{
					// Options for use on colonists
					foreach (var other in pawn.Map.mapPawns.FreeColonists)
						if ((other != pawn) && other.is_wearing_locked_apparel())
							yield return this.make_option(make_label(pawn, other), pawn, other, null);

					// Options for use on prisoners
					foreach (var prisoner in pawn.Map.mapPawns.PrisonersOfColony)
						if (prisoner.is_wearing_locked_apparel())
							yield return this.make_option(make_label(pawn, prisoner), pawn, prisoner, WorkTypeDefOf.Warden);

					// Options for use on corpses
					foreach (var q in pawn.Map.listerThings.ThingsInGroup(ThingRequestGroup.Corpse))
					{
						var corpse = q as Corpse;
						if (corpse.InnerPawn.is_wearing_locked_apparel())
							yield return this.make_option(make_label(pawn, corpse.InnerPawn), pawn, corpse, null);
					}
				}
			}
		}
	}
}


code:
using RimWorld;
using Verse;

namespace rjw
{
	public class CompUnlockBondageGear : CompUseEffect
	{
		public override float OrderPriority
		{
			get
			{
				return -69;
			}
		}

		public override void DoEffect(Pawn p)
		{
			base.DoEffect(p);
			var key_stamp = parent.GetComp<CompHoloCryptoStamped>();

			if ((key_stamp == null) || (p.MapHeld == null) || (p.apparel == null))
				return;

			Apparel locked_app = null;
			var any_locked = false;
			{
				foreach (var app in p.apparel.WornApparel)
				{
					var app_stamp = app.GetComp<CompHoloCryptoStamped>();
					if (app_stamp != null)
					{
						any_locked = true;
						if (app_stamp.matches(key_stamp))
						{
							locked_app = app;
							break;
						}
					}
				}
			}

			if (locked_app != null)
			{
				//locked_app.Notify_Stripped (p); // TODO This was removed. Necessary?

				p.apparel.Remove(locked_app);
				Thing dropped = null;
				GenThing.TryDropAndSetForbidden(locked_app, p.Position, p.MapHeld, ThingPlaceMode.Near, out dropped, false); //this will create a new key somehow.
				if (dropped != null)
				{
					Messages.Message("Unlocked " + locked_app.def.label, p, MessageTypeDefOf.SilentInput);
					IntVec3 keyPostition = parent.Position;
					parent.Destroy();
				}
				else if (PawnUtility.ShouldSendNotificationAbout(p))
				{
					Messages.Message("Couldn't drop " + locked_app.def.label, p, MessageTypeDefOf.NegativeEvent);
				}
			}
			else if (any_locked)
				Messages.Message("The key doesn't fit!", p, MessageTypeDefOf.NegativeEvent);
		}
	}
}


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

namespace rjw
{
	public class JobDriver_StruggleInBondageGear : JobDriver
	{
		public Apparel target_gear
		{
			get
			{
				return (Apparel)TargetA.Thing;
			}
		}

		public override bool TryMakePreToilReservations(bool errorOnFailed)
		{
			return this.pawn.Reserve(this.target_gear, this.job, 1, -1, null, errorOnFailed);
		}

		protected override IEnumerable<Toil> MakeNewToils()
		{
			yield return new Toil
			{
				initAction = delegate
				{
					pawn.pather.StopDead();
				},
				defaultCompleteMode = ToilCompleteMode.Delay,
				defaultDuration = 60
			};
			yield return new Toil
			{
				initAction = delegate
				{
					if (PawnUtility.ShouldSendNotificationAbout(pawn))
					{
						var pro = (pawn.gender == Gender.Male) ? "his" : "her";
						Messages.Message(xxx.get_pawnname(pawn) + " struggles to remove " + pro + " " + target_gear.def.label + ". It's no use!", pawn, MessageTypeDefOf.NegativeEvent);
					}
				},
				defaultCompleteMode = ToilCompleteMode.Instant
			};
		}
	}
}


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

namespace rjw
{
	public class JobDriver_UseItemOn : JobDriver_UseItem
	{
		public static Toil pickup_item(Pawn p, Thing item)
		{
			return new Toil
			{
				initAction = delegate
				{
					p.carryTracker.TryStartCarry(item, 1);
					if (item.Spawned) // If the item is still spawned that means the pawn failed to pick it up
						p.jobs.curDriver.EndJobWith(JobCondition.Incompletable);
				},
				defaultCompleteMode = ToilCompleteMode.Instant
			};
		}

		protected TargetIndex iitem = TargetIndex.A;

		protected TargetIndex itar = TargetIndex.B;

		protected Thing item
		{
			get
			{
				return base.job.GetTarget(iitem).Thing;
			}
		}

		protected Thing tar
		{
			get
			{
				return base.job.GetTarget(itar).Thing;
			}
		}

		protected override IEnumerable<Toil> MakeNewToils()
		{
			if (tar == null)
				foreach (var toil in base.MakeNewToils())
					yield return toil;
			else
			{
				// Find the pawn to use the item on.
				Pawn other;
				{
					var corpse = tar as Corpse;
					other = (corpse == null) ? (Pawn)tar : corpse.InnerPawn;
				}

				this.FailOnDespawnedNullOrForbidden(itar);
				if (!other.Dead)
					this.FailOnAggroMentalState(itar);
				yield return Toils_Reserve.Reserve(itar);

				if ((pawn.inventory != null) && pawn.inventory.Contains(item))
				{
					yield return Toils_Misc.TakeItemFromInventoryToCarrier(pawn, iitem);
				}
				else
				{
					yield return Toils_Reserve.Reserve(iitem);
					yield return Toils_Goto.GotoThing(iitem, PathEndMode.ClosestTouch).FailOnForbidden(iitem);
					yield return pickup_item(pawn, item);
				}

				yield return Toils_Goto.GotoThing(itar, PathEndMode.Touch);

				yield return new Toil
				{
					initAction = delegate
					{
						if (!other.Dead)
							PawnUtility.ForceWait(other, 60);
					},
					defaultCompleteMode = ToilCompleteMode.Delay,
					defaultDuration = 60
				};

				yield return new Toil
				{
					initAction = delegate
					{
						var effective_item = item;

						// Drop the item if it's some kind of apparel. This is because ApparelTracker.Wear only works properly
						// if the apparel to wear is spawned. (I'm just assuming that DoEffect for apparel wears it, which is
						// true for bondage gear)
						if ((effective_item as Apparel) != null)
						{
							Thing dropped_thing;
							if (pawn.carryTracker.TryDropCarriedThing(pawn.Position, ThingPlaceMode.Near, out dropped_thing))
								effective_item = dropped_thing as Apparel;
							else
							{
								ModLog.Error("Unable to drop " + effective_item.Label + " for use on " + xxx.get_pawnname(other) + " (apparel must be dropped before use)");
								effective_item = null;
							}
						}

						if (effective_item != null)
						{
							var eff = effective_item.TryGetComp<CompUseEffect>();
							if (eff != null)
								eff.DoEffect(other);
							else
								ModLog.Error("Unable to get CompUseEffect for use of " + effective_item.Label + " on " + xxx.get_pawnname(other) + " by " + xxx.get_pawnname(pawn));
						}
					},
					defaultCompleteMode = ToilCompleteMode.Instant
				};
			}
		}
	}
}


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

namespace rjw
{
	public class Recipe_InstallChastityBelt : Recipe_InstallImplant
	{
		public override IEnumerable<BodyPartRecord> GetPartsToApplyOn(Pawn p, RecipeDef r)
		{
			return base.GetPartsToApplyOn(p, r);
		}
	}

	public class Recipe_UnlockChastityBelt : Recipe_InstallImplant
	{
		public override IEnumerable<BodyPartRecord> GetPartsToApplyOn(Pawn p, RecipeDef r)
		{
			return base.GetPartsToApplyOn(p, r);
		}
	}
}


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

namespace rjw
{
	public class Recipe_ForceOffGear : Recipe_Surgery
	{
		public static bool is_wearing(Pawn p, ThingDef apparel_def)
		{
			if (p.apparel != null)
				foreach (var app in p.apparel.WornApparel)
					if (app.def == apparel_def)
						return true;
			return false;
		}

		public static BodyPartRecord find_part_record(BodyPartDef part_def, Pawn p)
		{
			return p.RaceProps.body.AllParts.Find((BodyPartRecord bpr) => bpr.def == part_def);
		}

		// Puts the recipe in the operations list only if "p" is wearing the relevant apparel. The little trick here is that yielding
		// null causes the game to put the recipe in the list but not actually apply it to a body part.
		public override IEnumerable<BodyPartRecord> GetPartsToApplyOn(Pawn p, RecipeDef generic_def)
		{
			var r = (force_off_gear_def)generic_def;
			if (is_wearing(p, r.removes_apparel))
				yield return null;
		}

		[SyncMethod]
		public static void apply_burns(Pawn p, List<BodyPartDef> parts, float min_severity, float max_severity)
		{
			foreach (var part in parts)
			{
				var rec = find_part_record(part, p);
				if (rec != null)
				{
					//Rand.PopState();
					//Rand.PushState(RJW_Multiplayer.PredictableSeed());
					var to_deal = Rand.Range(min_severity, max_severity) * part.GetMaxHealth(p);
					var dealt = 0.0f;
					var counter = 0;
					while ((counter < 100) && (dealt < to_deal) && (!p.health.hediffSet.PartIsMissing(rec)))
					{
						var dam = Rand.RangeInclusive(3, 5);
						p.TakeDamage(new DamageInfo(DamageDefOf.Burn, dam, 999, -1.0f, null, rec, null));
						++counter;
						dealt += (float)dam;
					}
				}
			}
		}

		[SyncMethod]
		public override void ApplyOnPawn(Pawn p, BodyPartRecord null_part, Pawn surgeon, List<Thing> ingredients,Bill bill)
		{
			var r = (force_off_gear_def)recipe;
			if ((surgeon != null) &&
				(p.apparel != null) &&
				(!CheckSurgeryFail(surgeon, p, ingredients, find_part_record(r.failure_affects, p),bill)))
			{
				// Remove apparel
				foreach (var app in p.apparel.WornApparel)
					if (app.def == r.removes_apparel)
					{
						p.apparel.Remove(app);
						break;
					}

				// Destroy parts
				//Rand.PopState();
				//Rand.PushState(RJW_Multiplayer.PredictableSeed());
				var def_to_destroy = r.destroys_one_of.RandomElement<BodyPartDef>();
				if (def_to_destroy != null)
				{
					var record_to_destroy = find_part_record(def_to_destroy, p);
					if (record_to_destroy != null)
					{
						var dam = (int)(1.5f * def_to_destroy.GetMaxHealth(p));
						p.TakeDamage(new DamageInfo(DamageDefOf.Burn, dam, 999, -1.0f, null, record_to_destroy, null));
					}
				}

				if (r.major_burns_on != null)
					apply_burns(p, r.major_burns_on, 0.30f, 0.60f);
				if (r.minor_burns_on != null)
					apply_burns(p, r.minor_burns_on, 0.15f, 0.35f);
			}
		}
	}
}


code:
using RimWorld;
using Verse;

namespace rjw
{
	public class ThoughtWorker_Bound : ThoughtWorker
	{
		protected override ThoughtState CurrentStateInternal(Pawn p)
		{
			if (p.apparel != null)
			{
				bool bound = false, gagged = false;
				foreach (var app in p.apparel.WornApparel)
				{
					var gear_def = app.def as bondage_gear_def;
					if (gear_def != null)
					{
						bound |= gear_def.gives_bound_moodlet;
						gagged |= gear_def.gives_gagged_moodlet;
					}
				}

				if (bound && gagged)
					return ThoughtState.ActiveAtStage(2);
				else if (gagged)
					return ThoughtState.ActiveAtStage(1);
				else if (bound)
					return ThoughtState.ActiveAtStage(0);
			}
			return ThoughtState.Inactive;
		}
	}
}


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

namespace rjw
{
	//public static class bondage_gear_tradeability
	//{
	//	public static void init()
	//	{
	//		// Allows bondage gear to be selled by traders
	//		if (xxx.config.bondage_gear_enabled)
	//		{
	//			foreach (var def in DefDatabase<bondage_gear_def>.AllDefs)
	//				def.tradeability = Tradeability.Sellable;
	//		}
	//		// Forbids bondage gear to be selled by traders
	//		else
	//		{
	//			foreach (var def in DefDatabase<bondage_gear_def>.AllDefs)
	//				def.tradeability = Tradeability.None;
	//		}
	//	}
	//}

	public static class bondage_gear_extensions
	{
		public static bool has_lock(this Apparel app)
		{
			return (app.TryGetComp<CompHoloCryptoStamped>() != null);
		}

		public static bool is_wearing_locked_apparel(this Pawn p)
		{
			if (p.apparel != null)
				foreach (var app in p.apparel.WornApparel)
					if (app.has_lock())
						return true;
			return false;
		}

		// Tries to get p started on the job of using an item on either another pawn or on themself (if "other" is null).
		// Of course in order for this method to work, the item's useJob has to be able to handle use on another pawn. This
		// is true for the holokey and bondage gear in RJW but not the items in the core game
		public static void start_job(this CompUsable usa, Pawn p, LocalTargetInfo tar)
		{
			if (p.CanReserveAndReach(usa.parent, PathEndMode.Touch, Danger.Some) &&
				((tar == null) || p.CanReserveAndReach(tar, PathEndMode.Touch, Danger.Some)))
			{
				var comfor = usa.parent.GetComp<CompForbiddable>();
				if (comfor != null)
					comfor.Forbidden = false;
				var job = JobMaker.MakeJob(((CompProperties_Usable)usa.props).useJob, usa.parent, tar);
				p.jobs.TryTakeOrderedJob(job);
			}
		}

		// Creates a menu option to use an item. "tar" is expected to be a pawn, corpse or null if it doesn't apply (in which
		// case the pawn will presumably use the item on themself). "required_work" can also be null.
		public static FloatMenuOption make_option(this CompUsable usa, string label, Pawn p, LocalTargetInfo tar, WorkTypeDef required_work)
		{
			if ((tar != null) && (!p.CanReserve(tar)))
			{
				string key = "Reserved";
				string text = TranslatorFormattedStringExtensions.Translate(key);
				return new FloatMenuOption(label + " (" + text + ")", null, MenuOptionPriority.DisabledOption);
			}
			else if ((tar != null) && (!p.CanReach(tar, PathEndMode.Touch, Danger.Some)))
			{
				string key = "NoPath";
				string text = TranslatorFormattedStringExtensions.Translate(key);
				return new FloatMenuOption(label + " (" + text + ")", null, MenuOptionPriority.DisabledOption);
			}
			else if ((required_work != null) && p.WorkTagIsDisabled(required_work.workTags))
			{
				string key = "CannotPrioritizeWorkTypeDisabled";
				string text = TranslatorFormattedStringExtensions.Translate(key, required_work.gerundLabel);
				return new FloatMenuOption(label + " (" + text + ")", null, MenuOptionPriority.DisabledOption);
			}
			else
				return new FloatMenuOption(
					label,
					delegate
					{
						usa.start_job(p, tar);
					},
					MenuOptionPriority.Default);
		}
	}

	public class bondage_gear_def : ThingDef
	{
		public Type soul_type;
		public HediffDef equipped_hediff = null;
		public bool gives_bound_moodlet = false;
		public bool gives_gagged_moodlet = false;
		public bool blocks_hands = false;
		public bool blocks_oral = false;
		public bool blocks_penis = false;
		public bool blocks_vagina = false;
		public bool blocks_anus = false;
		public bool blocks_breasts = false;
		private bondage_gear_soul soul_ins = null;

		public List<BodyPartDef> HediffTargetBodyPartDefs;      //field for optional list of targeted parts for hediff applying
		public List<BodyPartGroupDef> BoundBodyPartGroupDefs;   //field for optional list of groups restrained of verbcasting

		public bondage_gear_soul soul
		{
			get
			{
				if (soul_ins == null)
					soul_ins = (bondage_gear_soul)Activator.CreateInstance(soul_type);
				return soul_ins;
			}
		}
	}

	public class bondage_gear_soul
	{
		// Adds the bondage gear's associated HediffDef and spawns a matching holokey
		public virtual void on_wear(Pawn wearer, Apparel gear)
		{
			var def = (bondage_gear_def)gear.def;

			if (def.equipped_hediff != null && def.HediffTargetBodyPartDefs != null)
			{
				foreach (BodyPartDef partDef in def.HediffTargetBodyPartDefs)                           //getting BodyPartDef, for example "Arm"
				{
					foreach (BodyPartRecord partRec in wearer.RaceProps.body.GetPartsWithDef(partDef))  //applying hediff to every single arm found on pawn
					{
						wearer.health.AddHediff(def.equipped_hediff, partRec);
					}
				}
			}
			else if (def.equipped_hediff != null && def.HediffTargetBodyPartDefs == null)               //backward compatibility/simplified gear define without HediffTargetBodyPartDefs
			{                                                                                //Hediff applyed to whole body
				wearer.health.AddHediff(def.equipped_hediff);
			}

			var gear_stamp = gear.TryGetComp<CompHoloCryptoStamped>();
			if (gear_stamp != null)
			{
				var key = ThingMaker.MakeThing(ThingDef.Named("Holokey"));
				var key_stamp = key.TryGetComp<CompHoloCryptoStamped>();
				key_stamp.copy_stamp_from(gear_stamp);
				if (wearer.Map != null)
					GenSpawn.Spawn(key, wearer.Position, wearer.Map);
				else
					wearer.inventory.TryAddItemNotForSale(key);
			}
		}

		// Removes the gear's HediffDef
		public virtual void on_remove(Apparel gear, Pawn former_wearer)
		{
			var def = (bondage_gear_def)gear.def;
			if (def.equipped_hediff != null && def.HediffTargetBodyPartDefs != null)
			{                                                                                //getting all Hediffs according with equipped_hediff def
				List<Hediff> hediffs = former_wearer.health.hediffSet.hediffs.Where(x => x.def == def.equipped_hediff).ToList();
				foreach (Hediff hedToRemove in hediffs)
				{
					if (def.HediffTargetBodyPartDefs.Contains(hedToRemove.Part.def))            //removing if applyed by this bondage_gear
						former_wearer.health.RemoveHediff(hedToRemove);                         //assuming there can be several different bondages
				}                                                                               //with the same equipped_hediff def
			}
			else if (def.equipped_hediff != null && def.HediffTargetBodyPartDefs == null)       //backward compatibility/simplified gear define without HediffTargetBodyPartDefs
			{
				var hed = former_wearer.health.hediffSet.GetFirstHediffOfDef(def.equipped_hediff);
				if (hed != null)
					former_wearer.health.RemoveHediff(hed);
			}
		}
	}

	// Give bondage gear an extremely low score when it's not being worn so pawns never equip it on themselves and give
	// it an extremely high score when it is being worn so pawns never try to take it off to equip something "better".
	public class bondage_gear : Apparel
	{
		// made this method universal for any bondage_gear, won't affect anything if gear's BoundBodyPartGroupDefs is empty or null
		public override bool AllowVerbCast(Verb verb)
		{
			if ((this.def as bondage_gear_def).BoundBodyPartGroupDefs != null &&
				verb.tool != null &&
				(this.def as bondage_gear_def).BoundBodyPartGroupDefs.Contains(verb.tool.linkedBodyPartsGroup))
			{
				return false;
			}
			return true;
		}

		//needed for save compatibility only
		public override void ExposeData()
		{
			base.ExposeData();
			//if (Scribe.mode == LoadSaveMode.PostLoadInit)
			//	CheckHediffs();
		}

		//save compatibility insurance, will prevent Armbinder hediff with 0part efficiency on whole body
		private void CheckHediffs()
		{
			var def = (bondage_gear_def)this.def;
			if (this.Wearer == null || def.equipped_hediff == null) return;

			bool changedHediff = false;
			void ApplyHediffDirect(HediffDef hedDef, BodyPartRecord partRec)
			{
				Hediff hediff = (Hediff)HediffMaker.MakeHediff(hedDef, Wearer);
				if (partRec != null)
					hediff.Part = partRec;
				Wearer.health.hediffSet.AddDirect(hediff);
				changedHediff = true;
			}

			void RemoveHediffDirect(Hediff hed)
			{
				Wearer.health.hediffSet.hediffs.Remove(hed);
				changedHediff = true;
			}

			List<bondage_gear> wornBondageGear = Wearer.apparel.WornApparel.Where(x => x is bondage_gear).Cast<bondage_gear>().ToList();
			List<Hediff> hediffs = new List<Hediff>();
			foreach (Hediff h in this.Wearer.health.hediffSet.hediffs) hediffs.Add(h);

			//checking current hediffs defined by bondage_gear for being on defined place, cleaning up the misplaced
			bool equippedHediff;
			bool onPlace;
			foreach (Hediff hed in hediffs)
			{
				equippedHediff = false;
				onPlace = false;
				foreach (bondage_gear gear in wornBondageGear)
				{
					if (hed.def == (gear.def as bondage_gear_def).equipped_hediff)
					{
						//if hediff from bondage_gear and on it's defined place then don't touch it else remove
						//assuming there can be several different bondages with the same equipped_hediff def and different hediff target parts, don't know why
						equippedHediff = true;

						if ((hed.Part != null && (gear.def as bondage_gear_def).HediffTargetBodyPartDefs == null))
						{
							//pass
						}
						else if ((hed.Part == null && (gear.def as bondage_gear_def).HediffTargetBodyPartDefs == null))
						{
							onPlace = true;
							break;
						}
						else if (hed.Part != null && (gear.def as bondage_gear_def).HediffTargetBodyPartDefs.Contains(hed.Part.def))
						{
							onPlace = true;
							break;
						}
					}
				}
				if (equippedHediff && !onPlace)
				{
					ModLog.Message("Removing Hediff " + hed.Label + " from " + Wearer + (hed.Part == null ? "'s body" : "'s " + hed.Part));
					RemoveHediffDirect(hed);
				}
			}

			// now iterating every gear for having all hediffs in place, adding missing
			foreach (bondage_gear gear in wornBondageGear)
			{
				if ((gear.def as bondage_gear_def).equipped_hediff == null) continue;
				if ((gear.def as bondage_gear_def).HediffTargetBodyPartDefs == null)                        //handling gear without HediffTargetBodyPartDefs
				{

					Hediff hed = Wearer.health.hediffSet.hediffs.Find(x => (x.def == (gear.def as bondage_gear_def).equipped_hediff && x.Part == null));//checking hediff defined by gear on whole body
					if (hed == null)                                                                        //if no legit hediff, adding
					{
						ModLog.Message("Adding missing Hediff " + (gear.def as bondage_gear_def).equipped_hediff.label + " to " + Wearer + "'s body");
						ApplyHediffDirect((gear.def as bondage_gear_def).equipped_hediff, null);
					}
				}
				else                                                                                //handling gear with defined HediffTargetBodyPartDefs
				{
					foreach (BodyPartDef partDef in (gear.def as bondage_gear_def).HediffTargetBodyPartDefs)    //getting every partDef
					{
						foreach (BodyPartRecord partRec in Wearer.RaceProps.body.GetPartsWithDef(partDef))      //checking all parts of def for applyed hediff
						{
							Hediff hed = Wearer.health.hediffSet.hediffs.Find(x => (x.def == (gear.def as bondage_gear_def).equipped_hediff && x.Part == partRec));//checking hediff defined by gear on defined place
							if (hed == null)                                                                    //if hediff missing, adding
							{
								ModLog.Message("Adding missing Hediff " + (gear.def as bondage_gear_def).equipped_hediff.label + " to " + Wearer + "'s " + partRec.Label);
								ApplyHediffDirect((gear.def as bondage_gear_def).equipped_hediff, partRec);
							}
						}
					}
				}
				//possibility of several different items with THE SAME equipped_hediff def ON THE SAME PART is not considered, that's sick
			}
			if (changedHediff)
			{
				//Possible error in current toil on Notify_HediffChanged() if capacity.Manipulation is involved so making it in the end.
				//Probably harmless, shouldn't happen again after corrected hediffs will be saved
				Wearer.health.Notify_HediffChanged(null);
			}
		}
	}

	public class armbinder : bondage_gear
	{
		// Prevents pawns in armbinders from melee attacking
		//public override bool AllowVerbCast (IntVec3 root, TargetInfo targ)
		//{
		//	return false;
		//}
	}

	public class yoke : bondage_gear
	{
		// Prevents pawns in armbinders from melee attacking
		//public override bool AllowVerbCast (IntVec3 root, TargetInfo targ)
		//{
		//	return false;
		//}
	}

	public class Restraints : bondage_gear
	{
		// Prevents pawns in armbinders from melee attacking
		//public override bool AllowVerbCast (IntVec3 root, TargetInfo targ)
		//{
		//	return false;
		//}
	}

	public class force_off_gear_def : RecipeDef
	{
		public ThingDef removes_apparel;
		public BodyPartDef failure_affects;
		public List<BodyPartDef> destroys_one_of = null;
		public List<BodyPartDef> major_burns_on = null;
		public List<BodyPartDef> minor_burns_on = null;
	}
}


code:
namespace rjw.Modules.Genitals.Enums
{
	public enum Gender
	{
		Male,
		Female,
		Trap,
		Futa,

		MaleOvi,
		FemaleOvi,

		Unknown
	}
}


code:
using rjw.Modules.Genitals.Enums;
using rjw.Modules.Interactions.Objects;
using rjw.Modules.Shared.Extensions;
using rjw.Modules.Shared.Logs;
using System.Linq;

namespace rjw.Modules.Genitals.Helpers
{
	public class GenderHelper
	{
		private static ILog _log = LogManager.GetLogger<GenderHelper>();

		public static Gender GetGender(InteractionPawn pawn)
		{
			bool hasVagina = pawn.Parts.Vaginas.Any();
			bool hasPenis = pawn.Parts.Penises.Any();
			bool hasOviFemale = pawn.Parts.FemaleOvipositors.Any();
			bool hasOviMale = pawn.Parts.MaleOvipositors.Any();
			bool hasBreasts = pawn.HasBigBreasts();

			Gender result = Gender.Unknown;

			if (hasVagina && !hasPenis)
			{
				result = Gender.Female;
			}
			else
			if (hasPenis && hasVagina)
			{
				result = Gender.Futa;
			}
			else
			if (hasPenis && hasBreasts)
			{
				result = Gender.Trap;
			}
			else
			if (hasPenis)
			{
				result = Gender.Male;
			}
			else
			if (hasOviMale)
			{
				result = Gender.MaleOvi;
			}
			else
			if (hasOviFemale)
			{
				result = Gender.FemaleOvi;
			}
			else
			if (pawn.Pawn.gender == Verse.Gender.Male)
			{
				result = Gender.Male;
			}
			else
			if (pawn.Pawn.gender == Verse.Gender.Female)
			{
				result = Gender.Female;
			}

			_log.Debug($"{pawn.Pawn.GetName()} detected as {result}");

			return result;
		}
	}
}


code:
namespace rjw.Modules.Interactions.Contexts
{
	public class InteractionContext
	{
		public InteractionInputs Inputs { get; set; }
		public InteractionInternals Internals { get; set; }
		public InteractionOutputs Outputs { get; set; }

		public InteractionContext() { }
		public InteractionContext(InteractionInputs inputs)
		{
			Inputs = inputs;
			Internals = new InteractionInternals();
			Outputs = new InteractionOutputs();
		}
	}
}


code:
using Verse;

namespace rjw.Modules.Interactions.Contexts
{
	public class InteractionInputs
	{
		public Pawn Initiator { get; set; }
		public Pawn Partner { get; set; }

		public bool IsRape { get; set; }
		public bool IsWhoring { get; set; }
	}
}


code:
using rjw.Modules.Interactions.Enums;
using rjw.Modules.Interactions.Objects;

namespace rjw.Modules.Interactions.Contexts
{
	/// <summary>
	/// All the stuff stored in there shouldn't be used by other modules ... hence the "internal"
	/// </summary>
	public class InteractionInternals
	{
		public InteractionPawn Dominant { get; set; }
		public InteractionPawn Submissive { get; set; }

		public InteractionType InteractionType { get; set; }

		public bool IsReverse { get; set; }

		public InteractionWithExtension Selected { get; set; }

		public InteractionInternals()
		{
		}
	}
}


code:
using rjw.Modules.Interactions.Objects;

namespace rjw.Modules.Interactions.Contexts
{
	public class InteractionOutputs
	{
		public Interaction Generated { get; set; }

		public InteractionOutputs()
		{
			Generated = new Interaction();
		}
	}
}


code:
using RimWorld;
using Verse;

namespace rjw.Modules.Interactions.Contexts
{
	public class SpecificInteractionInputs
	{
		public Pawn Initiator { get; set; }
		public Pawn Partner { get; set; }

		public InteractionDef Interaction { get; set; }
	}
}


code:
using rjw.Modules.Interactions.Defs;
using rjw.Modules.Interactions.Defs.DefFragment;
using rjw.Modules.Interactions.Enums;
using System;
using System.Collections.Generic;
using System.Linq;
using Verse;

namespace rjw.Modules.Interactions.DefModExtensions
{
	public class InteractionSelectorExtension : DefModExtension
	{
		public List<RulePackDef> rulepacks = new List<RulePackDef>();
		public List<InteractionTag> tags = new List<InteractionTag>();
		public InteractionRequirement dominantRequirement;
		public InteractionRequirement submissiveRequirement;
		public string customRequirementHandler;
	}
}


code:
using rjw.Modules.Interactions.Defs;
using rjw.Modules.Genitals.Enums;
using rjw.Modules.Shared.Enums;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace rjw.Modules.Interactions.Defs.DefFragment
{
	public class InteractionRequirement
	{
		//Default = false
		public bool hand = false;
		public bool tail = false;
		public bool foot = false;
		public bool mouth = false;
		public bool beak = false;
		public bool mouthORbeak = false;
		public bool oral = false;
		public bool tongue = false;

		///Default = 1
		public Nullable<int> minimumCount = 1;

		///Default = 1
		///@ed86 - nullables are good for you ! don't use default values :b
		public Nullable<float> minimumSeverity;

		//Default = empty
		public List<string> partProps = new List<string>();

		//Default = empty
		public List<GenitalTag> tags = new List<GenitalTag>();

		//Default = empty
		public List<GenitalFamily> families = new List<GenitalFamily>();

		//Default = PawnState.Healthy
		public List<PawnState> pawnStates = new List<PawnState>();

		//overrides - edging, spanking?
		//public bool canOrgasm = true; // true - orgasm satisfy need, false - ruin orgasm/ dont satisfy need
		//public bool canCum = true; // orgasm with cum effects -semen, pregnancy etc
		//public float Sensetivity = 1.0f;
	}
}


code:
using RimWorld;
using rjw.Modules.Interactions.DefModExtensions;
using rjw.Modules.Interactions.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace rjw.Modules.Interactions.Defs
{
	public class InteractionDefOf
	{
		public static IList<InteractionWithExtension> Interactions { get; set; }

		public static InteractionWithExtension DefaultConsensualSex => Interactions
			.Where(e => e.Interaction.defName == "Sex_Vaginal")
			.FirstOrDefault();
		public static InteractionWithExtension DefaultWhoringSex => Interactions
			.Where(e => e.Interaction.defName == "Sex_Vaginal")
			.FirstOrDefault();

		public static InteractionWithExtension DefaultRapeSex => Interactions
			.Where(e => e.Interaction.defName == "Rape_Vaginal")
			.FirstOrDefault();

		public static InteractionWithExtension DefaultMechImplantSex => Interactions
			.Where(e => e.Interaction.defName == "Rape_MechImplant")
			.FirstOrDefault();

		public static InteractionWithExtension DefaultNecrophiliaSex => Interactions
			.Where(e => e.Interaction.defName == "Rape_Vaginal")
			.FirstOrDefault();

		public static InteractionWithExtension DefaultBestialitySex => Interactions
			.Where(e => e.Interaction.defName == "Bestiality_Vaginal")
			.FirstOrDefault();

		public static InteractionWithExtension DefaultAnimalSex => Interactions
			.Where(e => e.Interaction.defName == "Bestiality_Vaginal")
			.FirstOrDefault();

		static InteractionDefOf()
		{
			Interactions = DefDatabase<InteractionDef>.AllDefs
				.Where(def => def.HasModExtension<InteractionSelectorExtension>())
				.Where(def => def.HasModExtension<InteractionExtension>())
				.Select(def => new InteractionWithExtension
				{
					Interaction = def,
					Extension = def.GetModExtension<InteractionExtension>(),
					SelectorExtension = def.GetModExtension<InteractionSelectorExtension>()
				})
				.Where(defWithExtensions => ParseHelper.FromString<xxx.rjwSextype>(defWithExtensions.Extension.rjwSextype) != xxx.rjwSextype.None)
				.ToList();
		}
	}
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace rjw.Modules.Interactions.Enums
{
	public enum InteractionTag
	{
		Reverse,

		Fertilization,
		EggFertilization,
		EggLaying,

		Masturbation,
		Rape,
		Whoring,
		Consensual,
		Bestiality,
		Necrophilia,
		Animal,
		MechImplant
	}
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace rjw.Modules.Interactions.Enums
{
	public enum InteractionType
	{
		Masturbation,
		Consensual,
		Whoring,
		Rape,
		Bestiality,
		Animal,
		Necrophilia,
		Mechanoid
	}
}


code:
namespace rjw.Modules.Interactions.Enums
{
	public enum LewdablePartKind
	{
		Unsupported,

		Penis,
		Vagina,
		Anus,
		FemaleOvipositor,
		MaleOvipositor,
		Breasts,
		Udders,	// Unsupported

		Hand,
		Foot,
		Tail,
		Mouth,
		Beak,
		Tongue
	}
}


code:
using KTrie;
using rjw.Modules.Interactions.DefModExtensions;
using rjw.Modules.Interactions.Objects;
using rjw.Modules.Shared.Logs;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
using Verse;

namespace rjw.Modules.Interactions.Exposable
{
	public class HediffWithExtensionExposable : IExposable
	{
		private static ILog _log = LogManager.GetLogger<HediffWithExtensionExposable>();

		public Hediff hediff;

		public ISexPartHediff GenitalPart
		{
			get
			{
				return hediff as ISexPartHediff;
			}
		}

		public void ExposeData()
		{
			Scribe_References.Look(ref hediff, nameof(hediff));
		}

		/// <summary>
		/// Dummy method to not break build pending removal of unused submodule
		/// </summary>
		/// <param name="self"></param>
		/// <returns></returns>
		public static ISexPartHediff Convert(HediffWithExtensionExposable self)
		{
			_log.Debug(self.ToString());

			return self.hediff as ISexPartHediff;
		}

		/// <summary>
		/// Dummy method to not break build pending removal of unused submodule
		/// </summary>
		/// <param name="self"></param>
		/// <returns></returns>
		public static HediffWithExtensionExposable Convert(ISexPartHediff self)
		{
			return new HediffWithExtensionExposable()
			{
				hediff = self.AsHediff
			};
		}

		public override string ToString()
		{
			StringBuilder stringBuilder = new StringBuilder();

			stringBuilder.AppendLine($"{nameof(hediff)} = {hediff?.def.defName}");

			return stringBuilder.ToString();
		}
	}
}


code:
using rjw.Modules.Interactions.Enums;
using rjw.Modules.Interactions.Objects;
using rjw.Modules.Interactions.Objects.Parts;
using rjw.Modules.Shared.Extensions;
using rjw.Modules.Shared.Logs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Verse;

namespace rjw.Modules.Interactions.Exposable
{
	public class InteractionExposable : IExposable
	{
		private static ILog _log = LogManager.GetLogger<InteractionExposable>();

		private InteractionType interactionType;

		private bool isReverse;

		private InteractionPawnExposable initiator;
		private InteractionPawnExposable receiver;

		private InteractionWithExtensionExposable interactionWithExtension;

		private RulePackDef rulePack;

		private xxx.rjwSextype RjwSexType
		{
			get
			{
				return ParseHelper.FromString<xxx.rjwSextype>(interactionWithExtension.Extension.rjwSextype);
			}
		}

		private List<RJWLewdablePartExposable> selectedDominantParts_RJW;
		private List<VanillaLewdablePartExposable> selectedDominantParts_Vanilla;

		private List<RJWLewdablePartExposable> selectedSubmissiveParts_RJW;
		private List<VanillaLewdablePartExposable> selectedSubmissiveParts_Vanilla;

		public void ExposeData()
		{
			Scribe_Values.Look(ref interactionType, nameof(interactionType));

			Scribe_Values.Look(ref isReverse, nameof(isReverse));

			Scribe_Deep.Look(ref initiator, nameof(initiator));
			Scribe_Deep.Look(ref receiver, nameof(receiver));

			Scribe_Deep.Look(ref interactionWithExtension, nameof(interactionWithExtension));

			Scribe_Defs.Look(ref rulePack, nameof(rulePack));

			Scribe_Collections.Look(ref selectedDominantParts_RJW, nameof(selectedDominantParts_RJW));
			Scribe_Collections.Look(ref selectedDominantParts_Vanilla, nameof(selectedDominantParts_Vanilla));

			Scribe_Collections.Look(ref selectedSubmissiveParts_RJW, nameof(selectedSubmissiveParts_RJW));
			Scribe_Collections.Look(ref selectedSubmissiveParts_Vanilla, nameof(selectedSubmissiveParts_Vanilla));
		}

		public static Interaction Convert(InteractionExposable toCast)
		{
			return new Interaction()
			{
				InteractionType = toCast.interactionType,

				Initiator = InteractionPawnExposable.Convert(toCast.initiator),
				Receiver = InteractionPawnExposable.Convert(toCast.receiver),

				Dominant = InteractionPawnExposable.Convert(toCast.initiator),
				Submissive = InteractionPawnExposable.Convert(toCast.receiver),

				InteractionDef = InteractionWithExtensionExposable.Convert(toCast.interactionWithExtension),

				RjwSexType = toCast.RjwSexType,
				RulePack = toCast.rulePack,

				SelectedDominantParts = Enumerable.Concat(
					toCast.selectedDominantParts_RJW.Select(RJWLewdablePartExposable.Convert).OfType<ILewdablePart>(),
					toCast.selectedDominantParts_Vanilla.Select(VanillaLewdablePartExposable.Convert).OfType<ILewdablePart>()
					).ToList(),

				SelectedSubmissiveParts = Enumerable.Concat(
					toCast.selectedSubmissiveParts_RJW.Select(RJWLewdablePartExposable.Convert).OfType<ILewdablePart>(),
					toCast.selectedSubmissiveParts_Vanilla.Select(VanillaLewdablePartExposable.Convert).OfType<ILewdablePart>()
					).ToList()
			};
		}
		public static InteractionExposable Convert(Interaction toCast)
		{
			return new InteractionExposable()
			{
				interactionType = toCast.InteractionType,

				initiator = InteractionPawnExposable.Convert(toCast.Initiator),
				receiver = InteractionPawnExposable.Convert(toCast.Receiver),

				interactionWithExtension = InteractionWithExtensionExposable.Convert(toCast.InteractionDef),

				rulePack = toCast.RulePack,

				selectedDominantParts_RJW = toCast.SelectedDominantParts
					.OfType<RJWLewdablePart>()
					.Select(RJWLewdablePartExposable.Convert)
					.ToList(),
				selectedDominantParts_Vanilla = toCast.SelectedDominantParts
					.OfType<VanillaLewdablePart>()
					.Select(VanillaLewdablePartExposable.Convert)
					.ToList(),

				selectedSubmissiveParts_RJW = toCast.SelectedDominantParts
					.OfType<RJWLewdablePart>()
					.Select(RJWLewdablePartExposable.Convert)
					.ToList(),
				selectedSubmissiveParts_Vanilla = toCast.SelectedDominantParts
					.OfType<VanillaLewdablePart>()
					.Select(VanillaLewdablePartExposable.Convert)
					.ToList()
			};
		}

		public override string ToString()
		{
			StringBuilder stringBuilder = new StringBuilder();

			stringBuilder.AppendLine($"{nameof(interactionType)} = {interactionType}");
			stringBuilder.AppendLine($"{nameof(initiator)} = {initiator?.pawn.GetName()}");
			stringBuilder.AppendLine($"{nameof(receiver)} = {receiver?.pawn.GetName()}");

			stringBuilder.AppendLine($"{nameof(interactionWithExtension)} = {interactionWithExtension?.interactionDef.defName}");

			stringBuilder.AppendLine($"{nameof(rulePack)} = {rulePack?.defName}");

			return stringBuilder.ToString();
		}
	}
}


code:
using rjw.Modules.Interactions.Enums;
using rjw.Modules.Interactions.Objects;
using rjw.Modules.Shared.Extensions;
using rjw.Modules.Shared.Logs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Verse;

namespace rjw.Modules.Interactions.Exposable
{
	public class InteractionPawnExposable : IExposable
	{
		private static ILog _log = LogManager.GetLogger<InteractionPawnExposable>();

		public Pawn pawn;

		public List<BodyPartRecord> mouths;
		public List<BodyPartRecord> beaks;
		public List<BodyPartRecord> tongues;
		public List<BodyPartRecord> hands;
		public List<BodyPartRecord> tails;

		public List<HediffWithExtensionExposable> penises;
		public List<HediffWithExtensionExposable> vaginas;
		public List<HediffWithExtensionExposable> breasts;
		public List<HediffWithExtensionExposable> anuses;

		public List<HediffWithExtensionExposable> femaleOvipositors;
		public List<HediffWithExtensionExposable> maleOvipositors;

		public List<LewdablePartKind> blockedParts;
		public Dictionary<LewdablePartKind, float> partPreferences;

		public void ExposeData()
		{
			Scribe_References.Look(ref pawn, nameof(pawn));

			Scribe_Collections.Look(ref beaks, nameof(beaks), LookMode.BodyPart);
			Scribe_Collections.Look(ref mouths, nameof(mouths), LookMode.BodyPart);
			Scribe_Collections.Look(ref tongues, nameof(tongues), LookMode.BodyPart);
			Scribe_Collections.Look(ref hands, nameof(hands), LookMode.BodyPart);
			Scribe_Collections.Look(ref tails, nameof(tails), LookMode.BodyPart);

			Scribe_Collections.Look(ref penises, nameof(penises), LookMode.Deep);
			Scribe_Collections.Look(ref vaginas, nameof(vaginas), LookMode.Deep);
			Scribe_Collections.Look(ref breasts, nameof(breasts), LookMode.Deep);
			Scribe_Collections.Look(ref anuses, nameof(anuses), LookMode.Deep);

			Scribe_Collections.Look(ref femaleOvipositors, nameof(femaleOvipositors), LookMode.Deep);
			Scribe_Collections.Look(ref maleOvipositors, nameof(maleOvipositors), LookMode.Deep);

			Scribe_Collections.Look(ref blockedParts, nameof(blockedParts), LookMode.Value);
			Scribe_Collections.Look(ref partPreferences, nameof(partPreferences), LookMode.Value, LookMode.Value);
		}

		public static InteractionPawn Convert(InteractionPawnExposable toCast)
		{
			return new InteractionPawn()
			{
				Pawn = toCast.pawn,
				Parts = new SexablePawnParts()
				{
					Mouths = toCast.mouths,
					Beaks = toCast.beaks,
					Tongues = toCast.tongues,
					Hands = toCast.hands,
					Tails = toCast.tails,

					Penises = toCast.penises.Select(HediffWithExtensionExposable.Convert).ToList(),
					Vaginas = toCast.vaginas.Select(HediffWithExtensionExposable.Convert).ToList(),
					Breasts = toCast.breasts.Select(HediffWithExtensionExposable.Convert).ToList(),
					Anuses = toCast.anuses.Select(HediffWithExtensionExposable.Convert).ToList(),

					FemaleOvipositors = toCast.femaleOvipositors.Select(HediffWithExtensionExposable.Convert).ToList(),
					MaleOvipositors = toCast.maleOvipositors.Select(HediffWithExtensionExposable.Convert).ToList(),
				},
				BlockedParts = toCast.blockedParts,
				PartPreferences = toCast.partPreferences
			};
		}
		public static InteractionPawnExposable Convert(InteractionPawn toCast)
		{
			return new InteractionPawnExposable()
			{
				pawn = toCast.Pawn,

				mouths = toCast.Parts.Mouths.ToList(),
				beaks = toCast.Parts.Beaks.ToList(),
				tongues = toCast.Parts.Tongues.ToList(),
				hands = toCast.Parts.Hands.ToList(),
				tails = toCast.Parts.Tails.ToList(),

				penises = toCast.Parts.Penises.Select(HediffWithExtensionExposable.Convert).ToList(),
				vaginas = toCast.Parts.Vaginas.Select(HediffWithExtensionExposable.Convert).ToList(),
				breasts = toCast.Parts.Breasts.Select(HediffWithExtensionExposable.Convert).ToList(),
				anuses = toCast.Parts.Anuses.Select(HediffWithExtensionExposable.Convert).ToList(),

				femaleOvipositors = toCast.Parts.FemaleOvipositors.Select(HediffWithExtensionExposable.Convert).ToList(),
				maleOvipositors = toCast.Parts.MaleOvipositors.Select(HediffWithExtensionExposable.Convert).ToList(),

				blockedParts = toCast.BlockedParts.ToList(),
				partPreferences = toCast.PartPreferences.ToDictionary(e => e.Key, e => e.Value)
			};
		}

		public override string ToString()
		{
			StringBuilder stringBuilder = new StringBuilder();

			stringBuilder.AppendLine($"{nameof(pawn)} = {pawn.GetName()}");

			stringBuilder.AppendLine($"{nameof(blockedParts)} = {String.Join("/", blockedParts)}");
			stringBuilder.AppendLine($"{nameof(partPreferences)} = {String.Join("/", partPreferences.Select(e => $"{e.Key}-{e.Value}"))}");

			return stringBuilder.ToString();
		}
	}
}


code:
using RimWorld;
using rjw.Modules.Interactions.DefModExtensions;
using rjw.Modules.Interactions.Objects;
using rjw.Modules.Shared.Logs;
using System.Text;
using Verse;

namespace rjw.Modules.Interactions.Exposable
{
	public class InteractionWithExtensionExposable : IExposable
	{
		private static ILog _log = LogManager.GetLogger<InteractionWithExtensionExposable>();

		public InteractionDef interactionDef;

		public InteractionSelectorExtension InteractionSelectorExtension
		{
			get
			{
				return interactionDef.GetModExtension<InteractionSelectorExtension>();
			}
		}
		public InteractionExtension Extension
		{
			get
			{
				return interactionDef.GetModExtension<InteractionExtension>();
			}
		}

		public void ExposeData()
		{
			Scribe_Defs.Look(ref interactionDef, nameof(interactionDef));
		}

		public static InteractionWithExtension Convert(InteractionWithExtensionExposable toCast)
		{
			return new InteractionWithExtension()
			{
				Interaction = toCast.interactionDef,
				Extension = toCast.Extension,
				SelectorExtension = toCast.InteractionSelectorExtension
			};
		}
		public static InteractionWithExtensionExposable Convert(InteractionWithExtension toCast)
		{
			return new InteractionWithExtensionExposable()
			{
				interactionDef = toCast.Interaction
			};
		}

		public override string ToString()
		{
			StringBuilder stringBuilder = new StringBuilder();

			stringBuilder.AppendLine($"{nameof(interactionDef)} = {interactionDef?.defName}");

			return stringBuilder.ToString();
		}
	}
}


code:
using rjw.Modules.Interactions.Enums;
using rjw.Modules.Interactions.Objects.Parts;
using rjw.Modules.Shared.Logs;
using System.Collections.Generic;
using System.Text;
using Verse;

namespace rjw.Modules.Interactions.Exposable
{
	public class RJWLewdablePartExposable : IExposable
	{
		private static ILog _log = LogManager.GetLogger<RJWLewdablePartExposable>();

		public HediffWithExtensionExposable hediff;

		public LewdablePartKind partKind;

		public float Size => hediff.hediff.Severity;

		public IList<string> Props => (hediff.hediff.def as HediffDef_SexPart)?.partTags ?? new();

		public void ExposeData()
		{
			Scribe_Deep.Look(ref hediff, nameof(hediff));
			Scribe_Values.Look(ref partKind, nameof(partKind));
		}

		/// <summary>
		/// Dummy method to not break build pending removal of unused submodule
		/// </summary>
		/// <param name="toCast"></param>
		/// <returns></returns>
		public static RJWLewdablePart Convert(RJWLewdablePartExposable toCast)
		{
			return new RJWLewdablePart(
				toCast.hediff.hediff as ISexPartHediff,
				toCast.partKind
				);
		}
		public static RJWLewdablePartExposable Convert(RJWLewdablePart toCast)
		{
			return new RJWLewdablePartExposable()
			{
				hediff = HediffWithExtensionExposable.Convert(toCast.Part),
				partKind = toCast.PartKind
			};
		}

		public override string ToString()
		{
			StringBuilder stringBuilder = new StringBuilder();

			stringBuilder.AppendLine($"{nameof(partKind)} = {partKind}");

			return stringBuilder.ToString();
		}
	}
}


code:
using rjw.Modules.Interactions.Enums;
using rjw.Modules.Interactions.Objects.Parts;
using rjw.Modules.Shared.Logs;
using System.Text;
using Verse;

namespace rjw.Modules.Interactions.Exposable
{
	public class VanillaLewdablePartExposable : IExposable
	{
		private static ILog _log = LogManager.GetLogger<VanillaLewdablePartExposable>();

		public Pawn pawn;

		public BodyPartRecord part;

		public LewdablePartKind partKind;

		public void ExposeData()
		{
			Scribe_References.Look(ref pawn, nameof(pawn));
			Scribe_BodyParts.Look(ref part, nameof(part));
			Scribe_Values.Look(ref partKind, nameof(partKind));
		}

		public static VanillaLewdablePart Convert(VanillaLewdablePartExposable toCast)
		{
			return new VanillaLewdablePart(
					toCast.pawn,
					toCast.part,
					toCast.partKind
				);
		}
		public static VanillaLewdablePartExposable Convert(VanillaLewdablePart toCast)
		{
			return new VanillaLewdablePartExposable()
			{
				pawn = toCast.Owner,
				part = toCast.Part,
				partKind = toCast.PartKind
			};
		}

		public override string ToString()
		{
			StringBuilder stringBuilder = new StringBuilder();

			stringBuilder.AppendLine($"{nameof(partKind)} = {partKind}");

			return stringBuilder.ToString();
		}
	}
}


code:
using rjw.Modules.Interactions.Enums;
using rjw.Modules.Interactions.Objects;
using rjw.Modules.Interactions.Objects.Parts;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;

namespace rjw.Modules.Interactions.Extensions
{
	public static class EnumerableExtensions
	{
		public static IEnumerable<ILewdablePart> FilterSeverity(this IEnumerable<ILewdablePart> self, Nullable<float> severity)
		{
			IEnumerable<ILewdablePart> result = self;

			if (severity.HasValue == true)
			{
				result = Enumerable.Union(
					result.Where(e => e is VanillaLewdablePart),
					result.Where(e => e is RJWLewdablePart)
						.Where(e => (e as RJWLewdablePart).Part.AsHediff.Severity >= severity.Value)
					);
			}

			foreach (ILewdablePart part in result)
			{
				yield return part;
			}
		}

		public static IEnumerable<ILewdablePart> WithPartKind(this IEnumerable<ILewdablePart> self, LewdablePartKind partKind)
		{
			return self
				.Where(e => e.PartKind == partKind);
		}
		public static IEnumerable<ILewdablePart> WithPartTag(this IEnumerable<ILewdablePart> self, GenitalTag tag)
		{
			return self
				.OfType<RJWLewdablePart>()
				.Where(e => e.Part.Def.genitalTags.Contains(tag));
		}
		public static IEnumerable<ILewdablePart> WithPartKindAndTag(this IEnumerable<ILewdablePart> self, LewdablePartKind partKind, GenitalTag tag)
		{
			return self
				.WithPartKind(partKind)
				.WithPartTag(tag);
		}

		public static bool HasPartKind(this IEnumerable<ILewdablePart> self, LewdablePartKind partKind)
		{
			return self
				.WithPartKind(partKind)
				.Any();
		}
		public static bool HasPartTag(this IEnumerable<ILewdablePart> self, GenitalTag tag)
		{
			return self
				.WithPartTag(tag)
				.Any();
		}
		public static bool HasPartKindAndTag(this IEnumerable<ILewdablePart> self, LewdablePartKind partKind, GenitalTag tag)
		{
			return self
				.WithPartKindAndTag(partKind, tag)
				.Any();
		}

		public static IEnumerable<ISexPartHediff> BigBreasts(this IEnumerable<ISexPartHediff> self)
		{
			if (self == null)
			{
				return null;
			}

			return self
				.Where(e => e.Def.genitalFamily == GenitalFamily.Breasts)
				.Where(e => e.AsHediff.CurStageIndex >= 1);
		}

		public static bool HasBigBreasts(this IEnumerable<ISexPartHediff> self)
		{
			if (self == null)
			{
				return false;
			}

			return self
				.BigBreasts()
				.Any();
		}

		public static ISexPartHediff Largest(this IEnumerable<ISexPartHediff> self)
		{
			if (self == null)
			{
				return null;
			}

			return self
				.OrderByDescending(e => e.AsHediff.Severity)
				.FirstOrDefault();
		}
		public static ISexPartHediff Smallest(this IEnumerable<ISexPartHediff> self)
		{
			if (self == null)
			{
				return null;
			}

			return self
				.OrderBy(e => e.AsHediff.Severity)
				.FirstOrDefault();
		}

		public static ISexPartHediff GetBestSizeAppropriate(this IEnumerable<ISexPartHediff> self, ISexPartHediff toFit)
		{
			if (self == null)
			{
				return null;
			}

			return self
				.OrderBy(e => Mathf.Abs(e.AsHediff.Severity - toFit.AsHediff.Severity))
				.FirstOrDefault();
		}
	}
}


code:
using rjw.Modules.Interactions.Enums;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace rjw.Modules.Interactions.Extensions
{
	public static class GenitalFamilyExtension
	{
		public static LewdablePartKind ToPartKind(this GenitalFamily self)
		{
			switch (self)
			{
				case GenitalFamily.Vagina:
					return LewdablePartKind.Vagina;
				case GenitalFamily.Penis:
					return LewdablePartKind.Penis;
				case GenitalFamily.Breasts:
					return LewdablePartKind.Breasts;
				case GenitalFamily.Udders:
					return LewdablePartKind.Udders;
				case GenitalFamily.Anus:
					return LewdablePartKind.Anus;
				case GenitalFamily.FemaleOvipositor:
					return LewdablePartKind.FemaleOvipositor;
				case GenitalFamily.MaleOvipositor:
					return LewdablePartKind.MaleOvipositor;
				default:
					return LewdablePartKind.Unsupported;
			}
		}
	}
}


code:
using rjw.Modules.Interactions.Defs;
using rjw.Modules.Interactions.Enums;
using rjw.Modules.Interactions.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace rjw.Modules.Interactions.Extensions
{
	public static class HediffWithGenitalPartExtensions
	{
		//Vanilla
		public static IEnumerable<ISexPartHediff> Penises(this IEnumerable<ISexPartHediff> self) =>
			self.Where(e => e.Def.genitalFamily == GenitalFamily.Penis);
		public static IEnumerable<ISexPartHediff> Vaginas(this IEnumerable<ISexPartHediff> self) =>
			self.Where(e => e.Def.genitalFamily == GenitalFamily.Vagina);
		public static IEnumerable<ISexPartHediff> Breasts(this IEnumerable<ISexPartHediff> self) =>
			self.Where(e => e.Def.genitalFamily == GenitalFamily.Breasts);
		public static IEnumerable<ISexPartHediff> Udders(this IEnumerable<ISexPartHediff> self) =>
			self.Where(e => e.Def.genitalFamily == GenitalFamily.Udders);
		public static IEnumerable<ISexPartHediff> Anuses(this IEnumerable<ISexPartHediff> self) =>
			self.Where(e => e.Def.genitalFamily == GenitalFamily.Anus);

		//Ovi
		public static IEnumerable<ISexPartHediff> FemaleOvipositors(this IEnumerable<ISexPartHediff> self) =>
			self.Where(e => e.Def.genitalFamily == GenitalFamily.FemaleOvipositor);
		public static IEnumerable<ISexPartHediff> MaleOvipositors(this IEnumerable<ISexPartHediff> self) =>
			self.Where(e => e.Def.genitalFamily == GenitalFamily.MaleOvipositor);
	}
}


code:
using rjw.Modules.Interactions.Defs;
using rjw.Modules.Interactions.Enums;
using rjw.Modules.Interactions.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace rjw.Modules.Interactions.Extensions
{
	public static class InteractionWithExtensions
	{
		public static IEnumerable<InteractionWithExtension> Reverse(this IEnumerable<InteractionWithExtension> self)
		{
			return self
				.Where(interaction => interaction.HasInteractionTag(InteractionTag.Reverse));
		}
		public static IEnumerable<InteractionWithExtension> NonReverse(this IEnumerable<InteractionWithExtension> self)
		{
			return self
				.Where(interaction => interaction.HasInteractionTag(InteractionTag.Reverse) == false);
		}
		public static IEnumerable<InteractionWithExtension> FilterReverse(this IEnumerable<InteractionWithExtension> self, bool isReverse)
		{
			return self
				.Where(interaction => interaction.HasInteractionTag(InteractionTag.Reverse) == isReverse);
		}
	}
}


code:
using LudeonTK;
using RimWorld;
using rjw.Modules.Interactions.Objects;
using rjw.Modules.Shared.Extensions;
using System.Collections.Generic;
using System.Linq;
using Verse;

namespace rjw.Modules.Interactions.Extensions
{
	public static class PawnExtensions
	{
		public static SexablePawnParts GetSexablePawnParts(this Pawn self)
		{
			if (self == null)
			{
				return null;
			}

			//We get the genital parts once so we don't parse ALL the hediff all the time
			IList<ISexPartHediff> hediffWithGenitalParts = self.health.hediffSet.hediffs
				.OfType<ISexPartHediff>()
				.ToList();

			return new SexablePawnParts
			{
				Mouths = self.Mouths().WithoutMissing(self),
				Beaks = self.Beaks().WithoutMissing(self),
				Tongues = self.Tongues().WithoutMissing(self),
				Hands = self.Hands().WithoutMissing(self),
				Feet = self.Feet().WithoutMissing(self),
				Tails = self.Tails().WithoutMissing(self),

				AllParts = hediffWithGenitalParts,

				Penises = hediffWithGenitalParts.Penises(),
				Vaginas = hediffWithGenitalParts.Vaginas(),
				Breasts = hediffWithGenitalParts.Breasts(),
				Udders = hediffWithGenitalParts.Udders(),
				Anuses = hediffWithGenitalParts.Anuses(),

				FemaleOvipositors = hediffWithGenitalParts.FemaleOvipositors(),
				MaleOvipositors = hediffWithGenitalParts.MaleOvipositors()
			};
		}

		private static IList<BodyPartRecord> Mouths(this Pawn self)
		{
			//EatingSource = mouth
			return self.RaceProps.body.GetPartsWithTag(RimWorld.BodyPartTagDefOf.EatingSource)
				.Where(part => part.def.defName?.ToLower().Contains("beak") == false)
				.ToList();
		}

		private static IList<BodyPartRecord> Beaks(this Pawn self)
		{
			//EatingSource = mouth
			return self.RaceProps.body.GetPartsWithTag(RimWorld.BodyPartTagDefOf.EatingSource)
				.Where(part => part.def.defName?.ToLower().Contains("beak") == true)
				.ToList();
		}

		private static IList<BodyPartRecord> Tongues(this Pawn self)
		{
			BodyDef body = self.RaceProps.body;
			List<BodyPartRecord> possibleParts = body.GetPartsWithTag(RimWorld.BodyPartTagDefOf.Tongue);
			if (possibleParts.Count > 0)
			{
				return possibleParts;
			}

			// Vanilla animals don't have tongues
			possibleParts = body.GetPartsWithDef(RJWBodyPartDefOf.AnimalJaw);
			if (possibleParts.Count > 0)
			{
				return possibleParts;
			}

			// Fallback for races with custom tongue def
			return body.GetPartsWhereDefNameContains("tongue").ToList();
		}

		private static IList<BodyPartRecord> Tails(this Pawn self)
		{
			BodyDef body = self.RaceProps.body;
			List<BodyPartRecord> possibleParts = body.GetPartsWithDef(RJWBodyPartDefOf.Tail);
			if (possibleParts.Count > 0)
			{
				return possibleParts;
			}

			// Found at least one race with a custom tail def
			return body.GetPartsWhereDefNameContains("tail").ToList();
		}

		private static IList<BodyPartRecord> Hands(this Pawn self)
		{
			BodyDef body = self.RaceProps.body;
			List<BodyPartRecord> possibleParts = body.GetPartsWithDef(BodyPartDefOf.Hand);
			if (possibleParts.Count > 0)
			{
				return possibleParts;
			}

			// Some races just don't model hands
			possibleParts = body.GetPartsWithDef(BodyPartDefOf.Arm);
			if (possibleParts.Count > 0)
			{
				return possibleParts;
			}

			// At least one race only modelled shoulders
			possibleParts = body.GetPartsWithDef(BodyPartDefOf.Shoulder);
			if (possibleParts.Count > 0)
			{
				return possibleParts;
			}

			// Fallback for races with custom hand def
			return body.GetPartsWhereDefNameContains("hand").ToList();
		}

		private static IList<BodyPartRecord> Feet(this Pawn self)
		{
			BodyDef body = self.RaceProps.body;
			List<BodyPartRecord> possibleParts = body.GetPartsWithDef(RJWBodyPartDefOf.Foot);
			if (possibleParts.Count > 0)
			{
				return possibleParts;
			}

			// Animals have paws instead of feet
			possibleParts = body.GetPartsWithDef(RJWBodyPartDefOf.Paw);
			if (possibleParts.Count > 0)
			{
				return possibleParts;
			}

			// Don't know if there are races that don't model feet, but I'll add this
			possibleParts = body.GetPartsWithDef(RJWBodyPartDefOf.Leg);
			if (possibleParts.Count > 0)
			{
				return possibleParts;
			}

			// Fallback for races with custom foot def
			return body.GetPartsWhereDefNameContains("foot", "paw").ToList();
		}

		/// <summary>
		/// Find all BodyPartRecords where def name contains one of the strings.
		/// <br/>
		/// This method is slower than GetPartsWithTag and much slower than GetPartsWithDef.
		/// </summary>
		/// <param name="name">Strings to search in the def names. Lower case only</param>
		private static IEnumerable<BodyPartRecord> GetPartsWhereDefNameContains(this BodyDef body, params string[] name)
		{
			for (int i = 0; i < body.AllParts.Count; i++)
			{
				if (body.AllParts[i].def.defName?.ToLower().ContainsAny(name) == true)
				{
					yield return body.AllParts[i];
				}
			}
		}

		/// <summary>
		/// Create new list without the missing body parts
		/// </summary>
		/// <param name="self">List of body parts</param>
		/// <param name="owner">Pawn to check against</param>
		/// <returns>New list with filtered values from <paramref name="self"/></returns>
		private static IList<BodyPartRecord> WithoutMissing(this IList<BodyPartRecord> self, Pawn owner)
		{
			// body.GetPartsWithDef returns a ref to the cached list. Bad things will happen if we change it
			List<BodyPartRecord> result = new List<BodyPartRecord>();

			for (int i = 0; i < self.Count; i++)
			{
				if (!self[i].IsMissingForPawn(owner))
				{
					result.Add(self[i]);
				}
			}

			return result;
		}

		[DebugAction("RimJobWorld", "Dump SexablePawnParts", actionType = DebugActionType.ToolMapForPawns, allowedGameStates = AllowedGameStates.PlayingOnMap)]
		public static void DumpSexablePawnParts(Pawn pawn)
		{
			SexablePawnParts parts = pawn.GetSexablePawnParts();

			ModLog.Message($"Dumping SexablePawnParts for {pawn}");
			ModLog.Message($"Mouths ({parts.Mouths.Count()}):");
			foreach (var part in parts.Mouths)
			{
				ModLog.Message($"    {part}");
			}
			ModLog.Message($"Beaks ({parts.Beaks.Count()}):");
			foreach (var part in parts.Beaks)
			{
				ModLog.Message($"    {part}");
			}
			ModLog.Message($"Tongues ({parts.Tongues.Count()}):");
			foreach (var part in parts.Tongues)
			{
				ModLog.Message($"    {part}");
			}
			ModLog.Message($"Hands ({parts.Hands.Count()}):");
			foreach (var part in parts.Hands)
			{
				ModLog.Message($"    {part}");
			}
			ModLog.Message($"Feet ({parts.Feet.Count()}):");
			foreach (var part in parts.Feet)
			{
				ModLog.Message($"    {part}");
			}
			ModLog.Message($"Tails ({parts.Tails.Count()}):");
			foreach (var part in parts.Tails)
			{
				ModLog.Message($"    {part}");
			}

			ModLog.Message($"Penises ({parts.Penises.Count()}):");
			foreach (var part in parts.Penises)
			{
				ModLog.Message($"    {part}");
			}
			ModLog.Message($"Vaginas ({parts.Vaginas.Count()}):");
			foreach (var part in parts.Vaginas)
			{
				ModLog.Message($"    {part}");
			}
			ModLog.Message($"Breasts ({parts.Breasts.Count()}):");
			foreach (var part in parts.Breasts)
			{
				ModLog.Message($"    {part}");
			}
			ModLog.Message($"Udders ({parts.Udders.Count()}):");
			foreach (var part in parts.Udders)
			{
				ModLog.Message($"    {part}");
			}
			ModLog.Message($"Anuses ({parts.Anuses.Count()}):");
			foreach (var part in parts.Anuses)
			{
				ModLog.Message($"    {part}");
			}
			ModLog.Message($"FemaleOvipositors ({parts.FemaleOvipositors.Count()}):");
			foreach (var part in parts.FemaleOvipositors)
			{
				ModLog.Message($"    {part}");
			}
			ModLog.Message($"MaleOvipositors ({parts.MaleOvipositors.Count()}):");
			foreach (var part in parts.MaleOvipositors)
			{
				ModLog.Message($"    {part}");
			}

			ModLog.Message($"AllParts ({parts.AllParts.Count()}):");
			foreach (var part in parts.AllParts)
			{
				ModLog.Message($"    {part}");
			}
		}
	}
}


code:
using RimWorld;
using rjw.Modules.Interactions.DefModExtensions;
using rjw.Modules.Interactions.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace rjw.Modules.Interactions.Helpers
{
	public static class InteractionHelper
	{
		public static InteractionWithExtension GetWithExtension(InteractionDef interactionDef)
		{
			if (interactionDef.HasModExtension<InteractionSelectorExtension>() == false)
			{
				return null;
			}

			if (interactionDef.HasModExtension<InteractionExtension>() == false)
			{
				return null;
			}

			return new InteractionWithExtension
			{
				Interaction = interactionDef,
				Extension = interactionDef.GetModExtension<InteractionExtension>(),
				SelectorExtension = interactionDef.GetModExtension<InteractionSelectorExtension>()
			};
		}
	}
}


code:
using RimWorld;
using rjw.Modules.Interactions.DefModExtensions;
using rjw.Modules.Interactions.Enums;
using rjw.Modules.Interactions.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using rjw.Modules.Interactions.Extensions;
using rjw.Modules.Interactions.Objects.Parts;
using Verse;
using rjw.Modules.Interactions.Defs.DefFragment;

namespace rjw.Modules.Interactions.Helpers
{
	public static class PartHelper
	{
		private readonly static Internals.IPartFinderService _partFinderService;
		private readonly static Internals.IBlockedPartDetectorService _blockedPartDetectorService;

		static PartHelper()
		{
			_partFinderService = Internals.Implementation.PartFinderService.Instance;
			_blockedPartDetectorService = Internals.Implementation.BlockedPartDetectorService.Instance;
		}

		public static IList<ILewdablePart> FindParts(Pawn pawn, GenitalFamily family)
		{
			InteractionPawn interactionPawn = ToInteractionPawn(pawn);

			return _partFinderService.FindUnblockedForPawn(interactionPawn, family)
				.ToList();
		}
		public static IList<ILewdablePart> FindParts(Pawn pawn, GenitalFamily family, IList<string> props)
		{
			InteractionPawn interactionPawn = ToInteractionPawn(pawn);

			return _partFinderService.FindUnblockedForPawn(interactionPawn, family, props)
				.ToList();
		}
		public static IList<ILewdablePart> FindParts(Pawn pawn, GenitalTag tag)
		{
			InteractionPawn interactionPawn = ToInteractionPawn(pawn);

			return _partFinderService.FindUnblockedForPawn(interactionPawn, tag)
				.ToList();
		}
		public static IList<ILewdablePart> FindParts(Pawn pawn, GenitalTag tag, IList<string> props)
		{
			InteractionPawn interactionPawn = ToInteractionPawn(pawn);

			return _partFinderService.FindUnblockedForPawn(interactionPawn, tag, props)
				.ToList();
		}

		private static InteractionPawn ToInteractionPawn(Pawn pawn)
		{
			InteractionPawn interactionPawn = new InteractionPawn
			{
				Pawn = pawn,
				Parts = pawn.GetSexablePawnParts()
			};

			interactionPawn.BlockedParts = _blockedPartDetectorService.BlockedPartsForPawn(interactionPawn);

			return interactionPawn;
		}
	}
}


code:
using rjw.Modules.Interactions.Objects;

namespace rjw.Modules.Interactions
{
	public interface ICustomRequirementHandler
	{
		/// <summary>
		/// Will be used to match the field <see cref="InteractionSelectorExtension.customRequirementHandler"/>
		/// </summary>
		string HandlerKey { get; }

		bool FufillRequirements(InteractionWithExtension interaction, InteractionPawn dominant, InteractionPawn submissive);
	}
}


code:
using rjw.Modules.Interactions.Enums;
using rjw.Modules.Interactions.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace rjw.Modules.Interactions.Internals
{
	public interface IInteractionRequirementService
	{
		bool FufillRequirements(InteractionWithExtension interaction, InteractionPawn dominant, InteractionPawn submissive);
	}
}


code:
using rjw.Modules.Interactions.Contexts;

namespace rjw.Modules.Interactions
{
	public interface ILewdInteractionService
	{
		InteractionOutputs GenerateInteraction(InteractionInputs inputs);
	}
}


code:
using rjw.Modules.Interactions.Contexts;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RimWorld;
using Verse;

namespace rjw.Modules.Interactions
{
	public interface ILewdInteractionValidatorService
	{
		bool IsValid(InteractionDef interaction, Pawn dominant, Pawn submissive);
	}
}