code:
using Verse;
using Verse.AI;

namespace rjw
{
	public abstract class JobDriver_SexBaseInitiator : JobDriver_Sex
	{
		public readonly TargetIndex iTarget = TargetIndex.A;
		public new Pawn Target => (Pawn)(job.GetTarget(iTarget));

		public void Start()
		{
			if (Target.jobs?.curDriver is JobDriver_SexBaseReciever)
			{
				(Target.jobs.curDriver as JobDriver_SexBaseReciever).parteners.AddDistinct(pawn);
				(Target.jobs.curDriver as JobDriver_SexBaseReciever).increase_time(duration);
			}
			//prevent Receiver standing up and interrupting rape
			if (Target.health.hediffSet.HasHediff(HediffDef.Named("Hediff_Submitting")))
				Target.health.AddHediff(HediffDef.Named("Hediff_Submitting"));
		}

		//public void Change()
		//{
		//	if (pawn.jobs?.curDriver is JobDriver_SexBaseInitiator)
		//	{
		//		(pawn.jobs.curDriver as JobDriver_SexBaseInitiator).increase_time(duration);
		//	}
		//	if (Target.jobs?.curDriver is JobDriver_SexBaseReciever)
		//	{
		//		(Target.jobs.curDriver as JobDriver_SexBaseReciever).increase_time(duration);
		//	}
		//}

		public void End()
		{
			if (Target.jobs?.curDriver is JobDriver_SexBaseReciever)
			{
				(Target.jobs.curDriver as JobDriver_SexBaseReciever).parteners.Remove(pawn);
			}
		}

		public override bool TryMakePreToilReservations(bool errorOnFailed)
		{
			Log.Message("shouldreserve " + shouldreserve);
			if (shouldreserve)
				return pawn.Reserve(Target, job, xxx.max_rapists_per_prisoner, stackCount, null, errorOnFailed);
			else
				return true; // No reservations needed.

			//return this.pawn.Reserve(this.Partner, this.job, 1, 0, null) && this.pawn.Reserve(this.Bed, this.job, 1, 0, null);
		}
	}
}


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

namespace rjw
{
	public class JobDriver_SexBaseReciever : JobDriver_Sex
	{
		public List<Pawn> parteners = new List<Pawn>();
		public bool face2face = false;

		public override void ExposeData()
		{
			base.ExposeData();
			Scribe_Values.Look(ref parteners, "parteners", new List<Pawn>(), false);
			Scribe_Values.Look(ref face2face, "face2face", false, false);
		}

		[SyncMethod]
		public void Setface2face(float chance = 0.3f)
		{
			face2face = Rand.Chance(chance);
		}

		//this needs overhaul... someday
		public void ChangeFacingDirection(Pawn Initiator, Pawn Receiver)
		{
			if (Initiator == null || Receiver == null) return;
			bool partnerHasHands = Receiver.health.hediffSet.GetNotMissingParts().Any(part => part.IsInGroup(BodyPartGroupDefOf.RightHand) || part.IsInGroup(BodyPartGroupDefOf.LeftHand));

			// Hand check is for monstergirls and other bipedal 'animals'.
			if ((!xxx.is_animal(Initiator) && partnerHasHands) || face2face) // 30% chance of face-to-face regardless, for variety.
			{ // Face-to-face
				Initiator.rotationTracker.Face(Receiver.DrawPos);
				Receiver.rotationTracker.Face(Initiator.DrawPos);
			}
			else
			{ // From behind / animal stuff should mostly use this
				Initiator.rotationTracker.Face(Receiver.DrawPos);
				Receiver.Rotation = Initiator.Rotation;
			}
			// TODO: The above works, but something is forcing the partners to face each other during sex. Need to figure it out.
		}
	}
}

repo_name: Mewtopian/rjw

license: mit


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

namespace rjw
{
	public class JobDriver_SexBaseRecieverLoved : JobDriver_SexBaseReciever
	{
		public readonly TargetIndex ipartner = TargetIndex.A;
		public readonly TargetIndex ibed = TargetIndex.B;

		public Pawn Partner => (Pawn)(job.GetTarget(ipartner));
		public new Building_Bed Bed => (Building_Bed)(job.GetTarget(ibed));

		protected override IEnumerable<Toil> MakeNewToils()
		{
			setup_ticks();
			parteners.Add(Partner);// add job starter, so this wont fail, before Initiator starts his job
			//--Log.Message("[RJW]JobDriver_GettinLoved::MakeNewToils is called");

			float partner_ability = xxx.get_sex_ability(Partner);

			// More/less hearts based on partner ability.
			if (partner_ability < 0.8f)
				ticks_between_thrusts += 100;
			else if (partner_ability > 2.0f)
				ticks_between_thrusts -= 25;

			// More/less hearts based on opinion.
			if (pawn.relations.OpinionOf(Partner) < 0)
				ticks_between_hearts += 50;
			else if (pawn.relations.OpinionOf(Partner) > 60)
				ticks_between_hearts -= 25;

			this.FailOnDespawnedOrNull(ipartner);
			this.FailOn(() => !Partner.health.capacities.CanBeAwake);

			if (Partner.CurJob.def == xxx.casual_sex)
			{
				this.FailOn(() => pawn.Drafted);
				this.KeepLyingDown(ibed);
				yield return Toils_Reserve.Reserve(ipartner, 1, 0);
				yield return Toils_Reserve.Reserve(ibed, Bed.SleepingSlotsCount, 0);

				Toil get_loved = Toils_LayDown.LayDown(ibed, true, false, false, false);
				get_loved.FailOn(() => Partner.CurJob.def != xxx.casual_sex);
				get_loved.defaultCompleteMode = ToilCompleteMode.Never;
				get_loved.socialMode = RandomSocialMode.Off;
				get_loved.AddPreTickAction(delegate
				{
					if (pawn.IsHashIntervalTick(ticks_between_hearts))
						MoteMaker.ThrowMetaIcon(pawn.Position, pawn.Map, ThingDefOf.Mote_Heart);
					if (pawn.IsHashIntervalTick(ticks_between_thrusts))
						xxx.sexTick(pawn, Partner, false);
				});
				get_loved.AddFinishAction(delegate
				{
					if (xxx.is_human(pawn))
						pawn.Drawer.renderer.graphics.ResolveApparelGraphics();
				});
				yield return get_loved;
			}
			else if (Partner.CurJob.def == xxx.whore_is_serving_visitors)
			{
				this.FailOn(() => Partner.CurJob == null);
				yield return Toils_Goto.GotoThing(ipartner, PathEndMode.OnCell);
				yield return Toils_Reserve.Reserve(ipartner, 1, 0);

				Toil get_loved = new Toil();
				get_loved.FailOn(() => (Partner.CurJob.def != xxx.whore_is_serving_visitors));
				get_loved.defaultCompleteMode = ToilCompleteMode.Never;
				get_loved.socialMode = RandomSocialMode.Off;
				get_loved.initAction = delegate
				{
					Setface2face();
					ChangeFacingDirection(Partner, pawn);
				};
				get_loved.AddPreTickAction(delegate
				{
					if (pawn.IsHashIntervalTick(ticks_between_hearts))
						MoteMaker.ThrowMetaIcon(pawn.Position, pawn.Map, ThingDefOf.Mote_Heart);
					if (pawn.IsHashIntervalTick(ticks_between_thrusts))
						xxx.sexTick(pawn, Partner, false);
				});
				get_loved.AddEndCondition(new Func<JobCondition>(() =>
				{
					if ((ticks_remaining <= 0) || (parteners.Count <= 0))
					{
						return JobCondition.Succeeded;
					}
					return JobCondition.Ongoing;
				}));
				get_loved.AddFinishAction(delegate
				{
					if (xxx.is_human(pawn))
						pawn.Drawer.renderer.graphics.ResolveApparelGraphics();
				});
				yield return get_loved;
			}
			else if (Partner.CurJob.def == xxx.bestialityForFemale)
			{
				this.FailOn(() => Partner.CurJob == null);
				yield return Toils_Goto.GotoThing(ipartner, PathEndMode.OnCell);
				yield return Toils_Reserve.Reserve(ipartner, 1, 0);

				Toil get_loved = new Toil();
				get_loved.FailOn(() => (Partner.CurJob.def != xxx.bestialityForFemale));
				get_loved.defaultCompleteMode = ToilCompleteMode.Never;
				get_loved.socialMode = RandomSocialMode.Off;
				get_loved.initAction = delegate
				{
					Setface2face();
					ChangeFacingDirection(Partner, pawn);
				};
				get_loved.AddPreTickAction(delegate
				{
					if (pawn.IsHashIntervalTick(ticks_between_hearts))
						MoteMaker.ThrowMetaIcon(pawn.Position, pawn.Map, ThingDefOf.Mote_Heart);
					if (pawn.IsHashIntervalTick(ticks_between_thrusts))
						xxx.sexTick(pawn, Partner, false);
				});
				get_loved.AddEndCondition(new Func<JobCondition>(() =>
				{
					if ((ticks_remaining <= 0) || (parteners.Count <= 0))
					{
						return JobCondition.Succeeded;
					}
					return JobCondition.Ongoing;
				}));
				get_loved.AddFinishAction(delegate
				{
					if (xxx.is_human(pawn))
						pawn.Drawer.renderer.graphics.ResolveApparelGraphics();
				});
				get_loved.socialMode = RandomSocialMode.Off;
				yield return get_loved;
			}
		}
	}
}


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

namespace rjw
{
	public class JobDriver_SexBaseRecieverRaped : JobDriver_SexBaseReciever
	{
		public Pawn Initiator => (Pawn)(job.GetTarget(TargetIndex.A));
		public Pawn Receiver => (Pawn)(job.GetTarget(TargetIndex.B));

		protected override IEnumerable<Toil> MakeNewToils()
		{
			setup_ticks();
			parteners.Add(Initiator);// add job starter, so this wont fail, before Initiator starts his job

			var get_raped = new Toil();
			get_raped.defaultCompleteMode = ToilCompleteMode.Never;
			get_raped.initAction = delegate
			{
				pawn.pather.StopDead();
				pawn.jobs.curDriver.asleep = false;

				SexUtility.BeeingRapedAlert(Initiator, Receiver);

				Setface2face();
				ChangeFacingDirection(Initiator, Receiver);
			};
			get_raped.tickAction = delegate
			{
				--ticks_remaining;
				if ((parteners.Count > 0) && (pawn.IsHashIntervalTick(ticks_between_hearts / parteners.Count)))
					MoteMaker.ThrowMetaIcon(pawn.Position, pawn.Map, xxx.mote_noheart);

			};
			get_raped.AddEndCondition(new Func<JobCondition>(() =>
			{
				if ((ticks_remaining <= 0) || (parteners.Count <= 0))
				{
					return JobCondition.Succeeded;
				}
				return JobCondition.Ongoing;
			}));
			get_raped.AddFinishAction(delegate
			{
				if (xxx.is_human(pawn))
					pawn.Drawer.renderer.graphics.ResolveApparelGraphics();

				if (Bed != null && pawn.Downed)
				{
					Job tobed = JobMaker.MakeJob(JobDefOf.Rescue, pawn, Bed);
					tobed.count = 1;
					Initiator.jobs.jobQueue.EnqueueFirst(tobed);
					//Log.Message(xxx.get_pawnname(Initiator) + ": job tobed:" + tobed);
				}
				else if (Receiver.HostileTo(Initiator))
					Receiver.health.AddHediff(HediffDef.Named("Hediff_Submitting"));
				else
					Receiver.stances.stunner.StunFor(600, pawn);

			});
			get_raped.socialMode = RandomSocialMode.Off;
			yield return get_raped;

		}
	}
}


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

namespace rjw
{
	public class JobDriver_JoinInBed : JobDriver_SexBaseInitiator
	{
		public readonly TargetIndex ipartner = TargetIndex.A;
		public readonly TargetIndex ibed = TargetIndex.B;

		public Pawn Partner => (Pawn)(job.GetTarget(ipartner));
		public new Building_Bed Bed => (Building_Bed)(job.GetTarget(ibed));

		public override bool TryMakePreToilReservations(bool errorOnFailed)
		{
			return pawn.Reserve(Partner, job, xxx.max_rapists_per_prisoner, 0, null, errorOnFailed);
		}

		protected override IEnumerable<Toil> MakeNewToils()
		{
			//Log.Message("[RJW]" + this.GetType().ToString() + "::MakeNewToils() called");

			setup_ticks();

			this.FailOnDespawnedOrNull(ipartner);
			this.FailOnDespawnedOrNull(ibed);
			this.FailOn(() => !Partner.health.capacities.CanBeAwake);
			this.FailOn(() => !(Partner.InBed() || xxx.in_same_bed(Partner, pawn)));
			this.FailOn(() => pawn.Drafted);
			yield return Toils_Reserve.Reserve(ipartner, xxx.max_rapists_per_prisoner, 0);
			yield return Toils_Goto.GotoThing(ipartner, PathEndMode.OnCell);

			Toil StartPartnerJob = new Toil();
			StartPartnerJob.defaultCompleteMode = ToilCompleteMode.Instant;
			StartPartnerJob.socialMode = RandomSocialMode.Off;
			StartPartnerJob.initAction = delegate
			{
				Job gettin_loved = JobMaker.MakeJob(xxx.gettin_loved, pawn, Bed);
				Partner.jobs.StartJob(gettin_loved, JobCondition.InterruptForced);
			};
			yield return StartPartnerJob;

			Toil do_lovin = new Toil();
			do_lovin.FailOn(() => Partner.CurJob.def != xxx.gettin_loved);
			do_lovin.defaultCompleteMode = ToilCompleteMode.Never;
			do_lovin.socialMode = RandomSocialMode.Off;
			do_lovin.initAction = delegate
			{
				usedCondom = CondomUtility.TryUseCondom(pawn) || CondomUtility.TryUseCondom(Partner);
				Start();
			};
			do_lovin.AddPreTickAction(delegate
			{
				--ticks_left;
				if (pawn.IsHashIntervalTick(ticks_between_hearts))
					MoteMaker.ThrowMetaIcon(pawn.Position, pawn.Map, ThingDefOf.Mote_Heart);
				pawn.GainComfortFromCellIfPossible();
				Partner.GainComfortFromCellIfPossible();
				xxx.reduce_rest(Partner, 1);
				xxx.reduce_rest(pawn, 2);
				if (ticks_left <= 0)
					ReadyForNextToil();
			});
			do_lovin.AddFinishAction(delegate
			{
				if (xxx.is_human(pawn))
					pawn.Drawer.renderer.graphics.ResolveApparelGraphics();
				//End();
			});
			yield return do_lovin;

			yield return new Toil
			{
				initAction = delegate
				{
					// Trying to add some interactions and social logs
					SexUtility.ProcessSex(pawn, Partner, usedCondom, rape: false, isCoreLovin: false, whoring: false);
				},
				defaultCompleteMode = ToilCompleteMode.Instant
			};
		}
	}
}


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

namespace rjw
{
	//Rape to Prisoner of QuestPrisonerWillingToJoin
	class JobGiver_AIRapePrisoner : ThinkNode_JobGiver
	{
		[SyncMethod]
		public static 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

			IEnumerable<Pawn> targets = m.mapPawns.AllPawns.Where(x
				=> x != pawn
				&& IsPrisonerOf(x, pawn.Faction)
				&& xxx.can_get_raped(x)
				&& pawn.CanReserveAndReach(x, PathEndMode.Touch, Danger.Some, xxx.max_rapists_per_prisoner, 0)
				&& !x.Position.IsForbidden(pawn)
				);

			foreach (Pawn target in targets)
			{
				if (!xxx.can_path_to_target(pawn, target.Position))
					continue;// too far

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

				if (fuc > min_fuckability)
					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_targetsFiltered = valid_targets.Where(x => x.Value >= avg_fuckability);

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

			return chosentarget;
		}

		protected override Job TryGiveJob(Pawn pawn)
		{
			//Log.Message("[RJW] JobGiver_AIRapePrisoner::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) called ");

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

			if (SexUtility.ReadyForLovin(pawn) || xxx.need_some_sex(pawn) > 1f)
			{
				// don't allow pawns marked as comfort prisoners to rape others
				if (xxx.is_healthy(pawn))
				{
					Pawn prisoner = find_victim(pawn, pawn.Map);

					if (prisoner != null)
					{
						//--Log.Message("[RJW] JobGiver_RandomRape::TryGiveJob( " + xxx.get_pawnname(p) + " ) - found victim " + xxx.get_pawnname(prisoner));
						return JobMaker.MakeJob(xxx.RapeRandom, prisoner);
					}
				}
			}

			return null;
		}

		protected static bool IsPrisonerOf(Pawn pawn,Faction faction)
		{
			if (pawn?.guest == null) return false;
			return pawn.guest.HostFaction == faction && pawn.guest.IsPrisoner;
		}
	}
}


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

namespace rjw
{
	/// <summary>
	/// Pawn tries to find animal to do loving/raping.
	/// </summary>
	public class JobGiver_Bestiality : ThinkNode_JobGiver
	{
		protected override Job TryGiveJob(Pawn pawn)
		{
			if (pawn.Drafted) return null;

			// Most checks are now done in ThinkNode_ConditionalBestiality

			if (!SexUtility.ReadyForLovin(pawn) && !xxx.is_frustrated(pawn))
				return null;

			Pawn target = BreederHelper.find_breeder_animal(pawn, pawn.Map);

			if (target == null) return null;

			if (xxx.can_rape(pawn))
			{
				return JobMaker.MakeJob(xxx.bestiality, target);
			}

			Building_Bed bed = pawn.ownership.OwnedBed;
			if (!xxx.can_be_fucked(pawn) || bed == null || !target.CanReach(bed, PathEndMode.OnCell, Danger.Some) || target.Downed) return null;

			// TODO: Should rename this to BestialityInBed or somesuch, since it's not limited to females.
			return JobMaker.MakeJob(xxx.bestialityForFemale, target, bed, bed.SleepPosOfAssignedPawn(pawn));
		}
	}
}


code:
using Verse;
using Verse.AI;

namespace rjw
{
	/// <summary>
	/// Attempts to give a breeding job to an eligible animal.
	/// </summary>
	public class JobGiver_Breed : ThinkNode_JobGiver
	{
		protected override Job TryGiveJob(Pawn animal)
		{
			//Log.Message("[RJW] JobGiver_Breed::TryGiveJob( " + xxx.get_pawnname(animal) + " ) called0" + (SexUtility.ReadyForLovin(animal)));

			if (!SexUtility.ReadyForLovin(animal))
				return null;

			if(xxx.is_healthy(animal) && xxx.can_rape(animal))
			{
				//search for desiganted target to sex
				if (animal.IsDesignatedBreedingAnimal())
				{
					Pawn designated_target = BreederHelper.find_designated_breeder(animal, animal.Map);
					if (designated_target != null)
					{
						return JobMaker.MakeJob(DefDatabase<JobDef>.GetNamed("Breed"), designated_target, animal);
					}
				}
			}
			return null;
		}
	}
}


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

namespace rjw
{
	public class JobGiver_ComfortPrisonerRape : ThinkNode_JobGiver
	{
		[SyncMethod]
		public static Pawn find_targetCP(Pawn pawn, Map m)
		{
			if (!DesignatorsData.rjwComfort.Any()) return null;

			float min_fuckability = 0.10f;							// Don't rape prisoners 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

			IEnumerable<Pawn> targets = DesignatorsData.rjwComfort.Where(x
				=> x != pawn
				&& xxx.can_get_raped(x)
				&& pawn.CanReserveAndReach(x, PathEndMode.Touch, Danger.Some, xxx.max_rapists_per_prisoner, 0)
				&& !x.IsForbidden(pawn)
				&& SexAppraiser.would_rape(pawn, x)
				);

			if (xxx.is_animal(pawn))
			{
				// Animals only consider targets they can see, instead of seeking them out.
				targets = targets.Where(x => pawn.CanSee(x)).ToList();
			}

			foreach (Pawn target in targets)
			{
				if (!xxx.can_path_to_target(pawn, target.Position))
					continue;// too far

				float fuc = 0f;
				if (xxx.is_animal(target))
					fuc = SexAppraiser.would_fuck_animal(pawn, target, true);
				else if (xxx.is_human(target))
					fuc = SexAppraiser.would_fuck(pawn, target, true);

				if (fuc > min_fuckability)
					valid_targets.Add(target, fuc);
			}

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

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

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

			return chosentarget;
		}

		protected override Job TryGiveJob(Pawn pawn)
		{
			//Log.Message("[RJW] JobGiver_ComfortPrisonerRape::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) called0");

			if (!RJWSettings.WildMode)
			{
				// don't allow pawns marked as comfort prisoners to rape others
				if (!xxx.is_healthy(pawn) || pawn.IsDesignatedComfort() || (!SexUtility.ReadyForLovin(pawn) && !xxx.is_frustrated(pawn))) return null;
			}

			if (pawn.Drafted) return null;

			//Log.Message("[RJW] JobGiver_ComfortPrisonerRape::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) called1");
			if (!xxx.can_rape(pawn)) return null;

			// It's unnecessary to include other job checks. Pawns seem to only look for new jobs when between jobs or layind down idle.
			if (!(pawn.jobs.curJob == null || pawn.jobs.curJob.def == JobDefOf.LayDown)) return null;

			//--Log.Message("[RJW] JobGiver_ComfortPrisonerRape::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) called2");
			// Faction check.
			if (!(pawn.Faction?.IsPlayer ?? false) && !pawn.IsPrisonerOfColony) return null;

			Pawn target = find_targetCP(pawn, pawn.Map);
			//--Log.Message("[RJW] JobGiver_ComfortPrisonerRape::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) called3 - (" + ((target == null) ? "no target found" : xxx.get_pawnname(target))+") is the prisoner");

			if (target == null) return null;

			//Log.Message("giving job to " + pawner + " with target " + target);
			if (xxx.is_animal(target))
				return JobMaker.MakeJob(xxx.bestiality, target);
			else
				return JobMaker.MakeJob(xxx.RapeCP, target);
		}
	}
}


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

namespace rjw
{
	public class JobGiver_DoFappin : ThinkNode_JobGiver
	{
		[SyncMethod]
		public virtual IntVec3 FindFapLocation(Pawn pawn)
		{
			IntVec3 position = pawn.Position;
			int bestPosition = -100;
			IntVec3 cell = pawn.Position;
			int maxDistance = 40;

			FloatRange temperature = pawn.ComfortableTemperatureRange();
			bool is_somnophile = xxx.has_quirk(pawn, "Somnophile");
			bool is_exhibitionist = xxx.has_quirk(pawn, "Exhibitionist");
			List<Pawn> all_pawns = pawn.Map.mapPawns.AllPawnsSpawned.Where(x
				=> x.Position.DistanceTo(pawn.Position) < 100
				&& xxx.is_human(x)
				&& x != pawn
				).ToList();

			//Log.Message("[RJW] Pawn is " + xxx.get_pawnname(pawn) + ", current cell is " + cell);

			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			List<IntVec3> random_cells = new List<IntVec3>();
			for (int loop = 0; loop < 50; ++loop)
			{
				random_cells.Add(position + IntVec3.FromVector3(Vector3Utility.HorizontalVectorFromAngle(Rand.Range(0, 360)) * Rand.RangeInclusive(1, maxDistance)));
			}

			random_cells = random_cells.Where(x
				=> x.Standable(pawn.Map)
				&& x.InAllowedArea(pawn)
				&& x.GetDangerFor(pawn, pawn.Map) != Danger.Deadly
				&& !x.ContainsTrap(pawn.Map)
				&& !x.ContainsStaticFire(pawn.Map)
				).Distinct().ToList();

			//Log.Message("[RJW] Found " + random_cells.Count + " valid cells.");

			foreach (IntVec3 random_cell in random_cells)
			{
				if (!xxx.can_path_to_target(pawn, random_cell))
					continue;// too far

				int score = 0;
				Room room = random_cell.GetRoom(pawn.Map);

				bool might_be_seen = all_pawns.Any(x
					=> GenSight.LineOfSight(x.Position, random_cell, pawn.Map)
					&& x.Position.DistanceTo(random_cell) < 50
					&& x.Awake()
					);

				if (is_exhibitionist)
				{
					if (might_be_seen)
						score += 5;
					else
						score -= 10;
				}
				else
				{
					if (might_be_seen)
						score -= 30;
				}
				if (is_somnophile) // Fap while Watching someone sleep. Not creepy at all!
				{
					if (all_pawns.Any(x
						=> GenSight.LineOfSight(random_cell, x.Position, pawn.Map)
						&& x.Position.DistanceTo(random_cell) < 6
						&& !x.Awake()
						))
						score += 50;
				}

				if (random_cell.GetTemperature(pawn.Map) > temperature.min && random_cell.GetTemperature(pawn.Map) < temperature.max)
					score += 20;
				else
					score -= 20;
				if (random_cell.Roofed(pawn.Map))
					score += 5;
				if (random_cell.HasEatSurface(pawn.Map))
					score += 5; // Hide in vegetation.
				if (random_cell.GetDangerFor(pawn, pawn.Map) == Danger.Some)
					score -= 25;
				else if (random_cell.GetDangerFor(pawn, pawn.Map) == Danger.None)
					score += 5;
				if (random_cell.GetTerrain(pawn.Map) == TerrainDefOf.WaterShallow ||
					random_cell.GetTerrain(pawn.Map) == TerrainDefOf.WaterMovingShallow ||
					random_cell.GetTerrain(pawn.Map) == TerrainDefOf.WaterOceanShallow)
					score -= 20;

				if (random_cell.GetThingList(pawn.Map).Any(x => x.def.IsWithinCategory(ThingCategoryDefOf.Corpses)) && !xxx.is_necrophiliac(pawn))
					score -= 20;
				if (random_cell.GetThingList(pawn.Map).Any(x => x.def.IsWithinCategory(ThingCategoryDefOf.Foods)))
					score -= 10;

				if (room == pawn.Position.GetRoom(pawn.MapHeld))
					score -= 10;
				if (room.PsychologicallyOutdoors)
					score += 5;
				if (room.isPrisonCell)
					score += 5;
				if (room.IsHuge)
					score -= 5;
				if (room.ContainedBeds.Any())
					score += 5;
				if (room.IsDoorway)
					score -= 10;
				if (!room.Owners.Any())
					score += 10;
				else if (room.Owners.Contains(pawn))
					score += 20;
				if (room.Role == RoomRoleDefOf.Bedroom || room.Role == RoomRoleDefOf.PrisonCell)
					score += 10;
				else if (room.Role == RoomRoleDefOf.Barracks || room.Role == RoomRoleDefOf.Laboratory || room.Role == RoomRoleDefOf.RecRoom)
					score += 2;
				else if (room.Role == RoomRoleDefOf.DiningRoom || room.Role == RoomRoleDefOf.Hospital || room.Role == RoomRoleDefOf.PrisonBarracks)
					score -= 5;
				if (room.GetStat(RoomStatDefOf.Cleanliness) < 0.01f)
					score -= 5;
				if (room.GetStat(RoomStatDefOf.GraveVisitingJoyGainFactor) > 0.1f)
					score -= 5;

				if (score <= bestPosition) continue;

				bestPosition = score;
				cell = random_cell;
			}

			return cell;

			//Log.Message("[RJW] Best cell is " + cell);
		}

		protected override Job TryGiveJob(Pawn pawn)
		{
			//--Log.Message("[RJW] JobGiver_DoFappin::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) called");

			if (pawn.Drafted) return null;

			if (!xxx.can_be_fucked(pawn) && !xxx.can_fuck(pawn)) return null;

			// Whores only fap if frustrated, unless imprisoned.
			if ((SexUtility.ReadyForLovin(pawn) && (!xxx.is_whore(pawn) || pawn.IsPrisoner )) || xxx.is_frustrated(pawn))
			{
				if (pawn.jobs.curDriver is JobDriver_LayDown && RJWPreferenceSettings.FapInBed)
				{
					Building_Bed bed = ((JobDriver_LayDown)pawn.jobs.curDriver).Bed;
					if (bed != null) return JobMaker.MakeJob(xxx.Masturbate_Bed, bed);
				}
				else if ((xxx.is_frustrated(pawn) || xxx.has_quirk(pawn, "Exhibitionist")) && RJWPreferenceSettings.FapEverywhere)
				{
					return JobMaker.MakeJob(xxx.Masturbate_Quick, FindFapLocation(pawn));
				}
			}
			return null;
		}
	}
}


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

namespace rjw
{
	public class JobGiver_JoinInBed : ThinkNode_JobGiver
	{
		private const int MaxDistanceSquaredToFuck = 10000;

		private static bool CanFuck(Pawn target)
		{
			return xxx.can_fuck(target) || xxx.can_be_fucked(target);
		}

		[SyncMethod]
		private static bool roll_to_skip(Pawn pawn, Pawn target, out float fuckability)
		{
			fuckability = SexAppraiser.would_fuck(pawn, target); // 0.0 to 1.0
			if (fuckability < RJWHookupSettings.MinimumFuckabilityToHookup)
			{
				if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] roll_to_skip(I, {xxx.get_pawnname(pawn)} won't fuck {xxx.get_pawnname(target)}), ({fuckability})");
				return false;
			}

			float reciprocity = xxx.is_animal(target) ? 1.0f : SexAppraiser.would_fuck(target, pawn);
			if (reciprocity < RJWHookupSettings.MinimumFuckabilityToHookup)
			{
				if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] roll_to_skip({xxx.get_pawnname(target)} won't fuck me, {xxx.get_pawnname(pawn)}), ({reciprocity})");
				return false;
			}

			float chance_to_skip = 0.9f - 0.7f * fuckability;
			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			return Rand.Value < chance_to_skip;
		}

		[SyncMethod]
		public static Pawn find_pawn_to_fuck(Pawn pawn, Map map)
		{
			string pawnName = xxx.get_pawnname(pawn);
			if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({pawnName}): starting.");

			bool pawnIsNympho = xxx.is_nympho(pawn);
			bool pawnCanPickAnyone = RJWSettings.WildMode || (pawnIsNympho && RJWHookupSettings.NymphosCanPickAnyone);
			bool pawnCanPickAnimals = (pawnCanPickAnyone || xxx.is_zoophile(pawn)) && RJWSettings.bestiality_enabled;

			if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({pawnName}): nympho:{pawnIsNympho}, ignores rules:{pawnCanPickAnyone}, zoo:{pawnCanPickAnimals}");

			if (!RJWHookupSettings.ColonistsCanHookup && pawn.IsFreeColonist && !pawnCanPickAnyone)
			{
				if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({pawnName}): is a colonist and colonist hookups are disabled in mod settings");
				return null;
			}

			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());

			// Check AllPawns, not just colonists, to include guests.
			List<Pawn> targets = map.mapPawns.AllPawns.Where(x
				=> x.InBed()
				&& x != pawn
				&& !x.Position.IsForbidden(pawn)
				&& xxx.IsTargetPawnOkay(x)
				&& CanFuck(x)
				&& x.Map == pawn.Map
				&& !x.HostileTo(pawn)
				//&& (pawnCanPickAnimals || !xxx.is_animal(x))
				&& !xxx.is_animal(x)
				&& (xxx.is_laying_down_alone(x) || xxx.in_same_bed(x, pawn))
				).ToList();

			if (!targets.Any())
			{
				if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({pawnName}): no eligible targets");
				return null;
			}

			if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({pawnName}): considering {targets.Count} targets");

			// find lover/partner on same map
			List<Pawn> partners = targets.Where(x
				=> pawn.relations.DirectRelationExists(PawnRelationDefOf.Lover, x)
				|| pawn.relations.DirectRelationExists(PawnRelationDefOf.Fiance, x)
				|| pawn.relations.DirectRelationExists(PawnRelationDefOf.Spouse, x)
				).ToList();

			if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({pawnName}): considering {partners.Count} partners");

			if (partners.Any())
			{
				partners.Shuffle(); //Randomize order.
				foreach (Pawn target in partners)
				{
					if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({pawnName}): checking lover {xxx.get_pawnname(target)}");

					if (pawn.Position.DistanceToSquared(target.Position) < MaxDistanceSquaredToFuck
						&& pawn.CanReserveAndReach(target, PathEndMode.OnCell, Danger.Some, 1, 0)
						&& target.CanReserve(pawn, 1, 0)
						&& SexAppraiser.would_fuck(pawn, target) > 0.1f
						&& SexAppraiser.would_fuck(target, pawn) > 0.1f)
					{
						if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({pawnName}): banging lover {xxx.get_pawnname(target)}");
						return target;
					}
				}
			}

			// No lovers around... see if the pawn fancies a hookup.  Nymphos and frustrated pawns always do!
			if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({pawnName}): no partners available.  checking canHookup");
			bool canHookup = pawnIsNympho || pawnCanPickAnyone || xxx.is_frustrated(pawn) || (xxx.is_horny(pawn) && Rand.Value < RJWHookupSettings.HookupChanceForNonNymphos);
			if (!canHookup)
			{
				if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({pawnName}): no hookup today");
				return null;
			}

			// No cheating from casual hookups... would probably make colony relationship management too annoying
			if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({pawnName}): checking canHookupWithoutCheating");
			bool hookupWouldBeCheating = xxx.HasNonPolyPartnerOnCurrentMap(pawn);
			if (hookupWouldBeCheating)
			{
				if (RJWHookupSettings.NymphosCanCheat && pawnIsNympho && xxx.is_frustrated(pawn))
				{
					if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({pawnName}): I'm a nympho and I'm so frustrated that I'm going to cheat");
					// No return here so they continue searching for hookup
				}
				else
				{
					if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({pawnName}): I want to bang but that's cheating");
					return null;
				}
			}

			Pawn best_fuckee = FindBestPartner(pawn, targets, pawnCanPickAnyone, pawnIsNympho);
			return best_fuckee;
		}

		/// <summary> Checks all of our potential partners to see if anyone's eligible, returning the most attractive and convenient one. </summary>
		protected static Pawn FindBestPartner(Pawn pawn, List<Pawn> targets, bool pawnCanPickAnyone, bool pawnIsNympho)
		{
			string pawnName = xxx.get_pawnname(pawn);

			Pawn best_fuckee = null;
			float best_fuckability_score = 0;

			foreach (Pawn targetPawn in targets)
			{
				if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] FindBestPartner({pawnName}): checking hookup {xxx.get_pawnname(targetPawn)}");

				// Check to see if the mod settings for hookups allow this pairing
				if (!pawnCanPickAnyone && !HookupAllowedViaSettings(pawn, targetPawn))
					continue;

				// Check for homewrecking (banging a pawn who's in a relationship)
				if (!xxx.is_animal(targetPawn) &&
					xxx.HasNonPolyPartnerOnCurrentMap(targetPawn))
				{
					if (RJWHookupSettings.NymphosCanHomewreck && pawnIsNympho && xxx.is_frustrated(pawn))
					{
						// Hookup allowed... rip colony mood
					}
					else if (RJWHookupSettings.NymphosCanHomewreckReverse && xxx.is_nympho(targetPawn) && xxx.is_frustrated(targetPawn))
					{
						// Hookup allowed... rip colony mood
					}
					else
					{
						if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] FindBestPartner({pawnName}): not hooking up with {xxx.get_pawnname(targetPawn)} to avoid homewrecking");
						continue;
					}
				}

				// If the pawn has had sex recently and isn't horny right now, skip them.
				if (!SexUtility.ReadyForLovin(targetPawn) && !xxx.is_horny(targetPawn))
				{
					if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] FindBestPartner({pawnName}): hookup {xxx.get_pawnname(targetPawn)} isn't ready for lovin'");
					continue;
				}

				if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] FindBestPartner({pawnName}): hookup {xxx.get_pawnname(targetPawn)} is sufficiently single");

				if (!xxx.is_animal(targetPawn))
				{
					float relations = pawn.relations.OpinionOf(targetPawn);
					if (relations < RJWHookupSettings.MinimumRelationshipToHookup)
					{
						if (!(relations > 0 && xxx.is_nympho(pawn)))
						{
							if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] FindBestPartner({pawnName}): hookup {xxx.get_pawnname(targetPawn)}, i dont like them:({relations})");
							continue;
						}
					}

					relations = targetPawn.relations.OpinionOf(pawn);
					if (relations < RJWHookupSettings.MinimumRelationshipToHookup)
					{
						if (!(relations > 0 && xxx.is_nympho(targetPawn)))
						{
							if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] FindBestPartner({pawnName}): hookup {xxx.get_pawnname(targetPawn)}, dont like me:({relations})");
							continue;
						}
					}

					float attraction = pawn.relations.SecondaryRomanceChanceFactor(targetPawn);
					if (attraction < RJWHookupSettings.MinimumAttractivenessToHookup)
					{
						if (!(attraction > 0 && xxx.is_nympho(pawn)))
						{
							if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] FindBestPartner({pawnName}): hookup {xxx.get_pawnname(targetPawn)}, i dont find them attractive:({attraction})");
							continue;
						}
					}
					attraction = targetPawn.relations.SecondaryRomanceChanceFactor(pawn);
					if (attraction < RJWHookupSettings.MinimumAttractivenessToHookup)
					{
						if (!(attraction > 0 && xxx.is_nympho(targetPawn)))
						{
							if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] FindBestPartner({pawnName}): hookup {xxx.get_pawnname(targetPawn)}, doesnt find me attractive:({attraction})");
							continue;
						}
					}
				}

				// Check to see if the two pawns are willing to bang, and if so remember how much attractive we find them
				float fuckability = 0f;
				if (pawn.CanReserveAndReach(targetPawn, PathEndMode.OnCell, Danger.Some, 1, 0) &&
					targetPawn.CanReserve(pawn, 1, 0) &&
					roll_to_skip(pawn, targetPawn, out fuckability)) // do NOT check pawnIgnoresRules here - these checks, particularly roll_to_skip, are critical
				{
					int dis = pawn.Position.DistanceToSquared(targetPawn.Position);

					if (dis <= 4)
					{
						// Right next to me (in my bed)?  You'll do.
						if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] FindBestPartner({pawnName}): hookup {xxx.get_pawnname(targetPawn)} is right next to me.  we'll bang, ok?");
						best_fuckability_score = 1.0e6f;
						best_fuckee = targetPawn;
					}
					else if (dis > MaxDistanceSquaredToFuck)
					{
						// too far
						if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] FindBestPartner({pawnName}): hookup {xxx.get_pawnname(targetPawn)} is too far... distance:{dis} max:{MaxDistanceSquaredToFuck}");
						continue;
					}
					else
					{
						// scaling fuckability by distance may give us more varied results and give the less attractive folks a chance
						float fuckability_score = fuckability / GenMath.Sqrt(GenMath.Sqrt(dis));
						if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] FindBestPartner({pawnName}): hookup {xxx.get_pawnname(targetPawn)} is totally bangable.  attraction: {fuckability}, score:{fuckability_score}");

						if (fuckability_score > best_fuckability_score)
						{
							best_fuckee = targetPawn;
							best_fuckability_score = fuckability_score;
						}
					}
				}
			}

			if (best_fuckee == null)
			{
				if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] FindBestPartner({pawnName}): couldn't find anyone to bang");
			}
			else
			{
				if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] FindBestPartner({pawnName}): found rando {xxx.get_pawnname(best_fuckee)} with score {best_fuckability_score}");
			}

			return best_fuckee;
		}

		/// <summary> Checks to see if the mod settings allow the two pawns to hookup. </summary>
		protected static bool HookupAllowedViaSettings(Pawn pawn, Pawn targetPawn)
		{
			// Can prisoners hook up?
			if (pawn.IsPrisonerOfColony || pawn.IsPrisoner)
			{
				if (!RJWHookupSettings.PrisonersCanHookupWithNonPrisoner && !(targetPawn.IsPrisonerOfColony || targetPawn.IsPrisoner))
				{
					if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({xxx.get_pawnname(pawn)}): not hooking up with {xxx.get_pawnname(targetPawn)} due to mod setting PrisonersCanHookupWithNonPrisoner");
					return false;
				}

				if (!RJWHookupSettings.PrisonersCanHookupWithPrisoner && (targetPawn.IsPrisonerOfColony || targetPawn.IsPrisoner))
				{
					if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({xxx.get_pawnname(pawn)}): not hooking up with {xxx.get_pawnname(targetPawn)} due to mod setting PrisonersCanHookupWithPrisoner");
					return false;
				}
			}
			else
			{
				// Can non prisoners hook up with prisoners?
				if (!RJWHookupSettings.CanHookupWithPrisoner && (targetPawn.IsPrisonerOfColony || targetPawn.IsPrisoner))
				{
					if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({xxx.get_pawnname(pawn)}): not hooking up with {xxx.get_pawnname(targetPawn)} due to mod setting CanHookupWithPrisoner");
					return false;
				}
			}

			// Can colonist hook up with visitors?
			if (pawn.IsFreeColonist)
			{
				if (!RJWHookupSettings.ColonistsCanHookupWithVisitor && targetPawn.Faction != Faction.OfPlayer && !targetPawn.IsPrisonerOfColony)
				{
					if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({xxx.get_pawnname(pawn)}): not hooking up with {xxx.get_pawnname(targetPawn)} due to mod setting ColonistsCanHookupWithVisitor");
					return false;
				}
			}

			// Can visitors hook up?
			if (pawn.Faction != Faction.OfPlayer && !pawn.IsPrisonerOfColony)
			{
				// visitors vs colonist
				if (!RJWHookupSettings.VisitorsCanHookupWithColonists && targetPawn.IsColonist)
				{
					if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({xxx.get_pawnname(pawn)}): not hooking up with {xxx.get_pawnname(targetPawn)} due to mod setting VisitorsCanHookupWithColonists");
					return false;
				}

				// visitors vs visitors
				if (!RJWHookupSettings.VisitorsCanHookupWithVisitors && targetPawn.Faction != Faction.OfPlayer)
				{
					if (RJWSettings.DebugLogJoinInBed) Log.Message($"[RJW] find_pawn_to_fuck({xxx.get_pawnname(pawn)}): not hooking up with {xxx.get_pawnname(targetPawn)} due to mod setting VisitorsCanHookupWithVisitors");
					return false;
				}
			}

			// TODO: Not sure if this handles all the pawn-on-animal cases.

			return true;
		}

		protected override Job TryGiveJob(Pawn pawn)
		{
			if (!RJWHookupSettings.HookupsEnabled)
				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) Log.Message($"[RJW] 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 (xxx.can_fuck(pawn) || xxx.can_be_fucked(pawn))
				{
					//--Log.Message("   finding partner");
					Pawn partner = find_pawn_to_fuck(pawn, pawn.Map);

					//--Log.Message("   checking partner");
					if (partner == null)
						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;
		}
	}
}


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 (!xxx.can_path_to_target(pawn, target.Position))
						continue;// too far

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

					if (fuc > min_fuckability)
						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 (!xxx.can_path_to_target(pawn, target.Position))
					continue;// too far

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

				if (fuc > min_fuckability)
					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)
		{
			//Log.Message("[RJW] 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;

			//Log.Message("[RJW] 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)
		{
			//Log.Message("[RJW] JobGiver_RapeEnemy::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) called0");
			//Log.Message("[RJW] JobGiver_RapeEnemy::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) 0 " + SexUtility.ReadyForLovin(pawn));
			//Log.Message("[RJW] JobGiver_RapeEnemy::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) 1 " + (xxx.need_some_sex(pawn) <= 1f));
			//Log.Message("[RJW] JobGiver_RapeEnemy::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) 2 " + !(SexUtility.ReadyForLovin(pawn) || xxx.need_some_sex(pawn) <= 1f));
			//Log.Message("[RJW] JobGiver_RapeEnemy::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) 1 " + Find.TickManager.TicksGame);
			//Log.Message("[RJW] 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;
			//Log.Message("[RJW] 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;
					}
				}
			}

			//Log.Message("[RJW] JobGiver_RapeEnemy::ChoosedJobDef( " + xxx.get_pawnname(pawn) + " ) - " + rapeEnemyJobDef.ToString() + " choosed");
			Pawn victim = rapeEnemyJobDef?.FindVictim(pawn, pawn.Map);

			//Log.Message("[RJW] 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
			{
				//--Log.Message("[RJW]" + this.GetType().ToString() + "::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) - unable to find victim");
				pawn.mindState.canLovinTick = Find.TickManager.TicksGame + Rand.Range(75, 150);
			}
			*/
			//else {  //--Log.Message("[RJW] 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;

namespace rjw
{
	public class JobGiver_ViolateCorpse : ThinkNode_JobGiver
	{
		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 (!xxx.can_path_to_target(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)
					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;

			//--Log.Message("[RJW] JobGiver_ViolateCorpse::TryGiveJob for ( " + xxx.get_pawnname(pawn) + " )");
			if (SexUtility.ReadyForLovin(pawn) || xxx.need_some_sex(pawn) > 1f)
			{
				//--Log.Message("[RJW] JobGiver_ViolateCorpse::TryGiveJob, can love ");
				if (!xxx.can_rape(pawn)) return null;

				var target = find_corpse(pawn, pawn.Map);
				//--Log.Message("[RJW] 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.Collections.Generic;
using System.Linq;
using System;
using Verse;
using RimWorld;
using RimWorld.Planet;

namespace rjw.MainTab
{
	public class MainTabWindow_Brothel : MainTabWindow_PawnTable
	{
		private static PawnTableDef pawnTableDef;

		protected override PawnTableDef PawnTableDef => pawnTableDef ?? (pawnTableDef = DefDatabase<PawnTableDef>.GetNamed("Brothel"));

		protected override IEnumerable<Pawn> Pawns => Find.CurrentMap.mapPawns.AllPawns.Where(p => xxx.is_human(p) && (p.IsColonist || p.IsPrisonerOfColony));

		public override void PostOpen()
		{
			base.PostOpen();
			Find.World.renderer.wantedMode = WorldRenderMode.None;
		}
	}
}


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

namespace rjw.MainTab
{
	[StaticConstructorOnStartup]
	public class PawnColumnWorker_AverageMoneyByWhore : PawnColumnWorker_TextCenter
	{
		public static readonly RecordDef EarnedMoneyByWhore = DefDatabase<RecordDef>.GetNamed("EarnedMoneyByWhore");
		public static readonly RecordDef CountOfWhore = DefDatabase<RecordDef>.GetNamed("CountOfWhore");

		protected internal float score;

		protected override string GetTextFor(Pawn pawn)
		{
			float total = pawn.records.GetValue(EarnedMoneyByWhore);
			float count = pawn.records.GetValue(CountOfWhore);
			if ((int)count == 0)
			{
				return "-";
			}
			score = (total / count);
			return ((int)score).ToString();
		}

		public override int Compare(Pawn a, Pawn b)
		{
			return GetValueToCompare(a).CompareTo(GetValueToCompare(b));
		}

		private float GetValueToCompare(Pawn pawn)
		{
			return score;
		}
	}
}


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

namespace rjw.MainTab
{
	[StaticConstructorOnStartup]
	public class PawnColumnWorker_CountOfWhore : PawnColumnWorker_TextCenter
	{
		public static readonly RecordDef CountOfWhore = DefDatabase<RecordDef>.GetNamed("CountOfWhore");

		protected internal int score;

		protected override string GetTextFor(Pawn pawn)
		{
			score = pawn.records.GetAsInt(CountOfWhore);
			return pawn.records.GetValue(CountOfWhore).ToString();
		}

		public override int Compare(Pawn a, Pawn b)
		{
			return GetValueToCompare(a).CompareTo(GetValueToCompare(b));
		}

		private int GetValueToCompare(Pawn pawn)
		{
			return score;
		}
	}
}


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

namespace rjw.MainTab
{
	[StaticConstructorOnStartup]
	public class PawnColumnWorker_EarnedMoneyByWhore : PawnColumnWorker_TextCenter
	{
		public static readonly RecordDef EarnedMoneyByWhore = DefDatabase<RecordDef>.GetNamed("EarnedMoneyByWhore");

		protected internal int score;

		protected override string GetTextFor(Pawn pawn)
		{
			score = pawn.records.GetAsInt(EarnedMoneyByWhore);
			return pawn.records.GetValue(EarnedMoneyByWhore).ToString();
		}

		public override int Compare(Pawn a, Pawn b)
		{
			return GetValueToCompare(a).CompareTo(GetValueToCompare(b));
		}

		private int GetValueToCompare(Pawn pawn)
		{
			return score;
		}
	}
}


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

namespace rjw.MainTab
{
	[StaticConstructorOnStartup]
	public class PawnColumnWorker_IsPrisoner : PawnColumnWorker_Icon
	{
		private static readonly Texture2D comfortOn = ContentFinder<Texture2D>.Get("UI/Tab/ComfortPrisoner_on");
		private readonly Texture2D comfortOff = ContentFinder<Texture2D>.Get("UI/Tab/ComfortPrisoner_off");

		protected override Texture2D GetIconFor(Pawn pawn)
		{
			return pawn.IsPrisonerOfColony || xxx.is_slave(pawn) ? comfortOn : null;
		}
	}
}


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

namespace rjw.MainTab
{
	[StaticConstructorOnStartup]
	public class PawnColumnWorker_IsWhore : PawnColumnWorker_Icon
	{
		private static readonly Texture2D serviceOn = ContentFinder<Texture2D>.Get("UI/Tab/Service_on");
		private static readonly Texture2D serviceOff = ContentFinder<Texture2D>.Get("UI/Tab/Service_off");

		protected override Texture2D GetIconFor(Pawn pawn)
		{
			return pawn.IsDesignatedService() ? serviceOn : null;
		}
	}
}


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

namespace rjw.MainTab
{
	[StaticConstructorOnStartup]
	public class PawnColumnWorker_Mood : PawnColumnWorker_TextCenter
	{
		protected internal float score;

		protected override string GetTextFor(Pawn pawn)
		{
			score = pawn.needs.mood.CurLevelPercentage;
			return score.ToStringPercent();
		}

		public override int Compare(Pawn a, Pawn b)
		{
			return GetValueToCompare(a).CompareTo(GetValueToCompare(b));
		}

		private float GetValueToCompare(Pawn pawn)
		{
			return score;
		}
	}
}


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

namespace rjw.MainTab
{
	[StaticConstructorOnStartup]
	public class PawnColumnWorker_PriceRangeOfWhore : PawnColumnWorker_TextCenter
	{
		protected internal int min;
		protected internal int max;

		protected override string GetTextFor(Pawn pawn)
		{
			min = WhoringHelper.WhoreMinPrice(pawn);
			max = WhoringHelper.WhoreMaxPrice(pawn);
			return string.Format("{0} - {1}", min, max);
		}

		public override int Compare(Pawn a, Pawn b)
		{
			return GetValueToCompare(a).CompareTo(GetValueToCompare(b));
		}

		private int GetValueToCompare(Pawn pawn)
		{
			return min;
		}
	}
}


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

namespace rjw.MainTab
{
	[StaticConstructorOnStartup]
	public class PawnColumnWorker_RoomAdjustmentOfWhore : PawnColumnWorker_TextCenter
	{
		protected internal float score;

		protected override string GetTextFor(Pawn pawn)
		{
			score = WhoringHelper.WhoreRoomAdjustment(pawn);
			Room ownedRoom = pawn.ownership.OwnedRoom;
			int scoreStageIndex;
			if (ownedRoom == null)
			{
				scoreStageIndex = 0;
			}
			else
			{
				scoreStageIndex = RoomStatDefOf.Impressiveness.GetScoreStageIndex(ownedRoom.GetStat(RoomStatDefOf.Impressiveness));
			}
			return string.Format("{0} -> {1}", scoreStageIndex, score.ToStringPercent());
		}

		public override int Compare(Pawn a, Pawn b)
		{
			return GetValueToCompare(a).CompareTo(GetValueToCompare(b));
		}

		private float GetValueToCompare(Pawn pawn)
		{
			return score;
		}
	}
}


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

namespace rjw.MainTab
{
	public abstract class PawnColumnWorker_TextCenter : PawnColumnWorker_Text
	{
		public override void DoCell(Rect rect, Pawn pawn, PawnTable table)
		{
			Rect rect2 = new Rect(rect.x, rect.y, rect.width, Mathf.Min(rect.height, 30f));
			string textFor = GetTextFor(pawn);
			if (textFor != null)
			{
				Text.Font = GameFont.Small;
				Text.Anchor = TextAnchor.MiddleCenter;
				Text.WordWrap = false;
				Widgets.Label(rect2, textFor);
				Text.WordWrap = true;
				Text.Anchor = TextAnchor.UpperLeft;
				string tip = GetTip(pawn);
				if (!tip.NullOrEmpty())
				{
					TooltipHandler.TipRegion(rect2, tip);
				}
			}
		}
	}
}


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

namespace rjw.MainTab
{
	[StaticConstructorOnStartup]
	public class PawnColumnWorker_WhoreExperience : PawnColumnWorker_TextCenter
	{
		public static readonly RecordDef CountOfWhore = DefDatabase<RecordDef>.GetNamed("CountOfWhore");

		public static readonly HashSet<string> backstories = new HashSet<string>(DefDatabase<StringListDef>.GetNamed("WhoreBackstories").strings);

		protected override string GetTextFor(Pawn pawn)
		{

			int b = backstories.Contains(pawn.story?.adulthood?.titleShort) ? 30 : 0;
			int score = pawn.records.GetAsInt(CountOfWhore);
			return (score + b).ToString();
		}
	}
}


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

namespace rjw.MainTab
{
	public class PawnTable_Whores : PawnTable
	{
		public PawnTable_Whores(PawnTableDef def, Func<IEnumerable<Pawn>> pawnsGetter, int uiWidth, int uiHeight) : base(def, pawnsGetter, uiWidth, uiHeight) { }

		protected override IEnumerable<Pawn> LabelSortFunction(IEnumerable<Pawn> input)
		{
			//return input.OrderBy(p => p.Name?.Numerical != false).ThenBy(p => (p.Name as NameSingle)?.Number ?? 0).ThenBy(p => p.def.label);
			return input.OrderBy(p => xxx.get_pawnname(p));
		}

		protected override IEnumerable<Pawn> PrimarySortFunction(IEnumerable<Pawn> input)
		{
			///return input.OrderByDescending(p => p.Faction?.Name);
			//return input.OrderBy(p => xxx.get_pawnname(p));
			return input.OrderByDescending(p => p.IsColonist);
		}
	}
}


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))
				{
					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);
			}
			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_genitals(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_genitals(pawn);
			return (pawn.health.hediffSet.hediffs.Any((Hediff hed) =>
				(hed.Part == Part) &&
				(hed.def == Genital_Helper.archotech_vagina)
				));
		}

	}
}


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))
							yield return this.make_option(FloatMenuOptionLabel(pawn) + " on " + xxx.get_pawnname(other), pawn, other, other.IsPrisonerOfColony ? WorkTypeDefOf.Warden : null);
			}
		}
	}
}


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

namespace rjw
{
	public class CompCryptoStamped : 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_CryptoStamped : CompProperties
	{
		public CompProperties_CryptoStamped()
		{
			compClass = typeof(CompHoloCryptoStamped);
		}
	}
}


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
							{
								Log.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
								Log.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
	{
		public override float GetSpecialApparelScoreOffset()
		{
			return (Wearer == null) ? -1e5f : 1e5f;
		}

		// made this method universal for any bondage_gear, won't affect anything if gear's BoundBodyPartGroupDefs is empty or null
		public override bool AllowVerbCast(IntVec3 root, Map map, LocalTargetInfo targ, 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)
				{
					Log.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
					{
						Log.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
							{
								Log.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:
using RimWorld;
using Verse;

namespace rjw
{
	public class CompMilkableHuman : CompHasGatherableBodyResource
	{
		protected override int GatherResourcesIntervalDays => Props.milkIntervalDays;

		protected override int ResourceAmount => Props.milkAmount;

		protected override ThingDef ResourceDef => Props.milkDef;

		protected override string SaveKey => "milkFullness";

		public CompProperties_MilkableHuman Props => (CompProperties_MilkableHuman)props;

		protected override bool Active
		{
			get
			{
				if (!Active)
				{
					return false;
				}
				Pawn pawn = parent as Pawn;
				if (pawn != null)
				{
					//idk should probably remove non rjw stuff
					//should merge Lactating into .cs hediff?
					//vanilla
					//C&P?
					//rjw human
					//rjw animal
					if ((!pawn.health.hediffSet.HasHediff(HediffDef.Named("RJW_lactating"), false))
					&& ((pawn.health.hediffSet.HasHediff(HediffDef.Named("Pregnant"), false) && pawn.health.hediffSet.GetFirstHediffOfDef(HediffDef.Named("Pregnant"), false).Visible)
					//|| (pawn.health.hediffSet.HasHediff(HediffDef.Named("HumanPregnancy"), false) && pawn.health.hediffSet.GetFirstHediffOfDef(HediffDef.Named("HumanPregnancy"), false).Visible)
					|| (pawn.health.hediffSet.HasHediff(HediffDef.Named("RJW_pregnancy"), false) && pawn.health.hediffSet.GetFirstHediffOfDef(HediffDef.Named("RJW_pregnancy"), false).Visible)
					|| (pawn.health.hediffSet.HasHediff(HediffDef.Named("RJW_pregnancy_beast"), false) && pawn.health.hediffSet.GetFirstHediffOfDef(HediffDef.Named("RJW_pregnancy_beast"), false).Visible)))
					{
						pawn.health.AddHediff(HediffDef.Named("RJW_lactating"), null, null, null);
					}
					if ((!Props.milkFemaleOnly || pawn.gender == Gender.Female)
					&& (pawn.ageTracker.CurLifeStage.reproductive)
					&& (pawn.RaceProps.Humanlike)
					&& (pawn.health.hediffSet.HasHediff(HediffDef.Named("RJW_lactating"), false)
						//|| pawn.health.hediffSet.HasHediff(HediffDef.Named("Lactating_Permanent"), false)
						//|| pawn.health.hediffSet.HasHediff(HediffDef.Named("Lactating_Natural"), false)
						//|| pawn.health.hediffSet.HasHediff(HediffDef.Named("Lactating_Drug"), false)
						))
					{
						return true;
					}
				}
				return false;
			}
		}

		public override string CompInspectStringExtra()
		{
			if (!Active)
			{
				return null;
			}
			return Translator.Translate("MilkFullness") + ": " + GenText.ToStringPercent(this.Fullness);
		}
	}
}


code:
using Verse;

namespace rjw
{
	public class CompProperties_MilkableHuman : CompProperties
	{
		public int milkIntervalDays;

		public int milkAmount = 8;

		public ThingDef milkDef;

		public bool milkFemaleOnly = true;

		public CompProperties_MilkableHuman()
		{
			compClass = typeof(CompMilkableHuman);
		}
	}
}


code:
using RimWorld;
using Verse;

namespace rjw
{
	[DefOf]
	public static class JobDefOfZ
	{
		public static JobDef MilkHuman;

		static JobDefOfZ()
		{
			DefOfHelper.EnsureInitializedInCtor(typeof(JobDefOf));
		}
	}
}


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

namespace rjw
{
	public abstract class JobDriver_GatherHumanBodyResources : JobDriver_GatherAnimalBodyResources
	{
		private float gatherProgress;

		/*
		//maybe change?
		protected abstract int GatherResourcesIntervalDays
		{
			get;
		}

		//add breastsize modifier?
		protected abstract int ResourceAmount
		{
			get;
		}
		//add more  milks?
		protected abstract ThingDef ResourceDef
		{
			get;
		}
		*/

		protected override IEnumerable<Toil> MakeNewToils()
		{
			ToilFailConditions.FailOnDespawnedNullOrForbidden<JobDriver_GatherHumanBodyResources>(this, TargetIndex.A);
			ToilFailConditions.FailOnNotCasualInterruptible<JobDriver_GatherHumanBodyResources>(this, TargetIndex.A);
			yield return Toils_Goto.GotoThing(TargetIndex.A, PathEndMode.Touch);
			Toil wait = new Toil();
			wait.initAction = delegate
			{
				Pawn milker = base.pawn;
				LocalTargetInfo target = base.job.GetTarget(TargetIndex.A);
				Pawn target2 = (Pawn)target.Thing;
				milker.pather.StopDead();
				PawnUtility.ForceWait(target2, 15000, null, true);
			};
			wait.tickAction = delegate
			{
				Pawn milker = base.pawn;
				milker.skills.Learn(SkillDefOf.Animals, 0.13f, false);
				gatherProgress += StatExtension.GetStatValue(milker, StatDefOf.AnimalGatherSpeed, true);
				if (gatherProgress >= WorkTotal)
				{
					GetComp((Pawn)base.job.GetTarget(TargetIndex.A)).Gathered(base.pawn);
					milker.jobs.EndCurrentJob(JobCondition.Succeeded, true);
				}
			};
			wait.AddFinishAction((Action)delegate
			{
				Pawn milker = base.pawn;
				LocalTargetInfo target = base.job.GetTarget(TargetIndex.A);
				Pawn target2 = (Pawn)target.Thing;
				if (target2 != null && target2.CurJobDef == JobDefOf.Wait_MaintainPosture)
				{
					milker.jobs.EndCurrentJob(JobCondition.InterruptForced, true);
				}
			});
			ToilFailConditions.FailOnDespawnedOrNull<Toil>(wait, TargetIndex.A);
			ToilFailConditions.FailOnCannotTouch<Toil>(wait, TargetIndex.A, PathEndMode.Touch);
			wait.AddEndCondition((Func<JobCondition>)delegate
			{
				if (GetComp((Pawn)base.job.GetTarget(TargetIndex.A)).ActiveAndFull)
				{
					return JobCondition.Ongoing;
				}
				return JobCondition.Incompletable;
			});
			wait.defaultCompleteMode = ToilCompleteMode.Never;
			ToilEffects.WithProgressBar(wait, TargetIndex.A, (Func<float>)(() => gatherProgress / WorkTotal), false, -0.5f);
			wait.activeSkill = (() => SkillDefOf.Animals);
			yield return wait;
		}
	}
}