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

namespace rjwwhoring
{
	/// <summary>
	/// Rimworld object for storing the world/save info
	/// </summary>
	public class DataStore : WorldComponent
	{
		public Dictionary<int, BedData> bedData = new Dictionary<int, BedData>();
		public Dictionary<int, WhoringData> whoringData = new Dictionary<int, WhoringData>();

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

		public override void ExposeData()
		{
			if (Scribe.mode == LoadSaveMode.Saving)
			{
				bedData.RemoveAll(item => item.Value == null || !item.Value.IsValid);
				whoringData.RemoveAll(item => item.Value == null || !item.Value.IsValid);
			}

			base.ExposeData();
			Scribe_Collections.Look(ref bedData, "BedData", LookMode.Value, LookMode.Deep);
			Scribe_Collections.Look(ref whoringData, "WhoringData", LookMode.Value, LookMode.Deep);
			if (Scribe.mode == LoadSaveMode.LoadingVars)
			{
				if (bedData == null) bedData = new Dictionary<int, BedData>();
				if (whoringData == null) whoringData = new Dictionary<int, WhoringData>();
			}
		}

		public BedData GetBedData(Building_Bed bed)
		{
			BedData res;
			var filled = bedData.TryGetValue(bed.thingIDNumber, out res);
			if ((res == null) || (!res.IsValid))
			{
				if (filled)
				{
					bedData.Remove(bed.thingIDNumber);
				}
				res = new BedData(bed);
				bedData.Add(bed.thingIDNumber, res);
			}
			return res;
		}

		public WhoringData GetWhoringData(Pawn pawn)
		{
			WhoringData res;
			var filled = whoringData.TryGetValue(pawn.thingIDNumber, out res);
			if ((res == null) || (!res.IsValid))
			{
				if (filled)
				{
					whoringData.Remove(pawn.thingIDNumber);
				}
				res = new WhoringData(pawn);
				whoringData.Add(pawn.thingIDNumber, res);
			}
			return res;
		}
	}
}


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

namespace rjwwhoring
{
	public static class PawnExtensions
	{
		public static WhoringData WhoringData(this Pawn pawn)
		{
			return WhoringBase.DataStore.GetWhoringData(pawn);
		}
	}
}

repo_name: Angedore/rjw

license: unknown


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

namespace rjwwhoring
{
	/// <summary>
	/// Whore backstories from xml GET!
	/// Just a simplest form of passing data from xml to the code
	/// </summary>
	public class StringListDef : Def
	{
		public List<string> strings = new List<string>();
	}
}


code:
using System;
using Verse;
using System.Linq;
using RimWorld;
using static rjw.xxx;
using System.Collections.ObjectModel;

namespace rjwwhoring
{
	public class WhoringData : IExposable
	{
		public Pawn pawn;
		public bool allowedForWhoringOwner = true;
		public bool allowedForWhoringAll = false;
		public int reservedForPawnID = 0;

		public WhoringType WhoringPolicy = WhoringType.Silver;
		public enum WhoringType { Silver, Goodwill };

		public WhoringData() { }
		public WhoringData(Pawn pawn)
		{
			this.pawn = pawn;
		}

		public void ExposeData()
		{
			Scribe_References.Look(ref pawn, "pawn");
			Scribe_Values.Look(ref WhoringPolicy, "WhoringPolicy", WhoringType.Silver, true);
			Scribe_Values.Look(ref allowedForWhoringOwner, "allowedForWhoringOwner", true, true);
			Scribe_Values.Look(ref allowedForWhoringAll, "allowedForWhoringAll", false, true);
		}

		public bool IsValid { get { return pawn != null; } }
	}
}


code:
using RimWorld;
using Verse;

namespace rjwwhoring
{
	[DefOf]
	public static class RecordDefOf
	{
		public static RecordDef EarnedMoneyByWhore;
		public static RecordDef CountOfWhore;
	}
}


code:
using RimWorld;
using Verse;

namespace rjwwhoring
{
    [DefOf]
    public static class ThoughtDefOf
    {
        public static ThoughtDef_Whore Whorish_Thoughts;

        public static ThoughtDef_Whore Whorish_Thoughts_Captive;

        public static ThoughtDef SleptInBrothel;

        public static ThoughtDef RJWFailedSolicitation;

        public static ThoughtDef RJWTurnedDownWhore;
    }
}


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

namespace rjwwhoring
{
	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 (WhoringBase.DebugWhoring) ModLog.Message($"JobDriver_InvitingVisitors::DoesTargetPawnAcceptAdvance() - {xxx.get_pawnname(TargetPawn)}");
			//if (RJWSettings.WildMode) return true;

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

			if (WhoringBase.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;
					FleckMaker.ThrowMetaIcon(Whore.Position, Whore.Map, FleckDefOf.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)
				{
					FleckMaker.ThrowMetaIcon(TargetPawn.Position, TargetPawn.Map, FleckDefOf.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();

					FleckMaker.ThrowMetaIcon(TargetPawn.Position, TargetPawn.Map, FleckDefOf.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;
using rjw;

namespace rjwwhoring
{
	public class JobDriver_WhoreIsServingVisitors : JobDriver_SexBaseInitiator
	{
		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 (WhoringBase.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":MakeNewToils() - making toils");
			setup_ticks();
			var PartnerJob = xxx.gettin_loved;

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

			if (WhoringBase.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":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);
			int basePrice = WhoringHelper.PriceOfWhore(pawn);
			float bedMult = WhoreBed_Utility.CalculatePriceFactor(Bed);
			//yield return Toils_Reserve.Reserve(BedInd, Bed.SleepingSlotsCount, 0);

			if (WhoringBase.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":generate job toils");
			Toil gotoBed = new Toil();
			gotoBed.defaultCompleteMode = ToilCompleteMode.PatherArrival;
			gotoBed.FailOnBedNoLongerUsable(iBed, Bed);
			gotoBed.AddFailCondition(() => Partner.Downed);
			gotoBed.FailOn(() => !Partner.CanReach(Bed, PathEndMode.Touch, Danger.Deadly));
			gotoBed.initAction = delegate
			{
				if (WhoringBase.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":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 (WhoringBase.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":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 (WhoringBase.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":SexToil start");

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

				Start();

				// TODO: replace this quick n dirty way
				CondomUtility.GetCondomFromRoom(pawn);
				// Try to use whore's condom first, then client's
				Sexprops.usedCondom = CondomUtility.TryUseCondom(pawn) || CondomUtility.TryUseCondom(Partner);

				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(RimWorld.ThoughtDefOf.CheatedOnMe, Partner);
					}
				}
			};
			SexToil.AddPreTickAction(delegate
			{
				if (pawn.IsHashIntervalTick(ticks_between_hearts))
					if (xxx.is_nympho(pawn))
						FleckMaker.ThrowMetaIcon(pawn.Position, pawn.Map, FleckDefOf.Heart);
					else
						FleckMaker.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(Sexprops);

					Bed.UnreserveForWhoring();

					if (!(Partner.IsColonist && (pawn.IsPrisonerOfColony || pawn.IsColonist)))
					{
						if (pawn.WhoringData().WhoringPolicy == WhoringData.WhoringType.Silver)
						{
							int netPrice = (int) (basePrice * bedMult);
							if (netPrice == 0)
								netPrice += 1;

							int bedTip = netPrice - basePrice;
							int defect = WhoringHelper.PayPriceToWhore(Partner, netPrice, pawn);

							if (WhoringBase.DebugWhoring)
							{
								ModLog.Message($"{GetType()}:afterSex toil - {Partner} tried to pay {basePrice}(whore price) + {bedTip}(room modifier) silver to {pawn}");

								if (defect <= 0)
									ModLog.Message(" Paid full price");
								else if (defect <= bedTip)
									ModLog.Message(" Could not pay full tip");
								else
									ModLog.Message(" Failed to pay base price");
							}
							WhoringHelper.UpdateRecords(pawn, netPrice - defect);
						}
						else
						{
							int bedTip = 1;
							ModLog.Message($"{GetType()}:afterSex toil - {Partner} tried to pay {bedTip} goodwill to {pawn}");
							WhoringHelper.PayRespectToWhore(Partner, bedTip, pawn);
						}
					}

					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 HugsLib.Utils;
using RimWorld;
using rjw;
using Verse;
using Verse.AI;
//using Multiplayer.API;

namespace rjwwhoring
{
	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;
		}

		public static Thought_Memory GetMemory(Pawn pawn, Pawn target, ThoughtDef thought)
		{
			Thought_Memory val = pawn.needs.mood.thoughts.memories.Memories.Find(
				(Thought_Memory x) =>
                {
					if (x.def != thought)
						return false;

					if (x.otherPawn == null || x.otherPawn != target)
						return false;

					return true;
				}
			);
			return val;
		}

		//[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;
			if (WhoringBase.ClientAlwaysAccept)
			{
				return fuckability >= 0.1f && xxx.need_some_sex(client) >= 1f;
			}
			else
				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 && !xxx.is_slave(client) && !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 (WhoringBase.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
				&& !xxx.is_slave(x)
				&& !x.IsColonist
				//&& (!x.IsColonist || x.guest?.GuestStatus == GuestStatus.Guest)
				&& 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 (WhoringBase.DebugWhoring) ModLog.Message($" FindAttractivePawn number of all potential clients {potentialClients.Count()}");
			//if (WhoringBase.DebugWhoring) ModLog.Message($" FindAttractivePawn number of all potential clients {potentialClients.ListElements()}");

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

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

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

			//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()}");

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

			foreach(Pawn x in valid_targets)
			{
				bool canAfford = WhoringHelper.CanAfford(x, whore, priceOfWhore);
				Thought_Memory refusedMmeory = GetMemory(x, whore, ThoughtDef.Named("RJWFailedSolicitation"));
				bool refused = refusedMmeory != null;
				DirectPawnRelation relationship = LovePartnerRelationUtility.ExistingLoveRealtionshipBetween(whore, x);
				bool relation = relationship != null;
				bool differentFaction = x.Faction != whore.Faction;

				bool finalResult = canAfford && !refused && !relation && differentFaction;

				if (WhoringBase.DebugWhoring)
				{
					ModLog.Message($"Pawn {x.Name} is an {(finalResult ? "acceptable" : "unacceptable")} client for {whore.Name}. Explanation: canAfford {canAfford.ToString()} refused: {refused.ToString()} relation: {relation.ToString()} differentFaction: {differentFaction.ToString()}");
				}

				if (canAfford && !refused && !relation && differentFaction)
					guestsSpawned.Add(x);

			}

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

			return null;

			//use casual sex for colonist hooking
			if (WhoringBase.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 (WhoringBase.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 (WhoringBase.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 (WhoringBase.DebugWhoring) ModLog.Message($"JobGiver_WhoreInvitingVisitors.TryGiveJob:({xxx.get_pawnname(pawn)})");

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

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

			Building_Bed whorebed = WhoreBed_Utility.FindBed(pawn, client);
			if (whorebed == null)
			{
				if (WhoringBase.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 rjwwhoring
{

	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:
namespace rjwwhoring
{
	//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;
using rjw;

namespace rjwwhoring
{
	/// <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.Collections.Generic;

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

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

		protected List<int> Stages => ((ThoughtDef_Whore) def).stageCounts;
		protected int StoryOffset => ((ThoughtDef_Whore) def).storyOffset;

		public override int CurStageIndex
		{
			get
			{
				int timesWhored = pawn.records.GetAsInt(RecordDefOf.CountOfWhore);

				if (backstories.Contains(pawn.story?.Adulthood?.titleShort))
				{
					timesWhored += StoryOffset;
				}

				if (timesWhored > Stages[Stages.Count - 1])
				{
					return Stages.Count - 1;
				}

				return Stages.FindLastIndex(v => timesWhored > v) + 1;
			}
		}
	}
}


code:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{3FC2D442-19B8-4CF9-9D35-CD13B6AC7B28}</ProjectGuid>
    <OutputType>Library</OutputType>
    <RootNamespace>rjwwhoring</RootNamespace>
    <AssemblyName>RimJobWorldWhoring</AssemblyName>
    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>..\..\Assemblies\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>..\..\Assemblies\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="0Harmony, Version=2.2.2.0, Culture=neutral, processorArchitecture=MSIL">
      <HintPath>..\packages\Lib.Harmony.2.2.2\lib\net472\0Harmony.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="Assembly-CSharp">
      <HintPath>..\..\..\..\..\RimWorldWin64_Data\Managed\Assembly-CSharp.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="HugsLib">
      <HintPath>..\..\..\..\..\..\..\workshop\content\294100\818773962\v1.4\Assemblies\HugsLib.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="RJW">
      <HintPath>..\..\..\..\rjw\1.4\Assemblies\RJW.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="System" />
    <Reference Include="System.Runtime.InteropServices.RuntimeInformation" />
    <Reference Include="UnityEngine.CoreModule">
      <HintPath>..\..\..\..\..\RimWorldWin64_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="UnityEngine.IMGUIModule">
      <HintPath>..\..\..\..\..\RimWorldWin64_Data\Managed\UnityEngine.IMGUIModule.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="UnityEngine.InputLegacyModule">
      <HintPath>..\..\..\..\..\RimWorldWin64_Data\Managed\UnityEngine.InputLegacyModule.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="UnityEngine.InputModule">
      <HintPath>..\..\..\..\..\RimWorldWin64_Data\Managed\UnityEngine.InputModule.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="UnityEngine.TextRenderingModule">
      <HintPath>..\..\..\..\..\RimWorldWin64_Data\Managed\UnityEngine.TextRenderingModule.dll</HintPath>
      <Private>False</Private>
    </Reference>
  </ItemGroup>
  <ItemGroup>
    <AppDesigner Include="Properties\" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="..\Properties\AssemblyInfo.cs" />
    <Compile Include="Data\BedData.cs" />
    <Compile Include="Data\DataStore.cs" />
    <Compile Include="Data\StringListDef.cs" />
    <Compile Include="Data\WhoringData.cs" />
    <Compile Include="DefOf\RecordDefDefOf.cs" />
    <Compile Include="harmony_Building_BedPatches.cs" />
    <Compile Include="harmony_AftersexPatch.cs" />
    <Compile Include="harmony_RJWTab_patch.cs" />
    <Compile Include="JobDrivers\JobDriver_WhoreInvitingVisitors.cs" />
    <Compile Include="JobDrivers\JobDriver_WhoreIsServingVisitors.cs" />
    <Compile Include="JobGivers\JobGiver_WhoreInvitingVisitors.cs" />
    <Compile Include="Location\Brothel_Room.cs" />
    <Compile Include="Data\PawnExtensions.cs" />
    <Compile Include="WhoringTab\WhoringPolicyUIUtility.cs" />
    <Compile Include="WhoringTab\PawnColumnCheckbox_Whore.cs" />
    <Compile Include="WhoringTab\PawnColumnWorker_AverageMoneyByWhore.cs" />
    <Compile Include="WhoringTab\PawnColumnWorker_CountOfWhore.cs" />
    <Compile Include="WhoringTab\PawnColumnWorker_EarnedMoneyByWhore.cs" />
    <Compile Include="WhoringTab\PawnColumnWorker_IsWhore.cs" />
    <Compile Include="WhoringTab\PawnColumnWorker_Mood.cs" />
    <Compile Include="WhoringTab\PawnColumnWorker_PriceRangeOfWhore.cs" />
    <Compile Include="WhoringTab\PawnColumnWorker_TextCenter.cs" />
    <Compile Include="WhoringTab\PawnColumnWorker_WhoreExperience.cs" />
    <Compile Include="WhoringTab\PawnColumnWorker_WhoringPolicy.cs" />
    <Compile Include="WhoringTab\PawnTable_Whores.cs" />
    <Compile Include="WhoringTab\WhoreCheckbox.cs" />
    <Compile Include="ThinkTreeNodes\ThinkNode_ChancePerHour_Whore.cs" />
    <Compile Include="ThinkTreeNodes\ThinkNode_ConditionalWhore.cs" />
    <Compile Include="Thoughts\ThoughtWorker_Whore.cs" />
    <Compile Include="DefOf\ThoughtDefOf.cs" />
    <Compile Include="WhoringBase.cs" />
    <Compile Include="Whoring_Bed_Utilities.cs" />
    <Compile Include="Whoring_Helper.cs" />
  </ItemGroup>
  <ItemGroup>
    <None Include="packages.config" />
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>


code:
using System;
using HugsLib;
using HugsLib.Settings;
using RimWorld;
using Verse;

namespace rjwwhoring
{
	public class WhoringBase : ModBase
	{
		public override string ModIdentifier
		{
			get
			{
				return "RJW_Whoring";
			}
		}

		public static DataStore DataStore;//reference to savegame data, hopefully

		public override void SettingsChanged()
		{
			//ToggleTabIfNeeded();
		}
		public override void WorldLoaded()
		{
			DataStore = Find.World.GetComponent<DataStore>();
			//ToggleTabIfNeeded();
		}

		private void ToggleTabIfNeeded()
		{
			//DefDatabase<MainButtonDef>.GetNamed("RJW_Brothel").buttonVisible = whoringtab_enabled;
		}

		//public static SettingHandle<bool> whoringtab_enabled;
		public static SettingHandle<bool> show_whore_price_factor_on_bed;
		public static SettingHandle<bool> show_whore_widgets_on_bed;
		public static SettingHandle<bool> DebugWhoring;
		public static SettingHandle<bool> MoneyPrinting;
		public static SettingHandle<bool> ClientAlwaysAccept;

		public override void DefsLoaded()
		{
			//whoringtab_enabled = Settings.GetHandle("whoringtab_enabled",
			//						"whoringtab_enabled".Translate(),
			//						"whoringtab_enabled_desc".Translate(),
			//						true);
			show_whore_price_factor_on_bed = Settings.GetHandle("show_whore_price_factor_on_bed",
									"show_whore_price_factor_on_bed".Translate(),
									"show_whore_price_factor_on_bed_desc".Translate(),
									false);
			show_whore_widgets_on_bed = Settings.GetHandle("show_whore_widgets_on_bed",
									"show_whore_widgets_on_bed".Translate(),
									"show_whore_widgets_on_bed_desc".Translate(),
									false);
			MoneyPrinting = Settings.GetHandle("MoneyPrinting",
									"MoneyPrinting".Translate(),
									"MoneyPrinting_desc".Translate(),
									false);
			ClientAlwaysAccept = Settings.GetHandle("ClientAlwaysAccept",
									"ClientAlwaysAccept".Translate(),
									"ClientAlwaysAccept_desc".Translate(),
									false);
			DebugWhoring = Settings.GetHandle("DebugWhoring",
									"DebugWhoring".Translate(),
									"DebugWhoring_desc".Translate(),
									false);
		}
	}
}


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

namespace rjwwhoring.MainTab
{
	public abstract class PawnColumnCheckbox_Whore : PawnColumnWorker
	{
		public const int HorizontalPadding = 2;

		public override void DoCell(Rect rect, Pawn pawn, PawnTable table)
		{
			if (!this.HasCheckbox(pawn))
			{
				return;
			}
			int num = (int)((rect.width - 24f) / 2f);
			int num2 = Mathf.Max(3, 0);
			Vector2 vector = new Vector2(rect.x + (float)num, rect.y + (float)num2);
			Rect rect2 = new Rect(vector.x, vector.y, 24f, 24f);
			if (Find.TickManager.TicksGame % 60 == 0)
			{
				pawn.UpdatePermissions();
				//Log.Message("GetDisabled UpdateCanDesignateService for " + xxx.get_pawnname(pawn));
				//Log.Message("UpdateCanDesignateService " + pawn.UpdateCanDesignateService());
				//Log.Message("CanDesignateService " + pawn.CanDesignateService());
				//Log.Message("GetDisabled " + GetDisabled(pawn));
			}
			bool disabled = this.GetDisabled(pawn);
			bool value;
			if (disabled)
			{
				value = false;
			}
			else
			{
				value = this.GetValue(pawn);
			}

			bool flag = value;
			Vector2 topLeft = vector;
			WhoreCheckbox.Checkbox(topLeft, ref value, 24f, disabled, WhoreCheckbox.WhoreCheckboxOnTex, WhoreCheckbox.WhoreCheckboxOffTex, WhoreCheckbox.WhoreCheckboxDisabledTex);
			if (Mouse.IsOver(rect2))
			{
				string tip = this.GetTip(pawn);
				if (!tip.NullOrEmpty())
				{
					TooltipHandler.TipRegion(rect2, tip);
				}
			}
			if (value != flag)
			{
				this.SetValue(pawn, value);
			}
		}

		public override int GetMinWidth(PawnTable table)
		{
			return Mathf.Max(base.GetMinWidth(table), 28);
		}

		public override int GetMaxWidth(PawnTable table)
		{
			return Mathf.Min(base.GetMaxWidth(table), this.GetMinWidth(table));
		}

		public override int GetMinCellHeight(Pawn pawn)
		{
			return Mathf.Max(base.GetMinCellHeight(pawn), 24);
		}

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

		private int GetValueToCompare(Pawn pawn)
		{
			if (!this.HasCheckbox(pawn))
			{
				return 0;
			}
			if (!this.GetValue(pawn))
			{
				return 1;
			}
			return 2;
		}

		protected virtual string GetTip(Pawn pawn)
		{
			return null;
		}

		protected virtual bool HasCheckbox(Pawn pawn)
		{
			return true;
		}

		protected abstract bool GetValue(Pawn pawn);

		protected abstract void SetValue(Pawn pawn, bool value);

		protected abstract bool GetDisabled(Pawn pawn);

		protected override void HeaderClicked(Rect headerRect, PawnTable table)
		{
			base.HeaderClicked(headerRect, table);
			if (Event.current.shift)
			{
				List<Pawn> pawnsListForReading = table.PawnsListForReading;
				for (int i = 0; i < pawnsListForReading.Count; i++)
				{
					if (this.HasCheckbox(pawnsListForReading[i]))
					{
						if (Event.current.button == 0)
						{
							if (!this.GetValue(pawnsListForReading[i]))
							{
								this.SetValue(pawnsListForReading[i], true);
							}
						}
						else if (Event.current.button == 1 && this.GetValue(pawnsListForReading[i]))
						{
							this.SetValue(pawnsListForReading[i], false);
						}
					}
				}
				if (Event.current.button == 0)
				{
					SoundDefOf.Checkbox_TurnedOn.PlayOneShotOnCamera(null);
				}
				else if (Event.current.button == 1)
				{
					SoundDefOf.Checkbox_TurnedOff.PlayOneShotOnCamera(null);
				}
			}
		}

		protected override string GetHeaderTip(PawnTable table)
		{
			return base.GetHeaderTip(table) + "\n" + "CheckboxShiftClickTip".Translate();
		}
	}
}


code:
using Verse;

namespace rjwwhoring.MainTab
{
	[StaticConstructorOnStartup]
	public class PawnColumnWorker_AverageMoneyByWhore : PawnColumnWorker_TextCenter
	{
		protected override string GetTextFor(Pawn pawn)
		{
			return ((int)GetValueToCompare(pawn)).ToString();
		}

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

		private float GetValueToCompare(Pawn pawn)
		{
			float total = pawn.records.GetValue(RecordDefOf.EarnedMoneyByWhore);
			float count = pawn.records.GetValue(RecordDefOf.CountOfWhore);
			if ((int)count == 0)
			{
				return 0;
			}
			return (total / count);
		}
	}
}


code:
using Verse;

namespace rjwwhoring.MainTab
{
	[StaticConstructorOnStartup]
	public class PawnColumnWorker_CountOfWhore : PawnColumnWorker_TextCenter
	{
		protected override string GetTextFor(Pawn pawn)
		{
			return GetValueToCompare(pawn).ToString();
		}

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

		private int GetValueToCompare(Pawn pawn)
		{
			return pawn.records.GetAsInt(RecordDefOf.CountOfWhore);
		}
	}
}


code:
using Verse;

namespace rjwwhoring.MainTab
{
	[StaticConstructorOnStartup]
	public class PawnColumnWorker_EarnedMoneyByWhore : PawnColumnWorker_TextCenter
	{
		protected override string GetTextFor(Pawn pawn)
		{
			return GetValueToCompare(pawn).ToString();
		}

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

		private int GetValueToCompare(Pawn pawn)
		{
			return pawn.records.GetAsInt(RecordDefOf.EarnedMoneyByWhore);
		}
	}
}


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

namespace rjwwhoring.MainTab
{
	[StaticConstructorOnStartup]
	public class PawnColumnWorker_IsWhore : PawnColumnCheckbox_Whore
	{
		protected override bool GetDisabled(Pawn pawn)
		{
			return !pawn.CanDesignateService();
		}

		protected override bool GetValue(Pawn pawn)
		{
			return pawn.IsDesignatedService() && xxx.is_human(pawn);
		}

		protected override void SetValue(Pawn pawn, bool value)
		{
			if (value == this.GetValue(pawn)) return;

			pawn.ToggleService();
		}
		/*
		private static readonly Texture2D serviceOn = ContentFinder<Texture2D>.Get("UI/Tab/Service_on");
		private static readonly Texture2D serviceOff = ContentFinder<Texture2D>.Get("UI/Tab/Service_off");

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


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

namespace rjwwhoring.MainTab
{
	[StaticConstructorOnStartup]
	public class PawnColumnWorker_Mood : PawnColumnWorker_TextCenter
	{
		protected override string GetTextFor(Pawn pawn)
		{
			return GetValueToCompare(pawn).ToStringPercent();
		}

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

		private float GetValueToCompare(Pawn pawn)
		{
			return pawn.needs.mood.CurLevelPercentage;
		}
	}
}


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

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

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

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

		protected override string GetTip(Pawn pawn)
		{
			if (pawn.WhoringData().WhoringPolicy == WhoringData.WhoringType.Silver)
			{
				string minPriceTip = string.Format(
					"  Base: {0}\n  Traits: {1}",
					WhoringHelper.baseMinPrice,
					(WhoringHelper.WhoreTraitAdjustmentMin(pawn) - 1f).ToStringPercent()
				);
				string maxPriceTip = string.Format(
					"  Base: {0}\n  Traits: {1}",
					WhoringHelper.baseMaxPrice,
					(WhoringHelper.WhoreTraitAdjustmentMax(pawn) - 1f).ToStringPercent()
				);
				string bothTip = string.Format(
					"  Gender: {0}\n  Age: {1}\n  Injuries: {2}",
					(WhoringHelper.WhoreGenderAdjustment(pawn) - 1f).ToStringPercent(),
					(WhoringHelper.WhoreAgeAdjustment(pawn) - 1f).ToStringPercent(),
					(WhoringHelper.WhoreInjuryAdjustment(pawn) - 1f).ToStringPercent()
				);
				return string.Format("Min:\n{0}\nMax:\n{1}\nBoth:\n{2}", minPriceTip, maxPriceTip, bothTip);
			}
			else
			{
				return string.Format("Raise Goodwill by 1");
			}
		}

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


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

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


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

namespace rjwwhoring.MainTab
{
	[StaticConstructorOnStartup]
	public class PawnColumnWorker_WhoreExperience : PawnColumnWorker_TextCenter
	{
		public static readonly HashSet<string> backstories = new HashSet<string>(DefDatabase<StringListDef>.GetNamed("WhoreBackstories").strings);

		protected override string GetTextFor(Pawn pawn)
		{

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


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

namespace rjwwhoring.MainTab
{
	[StaticConstructorOnStartup]
	public class PawnColumnWorker_WhoringPolicy : PawnColumnWorker
	{
		public override void DoCell(Rect rect, Pawn pawn, PawnTable table)
		{
			if (pawn.drugs != null)
			{
				WhoringPolicyUIUtility.DoAssignWhoringPolicyButtons(rect, pawn);
			}
		}
	}
}


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

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

		//default sorting
		protected override IEnumerable<Pawn> LabelSortFunction(IEnumerable<Pawn> input)
		{
			//return input.OrderBy(p => p.Name);
			foreach (Pawn p in input)
				p.UpdatePermissions();
			return input.OrderByDescending(p => (p.IsPrisonerOfColony || p.IsSlaveOfColony) != false).ThenBy(p => xxx.get_pawnname(p));
			//return input.OrderByDescending(p => (p.IsPrisonerOfColony || p.IsSlaveOfColony) != false).ThenBy(p => (p.Name.ToStringShort.Colorize(Color.yellow)));
			//return input.OrderBy(p => xxx.get_pawnname(p));
		}

		protected override IEnumerable<Pawn> PrimarySortFunction(IEnumerable<Pawn> input)
		{
			foreach (Pawn p in input)
				p.UpdatePermissions();
			return input;
			//return base.PrimarySortFunction(input);
		}
	}
}


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

namespace rjwwhoring.MainTab
{
	[StaticConstructorOnStartup]
	public static class WhoreCheckbox
	{
		public static readonly Texture2D WhoreCheckboxOnTex = ContentFinder<Texture2D>.Get("UI/Commands/Service_on");
		public static readonly Texture2D WhoreCheckboxOffTex = ContentFinder<Texture2D>.Get("UI/Commands/Service_off");
		public static readonly Texture2D WhoreCheckboxDisabledTex = ContentFinder<Texture2D>.Get("UI/Commands/Service_Refuse");

		private static bool checkboxPainting;
		private static bool checkboxPaintingState;

		public static void Checkbox(Vector2 topLeft, ref bool checkOn, float size = 24f, bool disabled = false, Texture2D texChecked = null, Texture2D texUnchecked = null, Texture2D texDisabled = null)
		{
			WhoreCheckbox.Checkbox(topLeft.x, topLeft.y, ref checkOn, size, disabled, texChecked, texUnchecked);
		}

		public static void Checkbox(float x, float y, ref bool checkOn, float size = 24f, bool disabled = false, Texture2D texChecked = null, Texture2D texUnchecked = null, Texture2D texDisabled = null)
		{
			Rect rect = new Rect(x, y, size, size);
			WhoreCheckbox.CheckboxDraw(x, y, checkOn, disabled, size, texChecked, texUnchecked,texDisabled);
			if (!disabled)
			{
				MouseoverSounds.DoRegion(rect);
				bool flag = false;
				Widgets.DraggableResult draggableResult = Widgets.ButtonInvisibleDraggable(rect, false);
				if (draggableResult == Widgets.DraggableResult.Pressed)
				{
					checkOn = !checkOn;
					flag = true;
				}
				else if (draggableResult == Widgets.DraggableResult.Dragged)
				{
					checkOn = !checkOn;
					flag = true;
					WhoreCheckbox.checkboxPainting = true;
					WhoreCheckbox.checkboxPaintingState = checkOn;
				}
				if (Mouse.IsOver(rect) && WhoreCheckbox.checkboxPainting && Input.GetMouseButton(0) && checkOn != WhoreCheckbox.checkboxPaintingState)
				{
					checkOn = WhoreCheckbox.checkboxPaintingState;
					flag = true;
				}
				if (flag)
				{
					if (checkOn)
					{
						SoundDefOf.Checkbox_TurnedOn.PlayOneShotOnCamera(null);
					}
					else
					{
						SoundDefOf.Checkbox_TurnedOff.PlayOneShotOnCamera(null);
					}
				}
			}
		}

		private static void CheckboxDraw(float x, float y, bool active, bool disabled, float size = 24f, Texture2D texChecked = null, Texture2D texUnchecked = null, Texture2D texDisabled = null)
		{
			Texture2D image;
			if (disabled)
			{
				image = ((!(texDisabled != null)) ? WhoreCheckbox.WhoreCheckboxDisabledTex : texDisabled);
			}
			else if (active)
			{
				image = ((!(texChecked != null)) ? WhoreCheckbox.WhoreCheckboxOnTex : texChecked);
			}
			else
			{
				image = ((!(texUnchecked != null)) ? WhoreCheckbox.WhoreCheckboxOffTex : texUnchecked);
			}
			Rect position = new Rect(x, y, size, size);
			GUI.DrawTexture(position, image);
		}

	}
}


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

namespace rjwwhoring
{
	public static class WhoringPolicyUIUtility
	{
		//public const string AssigningDrugsTutorHighlightTag = "ButtonAssignDrugs";

		public static void DoAssignWhoringPolicyButtons(Rect rect, Pawn pawn)
		{
			int num = Mathf.FloorToInt(rect.width);
			float x = rect.x;
			Rect rect2 = new Rect(x, rect.y + 2f, num, rect.height - 4f);
			string text = pawn.WhoringData().WhoringPolicy.ToStringSafe();

			Widgets.Dropdown(rect2, pawn, (Pawn p) => p.WhoringData().WhoringPolicy, Button_GenerateMenu, text.Truncate(rect2.width), paintable: true);
			//Widgets.Dropdown(rect2, pawn, (Pawn p) => p.drugs.CurrentPolicy, Button_GenerateMenu, text.Truncate(((Rect)(ref rect2)).get_width()), null, pawn.drugs.CurrentPolicy.label, null, delegate
			//{
			//	PlayerKnowledgeDatabase.KnowledgeDemonstrated(ConceptDefOf.DrugPolicies, KnowledgeAmount.Total);
			//}, paintable: true);
			x += num;
			x += 4f;
			//UIHighlighter.HighlightOpportunity(rect2, "ButtonAssignDrugs");
		}

		private static IEnumerable<Widgets.DropdownMenuElement<Enum>> Button_GenerateMenu(Pawn pawn)
		{
			foreach (WhoringData.WhoringType option in Enum.GetValues(typeof(WhoringData.WhoringType)))
			{
				yield return new Widgets.DropdownMenuElement<Enum>
				{
					option = new FloatMenuOption(option.ToString(), delegate
					{
						pawn.WhoringData().WhoringPolicy = option;
					}),
					payload = option
				};
			}
		}
	}
}


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

namespace rjwwhoring
{
	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 CalculateRoomFactor(Room room, int num_humanlike_beds)
		{
			if (room == null || room.Role == RoomRoleDefOf.None || room.OutdoorsForWork)
				return 0.1f;

			float room_multiplier = 1f;

			if (room.Role == roleDefBrothel)
			{
				room_multiplier *= (float)Math.Pow(0.8, num_humanlike_beds - 1);
			}
			else // if(room.Role == RoomRoleDefOf.Barracks)
			{
				room_multiplier /= 2 * (num_humanlike_beds - 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));

			return Mathf.Max(room_multiplier, 0);
		}

		public static float CalculateBedFactorsForRoom(Room room, Building_Bed except_this_bed = null)
		{
			float room_factor = 0.1f;

			if (room == null)
				return room_factor;

			// get eligible beds
			IEnumerable<Building_Bed> humanlike_beds = room.ContainedBeds.Where(b => b.def.building.bed_humanlike);
			int num_humanlike_beds = humanlike_beds.Count();
			if (num_humanlike_beds <= 0)
			{
				return room_factor;
			}
			IEnumerable<Building_Bed> whoring_beds = humanlike_beds.Where(b => b.IsAllowedForWhoringOwner());

			if (whoring_beds.Any())
			{
				// if beds exist, calculate room score
				room_factor = CalculateRoomFactor(room, num_humanlike_beds);

				// and update all beds
				foreach (Building_Bed b in whoring_beds)
				{
					// except the bed given as parameter (will be calculated in that bed's function)
					if (except_this_bed == null || b.thingIDNumber != except_this_bed.thingIDNumber)
						CalculatePriceFactor(b, room_factor);
				}
			}
			return room_factor;
		}

		public static void ResetTicksUntilUpdate(Room room)
		{
			IEnumerable<Building_Bed> whoring_beds = room.ContainedBeds.Where(b => b.IsAllowedForWhoringOwner());

			foreach (Building_Bed bed in whoring_beds)
			{
				// set all to 0
				//	if one is needed, it updates all the other beds
				//	if none is needed, it doesn't matter
				// only setting one bed to update has the risk that a different bed's value is required that doesn't trigger an update
				WhoringBase.DataStore.GetBedData(bed).scoreUpdateTickDelay = 0;
			}
		}

		public static float CalculatePriceFactor(Building_Bed bed, float room_multiplier = -1f)
		{

			BedData saved_bed_data = WhoringBase.DataStore.GetBedData(bed);

			// cache result (no need for saving): if no result available, calculate; otherwise save tick at which it has been calculated.
			// additional parameter "room_multiplier" to skip room analysis
			if ((room_multiplier == -1 || room_multiplier >= 0 && room_multiplier == saved_bed_data.roomScore)
				&& saved_bed_data.bedScore >= 0f
				&& saved_bed_data.lastScoreUpdateTick >
					GenTicks.TicksGame - saved_bed_data.scoreUpdateTickDelay)
			{
				if (room_multiplier >= 0 && saved_bed_data.scoreUpdateTickDelay < 720)
				{
					// if saved value is used due to unchanged room multiplier, increase recalc delay
					saved_bed_data.scoreUpdateTickDelay += 60 + Rand.Int % 10;
				}
				//Log.Message("[RJW] lastScoreUpdateTick: " + BukkakeBase.DataStore.GetBedData(bed).lastScoreUpdateTick.ToString() + " / TicksGame: "+ GenTicks.TicksGame.ToString());
				return saved_bed_data.bedScore;
			}

			if (room_multiplier < 0)
			{
				Room room = bed.Map != null && bed.Map.regionAndRoomUpdater.Enabled ? bed.GetRoom() : null;
				room_multiplier = CalculateBedFactorsForRoom(room, bed);
			}

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

			// delay recalculation if result is the same as before
			// Rand.Int % 10 flattens the spike over time if many beds are toggled at once
			if (price_factor == saved_bed_data.bedScore)
			{
				if (saved_bed_data.scoreUpdateTickDelay < 720)
				{
					// slowly increase recalculation delay to two seconds on speed 3
					saved_bed_data.scoreUpdateTickDelay += 60 + Rand.Int % 10;
				}
			}
			else
			{
				// reset recalculation delay
				saved_bed_data.scoreUpdateTickDelay = 60 + Rand.Int % 10;
			}

			// update bed data
			saved_bed_data.lastScoreUpdateTick = GenTicks.TicksGame;
			saved_bed_data.bedScore = price_factor;
			saved_bed_data.roomScore = room_multiplier;

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

		// 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 (WhoringBase.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 (WhoringBase.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 (WhoringBase.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);
			}
			WhoringBase.DataStore.GetBedData(bed).allowedForWhoringOwner = isAllowed;
		}
		public static void ToggleAllowedForWhoringOwner(this Building_Bed bed)
		{
			bed.SetAllowedForWhoringOwner(!WhoringBase.DataStore.GetBedData(bed).allowedForWhoringOwner);
		}
		public static bool IsAllowedForWhoringOwner(this Building_Bed bed)
		{
			if (!bed.def.building.bed_humanlike || bed.Faction != Faction.OfPlayerSilentFail || 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 WhoringBase.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;
			}
			WhoringBase.DataStore.GetBedData(bed).allowedForWhoringAll = isAllowed;

			bed.GetRoom()?.Notify_BedTypeChanged();
			bed.Notify_ColorChanged();
		}
		public static void ToggleAllowedForWhoringAll(this Building_Bed bed)
		{
			bed.SetAllowedForWhoringAll(!WhoringBase.DataStore.GetBedData(bed).allowedForWhoringAll);
		}
		public static bool IsAllowedForWhoringAll(this Building_Bed bed)
		{
			if (WhoringBase.DataStore.GetBedData(bed).allowedForWhoringAll)
			{
				if (!bed.def.building.bed_humanlike || bed.Faction != Faction.OfPlayerSilentFail || 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)
		{
			WhoringBase.DataStore.GetBedData(bed).reservedUntilGameTick = GenTicks.TicksGame + num_ticks;
			WhoringBase.DataStore.GetBedData(bed).reservedForPawnID = p.thingIDNumber;
		}
		public static void UnreserveForWhoring(this Building_Bed bed)
		{
			WhoringBase.DataStore.GetBedData(bed).reservedUntilGameTick = 0;
			WhoringBase.DataStore.GetBedData(bed).reservedForPawnID = 0;
		}
		public static bool IsAvailableForWhoring(this Building_Bed bed, Pawn p)
		{
			// check for active reservation
			if (WhoringBase.DataStore.GetBedData(bed).reservedUntilGameTick > GenTicks.TicksGame)
			{
				if (WhoringBase.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:
// #define TESTMODE // Uncomment to enable logging.

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

namespace rjwwhoring
{
	/// <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(RecordDefOf.EarnedMoneyByWhore);
			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(RecordDefOf.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;
			if (whore.story.traits.HasTrait(TraitDefOf.Wimp)) // 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 (whore.WhoringData().WhoringPolicy == WhoringData.WhoringType.Goodwill) return true;

			if (WhoringBase.MoneyPrinting) 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 (WhoringBase.DebugWhoring) ModLog.Message($"PayPriceToWhore for client {xxx.get_pawnname(targetPawn)}");
			if (WhoringBase.MoneyPrinting)
			{
				DebugThingPlaceHelper.DebugSpawn(ThingDefOf.Silver, whore.PositionHeld, priceOfWhore, false, null);
				if (WhoringBase.DebugWhoring) ModLog.Message($" MoneyPrinting " + priceOfWhore + " silver to pay price");
				return 0;
			}
			int AmountLeft = priceOfWhore;
			if ((targetPawn.Faction == whore.Faction && targetPawn.GuestStatus != GuestStatus.Guest) || priceOfWhore == 0)
			{
				if (WhoringBase.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 (WhoringBase.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 (WhoringBase.DebugWhoring) ModLog.Message($" have no silver");
							continue;
						}
						AmountLeft -= resultingSilvers.stackCount;
						if (AmountLeft <= 0)
						{
							if (WhoringBase.DebugWhoring) ModLog.Message($" {xxx.get_pawnname(targetPawn)} paid {resultingSilvers.stackCount} silver");
							break;
						}
					}
					else
					{
						if (WhoringBase.DebugWhoring) ModLog.Message($" TryDrop failed");
						break;
					}
				}
				if (WhoringBase.DebugWhoring) ModLog.Message($" price: {priceOfWhore}, paid: {priceOfWhore - AmountLeft}");
			}
			else
			{
				if (WhoringBase.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 (WhoringBase.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 (WhoringBase.DebugWhoring) ModLog.Message($" have no silver");
								continue;
							}
							AmountLeft -= resultingSilvers.stackCount;
							if (AmountLeft <= 0)
							{
								if (WhoringBase.DebugWhoring) ModLog.Message($" {xxx.get_pawnname(caravanMember)} paid {resultingSilvers.stackCount} silver");
								break;
							}
						}
					}
				}
			}
			if (WhoringBase.DebugWhoring) ModLog.Message($" price: {priceOfWhore}, paid: {priceOfWhore - AmountLeft}");
			return AmountLeft;
		}

		public static int PayRespectToWhore(Pawn targetPawn, int priceOfWhore, Pawn whore)
		{
			if ((targetPawn.Faction != whore.Faction && targetPawn.GuestStatus != GuestStatus.Guest))
			{
				if (WhoringBase.DebugWhoring) ModLog.Message($" No need to pay respect");
				return 0;
			}
			targetPawn.Faction.TryAffectGoodwillWith(Faction.OfPlayer, priceOfWhore);
			if (WhoringBase.DebugWhoring) ModLog.Message($" price: {priceOfWhore}, paid: {priceOfWhore}");

			return priceOfWhore;
		}

		//[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;
			}
			if (WhoringBase.ClientAlwaysAccept)
			{
				return true;
			}
			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).
		/// </summary>
		//[SyncMethod]
		public static bool WillPawnTryHookup(Pawn target)
		{
			if (WhoringBase.ClientAlwaysAccept)
			{
				return true;
			}
			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;
		}

		/// <summary>
		/// Updates records for whoring.
		/// </summary>
		public static void UpdateRecords(Pawn pawn, int price)
		{
			pawn.records.AddTo(RecordDefOf.EarnedMoneyByWhore, price);
			pawn.records.Increment(RecordDefOf.CountOfWhore);
			//this is added by normal outcome
			//pawn.records.Increment(CountOfSex);
		}
	}
}


code:
using Verse;
using HarmonyLib;
using rjw;
using System;
using RimWorld;

namespace rjwwhoring
{
	[HarmonyPatch(typeof(AfterSexUtility), "think_about_sex", new Type[] {typeof(Pawn), typeof(Pawn), typeof(bool), typeof(SexProps), typeof(bool)})]
	[StaticConstructorOnStartup]
	static class Aftersex_WhoringhoughtApply
	{
		[HarmonyPostfix]
		private static void ThinkAboutWhoringPatch(Pawn pawn, Pawn partner, bool isReceiving, SexProps props, bool whoring = false)
		{
			try
			{
				AfterSexUtilityPatch.ThinkAboutWhoring(pawn, partner, isReceiving, props, whoring);
			}
			catch (Exception e)
			{
				Log.Error(e.ToString());
			}
		}
	}

	static class AfterSexUtilityPatch
	{
		///<summary>
		///add aftersex thoughts for whoring
		///</summary>
		public static void ThinkAboutWhoring(Pawn pawn, Pawn partner, bool isReceiving, SexProps props, bool whoring = false)
		{
			//no whoring in vanilla sex
			if (props.isCoreLovin)
				return;

			//masturbation?
			if (pawn == null || partner == null)
				return;

			//necro
			if (pawn.Dead || partner.Dead)
				return;

			//bestiality
			if (!(xxx.is_human(pawn) && xxx.is_human(partner)))
				return;

			//whoring, initiator is whore
			if (whoring && !props.isReceiver)
			{
				ThoughtDef memory;

				if (pawn.IsPrisoner || xxx.is_slave(pawn))
					memory = ThoughtDefOf.Whorish_Thoughts_Captive;
				else
					memory = ThoughtDefOf.Whorish_Thoughts;

				pawn.needs.mood.thoughts.memories.TryGainMemory(memory);
			}
		}
	}
}


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

/// <summary>
/// patches Building_Bed to add stuff for WhoreBeds
///
/// Also contains smaller patches for RoomRoleWorker_Barracks (don't count whore beds) (disabled) and Toils_LayDown.ApplyBedThoughts (slept in brothel thought)
/// </summary>

namespace rjwwhoring
{
	public static class harmony_Building_BedPatches
	{

		private static readonly Color sheetColorForWhores = new Color(181 / 255f, 55 / 255f, 109 / 255f);

		// Set color for whore beds
		[HarmonyPatch(typeof(Building_Bed))]
		[HarmonyPatch(nameof(Building_Bed.DrawColorTwo), MethodType.Getter)]
		public static class Building_Bed_DrawColor_Patch
		{
			[HarmonyPostfix]
			public static void Postfix(Building_Bed __instance, ref Color __result)
			{
				if (__instance.IsAllowedForWhoringAll())
				{
					__result = sheetColorForWhores;
				}
			}
		}

		// add whoring toggles to beds
		[HarmonyPatch(typeof(Building_Bed), nameof(Building_Bed.GetGizmos))]
		public static class Building_Bed_GetGizmos_Patch
		{
			[HarmonyPostfix]
			public static void Postfix(Building_Bed __instance, ref IEnumerable<Gizmo> __result)
			{
				if (!WhoringBase.show_whore_widgets_on_bed)
				{
					return;
				}
				__result = Process(__instance, __result);
			}

			private static IEnumerable<Gizmo> Process(Building_Bed __instance, IEnumerable<Gizmo> __result)
			{
				var isPrisonCell = __instance.GetRoom()?.IsPrisonCell == true;
				if (!__instance.ForPrisoners && !__instance.Medical && __instance.def.building.bed_humanlike && __instance.Faction == Faction.OfPlayerSilentFail && !__instance.def.defName.Contains("Guest") && !isPrisonCell)
				{

					yield return
						new Command_Toggle
						{
							defaultLabel = "CommandBedAllowWhoringLabel".Translate(),
							defaultDesc = "CommandBedAllowWhoringDesc".Translate(),
							icon = ContentFinder<Texture2D>.Get("UI/Commands/AsWhore"),
							isActive = __instance.IsAllowedForWhoringOwner,
							toggleAction = __instance.ToggleAllowedForWhoringOwner,
							hotKey = KeyBindingDefOf.Misc5, // Guest Beds uses Misc4
							disabled = !__instance.def.HasAssignableCompFrom(typeof(CompAssignableToPawn_Bed)),
							disabledReason = "This bed type is not assignable to pawns."
						};

					yield return
						new Command_Toggle
						{
							defaultLabel = "CommandBedSetAsWhoreBedLabel".Translate(),
							defaultDesc = "CommandBedSetAsWhoreBedDesc".Translate(),
							icon = ContentFinder<Texture2D>.Get("UI/Commands/AsWhoreMany"),
							isActive = __instance.IsAllowedForWhoringAll,
							toggleAction = __instance.ToggleAllowedForWhoringAll,
							hotKey = KeyBindingDefOf.Misc6, // Guest Beds uses Misc4
							disabled = !__instance.def.HasAssignableCompFrom(typeof(CompAssignableToPawn_Bed)),
							disabledReason = "This bed type is not assignable to pawns."
						};
				}

				foreach (var gizmo in __result)
				{
					if (__instance.IsAllowedForWhoringAll())
					{
						if (gizmo is Command_Toggle && ((Command_Toggle)gizmo).defaultLabel == "CommandBedSetAsGuestLabel".Translate())
						{
							// hide set as guest bed
							continue;
						};
						// old: instead of hiding, just disable
						/*switch (gizmo)
						{
							case Command_Toggle toggle:
								{
									// Disable prisoner and medical, and guest buttons
									if (//toggle.defaultLabel == "CommandBedSetForPrisonersLabel".Translate() ||
										//toggle.defaultLabel == "CommandBedSetAsMedicalLabel".Translate() ||
										toggle.defaultLabel == "CommandBedSetAsGuestLabel".Translate()) gizmo.Disable();
									break;
								}
						}//*/
					}
					yield return gizmo;
				}
			}
		}

		// add description of whore price factor to inspect string (bottom left corner if item selected)
		[HarmonyPatch(typeof(Building_Bed), nameof(Building_Bed.GetInspectString))]
		public static class Building_Bed_GetInspectString_Patch
		{
			[HarmonyPostfix]
			public static void Postfix(Building_Bed __instance, ref string __result)
			{
				if (__instance.def.building.bed_humanlike && __instance.Faction == Faction.OfPlayerSilentFail && (__instance.IsAllowedForWhoringAll() || __instance.IsAllowedForWhoringOwner()))
				{
					__result = __result + "\n" + "WhorePriceCalcDesc".Translate(WhoreBed_Utility.CalculatePriceFactor(__instance).ToString("F2"));
					if (WhoringBase.DebugWhoring)
					{
						__result = __result + "\nbed.thingIDNumber: " + __instance.thingIDNumber.ToString();

						__result = __result + "\nscoreUpdateTickDelay: " + WhoringBase.DataStore.GetBedData(__instance).scoreUpdateTickDelay.ToString();

						if (WhoringBase.DataStore.GetBedData(__instance).reservedUntilGameTick > GenTicks.TicksGame)
						{
							__result = __result + "\nreserved by pawn id: " + WhoringBase.DataStore.GetBedData(__instance).reservedForPawnID.ToString();
						}
					}
				}
			}
		}

		// add whore price factor as overlay
		[HarmonyPatch(typeof(Building_Bed), nameof(Building_Bed.DrawGUIOverlay))]
		public static class Building_Bed_DrawGUIOverlay_Patch
		{
			[HarmonyPrefix]
			public static bool Prefix(Building_Bed __instance)
			{
				if (WhoringBase.show_whore_price_factor_on_bed && __instance.def.building.bed_humanlike && __instance.Faction == Faction.OfPlayerSilentFail) {
					// if whore bed, print price factor on it
					if (Find.CameraDriver.CurrentZoom == CameraZoomRange.Closest
						&& ((__instance.IsAllowedForWhoringOwner() && __instance.OwnersForReading.Any<Pawn>())
							|| __instance.IsAllowedForWhoringAll()))
					{
						Color defaultThingLabelColor = GenMapUI.DefaultThingLabelColor;

						// make string
						float whore_price_factor = WhoreBed_Utility.CalculatePriceFactor(__instance);
						string wpf;
						if (Math.Abs(whore_price_factor) >= 100)
						{
							wpf = ((int)whore_price_factor).ToString("D");
						}
						else if (Math.Abs(whore_price_factor) >= 10)
						{
							wpf = whore_price_factor.ToString("F1");
						}
						else
						{
							wpf = whore_price_factor.ToString("F2");
						}

						// get dimensions of text and make it appear above names
						Vector2 textsize = Text.CalcSize(wpf);
						Vector2 baseLabelPos = GenMapUI.LabelDrawPosFor(__instance, -0.4f); // -0.4f is copied from vanilla code
						baseLabelPos.y -= textsize.y * 0.75f;

						GenMapUI.DrawThingLabel(baseLabelPos, wpf, defaultThingLabelColor);

						if (__instance.IsAllowedForWhoringAll() && !__instance.OwnersForReading.Any<Pawn>())
						{
							// hide "Unowned" if whore bed with no owner
							return false;
						}
					}
				}
				// after drawing whore price factor, draw the actual names
				// could have been done as a postfix, but I started with a prefix, hoping I could get by with only one draw call
				return true;
			}

		}

		// barracks don't count whore beds, so room type switches to brothel sooner
		// disabled - barracks have their own slept in ~ debuff; doesn't really matter; put some effort in your brothels!
		/*[HarmonyPatch(typeof(RoomRoleWorker_Barracks), nameof(RoomRoleWorker_Barracks.GetScore))]
		public static class RoomRoleWorker_Barracks_GetScore_Patch
		{
			public static bool Prefix(Room room, ref float __result)
			{
				int num = 0;
				int num2 = 0;
				List<Thing> containedAndAdjacentThings = room.ContainedAndAdjacentThings;
				for (int i = 0; i < containedAndAdjacentThings.Count; i++)
				{
					Building_Bed building_Bed = containedAndAdjacentThings[i] as Building_Bed;
					if (building_Bed != null && building_Bed.def.building.bed_humanlike)
					{
						if (building_Bed.ForPrisoners)
						{
							__result = 0f;
							return false;
						}
						num++;
						if (!building_Bed.Medical && !building_Bed.IsAllowedForWhoringAll())
						{
							num2++;
						}
					}
				}
				if (num <= 1)
				{
					__result = 0f;
					return false;
				}
				__result = (float)num2 * 100100f;
				return false;
			}
		}*/

		// if pawns sleep in a brothel or a whoring bed, they get a thought
		[HarmonyPatch(typeof(Toils_LayDown), "ApplyBedThoughts")]
		public class Toils_LayDown_ApplyBedThoughts_Patch
		{
			[HarmonyPostfix]
			public static void Postfix(Pawn actor)
			{
				if (actor?.needs?.mood == null) return;

				Building_Bed building_Bed = actor.CurrentBed();

				actor?.needs?.mood?.thoughts?.memories?.RemoveMemoriesOfDef(ThoughtDefOf.SleptInBrothel);

				if (building_Bed == null) return;

				if (building_Bed?.GetRoom()?.Role == WhoreBed_Utility.roleDefBrothel || building_Bed.IsAllowedForWhoringAll())
				{
					var memoryHandler = actor.needs.mood.thoughts.memories;
					int thoughtStage = 0;

					foreach (var thoughtDef in DefDatabase<ThoughtDef_Whore>.AllDefsListForReading)
					{
						var memory = memoryHandler.GetFirstMemoryOfDef(thoughtDef);
						if (memory?.CurStageIndex >= thoughtDef.stages.Count - 1)
						{
							thoughtStage = 1;
							break;
						}
					}

					memoryHandler.TryGainMemory(ThoughtMaker.MakeThought(ThoughtDefOf.SleptInBrothel, thoughtStage));
				}
			}
		}

		// if room stats are updated, update beds within
		//	"necessary" if beds are toggled during pause
		[HarmonyPatch(typeof(Room), "UpdateRoomStatsAndRole")]
		public class Room_UpdateRoomStatsAndRole_Patch
		{
			[HarmonyPostfix]
			public static void Postfix(Room __instance)
			{
				// note: with room stat display enabled, this get's called quite often for whatever room the mouse hovers over
				//	even large outdoor areas
				//	where iterating over all things to find all beds (even if there are none) is expensive
				//	for now, skip doing anything if even the game decides it's not worth it
				//	(game sets role to None if region count >36 or something)
				//	- the beds will update eventually

				if (/*Find.PlaySettings.showRoomStats && */__instance.Role == RoomRoleDefOf.None)
					return;

				if (Find.TickManager.Paused)
				{
					// if paused, update immediately
					WhoreBed_Utility.CalculateBedFactorsForRoom(__instance);
				}
				else
				{
					// else, just make beds update as soon as needed
					WhoreBed_Utility.ResetTicksUntilUpdate(__instance);
				}
			}
		}
	}
}


code:
using Verse;
using HarmonyLib;
using rjw;
using System;
using RimWorld;
using System.Collections.Generic;
using UnityEngine;
using rjw.MainTab.DefModExtensions;
using System.Linq;
using System.Reflection;

namespace rjwwhoring
{
	[HarmonyPatch(typeof(rjw.MainTab.MainTabWindow))]
	[HarmonyPatch(nameof(rjw.MainTab.MainTabWindow.MakeOptions))]
	static class RJWTab_Brothel
	{
		[HarmonyPostfix]
		private static void MakeOptionsPatch(rjw.MainTab.MainTabWindow __instance, ref List<FloatMenuOption> __result)
		{
			try
			{
				RJWTab_Brothel_Patch.MakeOptionsPatch(__instance, ref __result);
			}
			catch (Exception e)
			{
				Log.Error(e.ToString());
			}
		}
	}

	static class RJWTab_Brothel_Patch
	{
		public static List<FloatMenuOption> MakeOptionsPatch(rjw.MainTab.MainTabWindow __instance, ref List<FloatMenuOption> __result)
		{
			PawnTableDef RJW_Brothel = DefDatabase<PawnTableDef>.GetNamed("RJW_Brothel");
			ModLog.Message("0");
			__result.Add(new FloatMenuOption(RJW_Brothel.GetModExtension<RJW_PawnTable>().label, () =>
			{
				ModLog.Message("1");
				__instance.pawnTableDef = RJW_Brothel;
				ModLog.Message("2");
				__instance.pawns = Find.CurrentMap.mapPawns.AllPawns.Where(p => xxx.is_human(p) && (p.IsColonist || p.IsPrisonerOfColony));
				ModLog.Message("3");
				__instance.Notify_ResolutionChanged();
				ModLog.Message("4");
				rjw.MainTab.MainTabWindow.Reloadtab();
				ModLog.Message("5");
			}, MenuOptionPriority.Default));
			return __result;
		}
	}
}


code:
<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Lib.Harmony" version="2.2.2" targetFramework="net472" />
</packages>


code:
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("RimJobWorld Whoring")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("RimJobWorld Whoring")]
[assembly: AssemblyCopyright("Copyright © 2022")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("c2825019-7f0b-456d-85a3-479c1a2a8805")]

// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version
//      Build Number
//      Revision
//
[assembly: AssemblyVersion("1.0.0.0")]