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

namespace rjw
{
	public abstract class JobDriver_Sex : JobDriver
	{

		public readonly TargetIndex iTarget = TargetIndex.A;	//pawn or corpse
		public readonly TargetIndex iBed = TargetIndex.B;		//bed(maybe some furniture in future?)
		public readonly TargetIndex iCell = TargetIndex.C;		//cell/location to have sex at(fapping)

		public float satisfaction = 1.0f;

		public bool shouldreserve = true;

		public int stackCount = 0;

		public int ticks_between_hearts = 60;
		public int ticks_between_hits = 60;
		public int ticks_between_thrusts = 60;
		public int ticks_left = 1000;	//toil ticks
		public int sex_ticks = 1000;	//orgasm ticks
		public int orgasms = 0;

		// Sex ticks will decrease until it hits the orgasm tick, then orgasm happens and sex_ticks may be reset
		// Sex continues for around 3 seconds
		public int orgasmstick = 180; // ~3 sec
		public int orgasmStartTick = 5000;
		public int duration = 5000;

		public bool face2face = false;
		public bool isEndytophile = false;
		public bool isAnimalOnAnimal = false;
		public bool shouldGainFocus = false;
		public bool shouldGainFocusP = false;
		public bool isSuccubus = false;
		public bool isSuccubusP = false;

		//toggles
		public bool beatings = false;// toggle on off, todo maybe?
		public bool beatonce = false;
		public bool neverendingsex = false;

		public Thing Target			// for reservation
		{
			get
			{
				if (job == null)
				{
					return null;
				}

				if (job.GetTarget(TargetIndex.A).Pawn != null)
					return job.GetTarget(TargetIndex.A).Pawn;

				return job.GetTarget(TargetIndex.A).Thing;
			}
		}

		public Pawn Partner
		{
			get
			{
				if (PartnerPawn != null)
					return PartnerPawn;
				else if (Target is Pawn)
					return job.GetTarget(TargetIndex.A).Pawn;
				else if (Target is Corpse)
					return (job.GetTarget(TargetIndex.A).Thing as Corpse).InnerPawn;
				else
					return null;
			}
		}

		public Building_Bed Bed
		{
			get
			{
				if (pBed != null)
					return pBed;
				else if (job.GetTarget(TargetIndex.B).Thing is Building_Bed)
					return job.GetTarget(TargetIndex.B).Thing as Building_Bed;
				else
					return null;
			}
		}
		//not bed; chair, maybe something else in future
		public Building Building
		{
			get
			{
				if (job.GetTarget(TargetIndex.B).Thing is Building && !(job.GetTarget(TargetIndex.B).Thing is Building_Bed))
					return job.GetTarget(TargetIndex.B).Thing as Building;
				else
					return null;
			}
		}

		public Pawn PartnerPawn = null;
		public Building_Bed pBed = null;
		public SexProps Sexprops = null;

		[SyncMethod]
		public void setup_ticks()
		{
			SetupDurationTicks();
			SetupOrgasmTicks();

			ticks_between_hearts = Rand.RangeInclusive(70, 130);
			ticks_between_hits = Rand.Range(xxx.config.min_ticks_between_hits, xxx.config.max_ticks_between_hits);
			if (xxx.is_bloodlust(pawn))
				ticks_between_hits = (int)(ticks_between_hits * 0.75);
			if (xxx.is_brawler(pawn))
				ticks_between_hits = (int)(ticks_between_hits * 0.90);
			ticks_between_thrusts = 120;
		}

		public void Set_bed(Building_Bed newBed)
		{
			pBed = newBed;
		}

		public float OrgasmProgress {
			get {
				var orgasmDuration = orgasmStartTick - orgasmstick;
				var ticksUntilOrgasm = sex_ticks - orgasmstick;

				return 1f - (ticksUntilOrgasm / (float) orgasmDuration);
			}
		}

		public override void ExposeData()
		{
			base.ExposeData();
			Scribe_Values.Look(ref ticks_left, "ticks_left", 0, false);
			Scribe_Values.Look(ref ticks_between_hearts, "ticks_between_hearts", 0, false);
			Scribe_Values.Look(ref ticks_between_hits, "ticks_between_hits", 0, false);
			Scribe_Values.Look(ref ticks_between_thrusts, "ticks_between_thrusts", 0, false);
			Scribe_Values.Look(ref duration, "duration", 0, false);
			Scribe_Values.Look(ref sex_ticks, "sex_ticks", 0, false);
			Scribe_Values.Look(ref orgasms, "orgasms", 0, false);
			Scribe_Values.Look(ref orgasmstick, "orgasmstick", 0, false);
			Scribe_Values.Look(ref orgasmStartTick, "orgasmStartTick", 0, false);

			Scribe_References.Look(ref pBed, "pBed");
			Scribe_References.Look(ref PartnerPawn, "PartnerPawn");
			Scribe_Deep.Look(ref Sexprops, "Sexprops", new SexProps[0]);
			Scribe_Values.Look(ref beatings, "beatings");
			Scribe_Values.Look(ref beatonce, "beatonce");
			Scribe_Values.Look(ref neverendingsex, "neverendingsex");
			Scribe_Values.Look(ref face2face, "face2face");
			Scribe_Values.Look(ref isEndytophile, "isEndytophile");
			Scribe_Values.Look(ref shouldGainFocus, "shouldGainFocus");
			Scribe_Values.Look(ref shouldGainFocusP, "shouldGainFocusP");
			Scribe_Values.Look(ref isSuccubus, "isSuccubus");
			Scribe_Values.Look(ref isSuccubusP, "isSuccubusP");
		}

		public void SexTick(Pawn pawn, Thing target, bool pawnnude = true, bool partnernude = true)
		{
			ticks_left--;
			sex_ticks--;
			Orgasm();

			var partner = target as Pawn;
			if (partner != null)
			{
				if (partner.jobs?.curDriver is JobDriver_SexBaseReciever)//tick partner
				{
					((JobDriver_SexBaseReciever)partner.jobs.curDriver as JobDriver_SexBaseReciever).ticks_left--;
					((JobDriver_SexBaseReciever)partner.jobs.curDriver as JobDriver_SexBaseReciever).sex_ticks--;
					((JobDriver_SexBaseReciever)partner.jobs.curDriver as JobDriver_SexBaseReciever).Orgasm();
				}

				if (pawn.jobs?.curDriver is JobDriver_SexBaseInitiator)
				{
					var hit = false;
					if (beatonce)
					{
						beatonce = false;
						SexUtility.Sex_Beatings_Dohit(pawn, Partner, Sexprops.isRapist);
					}
					else if (pawn.IsHashIntervalTick(ticks_between_hits))
					{
						Roll_to_hit();
					}
					if (hit)
						if (!isEndytophile)
						{
							SexUtility.DrawNude(pawn);

							if (partner != null)
								SexUtility.DrawNude(partner);
						}
				}
			}

			if (pawn.IsHashIntervalTick(ticks_between_thrusts))
			{
				ChangePsyfocus(pawn, partner);
				Animate(pawn, partner);
				PlaySexSound();
				if (!Sexprops.isRape)
				{
					pawn.GainComfortFromCellIfPossible();
					if (partner != null)
						partner.GainComfortFromCellIfPossible();
				}
			}
		}

		/// <summary>
		/// simple rjw thrust animation
		/// </summary>
		public void Animate(Pawn pawn, Thing target)
		{
			RotatePawns(pawn, Partner);
			//attack/ride 1x2 cell cocksleeve/dildo?
			//if (Building != null)
			//	target = Building;
			if (target != null)
			{
				pawn.Drawer.Notify_MeleeAttackOn(target);

				var partner = target as Pawn;
				if (partner != null && !Sexprops.isRapist)
					partner.Drawer.Notify_MeleeAttackOn(pawn);

				//refresh DrawNude after beating and Notify_MeleeAttackOn
				// Endytophiles prefer clothed sex, everyone else gets nude.
				if (!isEndytophile)
				{
					SexUtility.DrawNude(pawn);

					if (partner != null)
						SexUtility.DrawNude(partner);
				}
			}
			else
			{
				//refresh DrawNude after beating and Notify_MeleeAttackOn
				// Endytophiles prefer clothed sex, everyone else gets nude.
				if (!isEndytophile)
				{
					SexUtility.DrawNude(pawn);
				}
			}
		}

		/// <summary>
		/// increase Psyfocus by having sex
		/// </summary>
		public void ChangePsyfocus(Pawn pawn, Thing target)
		{
			if (ModsConfig.RoyaltyActive)
			{
				if (pawn.jobs?.curDriver is JobDriver_ViolateCorpse)
					if (xxx.is_necrophiliac(pawn) && MeditationFocusTypeAvailabilityCache.PawnCanUse(pawn, DefDatabase<MeditationFocusDef>.GetNamedSilentFail("Morbid")))
					{
						SexUtility.OffsetPsyfocus(pawn, 0.01f);
					}

				if (target != null)
				{
					var partner = target as Pawn;
					if (partner != null)
					{
						if (shouldGainFocus)
							SexUtility.OffsetPsyfocus(pawn, 0.01f);

						if (shouldGainFocusP)
							SexUtility.OffsetPsyfocus(partner, 0.01f);

						if (isSuccubus)
							SexUtility.OffsetPsyfocus(pawn, 0.01f);

						if (isSuccubusP)
							SexUtility.OffsetPsyfocus(partner, 0.01f);
					}
				}
			}
		}

		/// <summary>
		/// rotate pawns
		/// </summary>
		public void RotatePawns(Pawn pawn, Thing target)
		{
			if (Building != null)
			{
				if (face2face)
					pawn.Rotation = Building.Rotation.Opposite;
				else
					pawn.Rotation = Building.Rotation;

				return;
			}
			if (target == null) // solo
			{
				//pawn.Rotation = Rot4.South;
				return;
			}

			if (target is not Pawn partner || partner.Dead) // necro
			{
				pawn.rotationTracker.Face(target.DrawPos);
				return;
			}

			if (partner.jobs?.curDriver is JobDriver_SexBaseReciever receiverDriver)
				if (receiverDriver.parteners.Count > 1)
					return;

			//maybe could do a hand check for monster girls but w/e
			//bool partnerHasHands = Receiver.health.hediffSet.GetNotMissingParts().Any(part => part.IsInGroup(BodyPartGroupDefOf.RightHand) || part.IsInGroup(BodyPartGroupDefOf.LeftHand));

			// most of animal sex is likely doggystyle.
			if (isAnimalOnAnimal)
			{
				if (Sexprops.sexType == xxx.rjwSextype.Anal || Sexprops.sexType == xxx.rjwSextype.Vaginal || Sexprops.sexType == xxx.rjwSextype.DoublePenetration)
				{
					//>>
					//Log.Message("animal doggy");
					pawn.rotationTracker.Face(partner.DrawPos);
					partner.Rotation = pawn.Rotation;
				}
				else
				{
					//><
					//Log.Message("animal non doggy");
					pawn.rotationTracker.Face(target.DrawPos);
					partner.rotationTracker.Face(pawn.DrawPos);
				}
			}
			else
			{
				if (this is JobDriver_BestialityForFemale)
				{
					if (Sexprops.sexType == xxx.rjwSextype.Anal || Sexprops.sexType == xxx.rjwSextype.Vaginal || Sexprops.sexType == xxx.rjwSextype.DoublePenetration)
					{
						//<<
						//Log.Message("bestialityFF doggy");
						partner.rotationTracker.Face(pawn.DrawPos);
						pawn.Rotation = partner.Rotation;
					}
					else
					{
						//><
						//Log.Message("bestialityFF non doggy");
						pawn.rotationTracker.Face(target.DrawPos);
						partner.rotationTracker.Face(pawn.DrawPos);
					}
				}
				else if (partner.GetPosture() == PawnPosture.LayingInBed)
				{
					//x^
					//Log.Message("loving/casualsex in bed");
					// this could use better handling for cowgirl/reverse cowgirl and who pen who, if such would be implemented
					//until then...

					if (!face2face && Sexprops.sexType == xxx.rjwSextype.Anal ||
										Sexprops.sexType == xxx.rjwSextype.Vaginal ||
										Sexprops.sexType == xxx.rjwSextype.DoublePenetration ||
										Sexprops.sexType == xxx.rjwSextype.Fisting)
						//if (xxx.is_female(pawn) && xxx.is_female(partner))
					{
						// in bed loving face down
						pawn.Rotation = partner.CurrentBed().Rotation.Opposite;
					}
					//else if (!(xxx.is_male(pawn) && xxx.is_male(partner)))
					//{
					//	// in bed loving face down
					//	pawn.Rotation = partner.CurrentBed().Rotation.Opposite;
					//}
					else
					{
						// in bed loving, face up
						pawn.Rotation = partner.CurrentBed().Rotation;
					}
				}
				// 30% chance of face-to-face regardless, for variety.
				else if (!face2face && (Sexprops.sexType == xxx.rjwSextype.Anal ||
										Sexprops.sexType == xxx.rjwSextype.Vaginal ||
										Sexprops.sexType == xxx.rjwSextype.DoublePenetration ||
										Sexprops.sexType == xxx.rjwSextype.Fisting))
				{
					//>>
					//Log.Message("doggy");
					pawn.rotationTracker.Face(target.DrawPos);
					partner.Rotation = pawn.Rotation;
				}
				// non doggystyle, or face-to-face regardless
				else
				{
					//><
					//Log.Message("non doggy");
					pawn.rotationTracker.Face(target.DrawPos);
					partner.rotationTracker.Face(pawn.DrawPos);
				}
			}
		}

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

		public void Roll_to_hit()
		{
			if (beatings || (Sexprops.isRapist && RJWSettings.rape_beating))
				SexUtility.Sex_Beatings(Sexprops);
		}
		public void ThrowMetaIcon(IntVec3 pos, Map map, ThingDef icon)
		{
			MoteMaker.MakeStaticMote(pos, map, icon);
		}
		public void ThrowMetaIconF(IntVec3 pos, Map map, FleckDef icon)
		{
			FleckMaker.ThrowMetaIcon(pos, map, icon);
		}
		public void PlaySexSound()
		{
			if (RJWSettings.sounds_enabled)
			{
				SoundInfo sound = new TargetInfo(pawn.Position, pawn.Map);
				sound.volumeFactor = RJWSettings.sounds_sex_volume;
				if(isAnimalOnAnimal)
					sound.volumeFactor *= RJWSettings.sounds_animal_on_animal_volume;
				SoundDef.Named("Sex").PlayOneShot(sound);
			}
		}
		public void PlayCumSound()
		{
			if (RJWSettings.sounds_enabled)
			{
				SoundInfo sound = new TargetInfo(pawn.Position, pawn.Map);
				sound.volumeFactor = RJWSettings.sounds_cum_volume;
				if (isAnimalOnAnimal)
					sound.volumeFactor *= RJWSettings.sounds_animal_on_animal_volume;
				SoundDef.Named("Cum").PlayOneShot(sound);
			}
		}
		public void PlaySexVoice()
		{
			//if (RJWSettings.sounds_enabled)
			//{
			//	SoundInfo sound = new TargetInfo(pawn.Position, pawn.Map);
			//	sound.volumeFactor = RJWSettings.sounds_voice_volume;
			//if (isAnimalOnAnimal)
			//	sound.volumeFactor *= RJWSettings.sounds_animal_on_animal_volume;
			//	SoundDef.Named("Sex").PlayOneShot(sound);
			//}
		}
		public void PlayOrgasmVoice()
		{
			//if (RJWSettings.sounds_enabled)
			//{
			//	SoundInfo sound = new TargetInfo(pawn.Position, pawn.Map);
			//	sound.volumeFactor = RJWSettings.sounds_orgasm_volume;
			//if (isAnimalOnAnimal)
			//	sound.volumeFactor *= RJWSettings.sounds_animal_on_animal_volume;
			//	SoundDef.Named("Orgasm").PlayOneShot(sound);
			//}
		}

		public void Orgasm()
		{
			if (sex_ticks > orgasmstick) //~3s at speed 1
			{
				return;
			}

			//if (RJWSettings.DevMode) Log.Message(xxx.get_pawnname(pawn) + " Orgasmed")
			orgasms++;
			// Sexprops are often passed to other mods for later evaluation of thoughts, memes, etc.
			// the original orgasm counter is kept for stability
			Sexprops.orgasms ++;

			PlayCumSound();
			PlayOrgasmVoice();

			//Log.Message(xxx.get_pawnname(Sexprops.Giver));
			//Log.Message(xxx.get_pawnname(Sexprops.Reciever));
			//Log.Message(Sexprops.SexType.ToStringSafe());
			//Log.Message(Sexprops.DictionaryKey.ToStringSafe());
			//Log.Message(Sexprops.RulePack.ToStringSafe());

			if (!Sexprops.usedCondom)
			{
				//apply cum to floor:
				SexUtility.CumFilthGenerator(pawn);
				if (Partner != null)
					if (!pawn.Dead && !Partner.Dead)
					{
						//TODO: someday unfuck this
						//if (!(this is JobDriver_SexBaseReciever))
							if (!Sexprops.usedCondom)
							{
								PregnancyHelper.impregnate(Sexprops);
								SexUtility.TransferNutrition(Sexprops);
							}
					}
			}
			CalculateSatisfactionPerTick();

			SexUtility.SatisfyPersonal(Sexprops, satisfaction);
			//else
			//	SexUtility.SatisfyPersonal(pawn, ((JobDriver_SexBaseReciever)pawn.jobs?.curDriver).parteners.FirstOrFallback(), Sexprops, Sexprops.isRapist, satisfaction);

			SetupOrgasmTicks();

			if (neverendingsex)
				ticks_left = duration;
		}

		[SyncMethod]
		protected virtual void SetupDurationTicks()
		{
			ticks_left = (int)(2000.0f * Rand.Range(0.50f, 0.90f));
			duration = ticks_left;
		}

		[SyncMethod]
		protected virtual void SetupOrgasmTicks()
		{
			float need;
			if (xxx.is_human(pawn)) {
				need = 1.0f + xxx.need_some_sex(pawn); //1-4
			} else {
				need = 1.0f;
			}

			orgasmstick = 180;
			sex_ticks = (int)((duration / need) * Rand.Range(0.75f, 0.90f));
			orgasmStartTick = sex_ticks;
		}

		public void CalculateSatisfactionPerTick()
		{
			satisfaction = 0.4f;
		}

		public static bool IsInOrByBed(Building_Bed b, Pawn p)
		{
			for (int i = 0; i < b.SleepingSlotsCount; i++)
			{
				if (b.GetSleepingSlotPos(i).InHorDistOf(p.Position, 1f))
				{
					return true;
				}
			}
			return false;
		}

		public override bool TryMakePreToilReservations(bool errorOnFailed)
		{
			return true; // No reservations needed.
		}

		protected override IEnumerable<Toil> MakeNewToils()
		{
			return null;
		}
	}
}


code:
using RimWorld;
using System.Linq;
using Verse;
using Verse.AI;
using System.Collections.Generic;
using rjw.Modules.Interactions.Enums;
using rjw.Modules.Interactions.Objects;

namespace rjw
{
	public abstract class JobDriver_SexBaseInitiator : JobDriver_Sex
	{

		public void Start()
		{
			bool isWhoring = false;
			bool isRape = false;
			var receiverDriver = Partner?.jobs?.curDriver as JobDriver_SexBaseReciever;

			isEndytophile = xxx.has_quirk(pawn, "Endytophile");
			isAnimalOnAnimal = xxx.is_animal(pawn) && xxx.is_animal(Partner);

			if (Partner == null || Partner == pawn)
			{
				// HERE COULD BE YOUR AD
			}
			else if (Partner.Dead)
			{
				isRape = true;
			}
			else if (receiverDriver is not null)
			{
				receiverDriver.parteners.AddDistinct(pawn);

				//prevent downed Receiver standing up and interrupting rape
				if (Partner.health.hediffSet.HasHediff(xxx.submitting))
					Partner.health.AddHediff(xxx.submitting);

				//(Target.jobs.curDriver as JobDriver_SexBaseReciever).parteners.Count; //TODO: add multipartner support so sex doesn't repeat, maybe, someday
				isRape = Partner?.CurJob.def == xxx.gettin_raped;
				isWhoring = pawn?.CurJob.def == xxx.whore_is_serving_visitors;

				//toggles
				NymphSucc();
				RoMSucc();
				NISucc();
			}

			if (Sexprops == null)
			{
				//Log.Message("rulePack1: " + Sexprops);
				Sexprops = pawn.GetRMBSexPropsCache();
				//Log.Message("rulePack2: " + Sexprops);
				pawn.GetRJWPawnData().SexProps = null;
				//Log.Message("rulePack3: " + Sexprops);
				//Log.Message("rulePack4: " + pawn.GetRJWPawnData().SexProps);

				if (Sexprops == null)
				{
					Sexprops = SexUtility.SelectSextype(pawn, Partner, isRape, isWhoring);
					Sexprops.isRapist = isRape;
					Sexprops.isWhoring = isWhoring;
				}
				var interaction = Modules.Interactions.Helpers.InteractionHelper.GetWithExtension(Sexprops.dictionaryKey);
				Sexprops.isRevese = interaction.HasInteractionTag(InteractionTag.Reverse);
			}

			if (receiverDriver is not null && receiverDriver.parteners.Count == 1 && receiverDriver.Sexprops == null)
			{
				receiverDriver.Sexprops = new SexProps()
				{
					pawn = Partner,
					partner = pawn,
					sexType = Sexprops.sexType,
					dictionaryKey = Sexprops.dictionaryKey,
					usedCondom = Sexprops.usedCondom,
					isRape = isRape,
					isReceiver = true,
					isRevese = Sexprops.isRevese
				};
			}
			SexUtility.LogSextype(Sexprops.pawn, Sexprops.partner, Sexprops.rulePack, Sexprops.dictionaryKey);
			if (RJWPreferenceSettings.sex_wear == RJWPreferenceSettings.Clothing.Clothed) return;
			var comp = CompRJW.Comp(pawn);
			if (comp != null)
			{
				comp.drawNude = true;
				pawn.Drawer.renderer.SetAllGraphicsDirty();
			}
			Pawn partner = Partner;
			if (partner != null)
			{
				comp = CompRJW.Comp(partner);
				if (comp != null)
				{
					comp.drawNude = true;
					partner.Drawer.renderer.SetAllGraphicsDirty();
				}
			}
		}

		//public void Change(xxx.rjwSextype sexType)
		//{
		//	if (pawn.jobs?.curDriver is JobDriver_SexBaseInitiator)
		//	{
		//		(pawn.jobs.curDriver as JobDriver_SexBaseInitiator).increase_time(duration);
		//		Sexprops = SexUtility.SelectSextype(pawn, Partner, isRape, isWhoring, Partner);
		//		sexType = Sexprops.SexType;
		//		SexUtility.LogSextype(Sexprops.Giver, Sexprops.Reciever, Sexprops.RulePack, Sexprops.DictionaryKey);
		//	}
		//	if (Partner.jobs?.curDriver is JobDriver_SexBaseReciever)
		//	{
		//		(Partner.jobs.curDriver as JobDriver_SexBaseReciever).increase_time(duration);
		//		Sexprops = SexUtility.SelectSextype(pawn, Partner, isRape, isWhoring, Partner);
		//		sexType = Sexprops.SexType;
		//		SexUtility.LogSextype(Sexprops.Giver, Sexprops.Reciever, Sexprops.RulePack, Sexprops.DictionaryKey);
		//	}
		//	sexType = sexType
		//}

		public void End()
		{
			if (xxx.is_human(pawn))
			{
				var comp = CompRJW.Comp(pawn);
				if (comp != null)
				{
					comp.drawNude = false;
					pawn.Drawer.renderer.SetAllGraphicsDirty();
				}
			}
			GlobalTextureAtlasManager.TryMarkPawnFrameSetDirty(pawn);

			if (Partner?.jobs?.curDriver is JobDriver_SexBaseReciever receiverDriver)
			{
				if (receiverDriver.parteners.Count == 1 && !Sexprops.isCoreLovin)
				{
					var isPenetrative = Sexprops.sexType switch
					{
						xxx.rjwSextype.Anal => true,
						xxx.rjwSextype.Vaginal => true,
						xxx.rjwSextype.DoublePenetration => true,
						_ => false
					};
					if (!isPenetrative) goto notKnotting;

					// reverse interaction, check if penetrating partner can knot
					var ableToKnot = Sexprops.isRevese ? canKnott(Partner) : canKnott(pawn);
					if (!ableToKnot) goto notKnotting;

					pawn.jobs.jobQueue.EnqueueFirst(JobMaker.MakeJob(xxx.knotted, Partner));
					Partner.jobs.jobQueue.EnqueueFirst(JobMaker.MakeJob(xxx.knotted, pawn));
				}

			notKnotting:
				receiverDriver.parteners.Remove(pawn);
			}
		}

		public bool canKnott(Pawn pawn)
		{
			// In case of necro, dead pawns can never knot.
			if (pawn.Dead) return false;

			foreach (var y in pawn.GetGenitalsList())
				if (PartProps.TryGetProps(y, out var propslist))
					if (propslist.Contains("Knotted")) return true;
			return false;
		}

		/// <summary>
		/// non succubus focus gain
		/// </summary>
		public void NymphSucc()
		{
			if (MeditationFocusTypeAvailabilityCache.PawnCanUse(pawn, xxx.SexMeditationFocus))
			{
				shouldGainFocus = true;
				SexUtility.OffsetPsyfocus(pawn, 0.01f);
			}
			else if (xxx.is_zoophile(pawn) && xxx.is_animal(Partner) && MeditationFocusTypeAvailabilityCache.PawnCanUse(pawn, MeditationFocusDefOf.Natural))
			{
				shouldGainFocus = true;
			}

			if (MeditationFocusTypeAvailabilityCache.PawnCanUse(Partner, xxx.SexMeditationFocus))
			{
				shouldGainFocusP = true;
			}
			else if (xxx.is_zoophile(Partner) && xxx.is_animal(pawn) && MeditationFocusTypeAvailabilityCache.PawnCanUse(Partner, MeditationFocusDefOf.Natural))
			{
				shouldGainFocusP = true;
			}
		}

		/// <summary>
		/// Rimworld of Magic succubus focus gain
		/// </summary>
		public void RoMSucc()
		{
			if (xxx.RoMIsActive)
			{
				if (xxx.has_traits(pawn))
					if (pawn.story.traits.HasTrait(xxx.Succubus))
					{
						isSuccubus = true;
					}

				if (xxx.has_traits(Partner))
					if (Partner.story.traits.HasTrait(xxx.Succubus))
					{
						isSuccubusP = true;
					}
			}
		}
		/// <summary>
		/// Nightmare Incarnation succubus focus gain
		/// </summary>
		public void NISucc()
		{
			if (xxx.NightmareIncarnationIsActive)
			{
				if (xxx.has_traits(pawn))
					foreach (var x in pawn.AllComps?.Where(x => x?.props?.ToStringSafe() == "NightmareIncarnation.CompProperties_SuccubusRace"))
					{
						isSuccubus = true;
						break;
					}

				if (xxx.has_traits(Partner))
					foreach (var x in Partner.AllComps?.Where(x => x?.props?.ToStringSafe() == "NightmareIncarnation.CompProperties_SuccubusRace"))
					{
						isSuccubusP = true;
						break;
					}
			}
		}

		public override bool TryMakePreToilReservations(bool errorOnFailed)
		{
			//ModLog.Message("shouldreserve " + shouldreserve);
			if (shouldreserve && Target != null)
				return pawn.Reserve(Target, job, xxx.max_rapists_per_prisoner, stackCount, null, errorOnFailed);
			else if (shouldreserve && Bed != null)
				return pawn.Reserve(Bed, job, Bed.SleepingSlotsCount, 0, 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);
		}
	}
}

license: mit


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

namespace rjw
{
	public class JobDriver_SexBaseReciever : JobDriver_Sex
	{
		//give this poor driver some love other than (Partner.jobs?.curDriver is JobDriver_SexBaseReciever)
		public List<Pawn> parteners = new();

		public override void ExposeData()
		{
			base.ExposeData();
			Scribe_Collections.Look(ref parteners, "parteners", LookMode.Reference);
		}

		/// <summary>
		/// Adds a predicate that a partner must fulfill to be considered valid.
		/// </summary>
		/// <param name="predicate">The predicate.</param>
		public void AddPartnerCondition(Predicate<Pawn> predicate) =>
			this.FailOn(() => parteners.Count > 0 && !parteners.Any(predicate));

		protected virtual void DoSetup()
		{
			setup_ticks();

			// Add sex initiator, so this wont fail before they start their job.
			parteners.AddDistinct(Partner);

			// Handles ending the job when no partners remain.
			AddEndCondition(() => parteners.Count <= 0 ? JobCondition.Succeeded : JobCondition.Ongoing);

			// These make sure at least one partner is still valid.  In a perfect world,
			// the partners will properly remove themselves when they're no longer
			// trying to screw this pawn ...but this is not a perfect world.
			AddPartnerCondition(MustBeSpawned);
			AddPartnerCondition(MustBeAwake);
			AddPartnerCondition(MustNotBeDrafted);
			AddPartnerCondition(MustBeMySexInitiator);
		}

		/// <summary>
		/// Checks that the partner is actually spawned on this map.
		/// </summary>
		protected bool MustBeSpawned(Pawn partner) =>
			partner.Spawned && partner.Map == pawn.Map;

		/// <summary>
		/// Checks that the partner is capable of being awake.
		/// </summary>
		protected bool MustBeAwake(Pawn partner) =>
			partner.health.capacities.CanBeAwake;

		/// <summary>
		/// Checks that the partner is not drafted.
		/// </summary>
		protected bool MustNotBeDrafted(Pawn partner) =>
			!partner.Drafted;

		/// <summary>
		/// Checks that the partner is still trying to fuck this driver's pawn.
		/// </summary>
		protected bool MustBeMySexInitiator(Pawn partner) =>
			(partner.jobs.curDriver as JobDriver_SexBaseInitiator)?.Partner == pawn;
	}
}


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

namespace rjw
{
	public class JobDriver_SexBaseRecieverLoved : JobDriver_SexBaseReciever
	{
		protected override void DoSetup()
		{
			base.DoSetup();

			// More/less hearts based on opinion.
			try
			{
				if (pawn.relations.OpinionOf(Partner) < 0)
					ticks_between_hearts += 50;
				else if (pawn.relations.OpinionOf(Partner) > 60)
					ticks_between_hearts -= 25;
			}
			catch
			{
				ModLog.Warning("Failed to resolve pawn relations, if on save load, this shouldn't matter too much.");
			}

			// For consensual sex, drafting the recipient will interrupt the job.
			this.FailOn(() => pawn.Drafted);
		}

		protected override IEnumerable<Toil> MakeNewToils()
		{
			DoSetup();
			// ModLog.Message("JobDriver_GettinLoved::MakeNewToils is called");
			// ModLog.Message("" + Partner.CurJob.def);

			if (Partner.CurJob.def == xxx.casual_sex) // sex in bed
			{
				this.KeepLyingDown(iBed);
				yield return Toils_Reserve.Reserve(iTarget, 1, 0);
				yield return Toils_Reserve.Reserve(iBed, Bed.SleepingSlotsCount, 0);

				var get_loved = MakeSexToil();
				get_loved.FailOn(() => Partner.CurJob?.def != xxx.casual_sex);
				yield return get_loved;
			}
			else if (Partner.CurJob.def == xxx.quick_sex)
			{
				yield return Toils_Reserve.Reserve(iTarget, 1, 0);

				var get_loved = MakeSexToil();
				get_loved.handlingFacing = false;
				yield return get_loved;
			}
			else if (Partner.CurJob.def == xxx.whore_is_serving_visitors)
			{
				this.FailOn(() => Partner.CurJob == null);
				yield return Toils_Reserve.Reserve(iTarget, 1, 0);

				var get_loved = MakeSexToil();
				get_loved.FailOn(() => Partner.CurJob?.def != xxx.whore_is_serving_visitors);
				yield return get_loved;
			}
			else if (Partner.CurJob.def == xxx.bestialityForFemale)
			{
				this.FailOn(() => Partner.CurJob == null);
				yield return Toils_Reserve.Reserve(iTarget, 1, 0);

				var get_loved = MakeSexToil();
				get_loved.FailOn(() => Partner.CurJob?.def != xxx.bestialityForFemale);
				yield return get_loved;
			}
			else if (Partner.CurJob.def == xxx.animalMate)
			{
				this.FailOn(() => Partner.CurJob == null);
				yield return Toils_Reserve.Reserve(iTarget, 1, 0);

				var get_loved = MakeSexToil();
				get_loved.FailOn(() => Partner.CurJob?.def != xxx.animalMate);
				yield return get_loved;
			}
		}

		private Toil MakeSexToil()
		{
			// Any other consensual sex besides casual sex is in a bed.
			var get_loved
				= Partner.CurJob.def != xxx.casual_sex ? new Toil()
				: Toils_LayDown.LayDown(iBed, true, false, false, false);

			get_loved.defaultCompleteMode = ToilCompleteMode.Never;
			get_loved.socialMode = RandomSocialMode.Off;
			get_loved.handlingFacing = true;
			get_loved.tickAction = () =>
			{
				if (pawn.IsHashIntervalTick(ticks_between_hearts))
					ThrowMetaIconF(pawn.Position, pawn.Map, FleckDefOf.Heart);
			};
			get_loved.AddFinishAction(() =>
			{
				if (xxx.is_human(pawn))
				{
					var comp = CompRJW.Comp(pawn);
					if (comp != null)
					{
						comp.drawNude = false;
						pawn.Drawer.renderer.SetAllGraphicsDirty();
					}
				}
				GlobalTextureAtlasManager.TryMarkPawnFrameSetDirty(pawn);
			});

			return get_loved;
		}
	}

	// Used by the quickie job for some reason.  Future expansion?
	public class JobDriver_SexBaseRecieverQuickie : JobDriver_SexBaseRecieverLoved { }
}


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

namespace rjw
{
	public class JobDriver_SexBaseRecieverRaped : JobDriver_SexBaseReciever
	{
		protected override IEnumerable<Toil> MakeNewToils()
		{
			DoSetup();

			var get_raped = new Toil
			{
				defaultCompleteMode = ToilCompleteMode.Never,
				handlingFacing = true,
				socialMode = RandomSocialMode.Off,
				initAction = () =>
				{
					pawn.pather.StopDead();
					pawn.jobs.curDriver.asleep = false;

					SexUtility.BeeingRapedAlert(Partner, pawn);
				},
				tickAction = () =>
				{
					if (parteners.Count <= 0) return;
					if (!pawn.IsHashIntervalTick(ticks_between_hearts / parteners.Count)) return;

					if (xxx.is_masochist(pawn) || xxx.is_psychopath(pawn))
						ThrowMetaIconF(pawn.Position, pawn.Map, FleckDefOf.Heart);
					else
						ThrowMetaIconF(pawn.Position, pawn.Map, xxx.mote_noheart);
				}
			};
			get_raped.AddFinishAction(() =>
			{
				if (xxx.is_human(pawn))
				{
					var comp = CompRJW.Comp(pawn);
					if (comp != null)
					{
						comp.drawNude = false;
						pawn.Drawer.renderer.SetAllGraphicsDirty();
					}
				}
				GlobalTextureAtlasManager.TryMarkPawnFrameSetDirty(pawn);

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

			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 override bool TryMakePreToilReservations(bool errorOnFailed)
		{
			return pawn.Reserve(Target, job, xxx.max_rapists_per_prisoner, 0, null, errorOnFailed);
		}

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

			setup_ticks();

			this.FailOnDespawnedOrNull(iTarget);
			this.FailOnDespawnedOrNull(iBed);
			this.FailOn(() => !Partner.health.capacities.CanBeAwake);
			this.FailOn(() => !(Partner.InBed() || Bed_Utility.in_same_bed(Partner, pawn)));
			this.FailOn(() => pawn.Drafted);
			yield return Toils_Reserve.Reserve(iTarget, xxx.max_rapists_per_prisoner, 0);
			yield return Toils_Goto.GotoThing(iTarget, 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 SexToil = new Toil();
			SexToil.defaultCompleteMode = ToilCompleteMode.Never;
			SexToil.socialMode = RandomSocialMode.Off;
			SexToil.handlingFacing = true;
			SexToil.initAction = delegate
			{
				Start();
				Sexprops.usedCondom = CondomUtility.TryUseCondom(pawn) || CondomUtility.TryUseCondom(Partner);
			};
			SexToil.FailOn(() => Partner.CurJob?.def != xxx.gettin_loved);
			SexToil.AddPreTickAction(delegate
			{
				if (pawn.IsHashIntervalTick(ticks_between_hearts))
					ThrowMetaIconF(pawn.Position, pawn.Map, FleckDefOf.Heart);
				SexTick(pawn, Partner);
				SexUtility.reduce_rest(Partner, 1);
				SexUtility.reduce_rest(pawn, 2);
				if (ticks_left <= 0)
					ReadyForNextToil();
			});
			SexToil.AddFinishAction(End);
			yield return SexToil;

			yield return new Toil
			{
				initAction = () => SexUtility.ProcessSex(Sexprops),
				defaultCompleteMode = ToilCompleteMode.Instant
			};
		}
	}
}


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

namespace rjw
{
	public class JobDriver_SexQuick : JobDriver_SexBaseInitiator
	{
		public override bool TryMakePreToilReservations(bool errorOnFailed)
		{
			return pawn.Reserve(Target, job, xxx.max_rapists_per_prisoner, 0, null, errorOnFailed);
		}

		protected override IEnumerable<Toil> MakeNewToils()
		{
			//ModLog.Message("" + this.GetType().ToString() + "::MakeNewToils() called");
			setup_ticks();
			var PartnerJob = xxx.getting_quickie;

			this.FailOnDespawnedNullOrForbidden(iTarget);
			this.FailOn(() => !Partner.health.capacities.CanBeAwake);
			this.FailOn(() => pawn.Drafted);

			yield return Toils_Goto.GotoThing(iTarget, PathEndMode.OnCell);

			Toil findQuickieSpot = new Toil();
			findQuickieSpot.defaultCompleteMode = ToilCompleteMode.PatherArrival;
			findQuickieSpot.initAction = delegate
			{
				//Needs this earlier to decide if current place is good enough
				var all_pawns = pawn.Map.mapPawns.AllPawnsSpawned.Where(x
				=> x.Position.DistanceTo(pawn.Position) < 100
				&& xxx.is_human(x)
				&& x != pawn
				&& x != Partner
				).ToList();

				FloatRange temperature = pawn.ComfortableTemperatureRange();
				float cellTemp = pawn.Position.GetTemperature(pawn.Map);

				if (Partner.IsPrisonerInPrisonCell() || (!CasualSex_Helper.MightBeSeen(all_pawns, pawn.Position, pawn, Partner) && (cellTemp > temperature.min && cellTemp < temperature.max)))
				{
					ReadyForNextToil();
				}
				else
				{
					var spot = CasualSex_Helper.FindSexLocation(pawn, Partner);
					pawn.pather.StartPath(spot, PathEndMode.OnCell);
					//sometimes errors with stuff like vomiting
					//sometimes partner is null ??? no idea how to fix this
					Partner?.jobs.StopAll();
					Job job = JobMaker.MakeJob(JobDefOf.GotoMindControlled, spot);
					Partner?.jobs.StartJob(job, JobCondition.InterruptForced);
				}
			};
			yield return findQuickieSpot;

			Toil WaitForPartner = new Toil();
			WaitForPartner.defaultCompleteMode = ToilCompleteMode.Delay;
			WaitForPartner.initAction = delegate
			{
				ticksLeftThisToil = 5000;
			};
			WaitForPartner.tickAction = delegate
			{
				pawn.GainComfortFromCellIfPossible();
				if (pawn.Position.DistanceTo(Partner.Position) <= 1f)
				{
					ReadyForNextToil();
				}
			};
			yield return WaitForPartner;

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

			Toil SexToil = new Toil();
			SexToil.defaultCompleteMode = ToilCompleteMode.Never;
			SexToil.socialMode = RandomSocialMode.Off;
			SexToil.defaultDuration = duration;
			SexToil.handlingFacing = true;
			SexToil.FailOn(() => Partner.CurJob?.def != PartnerJob);
			SexToil.initAction = delegate
			{
				Partner.pather.StopDead();
				Partner.jobs.curDriver.asleep = false;

				Start();
				Sexprops.usedCondom = CondomUtility.TryUseCondom(pawn) || CondomUtility.TryUseCondom(Partner);
			};
			SexToil.AddPreTickAction(delegate
			{
				if (pawn.IsHashIntervalTick(ticks_between_hearts))
					ThrowMetaIconF(pawn.Position, pawn.Map, FleckDefOf.Heart);
				SexTick(pawn, Partner);
				SexUtility.reduce_rest(Partner, 1);
				SexUtility.reduce_rest(pawn, 1);
				if (ticks_left <= 0)
					ReadyForNextToil();
			});
			SexToil.AddFinishAction(End);
			yield return SexToil;

			yield return new Toil
			{
				initAction = () => SexUtility.ProcessSex(Sexprops),
				defaultCompleteMode = ToilCompleteMode.Instant
			};
		}
	}
}


code:
    public class RJWSexJob : Job, IExposable
    {
        public VorePath VorePath;
        public VoreProposal Proposal;
        public Pawn Initiator;
        public bool IsForced = false;
        public bool IsKidnapping = false;
        public bool IsRitualRelated = false;

        // we hide base ExposeData here because Tynan thought it was a great idea to completely lock and seal the Job. This entire file is just a mess of workarounds for the restrictive Job class
        public new void ExposeData()
        {
            base.ExposeData();
            Scribe_Deep.Look(ref VorePath, "VorePath", new object[0]);
            Scribe_Deep.Look(ref Proposal, "Proposal", new object[0]);
            Scribe_References.Look(ref Initiator, "Initiator");
            Scribe_Values.Look(ref IsKidnapping, "IsKidnapping");
        }
    }

    public static class VoreJobMaker
    {
        public static VoreJob MakeJob()
        {
            VoreJob job = SimplePool<VoreJob>.Get();
            job.loadID = Find.UniqueIDsManager.GetNextJobID();
            return job;
        }
        public static VoreJob MakeJob(JobDef jobDef)
        {
            VoreJob job = MakeJob();
            job.def = jobDef;
            return job;
        }
        public static VoreJob MakeJob(JobDef jobDef, LocalTargetInfo targetA)
        {
            VoreJob job = MakeJob();
            job.def = jobDef;
            job.targetA = targetA;
... -> more MakeJob() overloads with targetA, targetB, etc. but msg too long
    }


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

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

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

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

				// choose pawns to fuck with above average fuckability
				var valid_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)
		{
			if (RJWSettings.DebugRape) ModLog.Message(" JobGiver_AIRapePrisoner::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) called ");

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

			if (SexUtility.ReadyForLovin(pawn) || xxx.is_hornyorfrustrated(pawn))
			{
				// 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)
					{
						if (RJWSettings.DebugRape) ModLog.Message(" JobGiver_RandomRape::TryGiveJob( " + xxx.get_pawnname(pawn) + " ) - 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 (AnimalPenUtility.NeedsToBeManagedByRope(target)) return null;
			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);
		}
	}
}


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)
		{
			//ModLog.Message(" JobGiver_Breed::TryGiveJob( " + xxx.get_pawnname(animal) + " ) ReadyForLovin " + (SexUtility.ReadyForLovin(animal)));

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

			//ModLog.Message(" ready to breed::is_healthy " + xxx.is_healthy(animal) + " can_rape " + xxx.can_rape(animal));
			if (xxx.is_healthy(animal) && xxx.can_rape(animal))
			{
				//search for desiganted target to sex
				Pawn designated_target = BreederHelper.find_designated_breeder(animal, animal.Map);
				if (designated_target != null)
				{
					return JobMaker.MakeJob(xxx.animalBreed, designated_target);
				}
			}
			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

			string pawnName = xxx.get_pawnname(pawn);
			if (RJWSettings.DebugRape) ModLog.Message($"FindComfortPrisoner({pawnName})");

			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 (RJWSettings.DebugRape) ModLog.Message($"FindComfortPrisoner({pawnName}): found {targets.Count()}");

			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 (!Pather_Utility.cells_to_target_rape(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) || xxx.is_mechanoid(target))
					fuc = SexAppraiser.would_fuck(pawn, target, true);

				if (RJWSettings.DebugRape) ModLog.Message($"FindComfortPrisoner({pawnName}): {fuc} has to be over {min_fuckability}");

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

			if (valid_targets.Any())
			{
				// avg_fuckability = valid_targets.Average(x => x.Value); // 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)
		{
			if (RJWSettings.DebugRape) ModLog.Message($"JobGiver_ComfortPrisonerRape::TryGiveJob({xxx.get_pawnname(pawn)}) called");

			if (!RJWSettings.WildMode)
			{
				// don't allow pawns marked as comfort prisoners to rape others
				if (RJWSettings.DebugRape) ModLog.Message($"JobGiver_ComfortPrisonerRape::TryGiveJob({xxx.get_pawnname(pawn)}): is healthy = {xxx.is_healthy(pawn)}, is cp = {pawn.IsDesignatedComfort()}, is ready = {SexUtility.ReadyForLovin(pawn)}, is frustrated = {xxx.is_frustrated(pawn)}");
				if (!xxx.is_healthy(pawn) || pawn.IsDesignatedComfort() || (!SexUtility.ReadyForLovin(pawn) && !xxx.is_frustrated(pawn))) return null;
			}

			if (RJWSettings.DebugRape) ModLog.Message($"FindComfortPrisoner({xxx.get_pawnname(pawn)}): can rape = {xxx.can_rape(pawn)}, is drafted = {pawn.Drafted}");
			if (pawn.Drafted || !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 laying down idle.
			if (!(pawn.jobs.curJob == null || pawn.jobs.curJob.def == JobDefOf.LayDown))
			{
				if (RJWSettings.DebugRape) ModLog.Message($"FindComfortPrisoner({xxx.get_pawnname(pawn)}): I already have a job ({pawn.CurJobDef})");
				return null;
			}

			// Faction check.
			if (!(pawn.Faction?.IsPlayer ?? false) && !pawn.IsPrisonerOfColony)
			{
				if (RJWSettings.DebugRape) ModLog.Message($"FindComfortPrisoner({xxx.get_pawnname(pawn)}): player faction: {pawn.Faction?.IsPlayer}, prisoner: {pawn.IsPrisonerOfColony}");
				return null;
			}

			Pawn target = find_targetCP(pawn, pawn.Map);
			if (target == null) return null;
			if (RJWSettings.DebugRape) ModLog.Message($"JobGiver_ComfortPrisonerRape::TryGiveJob({xxx.get_pawnname(pawn)}) with target {xxx.get_pawnname(target)}");

			if (xxx.is_animal(target))
				return JobMaker.MakeJob(xxx.bestiality, target);
			else
				return JobMaker.MakeJob(xxx.RapeCP, target);
		}
	}
}


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

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

			if (pawn.Drafted)
				return null;

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

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

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

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

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

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

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

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

			return null;
		}
	}
}


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

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

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

			if (pawn.Drafted)
				return null;

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

			string name = pawn.GetName();

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

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

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

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

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

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

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

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

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

			return null;
		}
	}
}


code:
using Verse;
using Verse.AI;

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

	}
}


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

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

			if (pawn.Drafted) return null;

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

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

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


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

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

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

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

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

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

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

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

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

					return chosentarget;
				}
			}

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

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

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

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

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

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

			return chosentarget;
		}

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

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

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

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


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

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

			if (pawn.Drafted) return null;

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

			return chosentarget;
		}

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

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

			if (pawn.Drafted) return null;

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

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

			return null;
		}
	}
}


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

namespace rjw
{
	public class MentalState_RandomRape : SexualMentalState
	{

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

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

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


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

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

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

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


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

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

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

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

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

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

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

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

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

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

	}
}


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

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

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

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

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

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

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

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

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

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

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

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

	}
#endif
}


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

			resultStore.Enqueue(ticks * nanoSecsPerTick);

			samples.SetOrAdd(methodName, resultStore);
		}

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

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


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

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


code:
using 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
							{
								ModLog.Error("Unable to drop " + effective_item.Label + " for use on " + xxx.get_pawnname(other) + " (apparel must be dropped before use)");
								effective_item = null;
							}
						}

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


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

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

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


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

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

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

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

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

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

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

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


code:
using RimWorld;
using Verse;

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

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