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

namespace rjw
{
	[StaticConstructorOnStartup]
	public static class SemenHelper
	{
		/*
		contains many important functions of use to the other classes
		*/

		//amount of semen per sex act:

		public static readonly Dictionary<key, values> splatchAdjust;//saves x (horizontal) and z (vertical) adjustments of texture positiion for each unique combination of bodyType and bodyPart
		public static readonly Dictionary<key_layer, values_layer> layerAdjust;//saves y adjustments (drawing plane) for left/right appendages + bodyPart combinations to hide spunk if pawn looks in the wrong direction

		//structs are used to pack related variables together - used as keys for the dictionaries
		public struct key//allows to save all unique combinations of bodyType and bodyPart
		{
			public readonly BodyTypeDef bodyType;
			public readonly BodyPartDef bodyPart;
			public key(BodyTypeDef bodyType, BodyPartDef bodyPart)
			{
				this.bodyType = bodyType;
				this.bodyPart = bodyPart;
			}
		}

		//for the 4 directions, use arrays to store the different adjust for north, east, south, west (in that order)
		public struct values
		{
			public readonly float[] x;
			public readonly float[] z;
			//public readonly bool over_clothing;//on gentials: hide when clothes are worn - in case of the other body parts it can't be said (for now) if it was added on the clothing or not
			public values(float[] xAdjust, float[] zAdjust)
			{
				x = xAdjust;
				z = zAdjust;
				//this.over_clothing = over_clothing;
			}
		}

		public struct key_layer//used to save left/right appendage + bodyPart combinations
		{
			public readonly bool left_side;
			public readonly BodyPartDef bodyPart;

			public key_layer(bool left_side, BodyPartDef bodyPart)
			{
				this.left_side = left_side;
				this.bodyPart = bodyPart;
			}
		}

		public struct values_layer//saves the y-adjustments for different body parts and sides -> e.g. allows hiding spunk on the right arm if pawn is looking to the left (aka west)
		{
			public readonly float[] y;

			public values_layer(float[] yAdjust)
			{
				y = yAdjust;
			}
		}

		//get defs of the rjw parts
		public static BodyPartDef genitalsDef = BodyDefOf.Human.AllParts.Find(bpr => string.Equals(bpr.def.defName, "Genitals")).def;
		public static BodyPartDef anusDef = BodyDefOf.Human.AllParts.Find(bpr => string.Equals(bpr.def.defName, "Anus")).def;
		public static BodyPartDef chestDef = BodyDefOf.Human.AllParts.Find(bpr => string.Equals(bpr.def.defName, "Chest")).def;

		static SemenHelper()
		{
			splatchAdjust = new Dictionary<key, values>();
			//maybe there is a more elegant way to save and load this data, but I don't know about it

			//structure explained:
			//1) key: struct consisting of bodyType + bodyPart (= unique key for every combination of bodyType + part)
			//2) values: struct containing positioning information (xAdjust: horizontal positioning, yAdjust: vertical positioning, zAdjust: whether to draw above or below pawn
			//note: arms, hands, and legs (which are only visible from one direction) values need not be inverted between west and east

			//BodyType Thin
			splatchAdjust.Add(new key(BodyTypeDefOf.Thin, BodyPartDefOf.Arm), new values(new float[] { -0.13f, 0.05f, 0.13f, 0.05f }, new float[] { 0f, 0f, 0f, 0f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Thin, BodyPartDefOf.Hand), new values(new float[] { -0.12f, 0.15f, 0.12f, 0.15f }, new float[] { -0.25f, -0.25f, -0.25f, -0.25f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Thin, BodyPartDefOf.Head), new values(new float[] { 0f, -0.23f, 0f, 0.23f }, new float[] { 0.37f, 0.35f, 0.33f, 0.35f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Thin, BodyPartDefOf.Jaw), new values(new float[] { 0f, -0.19f, 0f, 0.19f }, new float[] { 0.15f, 0.15f, 0.15f, 0.15f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Thin, BodyPartDefOf.Leg), new values(new float[] { -0.1f, 0.1f, 0.1f, 0.1f }, new float[] { -0.4f, -0.4f, -0.4f, -0.4f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Thin, BodyPartDefOf.Neck), new values(new float[] { 0f, -0.07f, 0f, 0.07f }, new float[] { 0.06f, 0.06f, 0.06f, 0.06f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Thin, BodyPartDefOf.Torso), new values(new float[] { 0f, 0f, 0f, 0f }, new float[] { -0.18f, -0.20f, -0.25f, -0.25f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Thin, genitalsDef), new values(new float[] { 0f, 0.01f, 0f, -0.01f }, new float[] { 0, -0.35f, -0.35f, -0.35f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Thin, anusDef), new values(new float[] { 0, 0.18f, 0, -0.18f }, new float[] { -0.42f, -0.35f, 0, -0.35f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Thin, chestDef), new values(new float[] { 0f, -0.1f, 0f, 0.1f }, new float[] { -0.06f, -0.05f, -0.06f, -0.05f }));

			//BodyType Female
			splatchAdjust.Add(new key(BodyTypeDefOf.Female, BodyPartDefOf.Arm), new values(new float[] { -0.17f, 0f, 0.17f, 0f }, new float[] { 0f, 0f, 0f, 0f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Female, BodyPartDefOf.Hand), new values(new float[] { -0.17f, 0.1f, 0.17f, 0.1f }, new float[] { -0.25f, -0.25f, -0.25f, -0.25f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Female, BodyPartDefOf.Head), new values(new float[] { 0f, -0.23f, 0f, 0.23f }, new float[] { 0.37f, 0.35f, 0.33f, 0.35f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Female, BodyPartDefOf.Jaw), new values(new float[] { 0f, -0.19f, 0f, 0.19f }, new float[] { 0.15f, 0.15f, 0.15f, 0.15f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Female, BodyPartDefOf.Leg), new values(new float[] { -0.2f, 0.1f, 0.2f, 0.1f }, new float[] { -0.4f, -0.4f, -0.4f, -0.4f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Female, BodyPartDefOf.Neck), new values(new float[] { 0f, -0.07f, 0f, 0.07f }, new float[] { 0.06f, 0.06f, 0.06f, 0.06f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Female, BodyPartDefOf.Torso), new values(new float[] { 0f, -0.05f, 0f, 0.05f }, new float[] { -0.20f, -0.25f, -0.25f, -0.25f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Female, genitalsDef), new values(new float[] { 0f, -0.10f, 0f, 0.10f }, new float[] { 0, -0.42f, -0.45f, -0.42f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Female, anusDef), new values(new float[] { 0, 0.26f, 0, -0.26f }, new float[] { -0.42f, -0.35f, 0, -0.35f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Female, chestDef), new values(new float[] { 0f, -0.12f, 0f, 0.12f }, new float[] { -0.06f, -0.05f, -0.06f, -0.05f }));

			//BodyType Male
			splatchAdjust.Add(new key(BodyTypeDefOf.Male, BodyPartDefOf.Arm), new values(new float[] { -0.21f, 0.05f, 0.21f, 0.05f }, new float[] { 0f, -0.02f, 0f, -0.02f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Male, BodyPartDefOf.Hand), new values(new float[] { -0.17f, 0.07f, 0.17f, 0.07f }, new float[] { -0.25f, -0.25f, -0.25f, -0.25f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Male, BodyPartDefOf.Head), new values(new float[] { 0f, -0.23f, 0f, 0.23f }, new float[] { 0.37f, 0.35f, 0.33f, 0.35f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Male, BodyPartDefOf.Jaw), new values(new float[] { 0f, -0.19f, 0f, 0.19f }, new float[] { 0.15f, 0.15f, 0.15f, 0.15f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Male, BodyPartDefOf.Leg), new values(new float[] { -0.17f, 0.07f, 0.17f, 0.07f }, new float[] { -0.4f, -0.4f, -0.4f, -0.4f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Male, BodyPartDefOf.Neck), new values(new float[] { 0f, -0.07f, 0f, 0.07f }, new float[] { 0.06f, 0.06f, 0.06f, 0.06f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Male, BodyPartDefOf.Torso), new values(new float[] { 0f, -0.05f, 0f, 0.05f }, new float[] { -0.20f, -0.25f, -0.25f, -0.25f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Male, genitalsDef), new values(new float[] { 0f, -0.07f, 0f, 0.07f }, new float[] { 0, -0.35f, -0.42f, -0.35f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Male, anusDef), new values(new float[] { 0, 0.17f, 0, -0.17f }, new float[] { -0.42f, -0.35f, 0, -0.35f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Male, chestDef), new values(new float[] { 0f, -0.16f, 0f, 0.16f }, new float[] { -0.06f, -0.05f, -0.06f, -0.05f }));

			//BodyType Hulk
			splatchAdjust.Add(new key(BodyTypeDefOf.Hulk, BodyPartDefOf.Arm), new values(new float[] { -0.3f, 0.05f, 0.3f, 0.05f }, new float[] { 0f, -0.02f, 0f, -0.02f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Hulk, BodyPartDefOf.Hand), new values(new float[] { -0.22f, 0.07f, 0.22f, 0.07f }, new float[] { -0.28f, -0.28f, -0.28f, -0.28f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Hulk, BodyPartDefOf.Head), new values(new float[] { 0f, -0.23f, 0f, 0.23f }, new float[] { 0.37f, 0.35f, 0.33f, 0.35f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Hulk, BodyPartDefOf.Jaw), new values(new float[] { 0f, -0.19f, 0f, 0.19f }, new float[] { 0.15f, 0.15f, 0.15f, 0.15f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Hulk, BodyPartDefOf.Leg), new values(new float[] { -0.17f, 0.07f, 0.17f, 0.07f }, new float[] { -0.5f, -0.5f, -0.5f, -0.5f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Hulk, BodyPartDefOf.Neck), new values(new float[] { 0f, -0.07f, 0f, 0.07f }, new float[] { 0.06f, 0.06f, 0.06f, 0.06f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Hulk, BodyPartDefOf.Torso), new values(new float[] { 0f, -0.05f, 0f, 0.05f }, new float[] { -0.20f, -0.3f, -0.3f, -0.3f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Hulk, genitalsDef), new values(new float[] { 0f, -0.02f, 0f, 0.02f }, new float[] { 0, -0.55f, -0.55f, -0.55f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Hulk, anusDef), new values(new float[] { 0, 0.35f, 0, -0.35f }, new float[] { -0.5f, -0.5f, 0, -0.5f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Hulk, chestDef), new values(new float[] { 0f, -0.22f, 0f, 0.22f }, new float[] { -0.06f, -0.05f, -0.06f, -0.05f }));

			//BodyType Fat
			splatchAdjust.Add(new key(BodyTypeDefOf.Fat, BodyPartDefOf.Arm), new values(new float[] { -0.3f, 0.05f, 0.3f, 0.05f }, new float[] { 0f, -0.02f, 0f, -0.02f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Fat, BodyPartDefOf.Hand), new values(new float[] { -0.32f, 0.07f, 0.32f, 0.07f }, new float[] { -0.28f, -0.28f, -0.28f, -0.28f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Fat, BodyPartDefOf.Head), new values(new float[] { 0f, -0.23f, 0f, 0.23f }, new float[] { 0.37f, 0.35f, 0.33f, 0.35f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Fat, BodyPartDefOf.Jaw), new values(new float[] { 0f, -0.19f, 0f, 0.19f }, new float[] { 0.15f, 0.15f, 0.15f, 0.15f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Fat, BodyPartDefOf.Leg), new values(new float[] { -0.17f, 0.07f, 0.17f, 0.07f }, new float[] { -0.45f, -0.45f, -0.45f, -0.45f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Fat, BodyPartDefOf.Neck), new values(new float[] { 0f, -0.07f, 0f, 0.07f }, new float[] { 0.06f, 0.06f, 0.06f, 0.06f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Fat, BodyPartDefOf.Torso), new values(new float[] { 0f, -0.15f, 0f, 0.15f }, new float[] { -0.20f, -0.3f, -0.3f, -0.3f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Fat, genitalsDef), new values(new float[] { 0f, -0.25f, 0f, 0.25f }, new float[] { 0, -0.45f, -0.50f, -0.45f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Fat, anusDef), new values(new float[] { 0, 0.35f, 0, -0.35f }, new float[] { -0.5f, -0.4f, 0, -0.4f }));
			splatchAdjust.Add(new key(BodyTypeDefOf.Fat, chestDef), new values(new float[] { 0f, -0.27f, 0f, 0.27f }, new float[] { -0.07f, -0.05f, -0.07f, -0.05f }));

			//now for the layer/plane adjustments:
			layerAdjust = new Dictionary<key_layer, values_layer>();

			//left body parts:
			//in theory, all body parts not coming in pairs should have the bool as false -> be listed as right, so I wouldn't need to add them here, but it doesn't hurt to be safe
			layerAdjust.Add(new key_layer(true, BodyPartDefOf.Arm), new values_layer(new float[] { 0f, -99f, 0f, 0f }));//0.00 = drawn over body (=visible) if the pawn looks in any direction except west, in which case it's hidden (-99)
			layerAdjust.Add(new key_layer(true, BodyPartDefOf.Hand), new values_layer(new float[] { 0f, -99f, 0f, 0f }));
			layerAdjust.Add(new key_layer(true, BodyPartDefOf.Leg), new values_layer(new float[] { 0f, -99f, 0f, 0f }));

			layerAdjust.Add(new key_layer(true, BodyPartDefOf.Head), new values_layer(new float[] { 0.02f, 0.02f, 0.02f, 0.02f }));//drawn from all directions, 0.02 = over hair
			layerAdjust.Add(new key_layer(true, BodyPartDefOf.Jaw), new values_layer(new float[] { -9f, 0.01f, 0.01f, 0.01f }));//0.01 = drawn over head but under hair, only hidden if facing north
			layerAdjust.Add(new key_layer(true, BodyPartDefOf.Neck), new values_layer(new float[] { 0f, 0f, 0f, 0f }));
			layerAdjust.Add(new key_layer(true, BodyPartDefOf.Torso), new values_layer(new float[] { 0f, 0f, 0f, 0f }));
			layerAdjust.Add(new key_layer(true, genitalsDef), new values_layer(new float[] { -99f, 0f, 0f, 0f }));//only hidden if facing north
			layerAdjust.Add(new key_layer(true, anusDef), new values_layer(new float[] { 0f, 0f, -99f, 0f }));
			layerAdjust.Add(new key_layer(true, chestDef), new values_layer(new float[] { -99f, 0f, 0f, 0f }));

			//right body parts:
			layerAdjust.Add(new key_layer(false, BodyPartDefOf.Arm), new values_layer(new float[] { 0f, 0f, 0f, -99f }));
			layerAdjust.Add(new key_layer(false, BodyPartDefOf.Hand), new values_layer(new float[] { 0f, 0f, 0f, -99f }));
			layerAdjust.Add(new key_layer(false, BodyPartDefOf.Leg), new values_layer(new float[] { 0f, 0f, 0f, -99f }));

			layerAdjust.Add(new key_layer(false, BodyPartDefOf.Head), new values_layer(new float[] { 0.02f, 0.02f, 0.02f, 0.02f }));
			layerAdjust.Add(new key_layer(false, BodyPartDefOf.Jaw), new values_layer(new float[] { -99f, 0.01f, 0.01f, 0.01f }));
			layerAdjust.Add(new key_layer(false, BodyPartDefOf.Neck), new values_layer(new float[] { 0f, 0f, 0f, 0f }));
			layerAdjust.Add(new key_layer(false, BodyPartDefOf.Torso), new values_layer(new float[] { 0f, 0f, 0f, 0f }));
			layerAdjust.Add(new key_layer(false, genitalsDef), new values_layer(new float[] { -99f, 0f, 0f, 0f }));
			layerAdjust.Add(new key_layer(false, anusDef), new values_layer(new float[] { 0f, 0f, -99f, 0f }));
			layerAdjust.Add(new key_layer(false, chestDef), new values_layer(new float[] { -99f, 0f, 0f, 0f }));

		}

		//all body parts that semen can theoretically be applied to:
		public static List<BodyPartDef> getAllowedBodyParts()
		{
			List<BodyPartDef> allowedParts = new List<BodyPartDef>();

			allowedParts.Add(BodyPartDefOf.Arm);
			allowedParts.Add(BodyPartDefOf.Hand);
			allowedParts.Add(BodyPartDefOf.Leg);
			allowedParts.Add(BodyPartDefOf.Head);
			allowedParts.Add(BodyPartDefOf.Jaw);
			allowedParts.Add(BodyPartDefOf.Neck);
			allowedParts.Add(BodyPartDefOf.Torso);
			allowedParts.Add(genitalsDef);
			allowedParts.Add(anusDef);
			allowedParts.Add(chestDef);

			return allowedParts;
		}

		//get valid body parts for a specific pawn
		public static IEnumerable<BodyPartRecord> getAvailableBodyParts(Pawn pawn)
		{
			//get all non-missing body parts:
			IEnumerable<BodyPartRecord> bodyParts = pawn.health.hediffSet.GetNotMissingParts(BodyPartHeight.Undefined, BodyPartDepth.Outside, null, null);
			BodyPartRecord anus = pawn.def.race.body.AllParts.Find(bpr => string.Equals(bpr.def.defName, "Anus"));//not found by above function since depth is "inside"

			if (anus != null)
			{
				bodyParts = bodyParts.AddItem(anus);
			}

			//filter for allowed body parts (e.g. no single fingers/toes):
			List<BodyPartDef> filterParts = SemenHelper.getAllowedBodyParts();

			IEnumerable<BodyPartRecord> filteredParts = bodyParts.Where(item1 => filterParts.Any(item2 => item2.Equals(item1.def)));
			return filteredParts;
		}

		public const int CUM_NORMAL = 0;
		public const int CUM_INSECT = 1;
		public const int CUM_MECHA = 2;

		public static readonly Color color_normal = new Color(0.95f, 0.95f, 0.95f);
		public static readonly Color color_insect = new Color(0.6f, 0.83f, 0.35f);//green-yellowish
		public static readonly Color color_mecha = new Color(0.37f, 0.71f, 0.82f);//cyan-ish

		//name should be self-explanatory:
		public static void cumOn(Pawn receiver, BodyPartRecord bodyPart, float amount = 0.2f, Pawn giver = null, int semenType = CUM_NORMAL)
		{
			Hediff_Semen hediff;
			if (semenType == CUM_NORMAL)
			{
				hediff = (Hediff_Semen)HediffMaker.MakeHediff(RJW_SemenoOverlayHediffDefOf.Hediff_Semen, receiver, null);
			}
			else if (semenType == CUM_INSECT)
			{
				hediff = (Hediff_Semen)HediffMaker.MakeHediff(RJW_SemenoOverlayHediffDefOf.Hediff_InsectSpunk, receiver, null);
			}
			else
			{
				hediff = (Hediff_Semen)HediffMaker.MakeHediff(RJW_SemenoOverlayHediffDefOf.Hediff_MechaFluids, receiver, null);
			}

			hediff.Severity = amount;//if this body part is already maximally full -> spill over to other parts

			//idea: here, a log entry that can act as source could be linked to the hediff - maybe reuse the playlog entry of rjw:
			hediff.semenType = semenType;

			try
			{
				//error when adding to missing part
				receiver.health.AddHediff(hediff, bodyPart, null, null);
			}
			catch
			{

			}

			//Log.Message(xxx.get_pawnname(receiver) + " cum amount" + amount);
			//causes significant memory leak, fixx someday
			//if (amount > 1f)//spillover in case of very large amounts: just apply hediff a second time
			//{
			//	Hediff_Semen hediff2 = (Hediff_Semen)HediffMaker.MakeHediff(hediff.def, receiver, null);
			//	hediff2.semenType = semenType;
			//	hediff2.Severity = amount - 1f;
			//	receiver.health.AddHediff(hediff2, bodyPart, null, null);
			//}

			//always also add bukkake hediff as manager
			receiver.health.AddHediff(RJW_SemenoOverlayHediffDefOf.Hediff_Bukkake);
		}

		//if spunk on one body part reaches a certain level, it can spill over to others, this function returns from where to where
		[SyncMethod]
		public static BodyPartDef spillover(BodyPartDef sourcePart)
		{
			//caution: danger of infinite loop if circular spillover between 2 full parts -> don't define possible circles
			BodyPartDef newPart = null;
			int sel;
			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			if (sourcePart == BodyPartDefOf.Torso)
			{
				sel = Rand.Range(0, 4);
				if (sel == 0)
				{
					newPart = BodyPartDefOf.Arm;
				}
				else if (sel == 1)
				{
					newPart = BodyPartDefOf.Leg;
				}
				else if (sel == 2)
				{
					newPart = BodyPartDefOf.Neck;
				}
				else if (sel == 3)
				{
					newPart = chestDef;
				}
			}
			else if (sourcePart == BodyPartDefOf.Jaw)
			{
				sel = Rand.Range(0, 4);
				if (sel == 0)
				{
					newPart = BodyPartDefOf.Head;
				}
				else if (sel == 1)
				{
					newPart = BodyPartDefOf.Torso;
				}
				else if (sel == 2)
				{
					newPart = BodyPartDefOf.Neck;
				}
				else if (sel == 3)
				{
					newPart = chestDef;
				}
			}
			else if (sourcePart == genitalsDef)
			{
				sel = Rand.Range(0, 2);
				if (sel == 0)
				{
					newPart = BodyPartDefOf.Leg;
				}
				else if (sel == 1)
				{
					newPart = BodyPartDefOf.Torso;
				}
			}
			else if (sourcePart == anusDef)
			{
				sel = Rand.Range(0, 2);
				if (sel == 0)
				{
					newPart = BodyPartDefOf.Leg;
				}
				else if (sel == 1)
				{
					newPart = BodyPartDefOf.Torso;
				}
			}
			else if (sourcePart == chestDef)
			{
				sel = Rand.Range(0, 3);
				if (sel == 0)
				{
					newPart = BodyPartDefOf.Arm;
				}
				else if (sel == 1)
				{
					newPart = BodyPartDefOf.Torso;
				}
				else if (sel == 2)
				{
					newPart = BodyPartDefOf.Neck;
				}
			}
			return newPart;
		}

		//determines who is the active male (or equivalent) in the exchange and the amount of semen dispensed and where to
		[SyncMethod]
		public static void calculateAndApplySemen(Pawn pawn, Pawn partner, xxx.rjwSextype sextype)
		{
			if (!RJWSettings.cum_on_body) return;

			Pawn giver, receiver;
			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());

			List<Hediff> giverparts;
			var pawnparts = Genital_Helper.get_PartsHediffList(pawn, Genital_Helper.get_genitalsBPR(pawn));
			var partnerparts = Genital_Helper.get_PartsHediffList(partner, Genital_Helper.get_genitalsBPR(partner));

			//dispenser of the seed
			if (Genital_Helper.has_penis_fertile(pawn, pawnparts) || xxx.is_mechanoid(pawn) || xxx.is_insect(pawn))
			{
				giver = pawn;
				giverparts = pawnparts;
				receiver = partner;
			}
			else if (partner != null && (Genital_Helper.has_penis_fertile(partner, partnerparts) || xxx.is_mechanoid(partner) || xxx.is_insect(partner)))
			{
				giver = partner;
				giverparts = partnerparts;
				receiver = pawn;
			}
			else//female on female or genderless - no semen dispensed; maybe add futa support?
			{
				return;
			}

			//slimes do not waste fluids?
			//if (xxx.is_slime(giver)) return;

			//determine entity:
			int entityType = SemenHelper.CUM_NORMAL;
			if (xxx.is_mechanoid(giver))
			{
				entityType = SemenHelper.CUM_MECHA;
			}
			else if (xxx.is_insect(giver))
			{
				entityType = SemenHelper.CUM_INSECT;
			}

			//ModLog.Message("giver " + xxx.get_pawnname(giver));
			//ModLog.Message("receiver " + xxx.get_pawnname(receiver));
			//ModLog.Message("Cumtype " + entityType);

			//get pawn genitalia:
			BodyPartRecord genitals;
			if (xxx.is_mechanoid(giver))
			{
				genitals = giver.RaceProps.body.AllParts.Find(x => string.Equals(x.def.defName, "MechGenitals"));
			}
			else//insects, animals, humans
			{
				genitals = giver.RaceProps.body.AllParts.Find(x => x.def == SemenHelper.genitalsDef);

			}
			//no cum without genitals
			if (genitals == null)
			{
				return;
			}

			float cumAmount = giver.BodySize; //fallback for mechanoinds and w/e without hediffs
			float horniness = 1f;
			float ageScale = Math.Min(80 / SexUtility.ScaleToHumanAge(giver), 1.0f);//calculation lifted from rjw

			if (xxx.is_mechanoid(giver) && giverparts.NullOrEmpty())
			{
				//use default above
			}
			else if (giverparts.NullOrEmpty())
				return;
			else
			{
				var penisHediff = giverparts.FindAll((Hediff hed) => hed.def.defName.ToLower().Contains("penis")).InRandomOrder().FirstOrDefault();

				if (penisHediff == null)
					penisHediff = giverparts.FindAll((Hediff hed) => hed.def.defName.ToLower().Contains("ovipositorf")).InRandomOrder().FirstOrDefault();
				if (penisHediff == null)
					penisHediff = giverparts.FindAll((Hediff hed) => hed.def.defName.ToLower().Contains("ovipositorm")).InRandomOrder().FirstOrDefault();
				if (penisHediff == null)
					penisHediff = giverparts.FindAll((Hediff hed) => hed.def.defName.ToLower().Contains("tentacle")).InRandomOrder().FirstOrDefault();

				if (penisHediff != null)
				{
					cumAmount = penisHediff.Severity * giver.BodySize;

					CompHediffBodyPart chdf = penisHediff.TryGetComp<rjw.CompHediffBodyPart>();
					if (chdf != null)
					{
						if (chdf.FluidAmmount != 0)
							cumAmount = chdf.FluidAmmount * chdf.FluidModifier;
					}
					//ModLog.Message("cumAmount " + cumAmount);

					Need sexNeed = giver?.needs?.AllNeeds.Find(x => string.Equals(x.def.defName, "Sex"));
					if (sexNeed != null)//non-humans don't have it - therefore just use the default value
					{
						horniness = 1f - sexNeed.CurLevel;
					}
				}
				else
				{
					//something is wrong... vagina?
					return;
				}
			}

			cumAmount = cumAmount * horniness * ageScale * RJWSettings.cum_on_body_amount_adjust;
			cumAmount /= 100;

			//TODO: SemenHelper Autofellatio
			//if no partner -> masturbation, apply some cum on self:
			//if (partner == null && sextype == xxx.rjwSextype.Autofellatio)
			//{
			//	if (!xxx.is_slime(giver))
			//		SemenHelper.cumOn(giver, BodyPartDefOf.Jaw, cumAmount, giver);
			//	return;
			//}
			if (partner == null && sextype == xxx.rjwSextype.Masturbation)
			{
				if (!xxx.is_slime(giver))
					SemenHelper.cumOn(giver, genitals, cumAmount * 0.3f, giver);//pawns are usually not super-messy -> only apply 30%
				return;
			}
			else if (partner != null)
			{
				List<BodyPartRecord> targetParts = new List<BodyPartRecord>();//which to apply semen on
				IEnumerable<BodyPartRecord> availableParts = SemenHelper.getAvailableBodyParts(receiver);
				BodyPartRecord randomPart;//not always needed

				switch (sextype)
				{
					case rjw.xxx.rjwSextype.Anal:
						targetParts.Add(receiver.RaceProps.body.AllParts.Find(x => x.def == SemenHelper.anusDef));
						break;
					case rjw.xxx.rjwSextype.Boobjob:
						targetParts.Add(receiver.RaceProps.body.AllParts.Find(x => x.def == SemenHelper.chestDef));
						break;
					case rjw.xxx.rjwSextype.DoublePenetration:
						targetParts.Add(receiver.RaceProps.body.AllParts.Find(x => x.def == SemenHelper.anusDef));
						targetParts.Add(receiver.RaceProps.body.AllParts.Find(x => x.def == SemenHelper.genitalsDef));
						break;
					case rjw.xxx.rjwSextype.Fingering:
						cumAmount = 0;
						break;
					case rjw.xxx.rjwSextype.Fisting:
						cumAmount = 0;
						break;
					case rjw.xxx.rjwSextype.Footjob:
						//random part:
						availableParts.TryRandomElement<BodyPartRecord>(out randomPart);
						targetParts.Add(randomPart);
						break;
					case rjw.xxx.rjwSextype.Handjob:
						//random part:
						availableParts.TryRandomElement<BodyPartRecord>(out randomPart);
						targetParts.Add(randomPart);
						break;
					case rjw.xxx.rjwSextype.Masturbation:
						cumAmount *= 2f;
						break;
					case rjw.xxx.rjwSextype.MechImplant:
						//one of the openings:
						int random = Rand.Range(0, 3);
						if (random == 0)
						{
							targetParts.Add(receiver.RaceProps.body.AllParts.Find(x => x.def == SemenHelper.genitalsDef));
						}
						else if (random == 1)
						{
							targetParts.Add(receiver.RaceProps.body.AllParts.Find(x => x.def == SemenHelper.anusDef));
						}
						else if (random == 2)
						{
							targetParts.Add(receiver.RaceProps.body.AllParts.Find(x => x.def == BodyPartDefOf.Jaw));
						}
						break;
					case rjw.xxx.rjwSextype.MutualMasturbation:
						//random
						availableParts.TryRandomElement<BodyPartRecord>(out randomPart);
						targetParts.Add(randomPart);
						break;
					case rjw.xxx.rjwSextype.None:
						cumAmount = 0;
						break;
					case rjw.xxx.rjwSextype.Oral:
						targetParts.Add(receiver.RaceProps.body.AllParts.Find(x => x.def == BodyPartDefOf.Jaw));
						break;
					case rjw.xxx.rjwSextype.Scissoring:
						//I guess if it came to here, a male must be involved?
						targetParts.Add(receiver.RaceProps.body.AllParts.Find(x => x.def == SemenHelper.genitalsDef));
						break;
					case rjw.xxx.rjwSextype.Vaginal:
						targetParts.Add(receiver.RaceProps.body.AllParts.Find(x => x.def == SemenHelper.genitalsDef));
						break;
				}

				if (cumAmount > 0)
				{
					if (xxx.is_slime(receiver))
					{
						//slime absorb cum
						//this needs balancing, since cumamount ranges 0-10(?) which is fine for cum/hentai but not very realistic for feeding
						//using TransferNutrition for now
						//Log.Message("cumAmount " + cumAmount);
						//float nutrition_amount = cumAmount/10;

						Need_Food need = need = giver.needs.TryGetNeed<Need_Food>();
						if (need == null)
						{
							//Log.Message("xxx::TransferNutrition() " + xxx.get_pawnname(pawn) + " doesn't track nutrition in itself, probably shouldn't feed the others");
							return;
						}

						if (receiver?.needs?.TryGetNeed<Need_Food>() != null)
						{
							//Log.Message("xxx::TransferNutrition() " +  xxx.get_pawnname(partner) + " can receive");
							float nutrition_amount = Math.Min(need.MaxLevel / 15f, need.CurLevel); //body size is taken into account implicitly by need.MaxLevel
							receiver.needs.food.CurLevel += nutrition_amount;
						}
					}
					else
					{
						SemenHelper.cumOn(giver, genitals, cumAmount * 0.3f, giver, entityType);//cum on self - smaller amount
						foreach (BodyPartRecord bpr in targetParts)
						{
							if (bpr != null)
							{
								SemenHelper.cumOn(receiver, bpr, cumAmount, giver, entityType);//cum on partner
							}
						}
					}
				}
			}
		}

	}
}


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

namespace rjw
{
	public class WorkGiver_CleanSelf : WorkGiver_Scanner
	{
		public override PathEndMode PathEndMode
		{
			get
			{
				return PathEndMode.InteractionCell;
			}
		}

		public override Danger MaxPathDanger(Pawn pawn)
		{
			return Danger.Deadly;
		}

		public override ThingRequest PotentialWorkThingRequest
		{
			get
			{
				return ThingRequest.ForGroup(ThingRequestGroup.Pawn);
			}
		}

		//conditions for self-cleaning job to be available
		public override bool HasJobOnThing(Pawn pawn, Thing t, bool forced = false)
		{
			if (xxx.DubsBadHygieneIsActive)
				return false;

			Hediff hediff = pawn.health.hediffSet.hediffs.Find(x => (x.def == RJW_SemenoOverlayHediffDefOf.Hediff_Bukkake));
			if (pawn != t || hediff == null)
				return false;

			if (!pawn.CanReserve(t, 1, -1, null, forced))
				return false;

			if (pawn.IsDesignatedHero())
			{
				if (!forced)
				{
					//ModLog.Message("WorkGiver_CleanSelf::not player interaction for hero, exit");
					return false;
				}
				if (!pawn.IsHeroOwner())
				{
					//ModLog.Message("WorkGiver_CleanSelf::player interaction for not owned hero, exit");
					return false;
				}
			}
			else
			{
				int minAge = 3 * 2500;//3 hours in-game must have passed
				if (!(hediff.ageTicks > minAge))
				{
					//ModLog.Message("WorkGiver_CleanSelf:: 3 hours in-game must pass to self-clean, exit");
					return false;
				}
			}

			return true;
		}

		public override Job JobOnThing(Pawn pawn, Thing t, bool forced = false)
		{
			return JobMaker.MakeJob(RJW_SemenOverlayJobDefOf.CleanSelf);
		}
	}
}

repo_name: aelina/rjw

license: mit


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

namespace rjw
{
	public class JobDriver_WhoreInvitingVisitors : JobDriver
	{
		// List of jobs that can be interrupted by whores.
		public static readonly List<JobDef> allowedJobs = new List<JobDef> { null, JobDefOf.Wait_Wander, JobDefOf.GotoWander, JobDefOf.Clean, JobDefOf.ClearSnow,
			JobDefOf.CutPlant, JobDefOf.HaulToCell, JobDefOf.Deconstruct, JobDefOf.Harvest, JobDefOf.LayDown, JobDefOf.Research, JobDefOf.SmoothFloor, JobDefOf.SmoothWall,
			JobDefOf.SocialRelax, JobDefOf.StandAndBeSociallyActive, JobDefOf.RemoveApparel, JobDefOf.Strip, JobDefOf.Tame, JobDefOf.Wait, JobDefOf.Wear, JobDefOf.FixBrokenDownBuilding,
			JobDefOf.FillFermentingBarrel, JobDefOf.DoBill, JobDefOf.Sow, JobDefOf.Shear, JobDefOf.BuildRoof, JobDefOf.DeliverFood,	JobDefOf.HaulToContainer, JobDefOf.Hunt, JobDefOf.Mine,
			JobDefOf.OperateDeepDrill, JobDefOf.OperateScanner, JobDefOf.RearmTurret, JobDefOf.Refuel, JobDefOf.RefuelAtomic, JobDefOf.RemoveFloor, JobDefOf.RemoveRoof, JobDefOf.Repair,
			JobDefOf.TakeBeerOutOfFermentingBarrel, JobDefOf.Train, JobDefOf.Uninstall, xxx.Masturbate};

		public bool successfulPass = true;

		private Pawn Whore => GetActor();
		private Pawn TargetPawn => TargetThingA as Pawn;
		private Building_Bed TargetBed => TargetThingB as Building_Bed;

		private readonly TargetIndex TargetPawnIndex = TargetIndex.A;
		private readonly TargetIndex TargetBedIndex = TargetIndex.B;

		private bool DoesTargetPawnAcceptAdvance()
		{
			if (RJWSettings.DebugWhoring) ModLog.Message($"JobDriver_InvitingVisitors::DoesTargetPawnAcceptAdvance() - {xxx.get_pawnname(TargetPawn)}");
			//if (RJWSettings.WildMode) return true;

			if (PawnUtility.EnemiesAreNearby(TargetPawn))
			{
				if (RJWSettings.DebugWhoring) ModLog.Message($" fail - enemy near");
				return false;
			}
			if (!allowedJobs.Contains(TargetPawn.jobs.curJob.def))
			{
				if (RJWSettings.DebugWhoring) ModLog.Message($" fail - not allowed job");
				return false;
			}

			if (RJWSettings.DebugWhoring)
			{
				ModLog.Message("Will try hookup " + WhoringHelper.WillPawnTryHookup(TargetPawn));
				ModLog.Message("Is whore appealing  " + WhoringHelper.IsHookupAppealing(TargetPawn, Whore));
				ModLog.Message("Can afford whore " + WhoringHelper.CanAfford(TargetPawn, Whore));
				ModLog.Message("Need sex " + (xxx.need_some_sex(TargetPawn) >= 1));
			}
			if (WhoringHelper.WillPawnTryHookup(TargetPawn) && WhoringHelper.IsHookupAppealing(TargetPawn, Whore) && WhoringHelper.CanAfford(TargetPawn, Whore) && xxx.need_some_sex(TargetPawn) >= 1f)
			{
				if (!Whore.IsPrisoner)
					Whore.skills.Learn(SkillDefOf.Social, 1.2f);
				return true;
			}
			return false;
		}

		public override bool TryMakePreToilReservations(bool errorOnFailed) => true;

		protected override IEnumerable<Toil> MakeNewToils()
		{
			this.FailOnDespawnedOrNull(TargetPawnIndex);
			this.FailOnDespawnedNullOrForbidden(TargetBedIndex);
			this.FailOn(() => Whore is null || !WhoreBed_Utility.CanUseForWhoring(Whore, TargetBed));//|| !Whore.CanReserve(TargetPawn)
			this.FailOn(() => pawn.Drafted);

			if (!Whore.IsPrisoner)
			{
				yield return Toils_Goto.GotoThing(TargetPawnIndex, PathEndMode.Touch);

				Toil TryItOn = new Toil();
				TryItOn.AddFailCondition(() => !xxx.IsTargetPawnOkay(TargetPawn));
				TryItOn.defaultCompleteMode = ToilCompleteMode.Delay;
				TryItOn.initAction = delegate
				{
					//ModLog.Message("JobDriver_InvitingVisitors::MakeNewToils - TryItOn - initAction is called");
					Whore.jobs.curDriver.ticksLeftThisToil = 50;
					MoteMaker.ThrowMetaIcon(Whore.Position, Whore.Map, ThingDefOf.Mote_Heart);
				};
				yield return TryItOn;
			}

			Toil AwaitResponse = new Toil();
			AwaitResponse.defaultCompleteMode = ToilCompleteMode.Delay;
			AwaitResponse.defaultDuration = 10;
			AwaitResponse.initAction = delegate
			{
				List<RulePackDef> extraSentencePacks = new List<RulePackDef>();
				successfulPass = DoesTargetPawnAcceptAdvance();
				//ModLog.Message("JobDriver_InvitingVisitors::MakeNewToils - AwaitResponse - initAction is called");
				if (successfulPass)
				{
					MoteMaker.ThrowMetaIcon(TargetPawn.Position, TargetPawn.Map, ThingDefOf.Mote_Heart);
					TargetPawn.jobs.EndCurrentJob(JobCondition.Incompletable);
					if (xxx.RomanceDiversifiedIsActive)
					{
						extraSentencePacks.Add(RulePackDef.Named("HookupSucceeded"));
					}
					if (Whore.health.HasHediffsNeedingTend())
					{
						successfulPass = false;
						string key = "RJW_VisitorSickWhore";
						string text = key.Translate(TargetPawn.LabelIndefinite(), Whore.LabelIndefinite()).CapitalizeFirst();
						Messages.Message(text, Whore, MessageTypeDefOf.TaskCompletion);
					}
					else
					{
						string key = "RJW_VisitorAcceptWhore";
						if (Whore.IsPrisoner)
						{
							key = "RJW_VisitorSolicitWhore";
						}
						string text = key.Translate(TargetPawn.LabelIndefinite(), Whore.LabelIndefinite()).CapitalizeFirst();
						Messages.Message(text, TargetPawn, MessageTypeDefOf.TaskCompletion);
					}
				}
				if (!successfulPass)
				{
					// remove bed reservation
					TargetBed.UnreserveForWhoring();

					MoteMaker.ThrowMetaIcon(TargetPawn.Position, TargetPawn.Map, ThingDefOf.Mote_IncapIcon);
					TargetPawn.needs.mood.thoughts.memories.TryGainMemory(
						TargetPawn.Faction == Whore.Faction
							? ThoughtDef.Named("RJWTurnedDownWhore")
							: ThoughtDef.Named("RJWFailedSolicitation"), Whore);

					if (xxx.RomanceDiversifiedIsActive)
					{
						Whore.needs.mood.thoughts.memories.TryGainMemory(ThoughtDef.Named("RebuffedMyHookupAttempt"), TargetPawn);
						TargetPawn.needs.mood.thoughts.memories.TryGainMemory(ThoughtDef.Named("FailedHookupAttemptOnMe"), Whore);
						extraSentencePacks.Add(RulePackDef.Named("HookupFailed"));
					}
					//Disabled rejection notifications
					//Messages.Message("RJW_VisitorRejectWhore".Translate(new object[] { xxx.get_pawnname(TargetPawn), xxx.get_pawnname(Whore) }), TargetPawn, MessageTypeDefOf.SilentInput);
				}
				if (xxx.RomanceDiversifiedIsActive)
				{
					Find.PlayLog.Add(new PlayLogEntry_Interaction(DefDatabase<InteractionDef>.GetNamed("TriedHookupWith"), pawn, TargetPawn, extraSentencePacks));
				}
				//Log.Message("[RJW] AwaitResponse initAction - successfulPass: " + successfulPass.ToString());
			};
			yield return AwaitResponse;

			Toil BothGoToBed = new Toil();
			BothGoToBed.AddFailCondition(() => !successfulPass || !WhoreBed_Utility.CanUseForWhoring(Whore, TargetBed));
			BothGoToBed.defaultCompleteMode = ToilCompleteMode.Instant;
			BothGoToBed.initAction = delegate
			{
				//ModLog.Message("JobDriver_InvitingVisitors::MakeNewToils - BothGoToBed - initAction is called0");
				if (!successfulPass) return;
				if (!WhoreBed_Utility.CanUseForWhoring(Whore, TargetBed) && Whore.CanReserve(TargetPawn, 1, 0))
				{
					//ModLog.Message("JobDriver_InvitingVisitors::MakeNewToils - BothGoToBed - cannot use the whore bed");
					return;
				}
				//ModLog.Message("JobDriver_InvitingVisitors::MakeNewToils - BothGoToBed - initAction is called1");

				TargetBed.ReserveForWhoring(Whore, 1800);//is 1800 ticks long enough to go to the bed (until next reservation is made?)

				Whore.jobs.jobQueue.EnqueueFirst(JobMaker.MakeJob(xxx.whore_is_serving_visitors, TargetPawn, TargetBed));
				//TargetPawn.jobs.jobQueue.EnqueueFirst(JobMaker.MakeJob(DefDatabase<JobDef>.GetNamed("WhoreIsServingVisitors"), Whore, TargetBed, (TargetBed.MaxAssignedPawnsCount>1)?TargetBed.GetSleepingSlotPos(1): TargetBed.)), null);
				Whore.jobs.curDriver.EndJobWith(JobCondition.InterruptOptional);
				//TargetPawn.jobs.curDriver.EndJobWith(JobCondition.InterruptOptional);
			};
			yield return BothGoToBed;
		}
	}
}


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

namespace rjw
{
	public class JobDriver_WhoreIsServingVisitors : JobDriver_SexBaseInitiator
	{
		public static readonly ThoughtDef thought_free = ThoughtDef.Named("Whorish_Thoughts");
		public static readonly ThoughtDef thought_captive = ThoughtDef.Named("Whorish_Thoughts_Captive");
		public IntVec3 SleepSpot => Bed.SleepPosOfAssignedPawn(pawn);

		public override bool TryMakePreToilReservations(bool errorOnFailed)
		{
			return pawn.Reserve(Target, job, 1, 0, null, errorOnFailed);
		}

		[SyncMethod]
		protected override IEnumerable<Toil> MakeNewToils()
		{
			if (RJWSettings.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":MakeNewToils() - making toils");
			setup_ticks();
			var PartnerJob = xxx.gettin_loved;

			this.FailOnDespawnedOrNull(iTarget);
			this.FailOnDespawnedNullOrForbidden(iBed);

			if (RJWSettings.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":MakeNewToils() fail conditions check " + !WhoreBed_Utility.CanUseForWhoring(pawn, Bed) + " " + !pawn.CanReserve(Partner));
			this.FailOn(() => !WhoreBed_Utility.CanUseForWhoring(pawn, Bed) || !pawn.CanReserve(Partner));
			this.FailOn(() => pawn.Drafted);
			this.FailOn(() => Partner.IsFighting());

			yield return Toils_Reserve.Reserve(iTarget, 1, 0);
			//yield return Toils_Reserve.Reserve(BedInd, Bed.SleepingSlotsCount, 0);

			if (RJWSettings.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":MakeNewToils() - generate toils");
			Toil gotoBed = new Toil();
			gotoBed.defaultCompleteMode = ToilCompleteMode.PatherArrival;
			gotoBed.FailOnWhorebedNoLongerUsable(iBed, Bed);
			gotoBed.AddFailCondition(() => Partner.Downed);
			gotoBed.FailOn(() => !Partner.CanReach(Bed, PathEndMode.Touch, Danger.Deadly));
			gotoBed.initAction = delegate
			{
				if (RJWSettings.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":MakeNewToils() - gotoWhoreBed");
				pawn.pather.StartPath(SleepSpot, PathEndMode.OnCell);
				Partner.jobs.StopAll();
				Job job = JobMaker.MakeJob(JobDefOf.GotoMindControlled, SleepSpot);
				Partner.jobs.StartJob(job, JobCondition.InterruptForced);
			};
			yield return gotoBed;

			ticks_left = (int)(2000.0f * Rand.Range(0.30f, 1.30f));

			Toil waitInBed = new Toil();
			waitInBed.initAction = delegate
			{
				ticksLeftThisToil = 5000;
			};
			waitInBed.tickAction = delegate
			{
				pawn.GainComfortFromCellIfPossible();
				if (IsInOrByBed(Bed, Partner) && pawn.PositionHeld == Partner.PositionHeld)
				{
					ReadyForNextToil();
				}
			};
			waitInBed.defaultCompleteMode = ToilCompleteMode.Delay;
			yield return waitInBed;

			Toil StartPartnerJob = new Toil();
			StartPartnerJob.defaultCompleteMode = ToilCompleteMode.Instant;
			StartPartnerJob.socialMode = RandomSocialMode.Off;
			StartPartnerJob.initAction = delegate
			{
				if (RJWSettings.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":MakeNewToils() - StartPartnerJob");
				var gettin_loved = JobMaker.MakeJob(PartnerJob, 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.FailOn(() => Partner.Dead);
			SexToil.FailOn(() => Partner.CurJob.def != PartnerJob);
			SexToil.initAction = delegate
			{
				if (RJWSettings.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":MakeNewToils() - loveToil");
				// TODO: replace this quick n dirty way
				CondomUtility.GetCondomFromRoom(pawn);

				// Try to use whore's condom first, then client's
				usedCondom = CondomUtility.TryUseCondom(pawn) || CondomUtility.TryUseCondom(Partner);

				// refresh bed reservation
				Bed.ReserveForWhoring(pawn, ticks_left+100);

				Start();

				if (!RJWSettings.HippieMode && xxx.HasNonPolyPartner(Partner, true))
				{
					Pawn lover = LovePartnerRelationUtility.ExistingLovePartner(Partner);
					// We have to do a few other checks because the pawn might have multiple lovers and ExistingLovePartner() might return the wrong one
					if (lover != null && pawn != lover && !lover.Dead && (lover.Map == Partner.Map || Rand.Value < 0.25) && GenSight.LineOfSight(lover.Position, Partner.Position, lover.Map))
					{
						lover.needs.mood.thoughts.memories.TryGainMemory(ThoughtDefOf.CheatedOnMe, Partner);
					}
				}
			};
			SexToil.AddPreTickAction(delegate
			{
				if (pawn.IsHashIntervalTick(ticks_between_hearts))
					if (xxx.is_nympho(pawn))
						ThrowMetaIcon(pawn.Position, pawn.Map, ThingDefOf.Mote_Heart);
					else
						ThrowMetaIcon(pawn.Position, pawn.Map, xxx.mote_noheart);
				SexTick(pawn, Partner);
				SexUtility.reduce_rest(Partner, 1);
				SexUtility.reduce_rest(pawn, 2);
				if (ticks_left % 100 == 0)
					Bed.ReserveForWhoring(pawn, ticks_left + 100); // without this, reservation sometimes expires before sex is finished
				if (ticks_left <= 0)
					ReadyForNextToil();
			});
			SexToil.AddFinishAction(delegate
			{
				End();
			});
			yield return SexToil;

			Toil afterSex = new Toil
			{
				initAction = delegate
				{
					// Adding interactions, social logs, etc
					SexUtility.ProcessSex(pawn, Partner, usedCondom: usedCondom, whoring: isWhoring, sextype: sexType);

					Bed.UnreserveForWhoring();

					if (!(Partner.IsColonist && (pawn.IsPrisonerOfColony || pawn.IsColonist)))
					{
						int unmodified_price = WhoringHelper.PriceOfWhore(pawn);

						float bed_factor = WhoreBed_Utility.CalculatePriceFactor(Bed);
						int price = (int)(unmodified_price * bed_factor);
						// bed_tip < 0 -> bad room/bed -> discount
						// bed_tip > 0 -> good room/bed -> bonus payment
						int bed_tip = price - unmodified_price;

						if (RJWSettings.DebugWhoring)
							ModLog.Message(this.GetType().ToString() + ":MakeNewToils() - will try to pay price: " + price.ToString() +
								((bed_tip < 0) ? (" (discounted by " + (-bed_tip).ToString() + " due to bad bed/room)")
									:((bed_tip > 0) ? (" (including tip of " + bed_tip.ToString() + " due to good bed/room)")
										:"")));

						int remainPrice = WhoringHelper.PayPriceToWhore(Partner, price, pawn);

						if (RJWSettings.DebugWhoring && remainPrice <= 0)
							ModLog.Message(" Paying price is success");
						else if (RJWSettings.DebugWhoring && remainPrice <= bed_tip)
							ModLog.Message(" Paying price is success (with less tip)");
						else if (RJWSettings.DebugWhoring)
							ModLog.Message(" Paying price failed");

						xxx.UpdateRecords(pawn, price - remainPrice);
					}

					var thought = (pawn.IsPrisoner || xxx.is_slave(pawn)) ? thought_captive : thought_free;
					pawn.needs.mood.thoughts.memories.TryGainMemory(thought);
					if (SexUtility.ConsiderCleaning(pawn))
					{
						LocalTargetInfo cum = pawn.PositionHeld.GetFirstThing<Filth>(pawn.Map);

						Job clean = JobMaker.MakeJob(JobDefOf.Clean);
						clean.AddQueuedTarget(TargetIndex.A, cum);

						pawn.jobs.jobQueue.EnqueueFirst(clean);
					}
				},
				defaultCompleteMode = ToilCompleteMode.Instant
			};
			yield return afterSex;
		}
	}
}


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

namespace rjw
{
	public class JobGiver_WhoreInvitingVisitors : ThinkNode_JobGiver
	{
		// Checks if pawn has a memory.
		// Maybe not the best place for function, might be useful elsewhere too.
		public static bool MemoryChecker(Pawn pawn, ThoughtDef thought)
		{
			Thought_Memory val = pawn.needs.mood.thoughts.memories.Memories.Find((Thought_Memory x) => (object)x.def == thought);
			return val == null ? false : true;
		}

		[SyncMethod]
		private static bool Roll_to_skip(Pawn client, Pawn whore)
		{
			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			float fuckability = SexAppraiser.would_fuck(client, whore); // 0.0 to 1.

			// More likely to skip other whores, because they're supposed to be working.
			if (client.IsDesignatedService())
				fuckability *= 0.6f;
			return fuckability >= 0.1f && xxx.need_some_sex(client) >= 1f && Rand.Chance(0.5f);
		}

		/*
		public static Pawn Find_pawn_to_fuck(Pawn whore, Map map)
		{
			Pawn best_fuckee = null;
			float best_distance = 1.0e6f;
			foreach (Pawn q in map.mapPawns.FreeColonists)
				if ((q != whore) &&
					xxx.need_some_sex(q)>0 &&
					whore.CanReserve(q, 1, 0) &&
					q.CanReserve(whore, 1, 0) &&
					Roll_to_skip(whore, q) &&
					(!q.Position.IsForbidden(whore)) &&
					xxx.is_healthy(q))
				{
					var dis = whore.Position.DistanceToSquared(q.Position);
					if (dis < best_distance)
					{
						best_fuckee = q;
						best_distance = dis;
					}
				}
			return best_fuckee;
		}
		*/

		private sealed class FindAttractivePawnHelper
		{
			internal Pawn whore;

			internal bool TraitCheckFail(Pawn client)
			{
				if (!xxx.is_human(client))
					return true;
				if (!xxx.has_traits(client))
					return true;
				if (!(xxx.can_fuck(client) || xxx.can_be_fucked(client)) || !xxx.IsTargetPawnOkay(client))
					return true;

				//Log.Message("client:" + client + " whore:" + whore);
				if (CompRJW.CheckPreference(client, whore) == false)
					return true;
				return false; // Everything ok.
			}

			//Use this check when client is not in the same faction as the whore
			internal bool FactionCheckPass(Pawn client)
			{
				return ((client.Map == whore.Map) && (client.Faction != null && client.Faction != whore.Faction) && !client.IsPrisoner && !client.HostileTo(whore));
			}

			//Use this check when client is in the same faction as the whore
			[SyncMethod]
			internal bool RelationCheckPass(Pawn client)
			{
				//Rand.PopState();
				//Rand.PushState(RJW_Multiplayer.PredictableSeed());
				if (xxx.isSingleOrPartnerNotHere(client) || xxx.is_lecher(client) || Rand.Value < 0.9f)
				{
					if (client != LovePartnerRelationUtility.ExistingLovePartner(whore))
					{ //Exception for prisoners to account for PrisonerWhoreSexualEmergencyTree, which allows prisoners to try to hook up with anyone who's around (mostly other prisoners or warden)
						return (client != whore) & (client.Map == whore.Map) && (client.Faction == whore.Faction || whore.IsPrisoner) && (client.IsColonist || whore.IsPrisoner) && WhoringHelper.IsHookupAppealing(whore, client);
					}
				}
				return false;
			}
		}

		[SyncMethod]
		public static Pawn FindAttractivePawn(Pawn whore, out int price)
		{
			price = 0;
			if (whore == null || xxx.is_asexual(whore))
			{
				if (RJWSettings.DebugWhoring) ModLog.Message($" {xxx.get_pawnname(whore)} is asexual, abort");
				return null;
			}
			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());

			FindAttractivePawnHelper client = new FindAttractivePawnHelper
			{
				whore = whore
			};
			price = WhoringHelper.PriceOfWhore(whore);
			int priceOfWhore = price;

			IntVec3 pos = whore.Position;

			IEnumerable<Pawn> potentialClients = whore.Map.mapPawns.AllPawnsSpawned;
			potentialClients = potentialClients.Where(x
				=> x != whore
				&& !x.IsForbidden(whore)
				&& !x.HostileTo(whore)
				&& !x.IsPrisoner
				&& x.Position.DistanceTo(pos) < 100
				&& xxx.is_healthy_enough(x));

			potentialClients = potentialClients.Except(potentialClients.Where(client.TraitCheckFail));

			if (!whore.IsPrisoner)
				potentialClients = potentialClients.Except(potentialClients.Where(x => !whore.CanReserveAndReach(x, PathEndMode.ClosestTouch, Danger.Some, 1)));
			else
				potentialClients = potentialClients.Except(potentialClients.Where(x => !x.CanReserveAndReach(whore, PathEndMode.ClosestTouch, Danger.Some, 1)));

			if (!potentialClients.Any()) return null;

			if (RJWSettings.DebugWhoring) ModLog.Message($" FindAttractivePawn number of all potential clients {potentialClients.Count()}");

			List<Pawn> valid_targets = new List<Pawn>();

			if (!whore.IsPrisoner)
				foreach (Pawn target in potentialClients)
				{
					if(xxx.cells_to_target_casual(whore, target.Position))
						if (xxx.can_path_to_target(whore, target.Position))
							valid_targets.Add(target);
				}
			else
				foreach (Pawn target in potentialClients)
				{
					if (xxx.cells_to_target_casual(target, whore.Position))
						if (xxx.can_path_to_target(target, whore.Position))
							valid_targets.Add(target);
				}

			if (RJWSettings.DebugWhoring) ModLog.Message($" number of reachable clients {valid_targets.Count()}");

			//IEnumerable<Pawn> guestsSpawned = valid_targets.Where(x => x.Faction != whore.Faction
			//	&& WhoringHelper.CanAfford(x, whore, priceOfWhore));

			//if (RJWSettings.DebugWhoring) ModLog.Message($" number of clients can afford {guestsSpawned.Count()}");

			//guestsSpawned = valid_targets.Where(x => x.Faction != whore.Faction
			//	&& x != LovePartnerRelationUtility.ExistingLovePartner(whore));

			//if (RJWSettings.DebugWhoring) ModLog.Message($" number of relations OK {guestsSpawned.Count()}");
			//guestsSpawned = valid_targets.Where(x => x.Faction != whore.Faction
			//	&& !MemoryChecker(x, ThoughtDef.Named("RJWFailedSolicitation")));

			//if (RJWSettings.DebugWhoring) ModLog.Message($" number of clients can memory OK {guestsSpawned.Count()}");

			IEnumerable<Pawn> guestsSpawned = valid_targets.Where(x => x.Faction != whore.Faction
				&& !MemoryChecker(x, ThoughtDef.Named("RJWFailedSolicitation"))
				&& WhoringHelper.CanAfford(x, whore, priceOfWhore)
				&& x != LovePartnerRelationUtility.ExistingLovePartner(whore));

			if (guestsSpawned.Any())
			{
				if (RJWSettings.DebugWhoring) ModLog.Message($" number of all acceptable Guests {guestsSpawned.Count()}");
				return guestsSpawned.RandomElement();
			}

			return null;

			//use casual sex for colonist hooking
			if (RJWSettings.DebugWhoring) ModLog.Message($" found no guests, trying colonists");

			if (!WhoringHelper.WillPawnTryHookup(whore))// will hookup colonists?
			{
				return null;
			}
			IEnumerable<Pawn> freeColonists = valid_targets.Where(x => x.Faction == whore.Faction
				&& Roll_to_skip(x, whore));

			if (RJWSettings.DebugWhoring) ModLog.Message($" number of free colonists {freeColonists.Count()}");

			freeColonists = freeColonists.Where(x => client.RelationCheckPass(x) && !MemoryChecker(x, ThoughtDef.Named("RJWTurnedDownWhore")));

			if (freeColonists.Any())
			{
				if (RJWSettings.DebugWhoring) ModLog.Message($" number of all acceptable Colonists {freeColonists.Count()}");
				return freeColonists.RandomElement();
			}

			return null;
		}

		protected override Job TryGiveJob(Pawn pawn)
		{

			// Most things are now checked in the ThinkNode_ConditionalWhore.

			if (pawn.Drafted) return null;
			if (MP.IsInMultiplayer) return null; //fix error someday, maybe

			if (pawn.jobs.curDriver is JobDriver_Sex || pawn.jobs.curDriver is JobDriver_WhoreInvitingVisitors) return null; // already having sex

			if (!SexUtility.ReadyForLovin(pawn))
			{
				//Whores need rest too, but this'll reduxe the wait a bit every it triggers.
				pawn.mindState.canLovinTick -= 40;
				return null;
			}

			if (RJWSettings.DebugWhoring) ModLog.Message($"JobGiver_WhoreInvitingVisitors.TryGiveJob:({xxx.get_pawnname(pawn)})");

			int price;
			Pawn client = FindAttractivePawn(pawn, out price);
			if (client == null)
			{
				if (RJWSettings.DebugWhoring) ModLog.Message($" no clients found");
				return null;
			}

			if (RJWSettings.DebugWhoring) ModLog.Message($" {xxx.get_pawnname(client)} is client");

			Building_Bed whorebed = WhoreBed_Utility.FindBed(pawn, client);
			if (whorebed == null)
			{
				if (RJWSettings.DebugWhoring) ModLog.Message($" {xxx.get_pawnname(pawn)} + {xxx.get_pawnname(client)} - no usable bed found");
				return null;
			}
			whorebed.ReserveForWhoring(pawn, 600); // reserve for a short while until whore can actually ask customer

			return JobMaker.MakeJob(xxx.whore_inviting_visitors, client, whorebed);
		}
	}
}


code:
using Verse;
using RimWorld;

namespace rjw
{

	public class RoomRoleWorker_Brothel : RoomRoleWorker
	{
		public override float GetScore(Room room)
		{
			int num = 0;
			var allContainedThings = room.ContainedAndAdjacentThings;
			foreach (var thing in allContainedThings)
			{
				var building_Bed = thing as Building_Bed;
				if (building_Bed?.def.building.bed_humanlike == true)
				{
					if (building_Bed.ForPrisoners) return 0;
					if (building_Bed.IsAllowedForWhoringAll()) num++;
				}
			}
			if (num < 1) return 0;
			return num * 110001; // higher than guest beds or "regular" beds when counting barracks
		}
	}

}


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

namespace rjw
{
	public static class WhoreBed_Utility
	{
		public static readonly RoomRoleDef roleDefBrothel = DefDatabase<RoomRoleDef>.GetNamed("Brothel");

		// find the best bed for a customer; whore is needed as parameter to only select beds that are reachable
		public static Building_Bed FindBed(Pawn whore, Pawn customer)
		{
			List<Building_Bed> b = FindReachableAndAvailableWhoreBeds(whore, customer);
			return GetBestBedForCustomer(customer, b);
		}
		public static bool CanUseForWhoring(Pawn pawn, Building_Bed bed)
		{
			bool flag = bed.IsAvailableForWhoring(pawn) && pawn.CanReserveAndReach(bed, PathEndMode.InteractionCell, Danger.Unspecified) && !bed.IsForbidden(pawn);
			return flag;
		}

		public static Building_Bed GetBestBedForCustomer(Pawn customer, List<Building_Bed> beds)
		{
			if (beds != null && beds.Any())
			{
				return beds.MaxBy(bed => CalculateBedScore(customer, bed));
			}
			else
			{
				return null;
			}
		}
		public static float GetCheapestBedFactor(Pawn whore, Pawn customer)
		{
			List<Building_Bed> beds = FindReachableAndAvailableWhoreBeds(whore, customer);
			return GetCheapestBedFactor(beds);
		}
		public static float GetCheapestBedFactor(List<Building_Bed> beds)
		{
			if (beds != null && beds.Any())
			{
				return CalculatePriceFactor(beds.MinBy(bed => CalculatePriceFactor(bed)));
			}
			else
			{
				return 0f;
			}
		}
		// unused
		/*public static float GetMostExpensiveBedFactor(List<Building_Bed> beds)
		{
			if (beds != null && beds.Any())
			{
				return CalculatePriceFactor(beds.MaxBy(bed => CalculatePriceFactor(bed)));
			}
			else
			{
				return 0f;
			}
		}//*/

		public static float CalculatePriceFactor(Building_Bed bed, bool force=false)
		{
			// cache result (no need for saving): if no result available, calculate; otherwise save tick at which it has been calculated.
			// additional parameter "force" to make it update in certain circumstances even if game is paused
			// Rand.Int % 10 flattens the spike over time if many beds are toggled at once
			if (!force
				&& SaveStorage.DataStore.GetBedData(bed).lastScoreUpdateTick >
					GenTicks.TicksGame - (SaveStorage.DataStore.GetBedData(bed).scoreUpdateTickDelay + Rand.Int % 10)
				&& SaveStorage.DataStore.GetBedData(bed).bedScore >= 0f)
			{
				//Log.Message("[RJW] lastScoreUpdateTick: " + SaveStorage.DataStore.GetBedData(bed).lastScoreUpdateTick.ToString() + " / TicksGame: "+ GenTicks.TicksGame.ToString());
				return SaveStorage.DataStore.GetBedData(bed).bedScore;
			}

			// uncomfortable beds reduce price, comfortable beds make customers pay a tip
			float comfort = bed.GetStatValue(StatDefOf.Comfort);

			//
			Room room = bed.Map != null && bed.Map.regionAndRoomUpdater.Enabled ? bed.GetRoom() : null;

			if (room == null || room.PsychologicallyOutdoors) // apparently "outside" is a room
			{
				//if (RJWSettings.DebugWhoring)
				//	Log.Message("[RJW] bed "+ bed.thingIDNumber.ToString() + ((room == null)?": room is null":" is outside") + " -> score = " + (0.1f * comfort).ToString("F2"));
				SaveStorage.DataStore.GetBedData(bed).lastScoreUpdateTick = GenTicks.TicksGame;
				SaveStorage.DataStore.GetBedData(bed).bedScore = 0.1f * comfort;
				return 0.1f * comfort;
			}

			float room_multiplier = 1f;

			//Room sharing is not liked by patrons
			int numBeds = room.ContainedBeds.Where(x => x.def.building.bed_humanlike).Count();

			// TODO: maybe more differentiation between various room types?
			if (room.Role == roleDefBrothel)
				room_multiplier *= (float)Math.Pow(0.8, numBeds - 1);
			else // if(room.Role == RoomRoleDefOf.Barracks)
				room_multiplier /= (2 * (numBeds - 1) + 1);

			int scoreStageIndex = RoomStatDefOf.Impressiveness.GetScoreStageIndex(room.GetStat(RoomStatDefOf.Impressiveness));
			//Room impressiveness factor
			//0 < scoreStageIndex < 10 (Last time checked)
			//3 is mediocore
			room_multiplier *= (float)(scoreStageIndex <= 3? .4f+scoreStageIndex*.2f : 1f+.3f*(scoreStageIndex-3));

			float price_factor = room_multiplier * comfort;

			// delay recalculation if result is the same as before
			if(price_factor == SaveStorage.DataStore.GetBedData(bed).bedScore)
			{
				if (SaveStorage.DataStore.GetBedData(bed).scoreUpdateTickDelay < 720)
				{
					// slowly increase recalculation delay to two seconds on speed 3
					SaveStorage.DataStore.GetBedData(bed).scoreUpdateTickDelay += 60;
				}
			} else
			{
				// reset recalculation delay
				SaveStorage.DataStore.GetBedData(bed).scoreUpdateTickDelay = 60;
			}

			// update bed data
			SaveStorage.DataStore.GetBedData(bed).lastScoreUpdateTick = GenTicks.TicksGame;
			SaveStorage.DataStore.GetBedData(bed).bedScore = price_factor;

			// this is quite spammy
			//if (RJWSettings.DebugWhoring)
			//	Log.Message("[RJW]CalculatePriceFactor for bed " + bed.thingIDNumber.ToString() + ": "
			//		+ "room_multiplier (num beds, impressiveness) ("+room_multiplier.ToString() +") * "
			//		+ "comfort (" + comfort.ToString() + ") = " + price_factor.ToString());

			return price_factor;
		}
		// "old" function
		/*public static float CalculatePriceFactorSimple(Building_Bed bed)
		{
			// copied from RJW :D
			float room_multiplier = 1f;
			Room room = bed.Map != null && bed.Map.regionAndRoomUpdater.Enabled ? bed.GetRoom() : null;

			if (room == null) return 0f;

			//Room sharing is not liked by patrons
			room_multiplier = room_multiplier / (2 * (room.ContainedBeds.Count() - 1) + 1);
			int scoreStageIndex = RoomStatDefOf.Impressiveness.GetScoreStageIndex(room.GetStat(RoomStatDefOf.Impressiveness));
			//Room impressiveness factor
			//0 < scoreStageIndex < 10 (Last time checked)
			//3 is mediocore
			if (scoreStageIndex == 0)
			{
				room_multiplier *= 0.3f;
			}
			else if (scoreStageIndex > 3)
			{
				room_multiplier *= 1 + ((float)scoreStageIndex - 3) / 3.5f;
			} //top room triples the price

			return room_multiplier;
		}*/

		// customers would want the best bed
		// TODO: price as factor, rebalance
		public static float CalculateBedScore(Pawn customer, Building_Bed bed)
		{
			float basePriceFactor = CalculatePriceFactor(bed);

			// ascetic pawns want the least impressive room
			if (customer.story.traits.HasTrait(TraitDefOf.Ascetic))
			{
				float comfort = bed.GetStatValue(StatDefOf.Comfort);
				basePriceFactor = comfort * comfort / basePriceFactor; // inverse room effects - may be cheap, but should still be comfortable. ascetic isn't masochistic!
				if (RJWSettings.DebugWhoring)
					Log.Message("[RJW]CalculateBedScore - Customer is ascetic");
			}

			basePriceFactor *= 200; // make a larger number for better distance scaling (and random effect)

			// horny pawns are in a hurry and want a closer bed
			int distance = 0;
			if (xxx.is_hornyorfrustrated(customer))
			{
				distance = (int)bed.Position.DistanceTo(customer.Position);
				//if (RJWSettings.DebugWhoring)
				//	Log.Message("[RJW]CalculateBedScore - Pawn is horny - distance = "+distance.ToString());
			}

			int random_factor = Rand.Int % 100;
			float score = basePriceFactor - distance + random_factor;

			if (RJWSettings.DebugWhoring)
				Log.Message("[RJW]CalculateBedScore for bed "+ bed.thingIDNumber.ToString() + ": "
					+ "score from price ("+ basePriceFactor.ToString() +") "
					+ "- distance ("+distance.ToString()+") "
					+ "+ randomness ("+random_factor.ToString()+") "
					+ "= " + score.ToString());

			return score;
		}

		public static List<Building_Bed> FindReachableAndAvailableWhoreBeds(Pawn whore, Pawn customer)
		{
			List<Building_Bed> wb = new List<Building_Bed>();

			wb = whore.MapHeld.GetWhoreBeds().Where(bed =>
				!bed.IsForbidden(whore) &&
				!bed.IsForbidden(customer) &&
				!bed.IsBurning() &&
				bed.WhoringIsAllowedForPawn(whore) &&
				bed.IsAvailableForWhoring(whore) &&
				whore.CanReserveAndReach(bed, PathEndMode.OnCell, Danger.Unspecified) &&
				customer.CanReach(bed, PathEndMode.OnCell, Danger.Some)
				// TODO: price/affordable?
				).ToList();
			if (RJWSettings.DebugWhoring)
				Log.Message("[RJW]FindReachableAndAvailableWhoreBeds - found "+ wb.Count().ToString() +" beds");
			return wb;
		}

		public static IEnumerable<Building_Bed> GetWhoreBeds(this Map map, Area area = null)
		{
			if (map == null) return new Building_Bed[0];
			if (area == null) return map.listerBuildings.AllBuildingsColonistOfClass<Building_Bed>();
			return map.listerBuildings.AllBuildingsColonistOfClass<Building_Bed>().Where(b => area[b.Position]);
		}

		public static bool WhoringIsAllowedForPawn(this Building_Bed bed, Pawn pawn)
		{
			if (bed.IsAllowedForWhoringAll())
				return true;
			if (bed == pawn.ownership.OwnedBed && bed.IsAllowedForWhoringOwner())
				return true;
			return false;
		}

		public static void SetAllowedForWhoringOwner(this Building_Bed bed, bool isAllowed)
		{
			if (!isAllowed)
			{
				// if whoring is disallowed for owner, also disallow for all
				bed.SetAllowedForWhoringAll(false);
			}
			SaveStorage.DataStore.GetBedData(bed).allowedForWhoringOwner = isAllowed;
		}
		public static void ToggleAllowedForWhoringOwner(this Building_Bed bed)
		{
			bed.SetAllowedForWhoringOwner(!SaveStorage.DataStore.GetBedData(bed).allowedForWhoringOwner);
		}
		public static bool IsAllowedForWhoringOwner(this Building_Bed bed)
		{
			if (bed.Medical || bed.def.defName.Contains("Guest"))
			{
				return false;
			}
			if (bed.ForPrisoners)
			{
				// no toggle on prisoner beds, they may always use their own bed (if they are supposed to whore, anyway)
				return true;
			}
			return SaveStorage.DataStore.GetBedData(bed).allowedForWhoringOwner;
		}

		public static void SetAllowedForWhoringAll(this Building_Bed bed, bool isAllowed)
		{
			if (isAllowed)
			{
				// if whoring is allowed for all, also visibly allow for owner
				bed.SetAllowedForWhoringOwner(true);
				// if bed is designated for whoring, disable prisoner/medical use
				bed.ForPrisoners = false;
				bed.Medical = false;
			}
			SaveStorage.DataStore.GetBedData(bed).allowedForWhoringAll = isAllowed;

			bed.GetRoom()?.Notify_BedTypeChanged();
			bed.Notify_ColorChanged();
		}
		public static void ToggleAllowedForWhoringAll(this Building_Bed bed)
		{
			bed.SetAllowedForWhoringAll(!SaveStorage.DataStore.GetBedData(bed).allowedForWhoringAll);
		}
		public static bool IsAllowedForWhoringAll(this Building_Bed bed)
		{
			if (SaveStorage.DataStore.GetBedData(bed).allowedForWhoringAll)
			{
				if (bed.Medical || bed.ForPrisoners || bed.def.defName.Contains("Guest") || (bed.GetRoom()?.isPrisonCell == true))
				{
					return false;
				}
				return true;
			}
			return false;
		}

		public static void ReserveForWhoring(this Building_Bed bed, Pawn p, int num_ticks)
		{
			SaveStorage.DataStore.GetBedData(bed).reservedUntilGameTick = GenTicks.TicksGame + num_ticks;
			SaveStorage.DataStore.GetBedData(bed).reservedForPawnID = p.thingIDNumber;
		}
		public static void UnreserveForWhoring(this Building_Bed bed)
		{
			SaveStorage.DataStore.GetBedData(bed).reservedUntilGameTick = 0;
			SaveStorage.DataStore.GetBedData(bed).reservedForPawnID = 0;
		}
		public static bool IsAvailableForWhoring(this Building_Bed bed, Pawn p)
		{
			// check for active reservation
			if (SaveStorage.DataStore.GetBedData(bed).reservedUntilGameTick > GenTicks.TicksGame)
			{
				if(SaveStorage.DataStore.GetBedData(bed).reservedForPawnID != p.thingIDNumber)
				{
					// a different pawn has reserved this bed
					return false;
				}
			}

			if(bed.OwnersForReading.Any())
			{
				for(int i=0; i<bed.OwnersForReading.Count; i++)
				{
					if(bed.OwnersForReading[i].InBed() && bed.OwnersForReading[i].CurrentBed() == bed)
					{
						// someone sleeping in this bed
						return false;
					}
				}
			}

			return true;
		}
	}
}


code:
namespace rjw
{
	//This class is not used now.
	/*
	public class ThinkNode_ChancePerHour_Whore : ThinkNode_ChancePerHour
	{
		protected override float MtbHours(Pawn pawn)
		{
			// Use the fappin mtb hours as the base number b/c it already accounts for things like age
			var base_mtb = xxx.config.whore_mtbh_mul * ThinkNode_ChancePerHour_Fappin.get_fappin_mtb_hours(pawn);
			if (base_mtb < 0.0f)
				return -1.0f;

			float desire_factor;
			{
				var need_sex = pawn.needs.TryGetNeed<Need_Sex>();
				if (need_sex != null)
				{
					if (need_sex.CurLevel <= need_sex.thresh_frustrated())
						desire_factor = 0.15f;
					else if (need_sex.CurLevel <= need_sex.thresh_horny())
						desire_factor = 0.60f;
					else
						desire_factor = 1.00f;
				}
				else
					desire_factor = 1.00f;
			}

			float personality_factor;
			{
				personality_factor = 1.0f;
				if (pawn.story != null)
				{
					foreach (var trait in pawn.story.traits.allTraits)
					{
						if (trait.def == xxx.nymphomaniac) personality_factor *= 0.25f;
						else if (trait.def == TraitDefOf.Greedy) personality_factor *= 0.50f;
						else if (xxx.RomanceDiversifiedIsActive&&(trait.def==xxx.philanderer || trait.def == xxx.polyamorous)) personality_factor *= 0.75f;
						else if (xxx.RomanceDiversifiedIsActive && (trait.def == xxx.faithful)&& LovePartnerRelationUtility.HasAnyLovePartner(pawn)) personality_factor *= 30f;
					}
				}
			}

			float fun_factor;
			{
				if ((pawn.needs.joy != null) && (xxx.is_nympho(pawn)))
					fun_factor = Mathf.Clamp01(0.50f + pawn.needs.joy.CurLevel);
				else
					fun_factor = 1.00f;
			}

			var gender_factor = (pawn.gender == Gender.Male) ? 1.0f : 3.0f;

			return base_mtb * desire_factor * personality_factor * fun_factor * gender_factor;
		}
	}
	*/
}


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

namespace rjw
{
	/// <summary>
	/// Whore/prisoner look for customers
	/// </summary>
	public class ThinkNode_ConditionalWhore : ThinkNode_Conditional
	{
		protected override bool Satisfied(Pawn p)
		{
			// No animal whorin' for now.
			if (xxx.is_animal(p))
				return false;

			if (!InteractionUtility.CanInitiateInteraction(p))
				return false;

			return xxx.is_whore(p);
		}
	}
}


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

namespace rjw
{
	/// <summary>
	/// Extends the standard thought to add a counter for the whore stages
	/// </summary>
	class ThoughtDef_Whore : ThoughtDef
	{
		public List<int> stageCounts = new List<int>();
		public int storyOffset = 0;
	}

	class ThoughtWorker_Whore : Thought_Memory
	{
		public static readonly HashSet<string> backstories = new HashSet<string>(DefDatabase<StringListDef>.GetNamed("WhoreBackstories").strings);
		protected List<int> stages { get {return ((ThoughtDef_Whore)def).stageCounts; } }
		protected int storyOffset { get { return ((ThoughtDef_Whore)def).storyOffset; } }
		//protected virtual readonly List<int> stages = new List<int>() { 10, 40};
		//protected virtual readonly int story_offset = 10;

		public override int CurStageIndex
		{
			get
			{
				//Log.Message("Static fields are not null " + !(backstories is null) + !(whore_count is null));
				var c = pawn.records.GetAsInt(xxx.CountOfWhore);
				//Log.Message("Whore count of " + pawn + " is " + c);
				var b = backstories.Contains(pawn.story?.adulthood?.titleShort) ? storyOffset : 0;
				//Log.Message("Backstory offset " + b);
				var score = c + b;
				if (score > stages[stages.Count-1])
				{
					return stages.Count - 1;
				}
				//Log.Message("Starting search");
				var stage = stages.FindLastIndex(v => score > v)+1;
				//Log.Message("Search done, stage is " + stage);
				return stage;
			}
		}
	}
}


code:
// #define TESTMODE // Uncomment to enable logging.

using Verse;
using Verse.AI;
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using System.Diagnostics;
using System;
using UnityEngine;
using Verse.AI.Group;
using Multiplayer.API;

namespace rjw
{
	/// <summary>
	/// Helper for whoring related stuff
	/// </summary>
	public class WhoringHelper
	{
		public const float baseMinPrice = 10f;
		public const float baseMaxPrice = 20f;

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

		public static int WhoreMinPrice(Pawn whore)
		{
			float min_price = baseMinPrice;
			min_price *= WhoreAgeAdjustment(whore);
			min_price *= WhoreGenderAdjustment(whore);
			min_price *= WhoreInjuryAdjustment(whore);
			min_price *= WhoreAbilityAdjustmentMin(whore);
			//min_price *= WhoreRoomAdjustment(whore);
			if (xxx.has_traits(whore))
				min_price *= WhoreTraitAdjustmentMin(whore);

			return (int)min_price;
		}

		public static int WhoreMaxPrice(Pawn whore)
		{
			float max_price = baseMaxPrice;
			max_price *= WhoreAgeAdjustment(whore);
			max_price *= WhoreGenderAdjustment(whore);
			max_price *= WhoreInjuryAdjustment(whore);
			max_price *= WhoreAbilityAdjustmentMax(whore);
			//max_price *= WhoreRoomAdjustment(whore);
			if (xxx.has_traits(whore))
				max_price *= WhoreTraitAdjustmentMax(whore);

			return (int)max_price;
		}

		public static float WhoreGenderAdjustment(Pawn whore)
		{
			if (GenderHelper.GetSex(whore) == GenderHelper.Sex.futa)
				return 1.2f;
			return 1f;
		}

		public static float WhoreAgeAdjustment(Pawn whore)
		{
			return AgeConfigDef.Instance.whoringPriceByAge.Evaluate(SexUtility.ScaleToHumanAge(whore));
		}

		public static float WhoreInjuryAdjustment(Pawn whore)
		{
			float modifier = 1.0f;
			int injuries = whore.health.hediffSet.hediffs.Count(x => x.Visible && x.def.everCurableByItem && x.IsPermanent());

			if (injuries == 0) return modifier;

			while (injuries > 0)
			{
				modifier *= 0.85f;
				injuries--;
			}
			return modifier;
		}

		public static float WhoreAbilityAdjustmentMin(Pawn whore)
		{
			int b = backstories.Contains(whore.story?.adulthood?.titleShort) ? 30 : 0;
			int score = whore.records.GetAsInt(xxx.CountOfWhore);
			float multiplier = (score + b) / 100;
			multiplier = Math.Min(multiplier, 2);
			multiplier = (multiplier - 0.5f) * 0.5f + 1;
			return multiplier;
		}

		public static float WhoreAbilityAdjustmentMax(Pawn whore)
		{
			int b = backstories.Contains(whore.story?.adulthood?.titleShort) ? 30 : 0;
			int score = whore.records.GetAsInt(xxx.CountOfWhore);
			float multiplier = (score + b) / 100;
			multiplier = Math.Min(multiplier, 2);
			multiplier = (multiplier - 0.5f) * 0.5f + 1;
			return multiplier;
		}

		public static float WhoreTraitAdjustmentMin(Pawn whore)
		{
			float multiplier = WhoreTraitAdjustment(whore);
			if (xxx.is_masochist(whore)) // Won't haggle, may settle for low price
				multiplier *= 0.7f;
			if (xxx.is_nympho(whore)) // Same as above
				multiplier *= 0.4f;
			return multiplier;
		}

		public static float WhoreTraitAdjustmentMax(Pawn whore)
		{
			float multiplier = WhoreTraitAdjustment(whore);
			if (xxx.IndividualityIsActive && whore.story.traits.HasTrait(xxx.SYR_Haggler))
				multiplier *= 1.5f;
			if (whore.story.traits.HasTrait(TraitDefOf.Greedy))
				multiplier *= 2f;
			return multiplier;
		}

		public static float WhoreTraitAdjustment(Pawn whore)
		{
			float multiplier = 1f;

			if (xxx.has_traits(whore))
			{
				if (whore.story.traits.DegreeOfTrait(TraitDefOf.Industriousness) == 2) // Industrious
					multiplier *= 1.2f;
				if (whore.story.traits.DegreeOfTrait(TraitDefOf.Industriousness) == 1) // Hard Worker
					multiplier *= 1.1f;
				if (whore.story.traits.HasTrait(TraitDefOf.CreepyBreathing))
					multiplier *= 0.75f;

				if (whore.GetStatValue(StatDefOf.PawnBeauty) >= 2)
					multiplier *= 3.5f;
				else if (whore.GetStatValue(StatDefOf.PawnBeauty) >= 1)
					multiplier *= 2f;
				else if (whore.GetStatValue(StatDefOf.PawnBeauty) < 0)
					if (whore.GetStatValue(StatDefOf.PawnBeauty) >= -1)
						multiplier *= 0.8f;
					else
						multiplier *= 0.6f;
			}
			return multiplier;
		}

		/*public static float WhoreRoomAdjustment(Pawn whore)
		{
			float room_multiplier = 1f;
			Room ownedRoom = whore.ownership.OwnedRoom;

			if (ownedRoom == null) return 0f;

			//Room sharing is not liked by patrons
			room_multiplier = room_multiplier / (2 * (ownedRoom.Owners.Count() - 1) + 1);
			int scoreStageIndex = RoomStatDefOf.Impressiveness.GetScoreStageIndex(ownedRoom.GetStat(RoomStatDefOf.Impressiveness));
			//Room impressiveness factor
			//0 < scoreStageIndex < 10 (Last time checked)
			//3 is mediocore
			if (scoreStageIndex == 0)
			{
				room_multiplier *= 0.3f;
			}
			else if (scoreStageIndex > 3)
			{
				room_multiplier *= 1 + ((float)scoreStageIndex - 3) / 3.5f;
			} //top room triples the price

			return room_multiplier;
		}//*/

		[SyncMethod]
		public static int PriceOfWhore(Pawn whore)
		{
			float NeedSexFactor = xxx.is_hornyorfrustrated(whore) ? 1 - xxx.need_some_sex(whore) / 8 : 1f;

			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			float price = Rand.Range(WhoreMinPrice(whore), WhoreMaxPrice(whore));

			price *= NeedSexFactor;
			//--ModLog.Message(" xxx::PriceOfWhore - price is " + price);

			return (int)Math.Round(price);
		}

		public static bool CanAfford(Pawn targetPawn, Pawn whore, int priceOfWhore = -1)
		{
			if (targetPawn.Faction == whore.Faction) return true;

			//if (RJWSettings.DebugWhoring) ModLog.Message($"CanAfford for client {xxx.get_pawnname(targetPawn)}");
			int price = priceOfWhore < 0 ? PriceOfWhore(whore) : priceOfWhore;
			if (price == 0)
				return true;

			// can customer afford the cheapest bed? - skip check, should rarely make a difference
			//float bed_factor = WhoreBed_Utility.GetCheapestBedFactor(whore, targetPawn);
			//price = (int)(price * bed_factor);

			//if (RJWSettings.DebugWhoring) ModLog.Message($" whore price {price}");

			Lord lord = targetPawn.GetLord();
			Faction faction = targetPawn.Faction;
			int clientAmountOfSilvers = targetPawn.inventory.innerContainer.TotalStackCountOfDef(ThingDefOf.Silver);
			int totalAmountOfSilvers = clientAmountOfSilvers;

			if (faction != null)
			{
				List<Pawn> caravanMembers = targetPawn.Map.mapPawns.PawnsInFaction(targetPawn.Faction).Where(x => x.GetLord() == lord && x.inventory?.innerContainer?.TotalStackCountOfDef(ThingDefOf.Silver) > 0).ToList();
				if (!caravanMembers.Any())
				{
					//if (RJWSettings.DebugWhoring) ModLog.Message($" client not in caravan");
					//if (RJWSettings.DebugWhoring) ModLog.Message("CanAfford::(" + xxx.get_pawnname(targetPawn) + "," + xxx.get_pawnname(whore) + ") - totalAmountOfSilvers is " + totalAmountOfSilvers);
					return totalAmountOfSilvers >= price;
				}

				totalAmountOfSilvers += caravanMembers.Sum(member => member.inventory.innerContainer.TotalStackCountOfDef(ThingDefOf.Silver));
			}
			//if (RJWSettings.DebugWhoring) ModLog.Message($" client silver = {clientAmountOfSilvers} caravan silver = {totalAmountOfSilvers - clientAmountOfSilvers}");

			//if (RJWSettings.DebugWhoring) ModLog.Message("CanAfford:: can afford the whore: " + (totalAmountOfSilvers >= price));
			return totalAmountOfSilvers >= price;
		}

		//priceOfWhore is assumed >=0, and targetPawn is assumed to be able to pay the price(either by caravan, or by himself)
		//This means that targetPawn has total stackcount of silvers >= priceOfWhore.
		public static int PayPriceToWhore(Pawn targetPawn, int priceOfWhore, Pawn whore)
		{
			if (RJWSettings.DebugWhoring) ModLog.Message($"PayPriceToWhore for client {xxx.get_pawnname(targetPawn)}");
			int AmountLeft = priceOfWhore;
			if (targetPawn.Faction == whore.Faction || priceOfWhore == 0)
			{
				if (RJWSettings.DebugWhoring) ModLog.Message($" No need to pay price");
				return AmountLeft;
			}
			Lord lord = targetPawn.GetLord();
			//Caravan guestCaravan = Find.WorldObjects.Caravans.Where(x => x.Spawned && x.ContainsPawn(targetPawn) && x.Faction == targetPawn.Faction && !x.IsPlayerControlled).FirstOrDefault();
			List<Pawn> caravan = targetPawn.Map.mapPawns.PawnsInFaction(targetPawn.Faction).Where(x => x.GetLord() == lord && x.inventory?.innerContainer != null && x.inventory.innerContainer.TotalStackCountOfDef(ThingDefOf.Silver) > 0).ToList();

			IEnumerable<Thing> TraderSilvers;
			if (!caravan.Any())
			{
				if (RJWSettings.DebugWhoring) ModLog.Message($" (not a caravan member), try to pay with own silver");
				TraderSilvers = targetPawn.inventory.innerContainer.Where(x => x.def == ThingDefOf.Silver);
				foreach (Thing silver in TraderSilvers)
				{
					if (AmountLeft <= 0)
						break;
					int dropAmount = silver.stackCount >= AmountLeft ? AmountLeft : silver.stackCount;
					if (targetPawn.inventory.innerContainer.TryDrop(silver, whore.Position, whore.Map, ThingPlaceMode.Near, dropAmount, out Thing resultingSilvers))
					{
						if (resultingSilvers is null)
						{
							if (RJWSettings.DebugWhoring) ModLog.Message($" have no silver");
								continue;
						}
						AmountLeft -= resultingSilvers.stackCount;
						if (AmountLeft <= 0)
						{
							if (RJWSettings.DebugWhoring) ModLog.Message($" {xxx.get_pawnname(targetPawn)} paid {resultingSilvers.stackCount} silver");
							break;
						}
					}
					else
					{
						if (RJWSettings.DebugWhoring) ModLog.Message($" TryDrop failed");
							break;
					}
				}
				if (RJWSettings.DebugWhoring) ModLog.Message($" price: {priceOfWhore}, paid: {priceOfWhore-AmountLeft}");
			}
			else
			{
				if (RJWSettings.DebugWhoring) ModLog.Message($" (caravan member), try to pay {AmountLeft} silver with caravan silver");
				foreach (Pawn caravanMember in caravan)
				{
					TraderSilvers = caravanMember.inventory.innerContainer.Where(x => x.def == ThingDefOf.Silver);
					if (RJWSettings.DebugWhoring) ModLog.Message($" try to pay with {xxx.get_pawnname(caravanMember)} silver");
					foreach (Thing silver in TraderSilvers)
					{
						if (AmountLeft <= 0)
							break;
						int dropAmount = silver.stackCount >= AmountLeft ? AmountLeft : silver.stackCount;
						if (caravanMember.inventory.innerContainer.TryDrop(silver, whore.Position, whore.Map, ThingPlaceMode.Near, dropAmount, out Thing resultingSilvers))
						{
							if (resultingSilvers is null)
							{
								if (RJWSettings.DebugWhoring) ModLog.Message($" have no silver");
								continue;
							}
							AmountLeft -= resultingSilvers.stackCount;
							if (AmountLeft <= 0)
							{
								if (RJWSettings.DebugWhoring) ModLog.Message($" {xxx.get_pawnname(caravanMember)} paid {resultingSilvers.stackCount} silver");
								break;
							}
						}
					}
				}
			}
			if (RJWSettings.DebugWhoring) ModLog.Message($" price: {priceOfWhore}, paid: {priceOfWhore - AmountLeft}");
			return AmountLeft;
		}

		[SyncMethod]
		public static bool IsHookupAppealing(Pawn target, Pawn whore)
		{
			if (PawnUtility.WillSoonHaveBasicNeed(target))
			{
				//Log.Message("IsHookupAppealing - fail: " + xxx.get_pawnname(target) + " has need to do");
				return false;
			}
			float num = target.relations.SecondaryRomanceChanceFactor(whore) / 1.5f;
			if (xxx.is_frustrated(target))
			{
				num *= 3.0f;
			}
			else if (xxx.is_hornyorfrustrated(target))
			{
				num *= 2.0f;
			}
			if (xxx.is_zoophile(target) && !xxx.is_animal(whore))
			{
				num *= 0.5f;
			}
			if (xxx.AlienFrameworkIsActive)
			{
				if (xxx.is_xenophile(target))
				{
					if (target.def.defName == whore.def.defName)
						num *= 0.5f; // Same species, xenophile less interested.
					else
						num *= 1.5f; // Different species, xenophile more interested.
				}
				else if (xxx.is_xenophobe(target))
				{
					if (target.def.defName != whore.def.defName)
						num *= 0.25f; // Different species, xenophobe less interested.
				}
			}

			num *= 0.8f + ((float)whore.skills.GetSkill(SkillDefOf.Social).Level / 40); // 0.8 to 1.3
			num *= Mathf.InverseLerp(-100f, 0f, target.relations.OpinionOf(whore)); // 1 to 0 reduce score by negative opinion/relations to whore
			//Log.Message("IsHookupAppealing - score: " + num);
			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			return Rand.Range(0.05f, 1f) < num;
		}

		// Summary:
		//   Check if the pawn is willing to hook up. Checked for both target and the whore(when hooking colonists).
		[SyncMethod]
		public static bool WillPawnTryHookup(Pawn target)
		{
			if (target.story.traits.HasTrait(TraitDefOf.Asexual))
			{
				return false;
			}
			Pawn lover = LovePartnerRelationUtility.ExistingMostLikedLovePartner(target, false);
			if (lover == null)
			{
				return true;
			}
			float num = target.relations.OpinionOf(lover);
			float num2 = Mathf.InverseLerp(30f, -80f, num);

			if (xxx.is_prude(target))
			{
				num2 = 0f;
			}
			else if (xxx.is_lecher(target))
			{
				//Lechers are always up for it.
				num2 = Mathf.InverseLerp(100f, 50f, num);
			}
			else if (target.Map == lover.Map)
			{
				//Less likely to cheat if the lover is on the same map.
				num2 = Mathf.InverseLerp(70f, 15f, num);
			}
			//else default values

			if (xxx.is_frustrated(target))
			{
				num2 *= 1.8f;
			}
			else if (xxx.is_hornyorfrustrated(target))
			{
				num2 *= 1.4f;
			}
			num2 /= 1.5f;
			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			return Rand.Range(0f, 1f) < num2;
		}
	}
}


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

namespace rjw
{
	public class Need_Sex : Need_Seeker
	{
		private bool isInvisible => pawn.Map == null;

		private bool BootStrapTriggered = false;

		private int needsex_tick = needsex_tick_timer;
		public const int needsex_tick_timer = 10;
		private static float decay_per_day = 0.3f;
		private float decay_rate_modifier = RJWSettings.sexneed_decay_rate;

		public float thresh_frustrated()
		{
			return 0.05f;
		}

		public float thresh_horny()
		{
			return 0.25f;
		}

		public float thresh_neutral()
		{
			return 0.50f;
		}

		public float thresh_satisfied()
		{
			return 0.75f;
		}

		public float thresh_ahegao()
		{
			return 0.95f;
		}

		public Need_Sex(Pawn pawn) : base(pawn)
		{
			//if (xxx.is_mechanoid(pawn)) return; //Added by nizhuan-jjr:Misc.Robots are not allowed to have sex, so they don't need sex actually.
			threshPercents = new List<float>
			{
				thresh_frustrated(),
				thresh_horny(),
				thresh_neutral(),
				thresh_satisfied(),
				thresh_ahegao()
			};
		}

		public static float brokenbodyfactor(Pawn pawn)
		{
			//This adds in the broken body system
			float broken_body_factor = 1f;
			if (pawn.health.hediffSet.HasHediff(xxx.feelingBroken))
			{
				switch (pawn.health.hediffSet.GetFirstHediffOfDef(xxx.feelingBroken).CurStageIndex)
				{
					case 0:
						return 0.75f;
					case 1:
						return 1.4f;
					case 2:
						return 2f;
				}
			}
			return broken_body_factor;
		}

		//public override bool ShowOnNeedList
		//{
		//	get
		//	{
		//		if (Genital_Helper.has_genitals(pawn))
		//			return true;

		//		ModLog.Message("curLevelInt " + curLevelInt);
		//		return false;
		//	}
		//}

		//public override string GetTipString()
		//{
		//	return string.Concat(new string[]
		//	{
		//		this.LabelCap,
		//		": ",
		//		this.CurLevelPercentage.ToStringPercent(),
		//		"\n",
		//		this.def.description,
		//		"\n",
		//	});
		//}

		public static float druggedfactor(Pawn pawn)
		{
			if (pawn.health.hediffSet.HasHediff(HediffDef.Named("HumpShroomAddiction")) && !pawn.health.hediffSet.HasHediff(HediffDef.Named("HumpShroomEffect")))
			{
				//ModLog.Message("Need_Sex::druggedfactor 0.5 pawn is " + xxx.get_pawnname(pawn));
				return 0.5f;
			}

			if (pawn.health.hediffSet.HasHediff(HediffDef.Named("HumpShroomEffect")))
			{
				//ModLog.Message("Need_Sex::druggedfactor 3 pawn is " + xxx.get_pawnname(pawn));
				return 3f;
			}

			//ModLog.Message("Need_Sex::druggedfactor 1 pawn is " + xxx.get_pawnname(pawn));
			return 1f;
		}

		static float diseasefactor(Pawn pawn)
		{
			if (pawn.health.hediffSet.HasHediff(HediffDef.Named("Boobitis")))
			{
				return 3f;
			}

			return 1f;
		}

		static float agefactor(Pawn pawn)
		{
			if (xxx.is_human(pawn))
			{
				int age = pawn.ageTracker.AgeBiologicalYears;

				Need_Sex horniness = pawn.needs.TryGetNeed<Need_Sex>();
				if (horniness.CurLevel > 0.5f)
					return 1f;

				if (age < RJWSettings.sex_minimum_age)
					return 0f;
			}
			return 1f;
		}

		static float fall_per_tick(Pawn pawn)
		{
			var partBPR = Genital_Helper.get_genitalsBPR(pawn);
			var parts = Genital_Helper.get_PartsHediffList(pawn, partBPR);

			var fall_per_tick =
				//def.fallPerDay *
				decay_per_day *
				brokenbodyfactor(pawn) *
				druggedfactor(pawn) *
				diseasefactor(pawn) *
				agefactor(pawn) *
				(((Genital_Helper.has_penis_fertile(pawn, parts) || Genital_Helper.has_penis_infertile(pawn, parts)) && Genital_Helper.has_vagina(pawn, parts)) ? 2.0f : 1.0f) /
				60000.0f;
			//--ModLog.Message("Need_Sex::NeedInterval is called - pawn is " + xxx.get_pawnname(pawn) + " is has both genders " + (Genital_Helper.has_penis(pawn) && Genital_Helper.has_vagina(pawn)));
			//ModLog.Message(" " + xxx.get_pawnname(pawn) + "'s sex need stats:: fall_per_tick: " + fall_per_tick + ", sex_need_factor_from_lifestage: " + sex_need_factor_from_lifestage(pawn) );
			return fall_per_tick;
		}

		public override void NeedInterval() //150 ticks between each calls from Pawn_NeedsTracker()
		{
			if (isInvisible) return; // no caravans

			if (needsex_tick <= 0) // every 10 ticks - real tick
			{
				std_updater.update(pawn);

				if (xxx.is_asexual(pawn))
				{
					CurLevel = 0.5f;
					return;
				}

				//--ModLog.Message("Need_Sex::NeedInterval is called0 - pawn is "+xxx.get_pawnname(pawn));
				needsex_tick = needsex_tick_timer;

				if (!def.freezeWhileSleeping || pawn.Awake())
				{
					var fall_per_call = 150 * needsex_tick_timer * fall_per_tick(pawn);
					CurLevel -= fall_per_call * xxx.get_sex_drive(pawn) * RJWSettings.sexneed_decay_rate;
					// Each day has 60000 ticks, each hour has 2500 ticks, so each hour has 50/3 calls, in other words, each call takes .06 hour.

					//ModLog.Message(" " + xxx.get_pawnname(pawn) + "'s sex need stats:: Decay/call: " + fall_per_call * decay_rate_modifier * xxx.get_sex_drive(pawn) + ", Cur.lvl: " + CurLevel + ", Dec. rate: " + decay_rate_modifier + ", Sex drive: " +  xxx.get_sex_drive(pawn));

					if (CurLevel < thresh_frustrated())
					{
						SexUtility.OffsetPsyfocus(pawn, -0.01f);
					}
					//if (CurLevel < thresh_horny())
					//{
					//	SexUtility.OffsetPsyfocus(pawn, -0.01f);
					//}
					//if (CurLevel < thresh_frustrated() || CurLevel > thresh_ahegao())
					//{
					//	SexUtility.OffsetPsyfocus(pawn, -0.05f);
					//}
				}

				//--ModLog.Message("Need_Sex::NeedInterval is called1");

				//If they need it, they should seek it
				if (CurLevel < thresh_horny() && (pawn.mindState.canLovinTick - Find.TickManager.TicksGame > 300) )
				{
					pawn.mindState.canLovinTick = Find.TickManager.TicksGame + 300;
				}

				// the bootstrap of the mapInjector will only be triggered once per visible pawn.
				if (!BootStrapTriggered)
				{
					//--ModLog.Message("Need_Sex::NeedInterval::calling boostrap - pawn is " + xxx.get_pawnname(pawn));
					xxx.bootstrap(pawn.Map);
					BootStrapTriggered = true;
				}
			}
			else
			{
				needsex_tick--;
			}
			//--ModLog.Message("Need_Sex::NeedInterval is called2 - needsex_tick is "+needsex_tick);
		}
	}
}


code:
using Verse;

namespace rjw
{
	/// <summary>
	/// Looks up and returns a BodyPartTagDef defined in the XML
	/// </summary>
	public static class BodyPartTagDefOf {
		public static BodyPartTagDef RJW_Fertility
		{
			get
			{
				if (a == null) a = (BodyPartTagDef)GenDefDatabase.GetDef(typeof(BodyPartTagDef), "RJW_Fertility");
				return a;
			}
		}
		private static BodyPartTagDef a;
	}

}


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

namespace rjw
{
	/// <summary>
	/// Calculates a pawn's fertility based on its age and fertility sources
	/// </summary>
	public class PawnCapacityWorker_Fertility : PawnCapacityWorker
	{
		public override float CalculateCapacityLevel(HediffSet diffSet, List<PawnCapacityUtility.CapacityImpactor> impactors = null)
		{
			Pawn pawn = diffSet.pawn;
			var partBPR = Genital_Helper.get_genitalsBPR(pawn);
			var parts = Genital_Helper.get_PartsHediffList(pawn, partBPR);

			if (!Genital_Helper.has_penis_fertile(pawn, parts) && !Genital_Helper.has_vagina(pawn, parts))
				return 0;

			if (Genital_Helper.has_ovipositorF(pawn, parts) || Genital_Helper.has_ovipositorM(pawn, parts))
				return 0;

			//ModLog.Message("PawnCapacityWorker_Fertility::CalculateCapacityLevel is called for: " + xxx.get_pawnname(pawn));
			RaceProperties race = diffSet.pawn.RaceProps;

			if (!pawn.RaceHasFertility())
			{
				//Log.Message(" Fertility_filter, no fertility for : " + pawn.kindDef.defName);
				return 0f;
			}

			//androids only fertile with archotech parts
			if (AndroidsCompatibility.IsAndroid(pawn) && !(AndroidsCompatibility.AndroidPenisFertility(pawn) || AndroidsCompatibility.AndroidVaginaFertility(pawn)))
			{
				//Log.Message(" Android has no archotech genitals set fertility to 0 for: " + pawn.kindDef.defName);
				return 0f;
			}

			float startAge = 0f;	//raise fertility
			float startMaxAge = 0f;	//max fertility
			float endAge = race.lifeExpectancy * (RJWPregnancySettings.fertility_endage_male * 0.7f); // Age when males start to lose potency.
			float zeroFertility = race.lifeExpectancy * RJWPregnancySettings.fertility_endage_male;	// Age when fertility hits 0%.

			if (xxx.is_female(pawn))
			{
				if (xxx.is_animal(pawn))
				{
					endAge = race.lifeExpectancy * (RJWPregnancySettings.fertility_endage_female_animal * 0.6f);
					zeroFertility = race.lifeExpectancy * RJWPregnancySettings.fertility_endage_female_animal;
				}
				else
				{
					endAge = race.lifeExpectancy * (RJWPregnancySettings.fertility_endage_female_humanlike * 0.6f); // Age when fertility begins to drop.
					zeroFertility = race.lifeExpectancy * RJWPregnancySettings.fertility_endage_female_humanlike; // Age when fertility hits 0%.
				}
			}

			foreach (LifeStageAge lifestage in race.lifeStageAges)
			{
				if (lifestage.def.reproductive)
					//presumably teen stage
					if (startAge == 0f && startMaxAge == 0f)
					{
						startAge = lifestage.minAge;
						startMaxAge = (Mathf.Max(startAge + (startAge + endAge) * 0.08f, startAge));
					}
					//presumably adult stage
					else
					{
						if (startMaxAge > lifestage.minAge)
							startMaxAge = lifestage.minAge;
					}
			}
			//Log.Message(" Fertility ages for " + pawn.Name + " are: " + startAge + ", " + startMaxAge + ", " + endAge + ", " + endMaxAge);

			float result = PawnCapacityUtility.CalculateTagEfficiency(diffSet, BodyPartTagDefOf.RJW_Fertility, 1f, FloatRange.ZeroToOne, impactors);
			result *= GenMath.FlatHill(startAge, startMaxAge, endAge, zeroFertility, pawn.ageTracker.AgeBiologicalYearsFloat);

			//ModLog.Message("PawnCapacityWorker_Fertility::CalculateCapacityLevel result is: " + result);
			return result;

		}

		public override bool CanHaveCapacity(BodyDef body)
		{
			return body.HasPartWithTag(BodyPartTagDefOf.RJW_Fertility);
		}
	}
}


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

namespace rjw
{
	public class Recipe_InstallAnus : Recipe_InstallPrivates
	{
		public override IEnumerable<BodyPartRecord> GetPartsToApplyOn(Pawn p, RecipeDef r)
		{
			var gen_blo = Genital_Helper.anus_blocked(p);
			foreach (BodyPartRecord part in base.GetPartsToApplyOn(p, r))
				if ((!gen_blo) || (part != xxx.anus))
					yield return part;
		}
	}
}


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

namespace rjw
{
	public class Recipe_InstallBreasts : Recipe_InstallPrivates
	{
		public override IEnumerable<BodyPartRecord> GetPartsToApplyOn(Pawn p, RecipeDef r)
		{
			var gen_blo = Genital_Helper.breasts_blocked(p);
			foreach (BodyPartRecord part in base.GetPartsToApplyOn(p, r))
				if ((!gen_blo) || (part != xxx.breasts))
					yield return part;
		}
	}
}


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

namespace rjw
{
	public class Recipe_InstallGenitals : Recipe_InstallPrivates
	{
		public override IEnumerable<BodyPartRecord> GetPartsToApplyOn(Pawn p, RecipeDef r)
		{
			var gen_blo = Genital_Helper.genitals_blocked(p);
			foreach (BodyPartRecord part in base.GetPartsToApplyOn(p, r))
				if ((!gen_blo) || (part != xxx.genitals))
					yield return part;

		}
	}
}


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

namespace rjw
{
	public class Recipe_InstallPart : rjw_CORE_EXPOSED.Recipe_InstallOrReplaceBodyPart
	{
		public override void ApplyOnPawn(Pawn pawn, BodyPartRecord part, Pawn billDoer, List<Thing> ingredients, Bill bill)
		{
			GenderHelper.Sex before = GenderHelper.GetSex(pawn);

			base.ApplyOnPawn(pawn, part, billDoer, ingredients, bill);

			GenderHelper.Sex after = GenderHelper.GetSex(pawn);

			GenderHelper.ChangeSex(pawn, before, after);
		}
	}

	public class Recipe_InstallGenitals : Recipe_InstallPart
	{
		public override bool blocked(Pawn p)
		{
			return (Genital_Helper.genitals_blocked(p) || xxx.is_slime(p));//|| xxx.is_demon(p)
		}
	}

	public class Recipe_InstallBreasts : Recipe_InstallPart
	{
		public override bool blocked(Pawn p)
		{
			return (Genital_Helper.breasts_blocked(p) || xxx.is_slime(p));//|| xxx.is_demon(p)
		}
	}

	public class Recipe_InstallAnus : Recipe_InstallPart
	{
		public override bool blocked(Pawn p)
		{
			return (Genital_Helper.anus_blocked(p) || xxx.is_slime(p));//|| xxx.is_demon(p)
		}
	}
}


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

namespace rjw
{
	public class Recipe_GrowBreasts : Recipe_Surgery
	{
		public override void ApplyOnPawn(Pawn pawn, BodyPartRecord part, Pawn billDoer, List<Thing> ingredients, Bill bill)
		{
			if (billDoer != null)
			{
				if (CheckSurgeryFail(billDoer, pawn, ingredients, part, bill))
				{
					return;
				}
				TaleRecorder.RecordTale(TaleDefOf.DidSurgery, new object[]
				{
					billDoer,
					pawn
				});
			}

			var oldBoobs = pawn.health.hediffSet.hediffs.FirstOrDefault(hediff => hediff.def == bill.recipe.removesHediff);
			var newBoobs = bill.recipe.addsHediff;
			var newSize = BreastSize_Helper.GetSize(newBoobs);

			GenderHelper.ChangeSex(pawn, () =>
			{
				BreastSize_Helper.HurtBreasts(pawn, part, 3 * (newSize - 1));
				if (pawn.health.hediffSet.PartIsMissing(part))
				{
					return;
				}
				if (oldBoobs != null)
				{
					pawn.health.RemoveHediff(oldBoobs);
				}
				pawn.health.AddHediff(newBoobs, part);
			});
		}

		public override IEnumerable<BodyPartRecord> GetPartsToApplyOn(Pawn pawn, RecipeDef recipeDef)
		{
			yield break;
			var chest = Genital_Helper.get_breastsBPR(pawn);

			if (pawn.health.hediffSet.PartIsMissing(chest)
				|| Genital_Helper.breasts_blocked(pawn))
			{
				yield break;
			}

			var old = recipeDef.removesHediff;
			if (old == null ? BreastSize_Helper.HasNipplesOnly(pawn, chest) : pawn.health.hediffSet.HasHediff(old, chest))
			{
				yield return chest;
			}
		}
	}
}


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

namespace rjw
{
	/// <summary>
	/// Removes heddifs (restraints/cocoon)
	/// </summary>
	public class Recipe_RemoveRestraints : Recipe_RemoveHediff
	{
		public override bool AvailableOnNow(Thing pawn)
		{
			return true;
		}

		public override IEnumerable<BodyPartRecord> GetPartsToApplyOn(Pawn pawn, RecipeDef recipe)
		{
			List<Hediff> allHediffs = pawn.health.hediffSet.hediffs;
			int i = 0;
			while (true)
			{
				if (i >= allHediffs.Count)
				{
					yield break;
				}
				if (allHediffs[i].def == recipe.removesHediff && allHediffs[i].Visible)
				{
					break;
				}
				i++;
			}

			yield return allHediffs[i].Part;
		}
	}
}


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

namespace rjw
{
	public class Recipe_ShrinkBreasts : Recipe_Surgery
	{
		public override void ApplyOnPawn(Pawn pawn, BodyPartRecord part, Pawn billDoer, List<Thing> ingredients, Bill bill)
		{
			if (billDoer != null)
			{
				if (CheckSurgeryFail(billDoer, pawn, ingredients, part, bill))
				{
					return;
				}
				TaleRecorder.RecordTale(TaleDefOf.DidSurgery, new object[]
				{
					billDoer,
					pawn
				});
			}

			if (!BreastSize_Helper.TryGetBreastSize(pawn, out var oldSize))
			{
				throw new ApplicationException("Recipe_ShrinkBreasts could not find any breasts to shrink.");
			}

			var oldBoobs = pawn.health.hediffSet.GetFirstHediffOfDef(BreastSize_Helper.GetHediffDef(oldSize));
			var newSize = oldSize - 1;
			var newBoobs = BreastSize_Helper.GetHediffDef(newSize);

			// I can't figure out how to spawn a stack of 2 meat.
			for (var i = 0; i < 2; i++)
			{
				GenSpawn.Spawn(pawn.RaceProps.meatDef, billDoer.Position, billDoer.Map);
			}

			GenderHelper.ChangeSex(pawn, () =>
			{
				BreastSize_Helper.HurtBreasts(pawn, part, 5);
				if (pawn.health.hediffSet.PartIsMissing(part))
				{
					return;
				}
				pawn.health.RemoveHediff(oldBoobs);
				pawn.health.AddHediff(newBoobs, part);
			});
		}

		public override IEnumerable<BodyPartRecord> GetPartsToApplyOn(Pawn pawn, RecipeDef recipeDef)
		{
			yield break;
			var chest = Genital_Helper.get_breastsBPR(pawn);

			if (Genital_Helper.breasts_blocked(pawn))
			{
				yield break;
			}

			if (BreastSize_Helper.TryGetBreastSize(pawn, out var size)
				//&& size > BreastSize_Helper.GetSize(Genital_Helper.flat_breasts))
				)
			{
				yield return chest;
			}
		}
	}
}