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

namespace rjw
{
	public class Recipe_ClaimChild :  RecipeWorker
	{
		public override IEnumerable<BodyPartRecord> GetPartsToApplyOn(Pawn pawn, RecipeDef recipe)
		{
			if (pawn != null)//I have no idea how it works but Recipe_ShutDown : RecipeWorker does not return values when not applicable
			{
				//Log.Message("RJW Claim child check on " + pawn);
				if (xxx.is_human(pawn) && !pawn.IsColonist)
				{
					if ( (pawn.ageTracker.CurLifeStageIndex < 2))//Guess it is hardcoded for now to `baby` and `toddler` of standard 4 stages of human life
					{
						BodyPartRecord brain = pawn.health.hediffSet.GetBrain();
						if (brain != null)
						{
							//Log.Message("RJW Claim child is applicable");
							yield return brain;
						}
					}
				}
			}
		}

		public override void ApplyOnPawn(Pawn pawn, BodyPartRecord part, Pawn billDoer, List<Thing> ingredients, Bill bill)
		{
			if (pawn == null)
			{
				ModLog.Error("Error applying medical recipe, pawn is null");
				return;
			}
			pawn.SetFaction(Faction.OfPlayer);
			//we could do
			//pawn.SetFaction(billDoer.Faction);
			//but that is useless because GetPartsToApplyOn does not support factions anyway and all recipes are hardcoded to player.
		}
	}
}


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

namespace rjw
{
	public class Recipe_DeterminePregnancy : RecipeWorker
	{
		public override IEnumerable<BodyPartRecord> GetPartsToApplyOn(Pawn pawn, RecipeDef recipe)
		{
			/* Males can be impregnated by mechanoids, probably
			if (!xxx.is_female(pawn))
			{
				yield break;
			}
			*/
			BodyPartRecord part = pawn.RaceProps.body.corePart;
			if (recipe.appliedOnFixedBodyParts[0] != null)
				part = pawn.RaceProps.body.AllParts.Find(x => x.def == recipe.appliedOnFixedBodyParts[0]);
			if (part != null && (pawn.ageTracker.CurLifeStage.reproductive)
				|| pawn.IsPregnant(true))
			{
				yield return part;
			}
		}

		public override void ApplyOnPawn(Pawn pawn, BodyPartRecord part, Pawn billDoer, List<Thing> ingredients, Bill bill)
		{
			var preg = (PregnancyHelper.GetPregnancy(pawn) as Hediff_BasePregnancy);

			if (preg != null)
			{
				preg.CheckPregnancy();
			}
			else
			{
				Messages.Message(xxx.get_pawnname(billDoer) + " has determined " + xxx.get_pawnname(pawn) + " is not pregnant.", MessageTypeDefOf.NeutralEvent);
			}
		}
	}
	public class Recipe_DeterminePaternity : RecipeWorker
	{
		public override IEnumerable<BodyPartRecord> GetPartsToApplyOn(Pawn pawn, RecipeDef recipe)
		{
				BodyPartRecord part = pawn.RaceProps.body.corePart;
			if (recipe.appliedOnFixedBodyParts[0] != null)
				part = pawn.RaceProps.body.AllParts.Find(x => x.def == recipe.appliedOnFixedBodyParts[0]);
			if (part != null && (pawn.ageTracker.CurLifeStage.reproductive)
				|| pawn.IsPregnant(true))
			{
				yield return part;
			}
		}

		public override void ApplyOnPawn(Pawn pawn, BodyPartRecord part, Pawn billDoer, List<Thing> ingredients, Bill bill)
		{
			var preg = (PregnancyHelper.GetPregnancy(pawn) as Hediff_BasePregnancy);

			if (preg != null)
			{
				Messages.Message(xxx.get_pawnname(billDoer) + " has determined "+ xxx.get_pawnname(pawn) + " is pregnant and " + preg.father + " is the father.", MessageTypeDefOf.NeutralEvent);

				preg.CheckPregnancy();
			}
			else
			{
				Messages.Message(xxx.get_pawnname(billDoer) + " has determined " + xxx.get_pawnname(pawn) + " is not pregnant.", MessageTypeDefOf.NeutralEvent);
			}
		}
	}
}

repo_name: LamkasDev/rjw

license: mit


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

namespace rjw
{
	/// <summary>
	/// IUD - prevent pregnancy
	/// </summary>
	public class Recipe_InstallIUD : Recipe_InstallImplantToExistParts
	{
		public override IEnumerable<BodyPartRecord> GetPartsToApplyOn(Pawn pawn, RecipeDef recipe)
		{
			if (!xxx.is_female(pawn))
			{
				return new List<BodyPartRecord>();
			}
			return base.GetPartsToApplyOn(pawn, recipe);
		}
	}
}


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

namespace rjw
{
	public class Recipe_PregnancyHackMech : RecipeWorker
	{
		public override IEnumerable<BodyPartRecord> GetPartsToApplyOn(Pawn pawn, RecipeDef recipe)
		{
			BodyPartRecord part = pawn.RaceProps.body.corePart;
			if (recipe.appliedOnFixedBodyParts[0] != null)
				part = pawn.RaceProps.body.AllParts.Find(x => x.def == recipe.appliedOnFixedBodyParts[0]);
			if (part != null && pawn.health.hediffSet.HasHediff(HediffDef.Named("RJW_pregnancy_mech"), part, true))
			{
				Hediff_MechanoidPregnancy pregnancy = (Hediff_MechanoidPregnancy)pawn.health.hediffSet.GetFirstHediffOfDef(HediffDef.Named("RJW_pregnancy_mech"));
				//Log.Message("RJW_pregnancy_mech hack check: " + pregnancy.is_checked);
				if (pregnancy.is_checked)
					yield return part;
			}
		}

		public override void ApplyOnPawn(Pawn pawn, BodyPartRecord part, Pawn billDoer, List<Thing> ingredients, Bill bill)
		{

			if (pawn.health.hediffSet.HasHediff(HediffDef.Named("RJW_pregnancy_mech")))
			{
				Hediff_MechanoidPregnancy pregnancy = (Hediff_MechanoidPregnancy)pawn.health.hediffSet.GetFirstHediffOfDef(HediffDef.Named("RJW_pregnancy_mech"));
				pregnancy.Hack();
			}
		}
	}
}


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

namespace rjw
{
	/// <summary>
	/// Sterilization
	/// </summary>
	public class Recipe_InstallImplantToExistParts : Recipe_InstallImplant
	{
		public override IEnumerable<BodyPartRecord> GetPartsToApplyOn(Pawn pawn, RecipeDef recipe)
		{
			bool blocked = Genital_Helper.genitals_blocked(pawn) || xxx.is_slime(pawn);

			if (!blocked)
				foreach (BodyPartRecord record in pawn.RaceProps.body.AllParts.Where(x => recipe.appliedOnFixedBodyParts.Contains(x.def)))
				{
					if (!pawn.health.hediffSet.hediffs.Any((Hediff x) => x.def == recipe.addsHediff))
					{
						yield return record;
					}
				}
		}
	}
}


code:
using RimWorld;
using Verse;

namespace rjw
{
	public class ThoughtWorker_ItchyCrotch : ThoughtWorker
	{
		protected override ThoughtState CurrentStateInternal(Pawn p)
		{
			int sev = std.genital_rash_severity(p);
			if (sev <= 0)
				return ThoughtState.Inactive;
			else if (sev == 1)
				return ThoughtState.ActiveAtStage(0);
			else
				return ThoughtState.ActiveAtStage(1);
		}
	}
}


code:
using RimWorld;
using Verse;

namespace rjw
{
	public class ThoughtWorker_SyphiliticThoughts : ThoughtWorker
	{
		protected override ThoughtState CurrentStateInternal(Pawn p)
		{
			//Log.Message("0");
			var syp = p.health.hediffSet.GetFirstHediffOfDef(std.syphilis.hediff_def);
			//Log.Message("1");
			if (syp != null)
			{
				//Log.Message("2");
				if (syp.Severity >= 0.80f)
				{
					//Log.Message("3");
					return ThoughtState.ActiveAtStage(1);
				}
				else if (syp.Severity >= 0.50f)
				{
					//Log.Message("4");
					return ThoughtState.ActiveAtStage(0);
				}
				//Log.Message("5");
			}
			//Log.Message("6");
			return ThoughtState.Inactive;
		}
	}
}


code:
using RimWorld;
using Verse;

namespace rjw
{
	public class ThoughtWorker_WastingAway : ThoughtWorker
	{
		protected override ThoughtState CurrentStateInternal(Pawn p)
		{
			if (!std.is_wasting_away(p))
				return ThoughtState.Inactive;
			else
				return ThoughtState.ActiveAtStage(0);
		}
	}
}


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

namespace rjw
{
	/// <summary>
	/// Common functions and constants relevant to STDs.
	/// </summary>
	public static class std
	{
		public static std_def hiv = DefDatabase<std_def>.GetNamed("HIV");
		public static std_def herpes = DefDatabase<std_def>.GetNamed("Herpes");
		public static std_def warts = DefDatabase<std_def>.GetNamed("Warts");
		public static std_def syphilis = DefDatabase<std_def>.GetNamed("Syphilis");
		public static std_def boobitis = DefDatabase<std_def>.GetNamed("Boobitis");

		public static readonly HediffDef immunodeficiency = DefDatabase<HediffDef>.GetNamed("Immunodeficiency");

		public static List<std_def> all => DefDatabase<std_def>.AllDefsListForReading;

		// Returns how severely affected this pawn's crotch is by rashes and warts, on a scale from 0 to 3.
		public static int genital_rash_severity(Pawn p)
		{
			int tr = 0;

			Hediff her = p.health.hediffSet.GetFirstHediffOfDef(herpes.hediff_def);
			if (her != null && her.Severity >= 0.25f)
				++tr;

			Hediff war = p.health.hediffSet.GetFirstHediffOfDef(warts.hediff_def);
			if (war != null)
				tr += war.Severity < 0.40f ? 1 : 2;

			return tr;
		}

		public static Hediff get_infection(Pawn p, std_def sd)
		{
			return p.health.hediffSet.GetFirstHediffOfDef(sd.hediff_def);
		}

		public static BodyPartRecord GetRelevantBodyPartRecord(Pawn pawn, std_def std)
		{
			if (std.appliedOnFixedBodyParts == null)
			{
				return null;
			}

			BodyPartDef target = std.appliedOnFixedBodyParts.Single();
			return pawn?.RaceProps.body.GetPartsWithDef(target).Single();
			//return pawn?.RaceProps.body.GetPartsWithDef(std.appliedOnFixedBodyParts.Single()).Single();
		}

		public static bool is_wasting_away(Pawn p)
		{
			Hediff id = p.health.hediffSet.GetFirstHediffOfDef(immunodeficiency);
			return id != null && id.CurStageIndex > 0;
		}

		public static bool IsImmune(Pawn pawn)
		{
			// Archotech genitalia automagically purge STDs.
			return !RJWSettings.stds_enabled
				|| pawn.health.hediffSet.HasHediff(Genital_Helper.archotech_vagina)
				|| pawn.health.hediffSet.HasHediff(Genital_Helper.archotech_penis)
				|| xxx.is_demon(pawn)
				|| xxx.is_slime(pawn)
				|| xxx.is_mechanoid(pawn);
		}
	}
}


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

namespace rjw
{
	/// <summary>
	/// Defines a disease that has a chance to spread during sex.
	/// </summary>
	public class std_def : Def
	{
		public HediffDef hediff_def;
		public HediffDef cohediff_def = null;
		public float catch_chance;
		public float environment_pitch_chance = 0.0f;
		public float spawn_chance = 0.0f;
		public float spawn_severity = 0.0f;
		public float autocure_below_severity = -1.0f;
		public List<BodyPartDef> appliedOnFixedBodyParts = null;
	}
}


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

namespace rjw
{
	/// <summary>
	/// Responsible for spreading STDs (adding STD hediffs). Usually happens during sex.
	/// </summary>
	public static class std_spreader
	{
		/// <summary>
		/// Check for spreading of every STD the pitcher has to the catcher.
		/// Includes a small chance to spread STDs that pitcher doesn't have.
		/// </summary>
		[SyncMethod]
		public static void roll_to_catch(Pawn catcher, Pawn pitcher)
		{
			if (std.IsImmune(catcher) || std.IsImmune(pitcher))
			{
				return;
			}
			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());

			float cleanliness_factor = GetCleanlinessFactor(catcher);

			foreach (std_def sd in std.all)
			{
				if (!catcher.health.hediffSet.HasHediff(sd.hediff_def))
				{
					if (catcher.health.immunity.GetImmunity(sd.hediff_def) <= 0.0f)
					{
						var bodyPartRecord = std.GetRelevantBodyPartRecord(catcher, sd);
						var artificial = bodyPartRecord != null && catcher.health.hediffSet.HasDirectlyAddedPartFor(bodyPartRecord);

						float catch_chance = GetCatchChance(catcher, sd);
						float catch_rv = Rand.Value;
						//if (xxx.config.std_show_roll_to_catch)
						//--Log.Message("  Chance to catch " + sd.label + ": " + catch_chance.ToStringPercent() + "; rolled: " + catch_rv.ToString());
						if (catch_rv < catch_chance)
						{
							string pitch_source; float pitch_chance;
							{
								if (get_severity(pitcher, sd) >= xxx.config.std_min_severity_to_pitch)
								{
									pitch_source = xxx.get_pawnname(pitcher);
									pitch_chance = 1.0f;
								}
								else
								{
									pitch_source = "the environment";
									pitch_chance = sd.environment_pitch_chance * cleanliness_factor;
									if (!RJWSettings.std_floor)
									{
										pitch_chance = -9001f;
									}
								}
							}
							float pitch_rv = Rand.Value;

							//if (xxx.config.std_show_roll_to_catch)
							//--Log.Message("	Chance to pitch (from " + pitch_source + "): " + pitch_chance.ToStringPercent() + "; rolled: " + pitch_rv.ToString());
							if (pitch_rv < pitch_chance)
							{
								infect(catcher, sd);
								show_infection_letter(catcher, sd, pitch_source, catch_chance * pitch_chance);
								//if (xxx.config.std_show_roll_to_catch)
								//--Log.Message("	  INFECTED!");
							}
						}
					}
					//else
					//if (xxx.config.std_show_roll_to_catch)
					//--Log.Message("  Still immune to " + sd.label);
				}
				//else
				//if (xxx.config.std_show_roll_to_catch)
				//--Log.Message("  Already infected with " + sd.label);
			}
		}

		public static float get_severity(Pawn p, std_def sd)
		{
			Hediff hed = std.get_infection(p, sd);
			return hed?.Severity ?? 0.0f;
		}

		public static Hediff infect(Pawn p, std_def sd, bool include_coinfection = true)
		{
			Hediff existing = std.get_infection(p, sd);
			if (existing != null)
			{
				return existing;
			}

			BodyPartRecord part = std.GetRelevantBodyPartRecord(p, sd);
			p.health.AddHediff(sd.hediff_def, part);
			if (include_coinfection && sd.cohediff_def != null)
			{
				p.health.AddHediff(sd.cohediff_def, part);
			}
			//--ModLog.Message("std::infect genitals std");
			return std.get_infection(p, sd);
		}

		static float GetCatchChance(Pawn pawn, std_def sd)
		{
			var bodyPartRecord = std.GetRelevantBodyPartRecord(pawn, sd);
			float artificialFactor = 1f;

			if (bodyPartRecord == null && pawn.health.hediffSet.HasDirectlyAddedPartFor(Genital_Helper.get_genitalsBPR(pawn)))
			{
				artificialFactor = .15f;
			}
			else if (pawn.health.hediffSet.HasDirectlyAddedPartFor(bodyPartRecord))
			{
				artificialFactor = 0f;
			}

			return sd.catch_chance * artificialFactor;
		}

		public static void show_infection_letter(Pawn p, std_def sd, string source = null, float? chance = null)
		{
			StringBuilder info;
			{
				info = new StringBuilder();
				info.Append(xxx.get_pawnname(p) + " has caught " + sd.label + (source != null ? " from " + source + "." : ""));
				if (chance.HasValue)
					info.Append(" (" + chance.Value.ToStringPercent() + " chance)");
				info.AppendLine(); info.AppendLine();
				info.Append(sd.description);
			}
			Find.LetterStack.ReceiveLetter("Infection: " + sd.label, info.ToString(), LetterDefOf.ThreatSmall, p);
		}

		static float GetCleanlinessFactor(Pawn catcher)
		{
			Room room = catcher.GetRoom();
			float cle = room?.GetStat(RoomStatDefOf.Cleanliness) ?? xxx.config.std_outdoor_cleanliness;
			float exa = cle >= 0.0f ? xxx.config.std_env_pitch_cleanliness_exaggeration : xxx.config.std_env_pitch_dirtiness_exaggeration;
			return Mathf.Max(0.0f, 1.0f - exa * cle);
		}

		// Not called anywhere?
		[SyncMethod]
		public static void generate_on(Pawn p)
		{
			if (p == null) return;
			//prevent error on world gen for pawns with broken bodies(no genitals)
			if (p.RaceProps.body.HasPartWithTag(BodyPartTagDefOf.RJW_Fertility))
				return;
			if (!xxx.is_human(p))
				return;
			float nymph_mul = !xxx.is_nympho(p) ? 1.0f : xxx.config.nymph_spawn_with_std_mul;
			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			foreach (std_def sd in std.all)
				if (Rand.Value < sd.spawn_chance * nymph_mul)
				{
					Hediff hed = infect(p, sd, false);
					float sev;
					{
						float r = Rand.Range(sd.hediff_def.minSeverity, sd.hediff_def.maxSeverity);
						sev = Mathf.Clamp(sd.spawn_severity * r, sd.hediff_def.minSeverity, sd.hediff_def.maxSeverity);
					}
					hed.Severity = sev;
				}
		}
	}
}


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

namespace rjw
{
	/// <summary>
	/// Responsible for handling the periodic effects of having an STD hediff.
	/// Not technically tied to the infection vector itself,
	/// but some of the STD effects are weird and complicated.
	/// </summary>
	public static class std_updater
	{
		public const float UpdatesPerDay = 60000f / 150f / (float)Need_Sex.needsex_tick_timer;

		public static void update(Pawn p)
		{
			update_immunodeficiency(p);

			// Check if any infections are below the autocure threshold and cure them if so
			foreach (std_def sd in std.all)
			{
				Hediff inf = std.get_infection(p, sd);
				if (inf != null && (inf.Severity < sd.autocure_below_severity || std.IsImmune(p)))
				{
					p.health.RemoveHediff(inf);
					if (sd.cohediff_def != null)
					{
						Hediff coinf = p.health.hediffSet.GetFirstHediffOfDef(sd.cohediff_def);
						if (coinf != null)
							p.health.RemoveHediff(coinf);
					}
				}
			}

			UpdateBoobitis(p);
			roll_for_syphilis_damage(p);
		}

		[SyncMethod]
		public static void roll_for_syphilis_damage(Pawn p)
		{
			Hediff syp = p.health.hediffSet.GetFirstHediffOfDef(std.syphilis.hediff_def);
			if (syp == null || !(syp.Severity >= 0.60f) || syp.FullyImmune()) return;

			// A 30% chance per day of getting any permanent damage works out to ~891 in 1 million for each roll
			// The equation is (1-x)^(60000/150)=.7
			// Important Note:
			// this function is called from Need_Sex::NeedInterval(), where it involves a needsex_tick and a std_tick to actually trigger this roll_for_syphilis_damage.
			// j(this is not exactly the same as the value in Need_Sex, that value is 0, but here j should be 1) std_ticks per this function called, k needsex_ticks per std_tick, 150 ticks per needsex_tick, and x is the chance per 150 ticks,
			// The new equation should be .7 = (1-x)^(400/kj)
			// 1-x = .7^(kj/400), x =1-.7^(kj/400)
			// Since k=10,j=1, so kj=10, new x is 1-.7^(10/400)=0.0088772362, let it be 888/100000
			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			if (Rand.RangeInclusive(1, 100000) <= 888)
			{
				BodyPartRecord part;
				float sev;
				var parts = p.RaceProps.body.AllParts;

				float rv = Rand.Value;
				if (rv < 0.10f)
				{
					part = parts.Find(bpr => string.Equals(bpr.def.defName, "Brain"));
					sev = 1.0f;
				}
				else if (rv < 0.50f)
				{
					part = parts.Find(bpr => string.Equals(bpr.def.defName, "Liver"));
					sev = Rand.RangeInclusive(1, 3);
				}
				else if (rv < 0.75f)
				{
					//LeftKidney, probably
					part = parts.Find(bpr => string.Equals(bpr.def.defName, "Kidney"));
					sev = Rand.RangeInclusive(1, 2);
				}
				else
				{
					//RightKidney, probably
					part = parts.FindLast(bpr => string.Equals(bpr.def.defName, "Kidney"));
					sev = Rand.RangeInclusive(1, 2);
				}

				if (part != null && !p.health.hediffSet.PartIsMissing(part) && !p.health.hediffSet.HasDirectlyAddedPartFor(part))
				{
					DamageDef vir_dam = DefDatabase<DamageDef>.GetNamed("ViralDamage");
					HediffDef dam_def = HealthUtility.GetHediffDefFromDamage(vir_dam, p, part);
					Hediff_Injury inj = (Hediff_Injury)HediffMaker.MakeHediff(dam_def, p, null);
					inj.Severity = sev;
					inj.TryGetComp<HediffComp_GetsPermanent>().IsPermanent = true;
					p.health.AddHediff(inj, part, null);
					string message_title = std.syphilis.label + " Damage";
					string baby_pronoun = p.gender == Gender.Male ? "his" : "her";
					string message_text = "RJW_Syphilis_Damage_Message".Translate(xxx.get_pawnname(p), baby_pronoun, part.def.label, std.syphilis.label).CapitalizeFirst();
					Find.LetterStack.ReceiveLetter(message_title, message_text, LetterDefOf.ThreatSmall, p);
				}
			}
		}

		[SyncMethod]
		public static void update_immunodeficiency(Pawn p)
		{
			float min_bf_for_id = 1.0f - std.immunodeficiency.minSeverity;
			Hediff id = p.health.hediffSet.GetFirstHediffOfDef(std.immunodeficiency);
			float bf = p.health.capacities.GetLevel(PawnCapacityDefOf.BloodFiltration);
			bool has = id != null;
			bool should_have = bf <= min_bf_for_id;

			if (has && !should_have)
			{
				p.health.RemoveHediff(id);
				id = null;
			}
			else if (!has && should_have)
			{
				p.health.AddHediff(std.immunodeficiency);
				id = p.health.hediffSet.GetFirstHediffOfDef(std.immunodeficiency);
			}

			if (id == null) return;

			id.Severity = 1.0f - bf;

			// Roll for and apply opportunistic infections:
			// Pawns will have a 90% chance for at least one infection each year at 0% filtration, and a 0%
			// chance at 40% filtration, scaling linearly.
			// Let x = chance infected per roll
			// Then chance not infected per roll = 1 - x
			// And chance not infected on any roll in one day = (1 - x) ^ (60000 / 150) = (1 - x) ^ 400
			// And chance not infected on any roll in one year = (1 - x) ^ (400 * 60) = (1 - x) ^ 24000
			// So 0.10 = (1 - x) ^ 24000
			//	log (0.10) = 24000 log (1 - x)
			//	x = 0.00009593644334648975435114691213 = ~96 in 1 million
			// Important Note:
			// this function is called from Need_Sex::NeedInterval(), where it involves a needsex_tick and a std_tick to actually trigger this update_immunodeficiency.
			// j(this is not exactly the same as the value in Need_Sex, that value is 0, but here j should be 1) std_ticks per this function called, k needsex_ticks per std_tick, 150 ticks per needsex_tick, and x is the chance per 150 ticks,
			// The new equation should be .1 = (1-x)^(24000/kj)
			// log(.1) = (24000/kj) log(1-x),  so log(1-x)= (kj/24000) log(.1), 1-x = .1^(kj/24000), x= 1-.1^(kj/24000)
			// Since k=10,j=1, so kj=10, new x is 1-.1^(10/24000)=0.0009589504, let it be 959/1000000
			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			if (Rand.RangeInclusive(1, 1000000) <= 959 && Rand.Value < bf / min_bf_for_id)
			{
				BodyPartRecord part;
				{
					float rv = Rand.Value;
					var parts = p.RaceProps.body.AllParts;
					if (rv < 0.25f)
						part = parts.Find(bpr => string.Equals(bpr.def.defName, "Jaw"));
					else if (rv < 0.50f)
						part = parts.Find(bpr => string.Equals(bpr.def.defName, "Lung"));
					else if (rv < 0.75f)
						part = parts.FindLast(bpr => string.Equals(bpr.def.defName, "Lung"));
					else
						part = parts.RandomElement();
				}

				if (part != null &&
					!p.health.hediffSet.PartIsMissing(part) && !p.health.hediffSet.HasDirectlyAddedPartFor(part) &&
					p.health.hediffSet.GetFirstHediffOfDef(HediffDefOf.WoundInfection) == null && // If the pawn already has a wound infection, we can't properly set the immunity for the new one
					p.health.immunity.GetImmunity(HediffDefOf.WoundInfection) <= 0.0f)
				{ // Dont spawn infection if pawn already has immunity
					p.health.AddHediff(HediffDefOf.WoundInfection, part);
					p.health.HealthTick(); // Creates the immunity record
					ImmunityRecord ir = p.health.immunity.GetImmunityRecord(HediffDefOf.WoundInfection);
					if (ir != null)
						ir.immunity = xxx.config.opp_inf_initial_immunity;
					const string message_title = "Opportunistic Infection";
					string message_text = "RJW_Opportunistic_Infection_Message".Translate(xxx.get_pawnname(p)).CapitalizeFirst();
					Find.LetterStack.ReceiveLetter(message_title, message_text, LetterDefOf.ThreatSmall);
				}
			}
		}

		/// <summary>
		/// For meanDays = 1.0, will return true on average once per day. For 2.0, will return true on average once every two days.
		/// </summary>
		[SyncMethod]
		static bool RollFor(float meanDays)
		{
			return Rand.Chance(1.0f / meanDays / UpdatesPerDay);
		}

		public static void UpdateBoobitis(Pawn pawn)
		{
			//var hediff = std.get_infection(pawn, std.boobitis);
			//if (hediff == null
			//	|| !(hediff.Severity >= 0.20f)
			//	|| hediff.FullyImmune()
			//	|| !BreastSize_Helper.TryGetBreastSize(pawn, out var oldSize, out var oldBoobs)
			//	|| oldSize >= BreastSize_Helper.MaxSize
			//	|| !RollFor(hediff.Severity > 0.90f ? 5f : 15f))
			//{
			//	return;
			//}
			//var chest = Genital_Helper.get_breastsBPR(pawn);
			//var newSize = oldSize + 1;
			//var newBoobs = BreastSize_Helper.GetHediffDef(newSize);

			//GenderHelper.ChangeSex(pawn, () =>
			//{
			//	if (oldBoobs != null)
			//	{
			//		pawn.health.RemoveHediff(oldBoobs);
			//	}
			//	pawn.health.AddHediff(newBoobs, chest);
			//});

			//var message = "RJW_BreastsHaveGrownFromBoobitis".Translate(pawn);
			//Messages.Message(message, pawn, MessageTypeDefOf.SilentInput);
		}
	}
}


code:
using Verse;
using UnityEngine;
using Multiplayer.API;

namespace rjw
{
	[StaticConstructorOnStartup]
	public static class BukkakeContent
	{
		//UI:
		public static readonly Texture2D SemenIcon_little = ContentFinder<Texture2D>.Get("Bukkake/SemenIcon_little", true);
		public static readonly Texture2D SemenIcon_some = ContentFinder<Texture2D>.Get("Bukkake/SemenIcon_some", true);
		public static readonly Texture2D SemenIcon_dripping = ContentFinder<Texture2D>.Get("Bukkake/SemenIcon_dripping", true);
		public static readonly Texture2D SemenIcon_drenched = ContentFinder<Texture2D>.Get("Bukkake/SemenIcon_drenched", true);

		//on pawn:
		public static readonly Material semenSplatch1 = MaterialPool.MatFrom("Bukkake/splatch_1", ShaderDatabase.Cutout);
		public static readonly Material semenSplatch2 = MaterialPool.MatFrom("Bukkake/splatch_2", ShaderDatabase.Cutout);
		public static readonly Material semenSplatch3 = MaterialPool.MatFrom("Bukkake/splatch_3", ShaderDatabase.Cutout);
		public static readonly Material semenSplatch4 = MaterialPool.MatFrom("Bukkake/splatch_4", ShaderDatabase.Cutout);
		public static readonly Material semenSplatch5 = MaterialPool.MatFrom("Bukkake/splatch_5", ShaderDatabase.Cutout);
		public static readonly Material semenSplatch6 = MaterialPool.MatFrom("Bukkake/splatch_6", ShaderDatabase.Cutout);
		public static readonly Material semenSplatch7 = MaterialPool.MatFrom("Bukkake/splatch_7", ShaderDatabase.Cutout);
		public static readonly Material semenSplatch8 = MaterialPool.MatFrom("Bukkake/splatch_8", ShaderDatabase.Cutout);
		public static readonly Material semenSplatch9 = MaterialPool.MatFrom("Bukkake/splatch_9", ShaderDatabase.Cutout);

		[SyncMethod]
		public static Material pickRandomSplatch()
		{
			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			int rand = Rand.Range(0, 8);
			switch (rand)
			{
				case 0:
					return semenSplatch1;
				case 1:
					return semenSplatch2;
				case 2:
					return semenSplatch3;
				case 3:
					return semenSplatch4;
				case 4:
					return semenSplatch5;
				case 5:
					return semenSplatch6;
				case 6:
					return semenSplatch7;
				case 7:
					return semenSplatch8;
				case 8:
					return semenSplatch9;
			}
			return null;
		}
	}
}


code:
using RimWorld;
using Verse;

namespace rjw
{
	//I took the liberty of adding the new DefOf files since I find that easier than managing all the defs via xxx.cs
	[DefOf]
	public static class RJW_SemenoOverlayHediffDefOf
	{
		public static HediffDef Hediff_Semen;//for humans & animals; also parent for insect and mech spunk
		public static HediffDef Hediff_InsectSpunk;
		public static HediffDef Hediff_MechaFluids;

		public static HediffDef Hediff_Bukkake;//Master + manager

	}

}


code:
using RimWorld;
using Verse;

namespace rjw
{
	[DefOf]
	static class RJW_SemenOverlayJobDefOf
	{
		public static JobDef CleanSelf;
	}
}


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

namespace rjw
{
	class Hediff_Bukkake : HediffWithComps
	{
		/*
		Whenever semen is applied, this hediff is also added to the pawn.
		Since there is always only a single hediff of this type on the pawn, it serves as master controller, adding up the individual Semen hediffs, applying debuffs and drawing overlays
		*/

		private static readonly float semenWeight = 0.2f;//how much individual semen_hediffs contribute to the overall bukkake severity
		List<Hediff> hediffs_semen;
		Dictionary<string, SemenSplatch> splatches;

		public override void ExposeData()
		{
			base.ExposeData();
			//Scribe_Values.Look<Dictionary<string, SemenSplatch>>(ref splatches, "splatches", new Dictionary<string, SemenSplatch>()); - causes errors when loading. for now, just make a new dictionary
			splatches = new Dictionary<string, SemenSplatch>();//instead of loading, just recreate anew
			hediffs_semen = new List<Hediff>();
		}

		public override void PostMake()
		{
			base.PostMake();
			splatches = new Dictionary<string, SemenSplatch>();
		}

		public override void PostTick()
		{
			if (pawn.RaceProps.Humanlike)//for now, only humans are supported
			{
				hediffs_semen = this.pawn.health.hediffSet.hediffs.FindAll(x => (x.def == RJW_SemenoOverlayHediffDefOf.Hediff_Semen || x.def == RJW_SemenoOverlayHediffDefOf.Hediff_InsectSpunk || x.def == RJW_SemenoOverlayHediffDefOf.Hediff_MechaFluids));
				float bukkakeLevel = CalculateBukkakeLevel();//sum of severity of all the semen hediffs x semenWeight
				this.Severity = bukkakeLevel;
				bool updatePortrait = false;

				//loop through all semen hediffs, add missing ones to dictionary
				for (int i = 0; i < hediffs_semen.Count(); i++)
				{
					Hediff_Semen h = (Hediff_Semen)hediffs_semen[i];
					string ID = h.GetUniqueLoadID();//unique ID for each hediff
					if (!splatches.ContainsKey(ID))//if it isn't here yet, make new object
					{
						updatePortrait = true;
						bool leftSide = h.Part.Label.Contains("left") ? true : false;//depending on whether the body part is left or right, drawing-offset on x-aixs may be inverted

						splatches[ID] = new SemenSplatch(h, pawn.story.bodyType, h.Part.def, leftSide, h.semenType);
					}
				}

				//remove splatch objects once their respective semen hediff is gone
				List<string> removeKeys = new List<string>();
				foreach (string key in splatches.Keys)
				{
					SemenSplatch s = splatches[key];

					if (!hediffs_semen.Contains(s.hediff_Semen))
					{
						removeKeys.Add(key);
						updatePortrait = true;
					}
				}
				//loop over and remove elements that should be destroyed:
				foreach (string key in removeKeys)
				{
					SemenSplatch s = splatches[key];
					splatches.Remove(key);
				}

				if (updatePortrait)//right now, portraits are only updated when a completely new semen hediff is added or an old one removed - maybe that should be done more frequently
				{
					PortraitsCache.SetDirty(pawn);
				}
			}
		}

		//called from the PawnWoundDrawer (see HarmonyPatches)
		public void DrawSemen(Vector3 drawLoc, Quaternion quat, bool forPortrait, float angle, bool inBed = false)
		{
			Rot4 bodyFacing = pawn.Rotation;
			int facingDir = bodyFacing.AsInt;//0: north, 1:east, 2:south, 3:west
			foreach (string key in splatches.Keys)
			{
				SemenSplatch s = splatches[key];
				s.Draw(drawLoc, quat, forPortrait, facingDir, angle, inBed);
			}
		}

		//new Hediff_Bukkake added to pawn -> just combine the two
		public override bool TryMergeWith(Hediff other)
		{
			if (other == null || other.def != this.def)
			{
				return false;
			}
			return true;
		}

		private float CalculateBukkakeLevel()
		{
			float num = 0f;
			for (int i = 0; i < hediffs_semen.Count; i++)
			{
				num += hediffs_semen[i].Severity * semenWeight;
			}
			return num;
		}

		//class for handling drawing of the individual splatches
		private class SemenSplatch
		{
			public readonly Hediff_Semen hediff_Semen;
			public readonly Material semenMaterial;
			public readonly BodyPartDef bodyPart;
			private bool mirrorMesh;

			private const float maxSize = 0.20f;//1.0 = 1 tile
			private const float minSize = 0.05f;

			//data taken from SemenHelper.cs:
			private readonly float[] xAdjust;
			private readonly float[] zAdjust;
			private readonly float[] yAdjust;

			public SemenSplatch(Hediff_Semen hediff, BodyTypeDef bodyType, BodyPartDef bodyPart, bool leftSide = false, int semenType = SemenHelper.CUM_NORMAL)
			{
				hediff_Semen = hediff;
				semenMaterial = new Material(BukkakeContent.pickRandomSplatch());//needs to create new material in order to allow for different colors
				semenMaterial.SetTextureScale("_MainTex", new Vector2(-1, 1));
				this.bodyPart = bodyPart;
				//Rand.PopState();
				//Rand.PushState(RJW_Multiplayer.PredictableSeed());

				//set color:
				switch (semenType)
				{
					case SemenHelper.CUM_NORMAL:
						semenMaterial.color = SemenHelper.color_normal;
						break;
					case SemenHelper.CUM_INSECT:
						semenMaterial.color = SemenHelper.color_insect;
						break;
					case SemenHelper.CUM_MECHA:
						semenMaterial.color = SemenHelper.color_mecha;
						break;
				}

				if (!MP.enabled)
					mirrorMesh = (Rand.Value > 0.5f);//in 50% of the cases, flip mesh horizontally for more variance

				//x,y,z adjustments to draw splatches over the approximately correct locations; values stored in semen helper - accessed by unique combinations of bodyTypes and bodyParts
				SemenHelper.key k = new SemenHelper.key(bodyType, bodyPart);
				if (SemenHelper.splatchAdjust.Keys.Contains(k))
				{
					SemenHelper.values helperValues = SemenHelper.splatchAdjust[k];

					//invert, x-adjust (horizontal) depending on left/right body side:
					if (!leftSide)
					{
						float[] xAdjTemp = new float[4];
						for (int i = 0; i < xAdjTemp.Length; i++)
						{
							xAdjTemp[i] = helperValues.x[i] * -1f;
						}
						xAdjust = xAdjTemp;
					}
					else
					{
						xAdjust = helperValues.x;
					}

					zAdjust = helperValues.z;//vertical adjustment

				}
				else//fallback in the the key can't be found
				{
					if (RJWSettings.DevMode)
					{
						ModLog.Message("created semen splatch for undefined body type or part. BodyType: " + bodyType + " , BodyPart: " + bodyPart);
					}
					xAdjust = new float[] { 0f, 0f, 0f, 0f };
					zAdjust = new float[] { 0f, 0f, 0f, 0f };
				}

				//y adjustments: plane/layer of drawing, > 0 -> above certain objects, < 0 -> below
				SemenHelper.key_layer k2 = new SemenHelper.key_layer(leftSide, bodyPart);

				if (SemenHelper.layerAdjust.Keys.Contains(k2))
				{
					SemenHelper.values_layer helperValues_layer = SemenHelper.layerAdjust[k2];
					yAdjust = helperValues_layer.y;
				}
				else
				{
					yAdjust = new float[] { 0.02f, 0.02f, 0.02f, 0.02f };//over body in every direction
				}

			}

			public void Draw(Vector3 drawPos, Quaternion quat, bool forPortrait, int facingDir = 0, float angle = 0,bool inBed=false)
			{
				if (inBed)
				{
					if (this.bodyPart != BodyPartDefOf.Jaw && this.bodyPart != BodyPartDefOf.Head)//when pawn is in bed (=bed with sheets), only draw semen on head
					{
						return;
					}
				}

				//these two create new mesh instance and never destroying it, filling ram and crashing
				//float size = minSize+((maxSize-minSize)*hediff_Semen.Severity);
				//mesh = MeshMakerPlanes.NewPlaneMesh(size);

				//use core MeshPool.plane025 instead

				//if (mirrorMesh)//horizontal flip
				//{
					//mesh = flipMesh(mesh);
				//}

				//rotation:
				if (angle == 0)//normal situation (pawn standing upright)
				{
					drawPos.x += xAdjust[facingDir];
					drawPos.z += zAdjust[facingDir];
				}
				else//if downed etc, more complex calculation becomes necessary
				{
					float radian = angle / 180 * (float)Math.PI;
					radian = -radian;
					drawPos.x += Mathf.Cos(radian) * xAdjust[hediff_Semen.pawn.Rotation.AsInt] - Mathf.Sin(radian) * zAdjust[facingDir];//facingDir doesn't appear to be chosen correctly in all cases
					drawPos.z += Mathf.Cos(radian) * zAdjust[hediff_Semen.pawn.Rotation.AsInt] + Mathf.Sin(radian) * xAdjust[facingDir];
				}

				//drawPos.y += yAdjust[facingDir];// 0.00: over body; 0.01: over body but under face, 0.02: over face, but under hair, -99 = "never" visible
				drawPos.y += yAdjust[facingDir];

				GenDraw.DrawMeshNowOrLater(MeshPool.plane025, drawPos, quat, semenMaterial, forPortrait);
			}

			//flips mesh UV horizontally, thereby mirroring the texture
			private Mesh flipMesh(Mesh meshToFlip)
			{
				var uvs = meshToFlip.uv;
				if (uvs.Length != 4)
				{
					return (meshToFlip);
				}
				for (var i = 0; i < uvs.Length; i++)
				{
					if (Mathf.Approximately(uvs[i].x, 1.0f))
						uvs[i].x = 0.0f;
					else
						uvs[i].x = 1.0f;
				}
				meshToFlip.uv = uvs;
				return (meshToFlip);
			}
		}
	}
}


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

namespace rjw
{
	public class Hediff_Semen : HediffWithComps
	{
		public int semenType = SemenHelper.CUM_NORMAL;//-> different colors

		public string giverName = null;//not utilized right now, maybe in the future save origin of the semen

		public override string LabelInBrackets
		{
			get
			{
				StringBuilder stringBuilder = new StringBuilder();
				stringBuilder.Append(base.LabelInBrackets);
				if (this.sourceHediffDef != null)
				{
					if (stringBuilder.Length != 0)
					{
						stringBuilder.Append(", ");
					}
					stringBuilder.Append(this.sourceHediffDef.label);
				}
				else if (this.source != null)
				{
					if (stringBuilder.Length != 0)
					{
						stringBuilder.Append(", ");
					}
					stringBuilder.Append(this.source.label);
					if (this.sourceBodyPartGroup != null)
					{
						stringBuilder.Append(" ");
						stringBuilder.Append(this.sourceBodyPartGroup.LabelShort);
					}
				}
				return stringBuilder.ToString();
			}
		}

		public override string SeverityLabel
		{
			get
			{
				if (this.Severity == 0f)
				{
					return null;
				}
				return this.Severity.ToString("F1");
			}
		}

		[SyncMethod]
		public override bool TryMergeWith(Hediff other)
		{
			//if a new Semen hediff is added to the same body part, they are combined. if severity reaches more than 1, spillover to other body parts occurs

			Hediff_Semen hediff_Semen = other as Hediff_Semen;
			if (hediff_Semen != null && hediff_Semen.def == this.def && hediff_Semen.Part == base.Part && this.def.injuryProps.canMerge)
			{
				semenType = hediff_Semen.semenType;//take over new creature color

				float totalAmount = hediff_Semen.Severity + this.Severity;
				if (totalAmount > 1.0f)
				{
					BodyPartDef spillOverTo = SemenHelper.spillover(this.Part.def);//SemenHelper saves valid other body parts for spillover
					if (spillOverTo != null)
					{
						//Rand.PopState();
						//Rand.PushState(RJW_Multiplayer.PredictableSeed());
						IEnumerable<BodyPartRecord> availableParts = SemenHelper.getAvailableBodyParts(pawn);//gets all non missing, valid body parts
						IEnumerable<BodyPartRecord> filteredParts = availableParts.Where(x => x.def == spillOverTo);//filters again for valid spill target
						if (!filteredParts.EnumerableNullOrEmpty())
						{
							BodyPartRecord spillPart = null;
							spillPart = filteredParts.RandomElement<BodyPartRecord>();//then pick one
							if (spillPart != null)
							{
								SemenHelper.cumOn(pawn, spillPart, totalAmount - this.Severity, null, semenType);
							}
						}
					}
				}

				return (base.TryMergeWith(other));

			}
			return (false);
		}

		public override void ExposeData()
		{
			base.ExposeData();
			Scribe_Values.Look<int>(ref semenType, "semenType", SemenHelper.CUM_NORMAL);

			if (Scribe.mode == LoadSaveMode.PostLoadInit && base.Part == null)
			{
				//Log.Error("Hediff_Semen has null part after loading.", false);
				this.pawn.health.hediffSet.hediffs.Remove(this);
				return;
			}
		}

		//handles the icon in the health tab and its color
		public override TextureAndColor StateIcon
		{
			get
			{
				TextureAndColor tex = TextureAndColor.None;
				Color color = Color.white;
				switch (semenType)
				{
					case SemenHelper.CUM_NORMAL:
						color = SemenHelper.color_normal;
						break;
					case SemenHelper.CUM_INSECT:
						color = SemenHelper.color_insect;
						break;
					case SemenHelper.CUM_MECHA:
						color = SemenHelper.color_mecha;
						break;
				}

				Texture2D tex2d = BukkakeContent.SemenIcon_little;
				switch (this.CurStageIndex)
				{
					case 0:
						tex2d = BukkakeContent.SemenIcon_little;
						break;
					case 1:
						tex2d = BukkakeContent.SemenIcon_some;
						break;
					case 2:
						tex2d = BukkakeContent.SemenIcon_dripping;
						break;
					case 3:
						tex2d = BukkakeContent.SemenIcon_drenched;
						break;
				}

				tex = new TextureAndColor(tex2d, color);

				return tex;
			}
		}

	}
}


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

namespace rjw
{
	class JobDriver_CleanSelf : JobDriver
	{
		float cleanAmount = 1f;//severity of a single SemenHediff removed per cleaning-round; 1f = remove entirely
		int cleaningTime = 120;//ticks - 120 = 2 real seconds, 3 in-game minutes

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

		protected override IEnumerable<Toil> MakeNewToils()
		{
			this.FailOn(delegate
			{
				List<Hediff> hediffs = pawn.health.hediffSet.hediffs;
				return !hediffs.Exists(x => x.def == RJW_SemenoOverlayHediffDefOf.Hediff_Bukkake);//fail if bukkake disappears - means that also all the semen is gone
			});
			Toil cleaning = Toils_General.Wait(cleaningTime, TargetIndex.None);//duration of
			cleaning.WithProgressBarToilDelay(TargetIndex.A);

			yield return cleaning;
			yield return new Toil()
			{
				initAction = delegate ()
				{
					//get one of the semen hediffs, reduce its severity
					Hediff hediff = pawn.health.hediffSet.hediffs.Find(x => (x.def == RJW_SemenoOverlayHediffDefOf.Hediff_Semen || x.def == RJW_SemenoOverlayHediffDefOf.Hediff_InsectSpunk || x.def == RJW_SemenoOverlayHediffDefOf.Hediff_MechaFluids));
					if (hediff != null)
					{
						hediff.Severity -= cleanAmount;
					}
				}
			};
			yield break;

		}
	}
}


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

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

		//amount of semen per sex act:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		}

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

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

			return allowedParts;
		}

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

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

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

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

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

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

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

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

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

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

			}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	}
}


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

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

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

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

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

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

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

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

			return true;
		}

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


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

namespace rjw
{
    public class Building_WhoreBed : Building_Bed
    {
        private static readonly Color WhoreFieldColor = new Color(170/255f, 79/255f, 255/255f);

        private static readonly Color sheetColorForWhores = new Color(89/255f, 55/255f, 121/255f);

        private static readonly List<IntVec3> WhoreField = new List<IntVec3>();

        public Pawn CurOccupant
        {
            get
            {
                var list = Map.thingGrid.ThingsListAt(Position);
                return list.OfType<Pawn>()
                    .Where(pawn => pawn.jobs.curJob != null)
                    .FirstOrDefault(pawn => pawn.jobs.curJob.def == JobDefOf.LayDown && pawn.jobs.curJob.targetA.Thing == this);
            }
        }

        public override Color DrawColor
        {
            get
            {
                if (def.MadeFromStuff)
                {
                    return base.DrawColor;
                }
                return DrawColorTwo;
            }
        }

        public override void Draw()
        {
            base.Draw();
            if (Medical) Medical = false;
            if (ForPrisoners) ForPrisoners = false;
        }

        public override Color DrawColorTwo { get { return sheetColorForWhores; } }

        public override void DeSpawn(DestroyMode mode = DestroyMode.Vanish)
        {
            foreach (var owner in owners.ToArray())
            {
                owner.ownership.UnclaimBed();
            }
            var room = Position.GetRoom(Map);
            base.DeSpawn(mode);
            if (room != null)
            {
                room.Notify_RoomShapeOrContainedBedsChanged();
            }
        }

        //public override void DrawExtraSelectionOverlays()
        //{
        //    base.DrawExtraSelectionOverlays();
        //    var room = this.GetRoom();
        //    if (room == null) return;
        //    if (room.isPrisonCell) return;
        //
        //    if (room.RegionCount < 20 && !room.TouchesMapEdge)
        //    {
        //        foreach (var current in room.Cells)
        //        {
        //            WhoreField.Add(current);
        //        }
        //        var color = WhoreFieldColor;
        //        color.a = Pulser.PulseBrightness(1f, 0.6f);
        //        GenDraw.DrawFieldEdges(WhoreField, color);
        //        WhoreField.Clear();
        //    }
        //}

        public override string GetInspectString()
        {
            var stringBuilder = new StringBuilder();
            //stringBuilder.Append(base.GetInspectString());
            stringBuilder.Append(InspectStringPartsFromComps());
            stringBuilder.AppendLine();
            stringBuilder.Append("ForWhoreUse".Translate());

            stringBuilder.AppendLine();
            if (owners.Count == 0)
            {
                stringBuilder.Append("Owner".Translate() + ": " + "Nobody".Translate());
            }
            else if (owners.Count == 1)
            {
                stringBuilder.Append("Owner".Translate() + ": " + owners[0].LabelCap);
            }
            else
            {
                stringBuilder.Append("Owners".Translate() + ": ");
                bool notFirst = false;
                foreach (Pawn owner in owners)
                {
                    if (notFirst)
                    {
                        stringBuilder.Append(", ");
                    }
                    notFirst = true;
                    stringBuilder.Append(owner.Label);
                }
                //if(notFirst) stringBuilder.AppendLine();
            }
            return stringBuilder.ToString();
        }

        // Is this one even being used??
        public override IEnumerable<Gizmo> GetGizmos()
        {
            // Get original gizmos from Building class
            var method = typeof(Building).GetMethod("GetGizmos");
            var ftn = method.MethodHandle.GetFunctionPointer();
            var func = (Func<IEnumerable<Gizmo>>)Activator.CreateInstance(typeof(Func<IEnumerable<Gizmo>>), this, ftn);

            foreach (var gizmo in func())
            {
                yield return gizmo;
            }

            if(def.building.bed_humanlike)
            {
                yield return
                        new Command_Toggle
                        {
                            defaultLabel = "CommandBedSetAsWhoreLabel".Translate(),
                            defaultDesc = "CommandBedSetAsWhoreDesc".Translate(),
                            icon = ContentFinder<Texture2D>.Get("UI/Commands/AsWhore"),
                            isActive = () => true,
                            toggleAction = () => Swap(this),
                            hotKey = KeyBindingDefOf.Misc4
                        };
            }
        }

        public override void PostMake()
        {
            base.PostMake();
            PlayerKnowledgeDatabase.KnowledgeDemonstrated(ConceptDef.Named("WhoreBeds"), KnowledgeAmount.Total);
        }

        public override void DrawGUIOverlay()
        {
            //if (Find.CameraMap.CurrentZoom == CameraZoomRange.Closest)
            //{
            //    if (owner != null && owner.InBed() && owner.CurrentBed().owner == owner)
            //    {
            //        return;
            //    }
            //    string text;
            //    if (owner != null)
            //    {
            //        text = owner.NameStringShort;
            //    }
            //    else
            //    {
            //        text = "Unowned".Translate();
            //    }
            //    GenWorldUI.DrawThingLabel(this, text, new Color(1f, 1f, 1f, 0.75f));
            //}
        }

        public static void Swap(Building_Bed bed)
        {
            Building_Bed newBed;
            if (bed is Building_WhoreBed)
            {
                newBed = (Building_Bed) MakeBed(bed, bed.def.defName.Split(new[] { "RJW_" }, StringSplitOptions.RemoveEmptyEntries)[0]);
            }
            else
            {
                newBed = (Building_WhoreBed) MakeBed(bed, "RJW_"+bed.def.defName);
            }
            newBed.SetFactionDirect(bed.Faction);
            var spawnedBed = (Building_Bed)GenSpawn.Spawn(newBed, bed.Position, bed.Map, bed.Rotation);
            spawnedBed.HitPoints = bed.HitPoints;
            spawnedBed.ForPrisoners = bed.ForPrisoners;

            var compQuality = spawnedBed.TryGetComp<CompQuality>();

            if(compQuality != null) compQuality.SetQuality(bed.GetComp<CompQuality>().Quality, ArtGenerationContext.Outsider);
            //var compArt = bed.TryGetComp<CompArt>();
            //if (compArt != null)
            //{
            //    var art = spawnedBed.GetComp<CompArt>();
            //    Traverse.Create(art).Field("authorNameInt").SetValue(Traverse.Create(compArt).Field("authorNameInt").GetValue());
            //    Traverse.Create(art).Field("titleInt").SetValue(Traverse.Create(compArt).Field("titleInt").GetValue());
            //    Traverse.Create(art).Field("taleRef").SetValue(Traverse.Create(compArt).Field("taleRef").GetValue());
            //
            //    // TODO: Make this work, art is now destroyed
            //}
            Find.Selector.Select(spawnedBed, false);
        }

        public static Thing MakeBed(Building_Bed bed, string defName)
        {
			Log.Message("1");
			ThingDef newDef = DefDatabase<ThingDef>.GetNamed(defName);
			Log.Message("2");
			return ThingMaker.MakeThing(newDef, bed.Stuff);
        }
    }
}


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

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

		public bool successfulPass = true;

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

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

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

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

			if (RJWSettings.DebugWhoring)
			{
				ModLog.Message("Will try hookup " + WhoringHelper.WillPawnTryHookup(TargetPawn));
				ModLog.Message("Is whore appealing  " + WhoringHelper.IsHookupAppealing(TargetPawn, Whore));
				ModLog.Message("Can afford whore " + WhoringHelper.CanAfford(TargetPawn, Whore));
				ModLog.Message("Need sex " + (xxx.need_some_sex(TargetPawn) >= 1));
			}
			if (WhoringHelper.WillPawnTryHookup(TargetPawn) && WhoringHelper.IsHookupAppealing(TargetPawn, Whore) && WhoringHelper.CanAfford(TargetPawn, Whore) && xxx.need_some_sex(TargetPawn) >= 1f)
			{
				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 || !xxx.CanUse(Whore, TargetBed));//|| !Whore.CanReserve(TargetPawn)
			this.FailOn(() => pawn.Drafted);
			yield return Toils_Goto.GotoThing(TargetPawnIndex, PathEndMode.Touch);

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

			Toil AwaitResponse = new Toil();
			AwaitResponse.AddFailCondition(() => !successfulPass);
			AwaitResponse.defaultCompleteMode = ToilCompleteMode.Instant;
			AwaitResponse.initAction = delegate
			{
				List<RulePackDef> extraSentencePacks = new List<RulePackDef>();
				successfulPass = DoesTargetPawnAcceptAdvance();
				//ModLog.Message("JobDriver_InvitingVisitors::MakeNewToils - AwaitResponse - initAction is called");
				if (successfulPass)
				{
					MoteMaker.ThrowMetaIcon(TargetPawn.Position, TargetPawn.Map, ThingDefOf.Mote_Heart);
					TargetPawn.jobs.EndCurrentJob(JobCondition.Incompletable);
					if (xxx.RomanceDiversifiedIsActive)
					{
						extraSentencePacks.Add(RulePackDef.Named("HookupSucceeded"));
					}
					if (Whore.health.HasHediffsNeedingTend())
					{
						successfulPass = false;
						const string key = "RJW_VisitorSickWhore";
						string text = key.Translate(TargetPawn.LabelIndefinite(), Whore.LabelIndefinite()).CapitalizeFirst();
						Messages.Message(text, Whore, MessageTypeDefOf.TaskCompletion);
					}
					else
					{
						const string key = "RJW_VisitorAcceptWhore";
						string text = key.Translate(TargetPawn.LabelIndefinite(), Whore.LabelIndefinite()).CapitalizeFirst();
						Messages.Message(text, TargetPawn, MessageTypeDefOf.TaskCompletion);
					}
				}
				if (!successfulPass)
				{
					MoteMaker.ThrowMetaIcon(TargetPawn.Position, TargetPawn.Map, ThingDefOf.Mote_IncapIcon);
					TargetPawn.needs.mood.thoughts.memories.TryGainMemory(
						TargetPawn.Faction == Whore.Faction
							? ThoughtDef.Named("RJWTurnedDownWhore")
							: ThoughtDef.Named("RJWFailedSolicitation"), Whore);

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

			Toil BothGoToBed = new Toil();
			BothGoToBed.AddFailCondition(() => !successfulPass || !xxx.CanUse(Whore, TargetBed));
			BothGoToBed.defaultCompleteMode = ToilCompleteMode.Instant;
			BothGoToBed.initAction = delegate
			{
				//ModLog.Message("JobDriver_InvitingVisitors::MakeNewToils - BothGoToBed - initAction is called0");
				if (!successfulPass) return;
				if (!xxx.CanUse(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");
				Whore.jobs.jobQueue.EnqueueFirst(JobMaker.MakeJob(xxx.whore_is_serving_visitors, TargetPawn, TargetBed));
				//TargetPawn.jobs.jobQueue.EnqueueFirst(JobMaker.MakeJob(DefDatabase<JobDef>.GetNamed("WhoreIsServingVisitors"), Whore, TargetBed, (TargetBed.MaxAssignedPawnsCount>1)?TargetBed.GetSleepingSlotPos(1): TargetBed.)), null);
				Whore.jobs.curDriver.EndJobWith(JobCondition.InterruptOptional);
				//TargetPawn.jobs.curDriver.EndJobWith(JobCondition.InterruptOptional);
			};
			yield return BothGoToBed;
		}
	}
}


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

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

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

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

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

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

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

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

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

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

			Toil StartPartnerJob = new Toil();
			StartPartnerJob.defaultCompleteMode = ToilCompleteMode.Instant;
			StartPartnerJob.socialMode = RandomSocialMode.Off;
			StartPartnerJob.initAction = delegate
			{
				if (RJWSettings.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":MakeNewToils() - StartPartnerJob");
				var gettin_loved = JobMaker.MakeJob(PartnerJob, pawn, Bed);
				Partner.jobs.StartJob(gettin_loved, JobCondition.InterruptForced);
			};
			yield return StartPartnerJob;

			Toil SexToil = new Toil();
			SexToil.defaultCompleteMode = ToilCompleteMode.Never;
			SexToil.socialMode = RandomSocialMode.Off;
			SexToil.handlingFacing = true;
			SexToil.FailOn(() => Partner.Dead);
			SexToil.FailOn(() => Partner.CurJob.def != PartnerJob);
			SexToil.initAction = delegate
			{
				if (RJWSettings.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":MakeNewToils() - loveToil");
				// TODO: replace this quick n dirty way
				CondomUtility.GetCondomFromRoom(pawn);

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

				Start();

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

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

					if (!(Partner.IsColonist && (pawn.IsPrisonerOfColony || pawn.IsColonist)))
					{
						int price = WhoringHelper.PriceOfWhore(pawn);
						if (RJWSettings.DebugWhoring) ModLog.Message(this.GetType().ToString() + ":MakeNewToils() - Partner should pay the price now in afterSex.initAction");
						int remainPrice = WhoringHelper.PayPriceToWhore(Partner, price, pawn);

						if (RJWSettings.DebugWhoring && remainPrice <= 0)
							ModLog.Message(this.GetType().ToString() + ":MakeNewToils() - Paying price is success");
						else if (RJWSettings.DebugWhoring)
							ModLog.Message(this.GetType().ToString() + ":MakeNewToils() - Paying price failed");

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

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

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

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