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

namespace rjw
{
	public class SexPartAdder
	{
		/// <summary>
		/// return true if going to set penis,
		/// return false for vagina
		/// </summary>
		public static bool IsAddingPenis(Pawn pawn, Gender gender)
		{
			return (pawn.gender == Gender.Male) ? (gender != Gender.Female) : (gender == Gender.Male);
		}

		/// <summary>
		/// generate part hediff
		/// </summary>
		public static Hediff MakePart(HediffDef def, Pawn pawn, BodyPartRecord bpr)
		{
			//Log.Message("SexPartAdder::PartMaker ( " + xxx.get_pawnname(pawn) + " ) " + def.defName);
			Hediff hd = HediffMaker.MakeHediff(def, pawn, bpr);
			//Log.Message("SexPartAdder::PartMaker ( " + xxx.get_pawnname(pawn) + " ) " + hd.def.defName);
			CompHediffBodyPart compHediff = hd.TryGetComp<CompHediffBodyPart>();
			if (compHediff != null)
			{
				//Log.Message("SexPartAdder::PartMaker init comps");
				compHediff.initComp(pawn);
				compHediff.updatesize();
			}
			return hd;
		}

		/// <summary>
		/// operation - move part data from thing to hediff
		/// </summary>
		public static Hediff recipePartAdder(RecipeDef recipe, Pawn pawn, BodyPartRecord part, List<Thing> ingredients)
		{
			Hediff hd = HediffMaker.MakeHediff(recipe.addsHediff, pawn, part);
			Thing thing = ingredients.Find(x => x.def.defName == recipe.addsHediff.defName);

			CompThingBodyPart CompThing = thing.TryGetComp<rjw.CompThingBodyPart>();
			CompHediffBodyPart CompHediff = hd.TryGetComp<rjw.CompHediffBodyPart>();

			if (CompHediff != null && CompThing != null)
				if (CompThing.SizeBase > 0)
				{
					CompHediff.FluidType = CompThing.FluidType;
					CompHediff.FluidAmmount = CompThing.FluidAmmount;
					CompHediff.FluidModifier = CompThing.FluidModifier;
					CompHediff.SizeBase = CompThing.SizeBase;
					CompHediff.SizeOwner = CompThing.SizeOwner;
					CompHediff.RaceOwner = CompThing.RaceOwner;
					CompHediff.PreviousOwner = CompThing.PreviousOwner;
					CompHediff.Eggs = CompThing.Eggs;
					CompHediff.Transplanted = true;
					CompHediff.updatesize();
				}
				else  //not initialised things, drop pods, trader?  //TODO: figure out how to populate rjw parts at gen
					hd = MakePart(hd.def, pawn, part);

			return hd;
		}

		/// <summary>
		/// operation - move part data from hediff to thing
		/// </summary>
		public static Thing recipePartRemover(Hediff hd)
		{
			Thing thing = ThingMaker.MakeThing(hd.def.spawnThingOnRemoved);

			CompThingBodyPart CompThing = thing.TryGetComp<rjw.CompThingBodyPart>();
			CompHediffBodyPart CompHediff = hd.TryGetComp<rjw.CompHediffBodyPart>();

			if (CompThing != null && CompHediff != null)
			{
					CompThing.FluidType = CompHediff.FluidType;
					CompThing.FluidAmmount = CompHediff.FluidAmmount;
					CompThing.FluidModifier = CompHediff.FluidModifier;
					CompThing.Size = CompHediff.Size;
					CompThing.SizeBase = CompHediff.SizeBase;
					CompThing.SizeOwner = CompHediff.SizeOwner;
					CompThing.RaceOwner = CompHediff.RaceOwner;
					CompThing.PreviousOwner = xxx.get_pawnname(CompHediff.Pawn);
					CompThing.Eggs = CompHediff.Eggs;
			}
			return thing;
		}

		[SyncMethod]
		public static void add_genitals(Pawn pawn, Pawn parent = null, Gender gender = Gender.None)
		{
			//--Log.Message("Genital_Helper::add_genitals( " + xxx.get_pawnname(pawn) + " ) called");
			BodyPartRecord partBPR = Genital_Helper.get_genitalsBPR(pawn);
			var parts = pawn.GetGenitalsList();

			//--Log.Message("Genital_Helper::add_genitals( " + xxx.get_pawnname(pawn) + " ) - checking genitals");
			if (partBPR == null)
			{
				//--ModLog.Message(" add_genitals( " + xxx.get_pawnname(pawn) + " ) doesn't have a genitals");
				return;
			}
			else if (pawn.health.hediffSet.PartIsMissing(partBPR))
			{
				//--ModLog.Message(" add_genitals( " + xxx.get_pawnname(pawn) + " ) had a genital but was removed.");
				return;
			}
			if (Genital_Helper.has_genitals(pawn, parts) && gender == Gender.None)//allow to add gender specific genitals(futa)
			{
				//--ModLog.Message(" add_genitals( " + xxx.get_pawnname(pawn) + " ) already has genitals");
				return;
			}

			HediffDef part;

			// maybe add some check based on bodysize of pawn for genitals size limit
			//Log.Message("Genital_Helper::add_genitals( " + pawn.RaceProps.baseBodySize + " ) - 1");
			//Log.Message("Genital_Helper::add_genitals( " + pawn.kindDef.race.size. + " ) - 2");

			part = (IsAddingPenis(pawn, gender)) ? Genital_Helper.generic_penis : Genital_Helper.generic_vagina;

			if (Genital_Helper.has_vagina(pawn, parts) && part == Genital_Helper.generic_vagina)
			{
				//--ModLog.Message(" add_genitals( " + xxx.get_pawnname(pawn) + " ) already has vagina");
				return;
			}
			if (Genital_Helper.has_male_bits(pawn, parts) && part == Genital_Helper.generic_penis)
			{
				//--ModLog.Message(" add_genitals( " + xxx.get_pawnname(pawn) + " ) already has penis");
				return;
			}

			//override race genitals
			if (part == Genital_Helper.generic_vagina && pawn.TryAddRacePart(SexPartType.FemaleGenital))
			{
				return;
			}
			if (part == Genital_Helper.generic_penis && pawn.TryAddRacePart(SexPartType.MaleGenital))
			{
				return;
			}
			LegacySexPartAdder.AddGenitals(pawn, parent, gender, partBPR, part);
		}

		public static void add_breasts(Pawn pawn, Pawn parent = null, Gender gender = Gender.None)
		{
			//--ModLog.Message(" add_breasts( " + xxx.get_pawnname(pawn) + " ) called");
			BodyPartRecord partBPR = Genital_Helper.get_breastsBPR(pawn);

			if (partBPR == null)
			{
				//--ModLog.Message(" add_breasts( " + xxx.get_pawnname(pawn) + " ) - pawn doesn't have a breasts");
				return;
			}
			else if (pawn.health.hediffSet.PartIsMissing(partBPR))
			{
				//--ModLog.Message(" add_breasts( " + xxx.get_pawnname(pawn) + " ) had breasts but were removed.");
				return;
			}
			if (pawn.GetBreastList().Count > 0)
			//if (Genital_Helper.has_breasts(pawn))
			{
				//--ModLog.Message(" add_breasts( " + xxx.get_pawnname(pawn) + " ) - pawn already has breasts");
				return;
			}

			//TODO: figure out how to add (flat) breasts to males
			//Check for (flat) breasts to males done in RacepartDef_Helper.cs when severity curve checked
			var racePartType = (pawn.gender == Gender.Female || gender == Gender.Female)
				? SexPartType.FemaleBreast
				: SexPartType.MaleBreast;
			if (pawn.TryAddRacePart(racePartType))
			{
				return;
			}

			LegacySexPartAdder.AddBreasts(pawn, partBPR, parent);
		}

		public static void add_anus(Pawn pawn, Pawn parent = null)
		{
			BodyPartRecord partBPR = Genital_Helper.get_anusBPR(pawn);

			if (partBPR == null)
			{
				//--ModLog.Message(" add_anus( " + xxx.get_pawnname(pawn) + " ) doesn't have an anus");
				return;
			}
			else if (pawn.health.hediffSet.PartIsMissing(partBPR))
			{
				//--ModLog.Message(" add_anus( " + xxx.get_pawnname(pawn) + " ) had an anus but was removed.");
				return;
			}
			if (pawn.GetAnusList().Count > 0)
			{
				//--ModLog.Message(" add_anus( " + xxx.get_pawnname(pawn) + " ) already has an anus");
				return;
			}

			if (pawn.TryAddRacePart(SexPartType.Anus))
			{
				return;
			}

			LegacySexPartAdder.AddAnus(pawn, partBPR, parent);
		}
	}
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using UnityEngine;
using Verse;
using Verse.AI;
using Verse.Sound;
using HarmonyLib;
using Multiplayer.API;
using rjw.Modules.Interactions.Contexts;
using rjw.Modules.Interactions.Implementation;
using rjw.Modules.Interactions;
using rjw.Modules.Interactions.Objects;
using rjw.Modules.Interactions.Enums;
using static HarmonyLib.Code;

namespace rjw
{
	public class SexUtility
	{
		private const float base_sat_per_fuck = 0.40f;
		private const float base_sat_per_quirk = 0.20f;

		public static readonly InteractionDef AnimalSexChat = DefDatabase<InteractionDef>.GetNamed("AnimalSexChat");

		private static readonly ThingDef cum = ThingDef.Named("FilthCum");
		private static readonly ThingDef girlcum = ThingDef.Named("FilthGirlCum");

		public static readonly List<InteractionDef> SexInterractions = DefDatabase<InteractionDef>.AllDefsListForReading.Where(x => x.HasModExtension<InteractionExtension>()).ToList();

		// Alert checker that is called from several jobs. Checks the pawn relation, and whether it should sound alert.
		// notification in top left corner
		// rape attempt
		public static void RapeTargetAlert(Pawn rapist, Pawn target)
		{
			if (target.IsDesignatedComfort() && rapist.jobs.curDriver.GetType() == typeof(JobDriver_RapeComfortPawn))
				if (!RJWPreferenceSettings.ShowForCP)
					return;
			if (target.IsDesignatedComfort() && rapist.jobs.curDriver.GetType() == typeof(JobDriver_Breeding))
				if (target.IsDesignatedBreeding())
					if (!RJWPreferenceSettings.ShowForBreeding)
						return;

			bool silent = false;
			PawnRelationDef relation = rapist.GetMostImportantRelation(target);
			string rapeverb = "rape";

			if (xxx.is_mechanoid(rapist)) rapeverb = "assault";
			else if (xxx.is_animal(rapist) || xxx.is_animal(target)) rapeverb = "breed";

			// TODO: Need to write a cherker method for family relations. Would be useful for other things than just this, such as incest settings/quirk.

			string message = (xxx.get_pawnname(rapist) + " is trying to " + rapeverb + " " + xxx.get_pawnname(target));
			message += relation == null ? "." : (", " + rapist.Possessive() + " " + relation.GetGenderSpecificLabel(target) + ".");

			switch (RJWPreferenceSettings.rape_attempt_alert)
			{
				case RJWPreferenceSettings.RapeAlert.Enabled:
					break;
				case RJWPreferenceSettings.RapeAlert.Humanlikes:
					if (!xxx.is_human(target))
						return;
					break;
				case RJWPreferenceSettings.RapeAlert.Colonists:
					if (!target.IsColonist)
						return;
					break;
				case RJWPreferenceSettings.RapeAlert.Silent:
					silent = true;
					break;
				default:
					return;
			}

			if (!silent)
			{
				Messages.Message(message, rapist, MessageTypeDefOf.NegativeEvent);
			}
			else
			{
				Messages.Message(message, rapist, MessageTypeDefOf.SilentInput);
			}
		}

		// Alert checker that is called from several jobs.
		// notification in top left corner
		// rape started
		public static void BeeingRapedAlert(Pawn rapist, Pawn target)
		{
			if (target.IsDesignatedComfort() && rapist.jobs.curDriver.GetType() == typeof(JobDriver_RapeComfortPawn))
				if (!RJWPreferenceSettings.ShowForCP)
					return;
			if (target.IsDesignatedComfort() && rapist.jobs.curDriver.GetType() == typeof(JobDriver_Breeding))
				if (target.IsDesignatedBreeding())
					if (!RJWPreferenceSettings.ShowForBreeding)
						return;

			bool silent = false;

			switch (RJWPreferenceSettings.rape_alert)
			{
				case RJWPreferenceSettings.RapeAlert.Enabled:
					break;
				case RJWPreferenceSettings.RapeAlert.Humanlikes:
					if (!xxx.is_human(target))
						return;
					break;
				case RJWPreferenceSettings.RapeAlert.Colonists:
					if (!target.IsColonist)
						return;
					break;
				case RJWPreferenceSettings.RapeAlert.Silent:
					silent = true;
					break;
				default:
					return;
			}

			if (!silent)
			{
				Messages.Message(xxx.get_pawnname(target) + " is getting raped.", target, MessageTypeDefOf.NegativeEvent);
			}
			else
			{
				Messages.Message(xxx.get_pawnname(target) + " is getting raped.", target, MessageTypeDefOf.SilentInput);
			}
		}

		// Quick method that return a body part by name. Used for checking if a pawn has a specific body part, etc.
		public static BodyPartRecord GetPawnBodyPart(Pawn pawn, string bodyPart)
		{
			return pawn.RaceProps.body.AllParts.Find(x => x.def == DefDatabase<BodyPartDef>.GetNamed(bodyPart));
		}

		public static void CumFilthGenerator(Pawn pawn)
		{
			if (pawn == null) return;
			if (pawn.Dead) return;
			if (xxx.is_slime(pawn)) return;
			if (!RJWSettings.cum_filth) return;

			// Larger creatures, larger messes.
			float pawn_cum = Math.Min(80 / ScaleToHumanAge(pawn), 2.0f) * pawn.BodySize;

			// Increased output if the pawn has the Messy quirk.
			if (xxx.has_quirk(pawn, "Messy"))
				pawn_cum *= 2.0f;

			var parts = pawn.GetGenitalsList();

			if (Genital_Helper.has_vagina(pawn, parts))
				FilthMaker.TryMakeFilth(pawn.PositionHeld, pawn.MapHeld, girlcum, pawn.LabelIndefinite(), (int)Math.Max(pawn_cum / 2, 1.0f));

			if (Genital_Helper.has_penis_fertile(pawn, parts))
				FilthMaker.TryMakeFilth(pawn.PositionHeld, pawn.MapHeld, cum, pawn.LabelIndefinite(), (int)Math.Max(pawn_cum, 1.0f));
		}

		// The pawn may or may not clean up the mess after fapping.
		[SyncMethod]
		public static bool ConsiderCleaning(Pawn fapper)
		{
			if (!RJWSettings.cum_filth) return false;
			if (!xxx.has_traits(fapper) || fapper.story == null) return false;
			if (fapper.WorkTagIsDisabled(WorkTags.Cleaning)) return false;

			float do_cleaning = 0.5f; // 50%

			if (!fapper.PositionHeld.Roofed(fapper.Map))
				do_cleaning -= 0.25f; // Less likely to clean if outdoors.

			if (xxx.CTIsActive && fapper.story.traits.HasTrait(xxx.RCT_NeatFreak))
				do_cleaning += 1.00f;

			if (xxx.has_quirk(fapper, "Messy"))
				do_cleaning -= 0.75f;

			switch (fapper.needs?.rest?.CurCategory)
			{
				case RestCategory.Exhausted:
					do_cleaning -= 0.5f;
					break;
				case RestCategory.VeryTired:
					do_cleaning -= 0.3f;
					break;
				case RestCategory.Tired:
					do_cleaning -= 0.1f;
					break;
				case RestCategory.Rested:
					do_cleaning += 0.3f;
					break;
			}

			if (fapper.story.traits.DegreeOfTrait(TraitDefOf.NaturalMood) == -2) // Depressive
				do_cleaning -= 0.3f;
			if (fapper.story.traits.DegreeOfTrait(TraitDefOf.Industriousness) == 2) // Industrious
				do_cleaning += 1.0f;
			else if (fapper.story.traits.DegreeOfTrait(TraitDefOf.Industriousness) == 1) // Hard worker
				do_cleaning += 0.5f;
			else if (fapper.story.traits.DegreeOfTrait(TraitDefOf.Industriousness) == -1) // Lazy
				do_cleaning -= 0.5f;
			else if (fapper.story.traits.DegreeOfTrait(TraitDefOf.Industriousness) == -2) // Slothful
				do_cleaning -= 1.0f;

			if (xxx.is_ascetic(fapper))
				do_cleaning += 0.2f;

			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			return Rand.Chance(do_cleaning);
		}

		/// Handles after-sex trait and thought gain, and fluid creation. Initiator of the act (whore, rapist, female zoophile, etc) should be first.
		[SyncMethod]
		public static void Aftersex(SexProps props)
		{
			if (props.sexType == xxx.rjwSextype.Masturbation)
			{
				AfterMasturbation(props);
				return;
			}

			bool bothInMap = false;

			if (!props.partner.Dead)
				bothInMap = props.pawn.Map != null && props.partner.Map != null; //Added by Hoge. false when called this function for despawned pawn: using for background rape like a kidnappee

			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			if (bothInMap)
			{
				//Catch-all timer increase, for ensuring that pawns don't get stuck repeating jobs.
				if (!props.isCoreLovin)
				{
					props.pawn.rotationTracker.Face(props.partner.DrawPos);
					props.pawn.rotationTracker.FaceCell(props.partner.Position);
				}
				if (!props.partner.Dead)
				{
					if (!props.isCoreLovin)
					{
						props.partner.rotationTracker.Face(props.pawn.DrawPos);
						props.partner.rotationTracker.FaceCell(props.pawn.Position);
					}
					if (RJWSettings.sounds_enabled)
					{
						if (props.isRape)
						{
							if (Rand.Value > 0.30f)
								LifeStageUtility.PlayNearestLifestageSound(props.partner, (ls) => ls.soundAngry, null, 1.2f);
							else
								LifeStageUtility.PlayNearestLifestageSound(props.partner, (ls) => ls.soundCall, g => g.soundCall, 1.2f);

							props.pawn.Drawer.Notify_MeleeAttackOn(props.partner);
							props.partner.stances.stagger.StaggerFor(Rand.Range(10, 300));
						}
						else
							LifeStageUtility.PlayNearestLifestageSound(props.partner, (ls) => ls.soundCall, g => g.soundCall);
					}
					if (props.sexType == xxx.rjwSextype.Vaginal || props.sexType == xxx.rjwSextype.DoublePenetration)
						if (xxx.is_Virgin(props.partner))
						{
							//TODO: bind virginity to parts of pawn
							/*
							string thingdef_penis_name = Genital_Helper.get_penis_all(pawn)?.def.defName ?? "";
							ThingDef thingdef_penis = null;

							Log.Message("SexUtility::thingdef_penis_name " + thingdef_penis_name);
							Log.Message("SexUtility::thingdef_penis 1 " + thingdef_penis);

							if (thingdef_penis_name != "")
								thingdef_penis = (from x in DefDatabase<ThingDef>.AllDefs where x.defName == thingdef_penis_name select x).RandomElement();
							Log.Message("SexUtility::thingdef_penis 2 " + thingdef_penis);

							partner.TakeDamage(new DamageInfo(DamageDefOf.Stab, 1, 999, -1.0f, null, xxx.genitals, thingdef_penis));
							*/
						}
				}

				if (RJWSettings.sounds_enabled && props.isCoreLovin)
					SoundDef.Named("Cum").PlayOneShot(!props.partner.Dead
						? new TargetInfo(props.partner.Position, props.pawn.Map)
						: new TargetInfo(props.pawn.Position, props.pawn.Map));

				if (props.isRape)
				{
					if (Rand.Value > 0.30f)
						LifeStageUtility.PlayNearestLifestageSound(props.pawn, (ls) => ls.soundAngry, null, 1.2f);
					else
						LifeStageUtility.PlayNearestLifestageSound(props.pawn, (ls) => ls.soundCall, g => g.soundCall, 1.2f);
				}
				else
					LifeStageUtility.PlayNearestLifestageSound(props.pawn, (ls) => ls.soundCall, g => g.soundCall);
			}

			if (props.usedCondom)
			{
				if (CondomUtility.UsedCondom != null)
					GenSpawn.Spawn(CondomUtility.UsedCondom, props.pawn.Position, props.pawn.Map);
				CondomUtility.useCondom(props.pawn);
				CondomUtility.useCondom(props.partner);
			}
			else
			{
				if (props.isCoreLovin) // non core handled by jobdriver
				{
					//apply cum to floor:
					CumFilthGenerator(props.pawn);
					CumFilthGenerator(props.partner);

					PregnancyHelper.impregnate(props);
					TransferNutrition(props);
				}
			}

			List<Trait> newInitiatorTraits = new(), newReceiverTraits = new();

			if (props.isRape && !props.partner.Dead)
				AfterSexUtility.processBrokenPawn(props.partner, newReceiverTraits);

			//Satisfy(pawn, partner, sextype, rape);

			//TODO: below is fucked up, unfuck it someday
			AfterSexUtility.UpdateRecords(props);
			GiveTraits(props, newInitiatorTraits);
			GiveTraits(props.GetForPartner(), newReceiverTraits);

			if (RJWSettings.sendTraitGainLetters)
			{
				SendTraitGainLetter(props.pawn, newInitiatorTraits);
				SendTraitGainLetter(props.partner, newReceiverTraits);
			}
		}

		// Solo acts.
		public static void AfterMasturbation(SexProps props)
		{
			IncreaseTicksToNextLovin(props.pawn);

			//apply cum to floor:
			CumFilthGenerator(props.pawn);

			AfterSexUtility.UpdateRecords(props);

			// No traits from solo. Enable if some are edded. (Voyerism?)
			//check_trait_gain(pawn);
		}

		// Scales alien lifespan to human age.
		// Some aliens have broken lifespans, that can be manually corrected here.
		public static int ScaleToHumanAge(Pawn pawn, int humanLifespan = 80)
		{
			float pawnAge = pawn.ageTracker.AgeBiologicalYearsFloat;
			float pawnLifespan = pawn.RaceProps.lifeExpectancy;

			if (pawn.def.defName == "Human") return (int)pawnAge; // Human, no need to scale anything.

			// Xen races, all broken and need a fix.
			if (pawn.def.defName.ContainsAny("Alien_Sergal", "Alien_SergalNME", "Alien_Xenn", "Alien_Racc", "Alien_Ferrex", "Alien_Wolvx", "Alien_Frijjid", "Alien_Fennex") && pawnLifespan >= 2000f)
			{
				pawnAge = Math.Min(pawnAge, 80f); // Clamp to 80.
				pawnLifespan = 80f;
			}
			if (pawn.def.defName.ContainsAny("Alien_Gnoll", "Alien_StripedGnoll") && pawnLifespan >= 2000f)
			{
				pawnAge = Math.Min(pawnAge, 60f); // Clamp to 60.
				pawnLifespan = 60f; // Mature faster than humans.
			}

			// Immortal races that mature at similar rate to humans.
			if (pawn.def.defName.ContainsAny("LF_Dragonia", "LotRE_ElfStandardRace", "Alien_Crystalloid", "Alien_CrystalValkyrie"))
			{
				pawnAge = Math.Min(pawnAge, 40f); // Clamp to 40 - never grow 'old'.
				pawnLifespan = 80f;
			}

			float age_scaling = humanLifespan / pawnLifespan;
			float scaled_age = pawnAge * age_scaling;

			if (scaled_age < 1)
				scaled_age = 1;

			return (int)scaled_age;
		}

		// Used in complex impregnation calculation. Pawns/animals with similar parts have better compatibility.
		public static float BodySimilarity(Pawn pawn, Pawn partner)
		{
			float size_adjustment = Mathf.Lerp(0.3f, 1.05f, 1.0f - Math.Abs(pawn.BodySize - partner.BodySize));

			//ModLog.Message(" Size adjustment: " + size_adjustment);

			List<BodyPartDef> pawn_partlist = new List<BodyPartDef> { };
			List<BodyPartDef> pawn_mismatched = new List<BodyPartDef> { };
			List<BodyPartDef> partner_mismatched = new List<BodyPartDef> { };

			//ModLog.Message("Checking compatibility for " + xxx.get_pawnname(pawn) + " and " + xxx.get_pawnname(partner));
			bool pawnHasHands = pawn.health.hediffSet.GetNotMissingParts().Any(part => part.IsInGroup(BodyPartGroupDefOf.RightHand) || part.IsInGroup(BodyPartGroupDefOf.LeftHand));

			foreach (BodyPartRecord part in pawn.RaceProps.body.AllParts)
			{
				pawn_partlist.Add(part.def);
			}
			float pawn_count = pawn_partlist.Count();

			foreach (BodyPartRecord part in partner.RaceProps.body.AllParts)
			{
				partner_mismatched.Add(part.def);
			}
			float partner_count = partner_mismatched.Count();

			foreach (BodyPartDef part in pawn_partlist)
			{
				if (partner_mismatched.Contains(part))
				{
					pawn_mismatched.Add(part);
					partner_mismatched.Remove(part);
				}
			}

			float pawn_mismatch = pawn_mismatched.Count() / pawn_count;
			float partner_mismatch = partner_mismatched.Count() / partner_count;

			//ModLog.Message("Body type similarity for " + xxx.get_pawnname(pawn) + " and " + xxx.get_pawnname(partner) + ": " + Math.Round(((pawn_mismatch + partner_mismatch) * 50) * size_adjustment, 1) + "%");

			return ((pawn_mismatch + partner_mismatch) / 2) * size_adjustment;
		}

		public static void SatisfyPersonal(SexProps props, float satisfaction = 0.4f)
		{
			Pawn pawn = props.pawn;
			Pawn partner = props.partner;
			//--Log.Message("xxx::satisfy( " + pawn_name + ", " + partner_name + ", " + violent + "," + isCoreLovin + " ) - modifying partner satisfaction");
			var sex_need = pawn?.needs?.TryGetNeed<Need_Sex>();
			if (sex_need == null) return;

			float Trait_Satisfaction = 1f;
			float Quirk_Satisfaction = 1f;
			float Stat_Satisfaction = 0f;
			float Circumstances_Satisfaction = 0f; // violence/broken

			float joysatisfaction = satisfaction;

			// Bonus satisfaction from traits
			if (pawn != null && partner != null)
			{
				if (xxx.is_animal(partner) && xxx.is_zoophile(pawn))
				{
					Trait_Satisfaction += 0.5f;
				}
				if (partner.Dead && xxx.is_necrophiliac(pawn))
				{
					Trait_Satisfaction += 0.5f;
				}
			}

			// Calculate bonus satisfaction from quirks
			var quirkCount = Quirk.CountSatisfiedQuirks(props);
			Quirk_Satisfaction += quirkCount * base_sat_per_quirk;

			// Apply sex satisfaction stat (min 0.1 default 1) as a modifier to total satisfaction
			Stat_Satisfaction += Math.Max(xxx.get_sex_satisfaction(pawn), 0.1f);

			// Apply extra multiplier for special/violence circumstances
			Circumstances_Satisfaction += get_satisfaction_circumstance_multiplier(props);

			satisfaction *= Trait_Satisfaction * Quirk_Satisfaction * Stat_Satisfaction;

			joysatisfaction = satisfaction;
			satisfaction *= Circumstances_Satisfaction;
			if (!RJWSettings.Disable_RecreationDrain)
				joysatisfaction *= (Circumstances_Satisfaction - 1);
			else
				joysatisfaction *= Circumstances_Satisfaction;

			//Log.Message("SatisfyPersonal( " + pawn + ", " + satisfaction + " ) - setting pawn sexneed");

			sex_need.CurLevel += satisfaction;

			if (quirkCount > 0)
			{
				Quirk.AddThought(pawn);
			}

			var joy_need = pawn.needs.TryGetNeed<Need_Joy>();
			if (joy_need == null) return;

			joysatisfaction *= joysatisfaction > 0 ? 0.5f : 1f;              // convert half of positive satisfaction to joy
																			 //Log.Message("SatisfyPersonal( " + pawn + ", " + joysatisfaction + " ) - setting pawn joyneed");
			pawn.needs.joy.CurLevel += joysatisfaction;

			//add Ahegao
			//sex&joy > 95%
			//sex&broken

			var lev = sex_need.CurLevel;
			if (lev >= sex_need.thresh_ahegao() &&
				(joy_need.CurLevelPercentage > sex_need.thresh_ahegao()) ||
				(AfterSexUtility.BodyIsBroken(pawn) && pawn.health.hediffSet.GetFirstHediffOfDef(xxx.feelingBroken)?.CurStageIndex >= 3))
			{
				var thoughtDef = DefDatabase<ThoughtDef>.GetNamed("RJW_Ahegao");
				pawn.needs.mood.thoughts.memories.TryGainMemory(thoughtDef);
			}
		}

		public static void GiveTraits(SexProps props, List<Trait> newTraits)
		{
			var pawn = props.pawn;
			var partner = props.partner;

			if (!xxx.has_traits(pawn) || pawn.records.GetValue(xxx.CountOfSex) <= 10)
			{
				return;
			}

			GiveQuirkTraits(props, newTraits, 0.05f);

			if (props.IsInitiator())
			{
				if (RJWSettings.AddTrait_Rapist && !xxx.is_rapist(pawn) && !xxx.is_masochist(pawn) && props.isRape && pawn.records.GetValue(xxx.CountOfRapedHumanlikes) > 0.12 * pawn.records.GetValue(xxx.CountOfSex))
				{
					var chance = 0.5f;
					if (xxx.is_kind(pawn)) chance -= 0.25f;
					if (xxx.is_prude(pawn)) chance -= 0.25f;
					if (xxx.is_zoophile(pawn)) chance -= 0.25f; // Less interested in raping humanlikes.
					if (xxx.is_ascetic(pawn)) chance -= 0.2f;
					if (xxx.is_bloodlust(pawn)) chance += 0.2f;
					if (xxx.is_psychopath(pawn)) chance += 0.25f;

					if (Rand.Chance(chance))
					{
						Trait rapist = new Trait(xxx.rapist);
						pawn.story.traits.GainTrait(rapist);
						newTraits.Add(rapist);
						//--Log.Message(xxx.get_pawnname(pawn) + " aftersex, not rapist, adding rapist trait");
					}
				}

				if (RJWSettings.AddTrait_Necrophiliac && !xxx.is_necrophiliac(pawn) && partner.Dead && pawn.records.GetValue(xxx.CountOfSexWithCorpse) > 0.5 * pawn.records.GetValue(xxx.CountOfSex))
				{
					Trait necropphiliac = new Trait(xxx.necrophiliac);
					pawn.story.traits.GainTrait(necropphiliac);
					newTraits.Add(necropphiliac);
					//Log.Message(xxx.get_pawnname(necro) + " aftersex, not necro, adding necro trait");
				}
			}

			if (RJWSettings.AddTrait_Zoophiliac && !xxx.is_zoophile(pawn) && xxx.is_animal(partner)
				&& (pawn.records.GetValue(xxx.CountOfSexWithAnimals) + pawn.records.GetValue(xxx.CountOfSexWithInsects) > 0.5 * pawn.records.GetValue(xxx.CountOfSex)))
			{
				Trait zoophile = new Trait(xxx.zoophile);
				pawn.story.traits.GainTrait(zoophile);
				newTraits.Add(zoophile);

				MemoryThoughtHandler memories = pawn.needs.mood.thoughts.memories;
				foreach (ThoughtDef memory in new[] {xxx.got_bred, xxx.got_anal_bred, xxx.got_groped, xxx.got_licked})
				{
					memories.RemoveMemoriesOfDef(memory);
				}
				//--Log.Message(xxx.get_pawnname(pawn) + " aftersex, not zoo, adding zoo trait");
			}

			if (RJWSettings.AddTrait_Nymphomaniac && !xxx.is_nympho(pawn))
			{
				if (pawn.health.hediffSet.HasHediff(RJWHediffDefOf.HumpShroomAddiction) && pawn.health.hediffSet.HasHediff(RJWHediffDefOf.HumpShroomEffect))
				{
					Trait nymphomaniac = new Trait(xxx.nymphomaniac);
					pawn.story.traits.GainTrait(nymphomaniac);
					newTraits.Add(nymphomaniac);
					//Log.Message(xxx.get_pawnname(pawn) + " is HumpShroomAddicted, not nymphomaniac, adding nymphomaniac trait");
				}
			}
		}

		[SyncMethod]
		private static void GiveQuirkTraits(SexProps props, List<Trait> newTraits, float traitGainChance = 0.05f)
		{
			var pawn = props.pawn;
			if (RJWPreferenceSettings.PlayerIsFootSlut && !pawn.Has(Quirk.Podophile))
			{
				if (props.sexType == xxx.rjwSextype.Footjob)
					if (props.IsSubmissive())
					{
						if (Rand.Chance(traitGainChance) || pawn.story.traits.HasTrait(xxx.footSlut))
						{
							pawn.Add(Quirk.Podophile);
							if (RJWSettings.AddTrait_FootSlut)
							{
								Trait footSlut = new Trait(xxx.footSlut);
								pawn.story.traits.GainTrait(footSlut);
								newTraits.Add(footSlut);
							}
						}
					}
			}
			if (RJWPreferenceSettings.PlayerIsCumSlut && !pawn.Has(Quirk.Cumslut))
			{
				if (props.sexType == xxx.rjwSextype.Fellatio)
					if (props.IsSubmissive())
					{
						if (Rand.Chance(traitGainChance) || pawn.story.traits.HasTrait(xxx.cumSlut))
						{
							pawn.Add(Quirk.Cumslut);
							if (RJWSettings.AddTrait_CumSlut)
							{
								Trait cumSlut = new Trait(xxx.cumSlut);
								pawn.story.traits.GainTrait(cumSlut);
								newTraits.Add(cumSlut);
							}
						}
					}
			}
			if (RJWPreferenceSettings.PlayerIsButtSlut && !pawn.Has(Quirk.Buttslut))
			{
				if (props.sexType == xxx.rjwSextype.Anal)
					if (props.IsSubmissive())
					{
						if (Rand.Chance(traitGainChance) || pawn.story.traits.HasTrait(xxx.buttSlut))
						{
							pawn.Add(Quirk.Buttslut);
							if (RJWSettings.AddTrait_ButtSlut)
							{
								Trait buttSlut = new Trait(xxx.buttSlut);
								pawn.story.traits.GainTrait(buttSlut);
								newTraits.Add(buttSlut);
							}
						}
					}
			}
		}

		private static void SendTraitGainLetter(Pawn pawn, List<Trait> newTraits)
		{
			if (newTraits.Count == 0 || !PawnUtility.ShouldSendNotificationAbout(pawn))
			{
				return;
			}

			TaggedString letterLabel;
			TaggedString letterDescription;
			if (newTraits.Count == 1)
			{
				string traitLabel = newTraits[0].Label;

				string letterLabelKey = TranslationKeyFor("LetterLabelGainedTraitFromSex", newTraits[0]);
				letterLabel = letterLabelKey.Translate(traitLabel.Named("TRAIT"), pawn.Named("PAWN")).CapitalizeFirst();

				string letterDescriptionKey = TranslationKeyFor("GainedTraitFromSex", newTraits[0]);
				letterDescription = letterDescriptionKey.Translate(traitLabel.Named("TRAIT"), pawn.Named("PAWN")).CapitalizeFirst();
			}
			else
			{
				letterLabel = "LetterLabelGainedMultipleTraitsFromSex".Translate(pawn.Named("PAWN"));
				letterDescription = "GainedMultipleTraitsFromSex".Translate(pawn.Named("PAWN"));

				letterDescription += "\n";
				foreach (var trait in newTraits)
				{
					letterDescription += "\n" + "GainedMultipleTraitsFromSexListItem".Translate(trait.LabelCap);
				}
			}

			Find.LetterStack.ReceiveLetter(letterLabel, letterDescription, LetterDefOf.NeutralEvent, pawn);

			string TranslationKeyFor(string stem, Trait trait)
			{
				string traitKey = stem + trait.def.defName;
				string traitKeyWithDegree = traitKey + trait.Degree;
				if (traitKeyWithDegree.CanTranslate())
				{
					return traitKeyWithDegree;
				}
				if (traitKey.CanTranslate())
				{
					return traitKey;
				}
				return stem;
			}
		}

		// Checks if enough time has passed from previous lovin'.
		public static bool ReadyForLovin(Pawn pawn)
		{
			return Find.TickManager.TicksGame > pawn.mindState.canLovinTick;
		}

		// Checks if enough time has passed from previous search for a hookup.
		// Checks if hookups allowed during working hours, exlcuding nymphs
		public static bool ReadyForHookup(Pawn pawn)
		{
			if (!xxx.is_nympho(pawn) && RJWHookupSettings.NoHookupsDuringWorkHours && ((pawn.timetable != null) ? pawn.timetable.CurrentAssignment : TimeAssignmentDefOf.Anything) == TimeAssignmentDefOf.Work) return false;
			return Find.TickManager.TicksGame > CompRJW.Comp(pawn).NextHookupTick;
		}

		private static void IncreaseTicksToNextLovin(Pawn pawn)
		{
			if (pawn == null || pawn.Dead) return;
			int currentTime = Find.TickManager.TicksGame;
			if (pawn.mindState.canLovinTick <= currentTime)
				pawn.mindState.canLovinTick = currentTime + GenerateMinTicksToNextLovin(pawn);
		}

		[SyncMethod]
		public static int GenerateMinTicksToNextLovin(Pawn pawn)
		{
			if (DebugSettings.alwaysDoLovin) return 100;
			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());

			float tick = 1.0f;

			// Nymphs automatically get the tick increase from the trait influence on sex drive.
			if (xxx.is_animal(pawn))
			{
				//var mateMtbHours = pawn.RaceProps.mateMtbHours / 24 * GenDate.TicksPerDay;
				//if (mateMtbHours > 0)
				//	interval = mateMtbHours
				if (RJWSettings.Animal_mating_cooldown == 0)
					tick = 0.75f;
				else
					return RJWSettings.Animal_mating_cooldown * 2500;
			}
			else if (xxx.is_prude(pawn))
				tick = 1.5f;

			if (pawn.Has(Quirk.Vigorous))
				tick *= 0.8f;

			float sex_drive = xxx.get_sex_drive(pawn);
			if (sex_drive <= 0.05f)
				sex_drive = 0.05f;

			float interval = AgeConfigDef.Instance.lovinIntervalHoursByAge.Evaluate(ScaleToHumanAge(pawn));
			float rinterval = Math.Max(0.5f, Rand.Gaussian(interval, 0.3f));
			return (int)(tick * rinterval * (2500.0f / sex_drive));
		}

		public static void IncreaseTicksToNextHookup(Pawn pawn)
		{
			if (pawn == null || pawn.Dead)
				return;

			// There are 2500 ticks per rimworld hour. Sleeping an hour between checks seems like a good start.
			// We could get fancier and weight it by sex drive and stuff, but would people even notice?
			const int TicksBetweenHookups = 2500;

			int currentTime = Find.TickManager.TicksGame;
			CompRJW.Comp(pawn).NextHookupTick = currentTime + TicksBetweenHookups;
		}

		///
		/// <para>Determines the sex type and handles the log output.</para>
		/// <para>`props.pawn` should be initiator of the act (rapist, whore, etc).</para>
		/// <para>`props.partner` should be the target.</para>
		///
		public static void ProcessSex(SexProps props)
		{
			//Log.Message("usedCondom=" + usedCondom);
			if (props.pawn == null || props.partner == null)
			{
				if (props.pawn == null)
					ModLog.Error("[SexUtility] ERROR: pawn is null.");
				if (props.partner == null)
					ModLog.Error("[SexUtility] ERROR: partner is null.");
				return;
			}

			IncreaseTicksToNextLovin(props.pawn);
			IncreaseTicksToNextLovin(props.partner);

			Aftersex(props);

			AfterSexUtility.think_about_sex(props);
		}

		[SyncMethod]
		public static SexProps SelectSextype(Pawn pawn, Pawn partner, bool rape, bool whoring)
		{
			var SP = new SexProps();
			SP.pawn = pawn;
			SP.partner = partner;
			SP.isRape = rape;
			SP.isWhoring = whoring;

			//Caufendra's magic is happening here
			InteractionInputs inputs = new InteractionInputs()
			{
				Initiator = pawn,
				Partner = partner,
				IsRape = rape,
				IsWhoring = whoring
			};

			//this should be added as a static readonly but ... since the class is so big,
			//it's probably best not to overload the static constructor
			ILewdInteractionService lewdInteractionService = LewdInteractionService.Instance;

			InteractionOutputs outputs = lewdInteractionService.GenerateInteraction(inputs);

			SP.sexType = outputs.Generated.RjwSexType;
			SP.rulePack = outputs.Generated.RulePack.defName;
			SP.dictionaryKey = outputs.Generated.InteractionDef.Interaction;

			return SP;
		}

		public static void LogSextype(Pawn giving, Pawn receiving, string rulepack, InteractionDef dictionaryKey)
		{
			List<RulePackDef> extraSentencePacks = new List<RulePackDef>();
			if (!rulepack.NullOrEmpty())
				extraSentencePacks.Add(RulePackDef.Named(rulepack));
			LogSextype(giving, receiving, extraSentencePacks, dictionaryKey);
		}

		public static void LogSextype(Pawn giving, Pawn receiving, List<RulePackDef> extraSentencePacks, InteractionDef dictionaryKey)
		{
			if (extraSentencePacks.NullOrEmpty())
			{
				extraSentencePacks = new List<RulePackDef>();
				string extraSentenceRulePack = SexRulePackGet(dictionaryKey);
				if (!extraSentenceRulePack.NullOrEmpty())
					extraSentencePacks.Add(RulePackDef.Named(extraSentenceRulePack));
			}
			PlayLogEntry_Interaction playLogEntry = new PlayLogEntry_Interaction(dictionaryKey, giving, receiving, extraSentencePacks);
			Find.PlayLog.Add(playLogEntry);
		}

		[SyncMethod]
		public static string SexRulePackGet(InteractionDef dictionaryKey)
		{
			var extension = Modules.Interactions.Helpers.InteractionHelper.GetWithExtension(dictionaryKey).Extension;
			string extraSentenceRulePack = "";
			if (!extension.rulepack_defs.NullOrEmpty())
			{
				extraSentenceRulePack = extension.rulepack_defs.RandomElement();
			}

			try
			{
				if (RulePackDef.Named(extraSentenceRulePack) != null)
				{
				}
			}
			catch
			{
				ModLog.Warning("RulePackDef " + extraSentenceRulePack + " for " + dictionaryKey + " not found");
				extraSentenceRulePack = "";
			}
			return extraSentenceRulePack;
		}

		public static xxx.rjwSextype rjwSextypeGet(InteractionDef dictionaryKey)
		{
			var extension = Modules.Interactions.Helpers.InteractionHelper.GetWithExtension(dictionaryKey).Extension;
			var sextype = xxx.rjwSextype.None;
			if (!extension.rjwSextype.NullOrEmpty())
				sextype = ParseHelper.FromString<xxx.rjwSextype>(extension.rjwSextype);

			if (RJWSettings.DevMode) ModLog.Message("rjwSextypeGet:dictionaryKey " + dictionaryKey + " sextype " + sextype);
			return sextype;
		}

		[SyncMethod]
		public static void Sex_Beatings(SexProps props)
		{
			Pawn pawn = props.pawn;
			Pawn partner = props.partner;
			if ((xxx.is_animal(pawn) && xxx.is_animal(partner)))
				return;

			//dont remember what it does, probably manhunter stuff or not? disable and wait reports
			//if (!xxx.is_human(pawn))
			//	return;

			//If a pawn is incapable of violence/has low melee, they most likely won't beat their partner
			if (pawn.skills?.GetSkill(SkillDefOf.Melee).Level < 1)
				return;

			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			float rand_value = Rand.Value;
			//float rand_value = RJW_Multiplayer.RJW_MP_RAND();
			float victim_pain = partner.health.hediffSet.PainTotal;
			// bloodlust makes the aggressor more likely to hit the prisoner
			float beating_chance = xxx.config.base_chance_to_hit_prisoner * (xxx.is_bloodlust(pawn) ? 1.5f : 1.0f);
			// psychopath makes the aggressor more likely to hit the prisoner past the significant_pain_threshold
			float beating_threshold = xxx.is_psychopath(pawn) ? xxx.config.extreme_pain_threshold : pawn.HostileTo(partner) ? xxx.config.significant_pain_threshold : xxx.config.minor_pain_threshold;

			//--Log.Message("roll_to_hit:  rand = " + rand_value + ", beating_chance = " + beating_chance + ", victim_pain = " + victim_pain + ", beating_threshold = " + beating_threshold);
			if ((victim_pain < beating_threshold && rand_value < beating_chance) || (rand_value < (beating_chance / 2) && xxx.is_bloodlust(pawn)))
			{
				Sex_Beatings_Dohit(pawn, partner, props.isRapist);
			}
		}

		public static void Sex_Beatings_Dohit(Pawn pawn, Pawn partner, bool isRape = false)
		{
			//--Log.Message("   done told her twice already...");
			if (InteractionUtility.TryGetRandomVerbForSocialFight(pawn, out Verb v))
			{
				//Log.Message("   v. : " + v);
				//Log.Message("   v.GetDamageDef : " + v.GetDamageDef());
				//Log.Message("   v.v.tool - " + v.tool.label);
				//Log.Message("   v.v.tool.power base - " + v.tool.power);
				var orgpower = v.tool.power;
				//in case something goes wrong
				try
				{
					//Log.Message("   v.v.tool.power base - " + v.tool.power);
					if (RJWSettings.gentle_rape_beating || !isRape)
					{
						v.tool.power = 0;
						//partner.stances.stunner.StunFor(600, pawn);
					}
					//Log.Message("   v.v.tool.power mod - " + v.tool.power);
					var guilty = true;
					if (!pawn.guilt.IsGuilty)
					{
						guilty = false;
					}
					pawn.meleeVerbs.TryMeleeAttack(partner, v);

					if (pawn.guilt.IsGuilty && !guilty)
						pawn.guilt.Notify_Guilty(0);
				}
				catch
				{ }
				v.tool.power = orgpower;
				//Log.Message("   v.v.tool.power reset - " + v.tool.power);
			}
		}

		// Overrides the current clothing. Defaults to nude, with option to keep headgear on.
		public static void DrawNude(Pawn pawn, bool keep_hat_on = false)
		{
			if (!xxx.is_human(pawn)) return;
			if (pawn.Map != Find.CurrentMap) return;
			if (RJWPreferenceSettings.sex_wear == RJWPreferenceSettings.Clothing.Clothed) return;

			//undress
			pawn.Drawer.renderer.graphics.ClearCache();
			pawn.Drawer.renderer.graphics.apparelGraphics.Clear();
			//pawn.Drawer.renderer.graphics.ResolveApparelGraphics();
			//Log.Message("DrawNude: " + pawn.Name);

			//add "clothes"
			foreach (Apparel current in pawn.apparel.WornApparel.Where(x
				=> x.def is bondage_gear_def
				|| (!x.def.thingCategories.NullOrEmpty() && x.def.thingCategories.Any(x => x.defName.ToLower().ContainsAny("vibrator", "piercing", "strapon")))
				|| RJWPreferenceSettings.sex_wear == RJWPreferenceSettings.Clothing.Headgear
				|| keep_hat_on
				&& (!x.def.apparel.bodyPartGroups.NullOrEmpty() && (x.def.apparel.bodyPartGroups.Contains(BodyPartGroupDefOf.FullHead)
					|| x.def.apparel.bodyPartGroups.Contains(BodyPartGroupDefOf.UpperHead)))))
			{
				ApparelGraphicRecord item;
				if (ApparelGraphicRecordGetter.TryGetGraphicApparel(current, pawn.story.bodyType, out item))
				{
					pawn.Drawer.renderer.graphics.apparelGraphics.Add(item);
				}
			}
			GlobalTextureAtlasManager.TryMarkPawnFrameSetDirty(pawn);
		}

		public static void reduce_rest(Pawn pawn, float x = 1f)
		{
			if (pawn.Has(Quirk.Vigorous)) x -= x / 2;

			Need_Rest need_rest = pawn.needs.TryGetNeed<Need_Rest>();
			if (need_rest == null)
				return;

			need_rest.CurLevel -= need_rest.RestFallPerTick * x;
		}
		public static void OffsetPsyfocus(Pawn pawn, float x = 0)//0-1
		{
			if (ModsConfig.RoyaltyActive)
			{
				//pawn.psychicEntropy.Notify_Meditated();
				if (pawn.HasPsylink)
				{
					pawn.psychicEntropy.OffsetPsyfocusDirectly(x);
				}
			}
		}

		//Takes the nutrition away from the one penetrating(probably) and injects it to the one on the receiving end
		//As with everything in the mod, this could be greatly extended, current focus though is to prevent starvation of those caught in a huge horde of rappers (that may happen with some mods)
		public static void TransferNutrition(SexProps props)
		{
			//Log.Message("xxx::TransferNutrition:: " + xxx.get_pawnname(pawn) + " => " + xxx.get_pawnname(partner));
			if (props.partner?.needs == null)
			{
				//Log.Message("xxx::TransferNutrition() failed due to lack of transfer equipment or pawn ");
				return;
			}
			if (props.pawn?.needs == null)
			{
				//Log.Message("xxx::TransferNutrition() failed due to lack of transfer equipment or pawn ");
				return;
			}

			//transfer nutrition from presumanbly "giver" to partner
			if (props.sexType == xxx.rjwSextype.Sixtynine ||
				props.sexType == xxx.rjwSextype.Oral ||
				props.sexType == xxx.rjwSextype.Cunnilingus ||
				props.sexType == xxx.rjwSextype.Fellatio)
			{
				Pawn pawn = props.pawn;
				Pawn partner = props.partner;
				Pawn giver, receiver;
				List<Hediff> pawnparts = pawn.GetGenitalsList();
				List<Hediff> partnerparts = partner.GetGenitalsList();
				bool t = false;

				if (props.sexType == xxx.rjwSextype.Sixtynine || Genital_Helper.has_penis_fertile(pawn, pawnparts) || Genital_Helper.has_ovipositorF(pawn, pawnparts))
				{
					giver = pawn;
					receiver = partner;

					Need_Food need = giver.needs?.TryGetNeed<Need_Food>();
					if (need == null)
					{
						//Log.Message("TransferNutrition() " + xxx.get_pawnname(giver) + " doesn't track nutrition in itself, probably shouldn't feed the others");
						return;
					}
					float nutrition_amount = Math.Min(need.MaxLevel / 15f, need.CurLevel); //body size is taken into account implicitly by need.MaxLevel
					giver.needs.food.CurLevel = need.CurLevel - nutrition_amount;
					//Log.Message("TransferNutrition() " + xxx.get_pawnname(giver) + " sent " + nutrition_amount + " of nutrition");

					if (receiver.needs?.TryGetNeed<Need_Food>() != null)
					{
						//Log.Message("xxx::TransferNutrition() " +  xxx.get_pawnname(receiver) + " can receive");
						receiver.needs.food.CurLevel += nutrition_amount;
					}
					TransferThirst(giver, receiver);
					t = true;
				}

				if (props.sexType == xxx.rjwSextype.Sixtynine || (t == false && props.isCoreLovin && (Genital_Helper.has_penis_fertile(partner, partnerparts) || Genital_Helper.has_ovipositorF(partner, partnerparts))))
				{
					giver = partner;
					receiver = pawn;

					Need_Food need = giver.needs?.TryGetNeed<Need_Food>();
					if (need == null)
					{
						//Log.Message("TransferNutrition() " + xxx.get_pawnname(giver) + " doesn't track nutrition in itself, probably shouldn't feed the others");
						return;
					}
					float nutrition_amount = Math.Min(need.MaxLevel / 15f, need.CurLevel); //body size is taken into account implicitly by need.MaxLevel
					giver.needs.food.CurLevel = need.CurLevel - nutrition_amount;
					//Log.Message("TransferNutrition() " + xxx.get_pawnname(giver) + " sent " + nutrition_amount + " of nutrition");

					if (receiver.needs?.TryGetNeed<Need_Food>() != null)
					{
						//Log.Message("TransferNutrition() " +  xxx.get_pawnname(receiver) + " can receive");
						receiver.needs.food.CurLevel += nutrition_amount;
					}
					TransferThirst(giver, receiver);
				}
			}
			TransferNutritionSucc(props);
		}

		public static void TransferThirst(Pawn pawn, Pawn receiver)
		{
			if (xxx.DubsBadHygieneIsActive)
			{
				Need DBHThirst = receiver.needs?.AllNeeds.Find(x => x.def == xxx.DBHThirst);
				if (DBHThirst != null)
				{
					//Log.Message("TransferThirst() " + xxx.get_pawnname(receiver) + " decreasing thirst");
					receiver.needs.TryGetNeed(DBHThirst.def).CurLevel += 0.1f;
				}
			}
		}

		public static void TransferNutritionSucc(SexProps props)
		{
			//succubus mana and rest regen stuff
			if (props.sexType == xxx.rjwSextype.Oral ||
				props.sexType == xxx.rjwSextype.Cunnilingus ||
				props.sexType == xxx.rjwSextype.Fellatio ||
				props.sexType == xxx.rjwSextype.Vaginal ||
				props.sexType == xxx.rjwSextype.Anal ||
				props.sexType == xxx.rjwSextype.DoublePenetration)
			{
				if (xxx.has_traits(props.partner))
				{
					bool gainrest = false;
					if (xxx.RoMIsActive && (props.partner.story.traits.HasTrait(xxx.Succubus) || props.partner.story.traits.HasTrait(xxx.Warlock)))
					{
						Need TM_Mana = props.partner?.needs?.AllNeeds.Find(x => x.def == xxx.TM_Mana);
						if (TM_Mana != null)
						{
							//Log.Message("TransferNutritionSucc() " + xxx.get_pawnname(partner) + " increase mana");
							props.partner.needs.TryGetNeed(TM_Mana.def).CurLevel += 0.1f;
						}
						gainrest = true;
					}

					if (xxx.NightmareIncarnationIsActive)
					{
						foreach (var x in props.partner.AllComps?.Where(x => x.props?.ToString() == "NightmareIncarnation.CompProperties_SuccubusRace"))
						{
							Need NI_Need_Mana = props.partner?.needs?.AllNeeds.Find(x => x.def == xxx.NI_Need_Mana);
							if (NI_Need_Mana != null)
							{
								//Log.Message("TransferNutritionSucc() " + xxx.get_pawnname(partner) + " increase mana");
								props.partner.needs.TryGetNeed(NI_Need_Mana.def).CurLevel += 0.1f;
							}
							gainrest = true;
							break;
						}
					}

					if (gainrest)
					{
						Need_Rest need1 = props.pawn.needs?.TryGetNeed<Need_Rest>();
						Need_Rest need2 = props.partner.needs?.TryGetNeed<Need_Rest>();
						if (need1 != null && need2 != null)
						{
							//Log.Message("TransferNutritionSucc() " + xxx.get_pawnname(partner) + " increase rest");
							props.partner.needs.TryGetNeed(need2.def).CurLevel += 0.25f;
							//Log.Message("TransferNutritionSucc() " + xxx.get_pawnname(pawn) + " decrease rest");
							props.pawn.needs.TryGetNeed(need1.def).CurLevel -= 0.25f;
						}
					}
				}

				if (xxx.has_traits(props.pawn))
				{
					bool gainrest = false;
					if (xxx.RoMIsActive && (props.pawn.story.traits.HasTrait(xxx.Succubus) || props.pawn.story.traits.HasTrait(xxx.Warlock)))
					{
						Need TM_Mana = props.pawn?.needs?.AllNeeds.Find(x => x.def == xxx.TM_Mana);
						if (TM_Mana != null)
						{
							//Log.Message("TransferNutritionSucc() " + xxx.get_pawnname(pawn) + " increase mana");
							props.pawn.needs.TryGetNeed(TM_Mana.def).CurLevel += 0.1f;
						}
					}

					if (xxx.NightmareIncarnationIsActive)
					{
						foreach (var x in props.pawn.AllComps?.Where(x => x.props?.ToString() == "NightmareIncarnation.CompProperties_SuccubusRace"))
						{
							Need NI_Need_Mana = props.pawn?.needs?.AllNeeds.Find(x => x.def == xxx.NI_Need_Mana);
							if (NI_Need_Mana != null)
							{
								//Log.Message("TransferNutritionSucc() " + xxx.get_pawnname(partner) + " increase mana");
								props.pawn.needs.TryGetNeed(NI_Need_Mana.def).CurLevel += 0.1f;
							}
							gainrest = true;
							break;
						}
					}

					if (gainrest)
					{
						Need_Rest need1 = props.partner.needs.TryGetNeed<Need_Rest>();
						Need_Rest need2 = props.pawn.needs.TryGetNeed<Need_Rest>();
						if (need1 != null && need2 != null)
						{
							//Log.Message("TransferNutritionSucc() " + xxx.get_pawnname(pawn) + " increase rest");
							props.pawn.needs.TryGetNeed(need2.def).CurLevel += 0.25f;
							//Log.Message("TransferNutritionSucc() " + xxx.get_pawnname(partner) + " decrease rest");
							props.partner.needs.TryGetNeed(need1.def).CurLevel -= 0.25f;
						}
					}
				}
			}
		}

		public static float get_broken_consciousness_debuff(Pawn pawn)
		{
			if (pawn == null)
			{
				return 1.0f;
			}

			try
			{
				Hediff broken = pawn.health.hediffSet.GetFirstHediffOfDef(xxx.feelingBroken);
				foreach (PawnCapacityModifier capMod in broken.CapMods)
				{
					if (capMod.capacity == PawnCapacityDefOf.Consciousness)
					{
						if (RJWSettings.DevMode) ModLog.Message("Broken Pawn Consciousness factor: " + capMod.postFactor);
						return capMod.postFactor;
					}
				}

				// fallback
				return 1.0f;
			}
			catch (NullReferenceException)
			{
				//Log.Warning(e.ToString());
				return 1f;
			}
		}

		public static float get_satisfaction_circumstance_multiplier(SexProps props)
		{
			// Get a multiplier for satisfaction that should apply on top of the sex satisfaction stat
			// This is mostly for traits that only affect satisfaction in some circumstances

			Pawn pawn = props.pawn;
			Boolean isViolentSex = props.isRape;

			float multiplier = 1.0f;
			if (xxx.is_human(pawn))
			{
				// Negate consciousness debuffs for broken pawns (counters the sex satisfaction score being affected by low consciousness)
				multiplier = (1.0f / get_broken_consciousness_debuff(pawn));

				// Multiplier bonus for violent traits and violent sex
				if (!props.isReceiver)
				{
					// Rapists/Bloodlusts get 50% more satisfaction from violent encounters, and less from non-violent
					if (isViolentSex)
					{
						if (xxx.is_rapist(pawn) || xxx.is_bloodlust(pawn) || xxx.is_psychopath(pawn))
							multiplier += 0.5f;
						else
							multiplier -= 0.2f;
					}
					else
					{
						if (xxx.is_rapist(pawn) || xxx.is_bloodlust(pawn))
							multiplier -= 0.2f;
						else
							multiplier += 0.2f;
					}
				}
				else if (props.isReceiver)
				{
					// Masochists get 50% more satisfaction from receiving violent sex and 20% less from normal sex
					if (isViolentSex)
					{
						if (xxx.is_masochist(pawn))
							multiplier += 0.5f;
						else
							multiplier -= 0.2f;
					}
					else
					{
						if (xxx.is_masochist(pawn))
							multiplier -= 0.2f;
						else
							multiplier += 0.2f;
					}

					// Multiplier for broken pawns
					if (isViolentSex && AfterSexUtility.BodyIsBroken(pawn))
					{
						// Add bonus satisfaction based on stage
						switch (pawn.health.hediffSet.GetFirstHediffOfDef(xxx.feelingBroken).CurStageIndex)
						{
							case 0:
								break;

							case 1:
								// 50% bonus for stage 2
								//multiplier += 0.5f;
								break;

							case 2:
								// 100% bonus for stage 2
								multiplier += 1.0f;
								break;
						}
					}
				}
				//ModLog.Message("Sex satisfaction multiplier: " + multiplier);
			}
			return multiplier;
		}
	}
}

repo_name: Keagro/rjw

license: mit


code:
using Multiplayer.API;
using Verse;

namespace rjw
{
	public static class Sexualizer
	{
		static void SexualizeSingleGenderPawn(Pawn pawn)
		{
			// Single gender is futa without the female gender change.
			SexPartAdder.add_genitals(pawn, null, Gender.Male);
			SexPartAdder.add_genitals(pawn, null, Gender.Female);
			SexPartAdder.add_breasts(pawn, null, Gender.Female);
			SexPartAdder.add_anus(pawn, null);
		}

		static void SexualizeGenderlessPawn(Pawn pawn)
		{
			if (RJWSettings.GenderlessAsFuta && !xxx.is_mechanoid(pawn) && (pawn.RaceProps.Animal || pawn.RaceProps.Humanlike))
			{
				if (RJWSettings.DevMode)
					ModLog.Message(" SexualizeGenderlessPawn() - genderless pawn, treating Genderless pawn As Futa" + xxx.get_pawnname(pawn));
				//set gender to female for futas
				pawn.gender = Gender.Female;
				SexPartAdder.add_genitals(pawn, null, Gender.Male);
				SexPartAdder.add_genitals(pawn, null, Gender.Female);
				SexPartAdder.add_breasts(pawn, null, Gender.Female);
				SexPartAdder.add_anus(pawn, null);
			}
			else
			{
				if (RJWSettings.DevMode)
					ModLog.Message(" SexualizeGenderlessPawn() - unable to sexualize genderless pawn " + xxx.get_pawnname(pawn) + " gender: " + pawn.gender);
			}
		}

		[SyncMethod]
		static void SexualizeGenderedPawn(Pawn pawn)
		{
			//apply normal gender
			SexPartAdder.add_genitals(pawn, null, pawn.gender);

			//apply futa gender
			//if (pawn.gender == Gender.Female) // changing male to futa will break pawn generation due to relations

			if (pawn.Faction != null && !xxx.is_animal(pawn)) //null faction throws error
			{
				//ModLog.Message(" SexualizeGenderedPawn( " + xxx.get_pawnname(pawn) + " ) techLevel: " + (int)pawn.Faction.def.techLevel);
				//ModLog.Message(" SexualizeGenderedPawn( " + xxx.get_pawnname(pawn) + " ) techLevel: " + pawn.Faction.Name);

				//natives/spacer futa
				float chance = (int)pawn.Faction.def.techLevel < 5 ? RJWSettings.futa_natives_chance : RJWSettings.futa_spacers_chance;
				//nymph futa gender
				chance = xxx.is_nympho(pawn) ? RJWSettings.futa_nymph_chance : chance;

				// ModLog.Message($"SexualizeGenderedPawn {chance} from {RJWSettings.futa_nymph_chance} {RJWSettings.futa_natives_chance} {RJWSettings.futa_spacers_chance}");
				if (Rand.Chance(chance))
				{
					//make futa
					if (pawn.gender == Gender.Female && RJWSettings.FemaleFuta)
						SexPartAdder.add_genitals(pawn, null, Gender.Male);
					//make trap
					else if (pawn.gender == Gender.Male && RJWSettings.MaleTrap)
						SexPartAdder.add_breasts(pawn, null, Gender.Female);
				}
			}
			SexPartAdder.add_breasts(pawn, null, pawn.gender);
			SexPartAdder.add_anus(pawn, null);
		}

		[SyncMethod]
		public static void sexualize_pawn(Pawn pawn)
		{
			//ModLog.Message(" sexualize_pawn( " + xxx.get_pawnname(pawn) + " ) called");
			if (pawn == null)
			{
				return;
			}

			//override pawn
			if (RacePartDef_Helper.TryRacePartDef_partAdders(pawn))
				return;

			if (RaceGroupDef_Helper.TryGetRaceGroupDef(pawn, out var raceGroupDef) &&
				raceGroupDef.hasSingleGender)
			{
				if (RJWSettings.DevMode)
					ModLog.Message($"sexualize_pawn() - sexualizing single gender pawn {xxx.get_pawnname(pawn)}  race: {raceGroupDef.defName}");
				SexualizeSingleGenderPawn(pawn);
			}
			else if (pawn.RaceProps.hasGenders)
			{
				SexualizeGenderedPawn(pawn);
			}
			else
			{
				if (Current.ProgramState == ProgramState.Playing) // DO NOT run at world generation, throws error in generating relationship stuff
				{
					SexualizeGenderlessPawn(pawn);
					return;
				}
			}

			if (!pawn.Dead)
			{
				//Add ticks at generation, so pawns don't instantly start lovin' when generated (esp. on scenario start).
				//if (xxx.RoMIsActive && pawn.health.hediffSet.HasHediff(HediffDef.Named("TM_ShapeshiftHD"))) // Rimworld of Magic's polymorph/shapeshift
				//	pawn.mindState.canLovinTick = Find.TickManager.TicksGame + (int)(SexUtility.GenerateMinTicksToNextLovin(pawn) * Rand.Range(0.01f, 0.05f));
				if (!xxx.is_insect(pawn))
					pawn.mindState.canLovinTick = Find.TickManager.TicksGame + (int)(SexUtility.GenerateMinTicksToNextLovin(pawn) * Rand.Range(0.1f, 1.0f));
				else
					pawn.mindState.canLovinTick = Find.TickManager.TicksGame + (int)(SexUtility.GenerateMinTicksToNextLovin(pawn) * Rand.Range(0.01f, 0.2f));

				//ModLog.Message(" sexualize_pawn( " + xxx.get_pawnname(pawn) + " ) add sexneed");
				if (pawn.RaceProps.Humanlike)
				{
					var sex_need = pawn.needs.TryGetNeed<Need_Sex>();
					if (pawn.Faction != null && !(pawn.Faction?.IsPlayer ?? false) && sex_need != null)
					{
						sex_need.CurLevel = Rand.Range(0.01f, 0.75f);
					}
				}
			}
		}
	}
}


code:
using System;
using System.Diagnostics;
using UnityEngine;
using Verse;

namespace rjw
{
	public static class Logger
	{
		private static readonly LogMessageQueue messageQueueRJW = new LogMessageQueue();
		public static void Message(string text)
		{
			bool DevModeEnabled = RJWSettings.DevMode;
			if (!DevModeEnabled) return;
			UnityEngine.Debug.Log(text);
			messageQueueRJW.Enqueue(new LogMessage(LogMessageType.Message, text, StackTraceUtility.ExtractStackTrace()));
		}
		public static void Warning(string text)
		{
			bool DevModeEnabled = RJWSettings.DevMode;
			if (!DevModeEnabled) return;
			UnityEngine.Debug.Log(text);
			messageQueueRJW.Enqueue(new LogMessage(LogMessageType.Warning, text, StackTraceUtility.ExtractStackTrace()));
		}
		public static void Error(string text)
		{
			bool DevModeEnabled = RJWSettings.DevMode;
			if (!DevModeEnabled) return;
			UnityEngine.Debug.Log(text);
			messageQueueRJW.Enqueue(new LogMessage(LogMessageType.Error, text, StackTraceUtility.ExtractStackTrace()));
		}

		public static TimeSpan Time(Action action)
		{
			Stopwatch stopwatch = Stopwatch.StartNew();
			action();
			stopwatch.Stop();
			return stopwatch.Elapsed;
		}
	}
}


code:
using Verse;

namespace rjw
{
	public class MapCom_Injector : MapComponent
	{
		public bool injected_designator = false;

		public bool triggered_after_load = false;

		public MapCom_Injector(Map m) : base(m)
		{
		}

		public override void MapComponentUpdate()
		{
		}

		public override void MapComponentTick()
		{
		}

		public override void MapComponentOnGUI()
		{
			var currently_visible = Find.CurrentMap == map;

			if ((!injected_designator) && currently_visible)
			{
				//Find.ReverseDesignatorDatabase.AllDesignators.Add(new Designator_ComfortPrisoner());
				//Find.ReverseDesignatorDatabase.AllDesignators.Add(new Designator_Breed());
				injected_designator = true;
			}
			else if (injected_designator && (!currently_visible))
				injected_designator = false;
		}

		public override void ExposeData()
		{
		}
	}
}


code:
using System;
using Verse;

namespace rjw
{
	public sealed class MiscTranslationDef : Def
	{
		public Type targetClass;
		public string stringA = null;
		public string stringB = null;
		public string stringC = null;

		private void Assert(bool check, string errorMessage)
		{
			if (!check)
			{
				ModLog.Error($"Invalid data in MiscTranslationDef {defName}: {errorMessage}");
			}
		}

		public override void PostLoad()
		{
			Assert(targetClass != null, "targetClass field must be set");
		}

		public override void ResolveReferences()
		{
			base.ResolveReferences();
		}
	}
}


code:
using Verse;

namespace rjw
{
	public static class ModLog
	{
		/// <summary>
		/// Logs the given message with [SaveStorage.ModId] appended.
		/// </summary>
		public static void Error(string message)
		{
			Log.Error($"[{SaveStorage.ModId}] {message}");
		}

		/// <summary>
		/// Logs the given message with [SaveStorage.ModId] appended.
		/// </summary>
		public static void Message(string message)
		{
			Log.Message($"[{SaveStorage.ModId}] {message}");
		}

		/// <summary>
		/// Logs the given message with [SaveStorage.ModId] appended.
		/// </summary>
		public static void Warning(string message)
		{
			Log.Warning($"[{SaveStorage.ModId}] {message}");
		}
	}
}


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

namespace rjw
{
	public static class PawnExtensions
	{
		public static bool RaceHasFertility(this Pawn pawn)
		{
			// True by default.
			if (RaceGroupDef_Helper.TryGetRaceGroupDef(pawn, out var raceGroupDef))
				return raceGroupDef.hasFertility;
			return true;
		}

		public static bool RaceHasPregnancy(this Pawn pawn)
		{
			// True by default.
			if (RaceGroupDef_Helper.TryGetRaceGroupDef(pawn, out var raceGroupDef))
				return raceGroupDef.hasPregnancy;
			return true;
		}

		public static bool RaceHasOviPregnancy(this Pawn pawn)
		{
			// False by default.
			if (RaceGroupDef_Helper.TryGetRaceGroupDef(pawn, out var raceGroupDef))
				return raceGroupDef.oviPregnancy;
			return false;
		}

		public static bool RaceImplantEggs(this Pawn pawn)
		{
			// False by default.
			if (RaceGroupDef_Helper.TryGetRaceGroupDef(pawn, out var raceGroupDef))
				return raceGroupDef.ImplantEggs;
			return false;
		}

		public static bool RaceHasSexNeed(this Pawn pawn)
		{
			// True by default.
			if (RaceGroupDef_Helper.TryGetRaceGroupDef(pawn, out var raceGroupDef))
				return raceGroupDef.hasSexNeed;
			return true;
		}

		public static bool TryAddRacePart(this Pawn pawn, SexPartType sexPartType)
		{
			return RaceGroupDef_Helper.TryAddRacePart(pawn, sexPartType);
		}

		public static bool Has(this Pawn pawn, Quirk quirk)
		{
			return xxx.has_quirk(pawn, quirk.Key);
		}

		public static bool Has(this Pawn pawn, RaceTag tag)
		{
			if (RaceGroupDef_Helper.TryGetRaceGroupDef(pawn, out var raceGroupDef))
			{
				return raceGroupDef.tags != null && raceGroupDef.tags.Contains(tag.Key);
			}
			else
			{
				return tag.DefaultWhenNoRaceGroupDef(pawn);
			}
		}

		public static void Add(this Pawn pawn, Quirk quirk)
		{
			QuirkAdder.Add(pawn, quirk);
		}

		public static bool IsSexyRobot(this Pawn pawn)
		{
			return AndroidsCompatibility.IsAndroid(pawn);
		}

		// In theory I think this should involve RaceGroupDef.
		public static bool IsUnsexyRobot(this Pawn pawn)
		{
			return !IsSexyRobot(pawn)
				&& (xxx.is_mechanoid(pawn) || pawn.kindDef.race.defName.ToLower().Contains("droid"));
		}

		public static bool IsAnimal(this Pawn pawn)
		{
			return xxx.is_animal(pawn);
		}

		public static bool IsVisiblyPregnant(this Pawn pawn)
		{
			return pawn.IsPregnant(true);
		}

		public static bool IsPregnant(this Pawn pawn, bool mustBeVisible = false)
		{
			var set = pawn.health.hediffSet;
			return set.HasHediff(HediffDefOf.PregnantHuman, mustBeVisible) ||
				set.HasHediff(HediffDefOf.Pregnant, mustBeVisible) ||
				Hediff_BasePregnancy.KnownPregnancies().Any(x => set.HasHediff(HediffDef.Named(x), mustBeVisible));
		}

		public static List<Hediff> GetGenitalsList(this Pawn pawn)
		{
			List<Hediff> set;
			try//error at world gen
			{
				set = pawn.GetRJWPawnData().genitals;
				if (set.NullOrEmpty())
				{
					var partBPR = Genital_Helper.get_genitalsBPR(pawn);
					set = Genital_Helper.get_PartsHediffList(pawn, partBPR);
					pawn.GetRJWPawnData().genitals = set;
				}

			}
			catch
			{
				var partBPR = Genital_Helper.get_genitalsBPR(pawn);
				set = Genital_Helper.get_PartsHediffList(pawn, partBPR);
			}
			return set;
		}

		public static List<Hediff> GetBreastList(this Pawn pawn)
		{
			List<Hediff> set;
			try//error at world gen
			{
				set = pawn.GetRJWPawnData().breasts;
				if (set.NullOrEmpty())
				{
					var partBPR = Genital_Helper.get_breastsBPR(pawn);
					set = Genital_Helper.get_PartsHediffList(pawn, partBPR);
					pawn.GetRJWPawnData().breasts = set;
				}

			}
			catch
			{
				var partBPR = Genital_Helper.get_breastsBPR(pawn);
				set = Genital_Helper.get_PartsHediffList(pawn, partBPR);
			}
			return set;
		}
		public static List<Hediff> GetUdderList(this Pawn pawn)
		{
			List<Hediff> set;
			try//error at world gen
			{
				set = pawn.GetRJWPawnData().udders;
				if (set.NullOrEmpty())
				{
					var partBPR = Genital_Helper.get_uddersBPR(pawn);
					set = Genital_Helper.get_PartsHediffList(pawn, partBPR);
					pawn.GetRJWPawnData().udders = set;
				}

			}
			catch
			{
				var partBPR = Genital_Helper.get_breastsBPR(pawn);
				set = Genital_Helper.get_PartsHediffList(pawn, partBPR);
			}
			return set;
		}
		public static List<Hediff> GetAnusList(this Pawn pawn)
		{
			List<Hediff> set;
			try//error at world gen
			{
				set = pawn.GetRJWPawnData().anus;
				if (set.NullOrEmpty())
				{
					var partBPR = Genital_Helper.get_anusBPR(pawn);
					set = Genital_Helper.get_PartsHediffList(pawn, partBPR);
					pawn.GetRJWPawnData().anus = set;
				}

			}
			catch
			{
				var partBPR = Genital_Helper.get_anusBPR(pawn);
				set = Genital_Helper.get_PartsHediffList(pawn, partBPR);
			}
			return set;
		}

		public static List<Hediff> GetTorsoList(this Pawn pawn)
		{
			List<Hediff> set;
			try//error at world gen
			{
				set = pawn.GetRJWPawnData().torso;
				if (set.NullOrEmpty())
				{
					var partBPR = Genital_Helper.get_torsoBPR(pawn);
					set = Genital_Helper.get_PartsHediffList(pawn, partBPR);
					pawn.GetRJWPawnData().torso = set;
				}

			}
			catch
			{
				var partBPR = Genital_Helper.get_torsoBPR(pawn);
				set = Genital_Helper.get_PartsHediffList(pawn, partBPR);
			}
			return set;
		}
		public static SexProps GetRMBSexPropsCache(this Pawn pawn)
		{
			SexProps set;
			set = pawn.GetRJWPawnData().SexProps;
			return set;
		}

		public static PawnData GetRJWPawnData(this Pawn pawn)
		{
			return SaveStorage.DataStore.GetPawnData(pawn);
		}
	}
}


code:
using HarmonyLib;
using RimWorld;
using System.Collections.Generic;
using System.Linq;
using Verse;
using Verse.AI;
using Multiplayer.API;
using rjw.Modules.Interactions;
using rjw.Modules.Interactions.Implementation;

namespace rjw
{
	[StaticConstructorOnStartup]
	static class RMB_Masturbate
	{

		static RMB_Masturbate()
		{
			Harmony harmony = new Harmony("rjw");
			//start sex options
			harmony.Patch(AccessTools.Method(typeof(RMB_Menu), "GenerateRMBOptions"), prefix: null,
				postfix: new HarmonyMethod(typeof(RMB_Masturbate), nameof(ChoicesAtFor)));
		}

		public static List<FloatMenuOption> ChoicesAtFor(List<FloatMenuOption> __instance, Pawn pawn, LocalTargetInfo target)
		{
			FloatMenuOption(pawn, ref __instance, ref target);
			return __instance;
		}
		public static void FloatMenuOption(Pawn pawn, ref List<FloatMenuOption> opts, ref LocalTargetInfo target)
		{
			opts.AddRange(GenerateRMBOptions(pawn, target).Where(x => x.action != null));
		}

		public static List<FloatMenuOption> GenerateRMBOptions(Pawn pawn, LocalTargetInfo target)
		{
			List<FloatMenuOption> opts = new List<FloatMenuOption>();
			FloatMenuOption option = null;

			if (target.Pawn == pawn)
			{
				option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Masturbate".Translate(), delegate ()
				{
					FloatMenuUtility.MakeMenu(GenerateSoloSexRoleOptions(pawn, target).Where(x => x.action != null), (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
				}, MenuOptionPriority.High), pawn, target);
				opts.AddDistinct(option);
			}
			return opts;
		}

		//this options probably can be merged into 1, but for now/testing keep it separate
		public static List<FloatMenuOption> GenerateSoloSexRoleOptions(Pawn pawn, LocalTargetInfo target)
		{
			List<FloatMenuOption> opts = new List<FloatMenuOption>();
			FloatMenuOption option = null;

			option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Masturbate_Bed".Translate(), delegate ()
			{
				Find.Targeter.BeginTargeting(TargetParemetersMasturbationChairOrBed(target), (LocalTargetInfo targetThing) =>
				{
					FloatMenuUtility.MakeMenu(GenerateSoloSexPoseOptions(pawn, targetThing).Where(x => x.action != null), (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
				});
			}, MenuOptionPriority.High), pawn, target);
			opts.AddDistinct(option);

			option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Masturbate_At".Translate(), delegate ()
			{
				Find.Targeter.BeginTargeting(TargetParemetersMasturbationLoc(target), (LocalTargetInfo targetThing) =>
				{
					FloatMenuUtility.MakeMenu(GenerateSoloSexPoseOptions(pawn, targetThing).Where(x => x.action != null), (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
				});
			}, MenuOptionPriority.High), pawn, target);
			opts.AddDistinct(option);

			option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Masturbate_Here".Translate(), delegate ()
			{
				FloatMenuUtility.MakeMenu(GenerateSoloSexPoseOptions(pawn, target).Where(x => x.action != null), (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
			}, MenuOptionPriority.High), pawn, target);
			opts.AddDistinct(option);
			return opts;
		}

		public static TargetingParameters TargetParemetersMasturbationChairOrBed(LocalTargetInfo target)
		{
			return new TargetingParameters()
			{
				canTargetBuildings = true,
				mapObjectTargetsMustBeAutoAttackable = false,
				validator = (TargetInfo target) =>
				{
					if (!target.HasThing)
						return false;
					Building building = target.Thing as Building;
					if (building == null)
						return false;
					if (building.def.building.isSittable)
						return true;
					if (building is Building_Bed)
						return true;
					return false;
				}
			};
		}
		public static TargetingParameters TargetParemetersMasturbationLoc(LocalTargetInfo target)
		{
			return new TargetingParameters()
			{
				canTargetLocations = true,
				mapObjectTargetsMustBeAutoAttackable = false,
				validator = (TargetInfo target) =>
				{
					if (!target.HasThing)
						return true;
					return false;
				}
			};
		}
		public static List<FloatMenuOption> GenerateSoloSexPoseOptions(Pawn pawn, LocalTargetInfo target)
		{
			List<FloatMenuOption> opts = new List<FloatMenuOption>();
			FloatMenuOption option = null;
			var job = xxx.Masturbate;

			List<InteractionDef> validintdefs = new List<InteractionDef>();
			foreach (InteractionDef d in SexUtility.SexInterractions)
			{
				var interaction = Modules.Interactions.Helpers.InteractionHelper.GetWithExtension(d);
				if (interaction.Extension.rjwSextype != xxx.rjwSextype.Masturbation.ToStringSafe())
					continue;

				ILewdInteractionValidatorService service = LewdInteractionValidatorService.Instance;
				if (service.IsValid(d, pawn, pawn))
					validintdefs.Add(d);
			}

			foreach (InteractionDef dictionaryKey in validintdefs)
			{
				var interaction = Modules.Interactions.Helpers.InteractionHelper.GetWithExtension(dictionaryKey);
				var dev = "";
				if (RJWSettings.DevMode)
					dev = " ( defName: " + dictionaryKey.defName + ")";

				var label = interaction.Extension.RMBLabel.CapitalizeFirst() + dev;
				option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption(label, delegate ()
				{
					RMB_Menu.HaveSex(pawn, job, target, dictionaryKey);
				}, MenuOptionPriority.High), pawn, target);
				opts.AddDistinct(option);
			}

			if (opts.NullOrEmpty())
				opts.AddDistinct(new FloatMenuOption("none", null));

			return opts;
		}
	}
}


code:
using HarmonyLib;
using RimWorld;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Verse;
using Verse.AI;
using Multiplayer.API;
using rjw.Modules.Interactions.Enums;
using rjw.Modules.Interactions;
using rjw.Modules.Interactions.Implementation;
using rjw.Modules.Interactions.Objects;
using static RimWorld.MechClusterSketch;

namespace rjw
{
	[StaticConstructorOnStartup]
	static class RMB_Menu
	{
		static RMB_Menu()
		{
			Harmony harmony = new Harmony("rjw");
			//start sex options
			harmony.Patch(AccessTools.Method(typeof(FloatMenuMakerMap), "ChoicesAtFor"), prefix: null,
				postfix: new HarmonyMethod(typeof(RMB_Menu), nameof(ChoicesAtFor)));
		}

		//show rmb on
		public static TargetingParameters TargetParameters
		{
			get
			{
				if (targetParameters == null)
				{
					targetParameters = new TargetingParameters()
					{
						canTargetHumans = true,
						canTargetAnimals = true,
						canTargetItems = true,
						mapObjectTargetsMustBeAutoAttackable = false,
					};
				}
				return targetParameters;
			}
		}

		private static TargetingParameters targetParameters = null;
		private static Vector3 rjwclickPos;

		public static TargetingParameters TargetParemetersMasturbationChairOrBed(LocalTargetInfo target)
		{
			return new TargetingParameters()
			{
				canTargetBuildings = true,
				mapObjectTargetsMustBeAutoAttackable = false,
				validator = (TargetInfo target) =>
				{
					if (!target.HasThing)
						return false;
					Building building = target.Thing as Building;
					if (building == null)
						return false;
					if (building.def.building.isSittable)
						return true;
					if (building is Building_Bed)
						return true;
					return false;
				}
			};
		}

		public static TargetingParameters TargetParemetersMasturbationLoc(LocalTargetInfo target)
		{
			return new TargetingParameters()
			{
				canTargetLocations = true,
				mapObjectTargetsMustBeAutoAttackable = false,
				validator = (TargetInfo target) =>
				{
					if (!target.HasThing)
						return true;
					return false;
				}
			};
		}

		//TODO: dildo selection for masturbation/sex
		public static TargetingParameters TargetParemetersDildos(LocalTargetInfo target)
		{
			return new TargetingParameters()
			{
				canTargetItems = true,
				mapObjectTargetsMustBeAutoAttackable = false,
				validator = ((TargetInfo target) =>
				{
					if (!target.HasThing)
						return false;
					Thing dildo = target.Thing as Thing;
					if (dildo == null)
						return false;

					return true;
				})
			};
		}

		public static List<FloatMenuOption> ChoicesAtFor(List<FloatMenuOption> __instance, Vector3 clickPos, Pawn pawn, bool suppressAutoTakeableGoto = false)
		{
			rjwclickPos = clickPos;
			SexFloatMenuOption(pawn, ref __instance);
			return __instance;
		}
		public static void SexFloatMenuOption(Pawn pawn, ref List<FloatMenuOption> opts)
		{
			if (!ShowRMB(pawn)) return;

			//Log.Message("show options");

			// Find valid targets for sex.
			var validtargets = GenUI.TargetsAt(rjwclickPos, TargetParameters);
			//Log.Message("targets count " + validtargets.Count());

			foreach (LocalTargetInfo target in validtargets)
			{
				if (target.Pawn != null && target.Pawn.Drafted)
					continue;
				// Ensure target is reachable.
				if (!pawn.CanReach(target, PathEndMode.ClosestTouch, Danger.Deadly))
				{
					//option = new FloatMenuOption("CannotReach".Translate(target.Thing.LabelCap, target.Thing) + " (" + "NoPath".Translate() + ")", null);
					continue;
				}

				//Log.Message("target " +  target.Label);
				opts.AddRange(GenerateRMBOptions(pawn, target).Where(x => x.action != null));
				//sex-role?-pose ?
				//rjw?-sex(do fuck/rape checks)-role?-pose ?
				//sex-rjwsextype?-interactiondefs?
			}
		}

		public static bool ShowRMB(Pawn pawn)
		{
			// If the pawn in question cannot take jobs, don't bother.
			if (pawn.jobs == null)
				return false;

			// If the pawn is drafted - quit.
			if (pawn.Drafted)
				return false;

			// Getting raped - no control
			if (pawn.jobs.curDriver is JobDriver_SexBaseRecieverRaped)
				return false;

			//is colonist?, is hospitality colonist/guest?, no control for guests
			if (!pawn.IsFreeColonist || pawn.Faction == null || pawn.GetExtraHomeFaction(null) != null)
				return false;

			//not hero mode or override_control - quit
			if (!(RJWSettings.RPG_hero_control || RJWSettings.override_control))
				return false;

			var HeroOK0 = false;    //is hero
			var HeroOK1 = false;    //owned hero?
			var HeroOK2 = false;    //not owned hero? maybe prison check etc in future
									// || xxx.is_slave(pawn)
			if (RJWSettings.RPG_hero_control)
			{
				HeroOK0 = pawn.IsDesignatedHero();
				HeroOK1 = HeroOK0 && pawn.IsHeroOwner();
				HeroOK2 = HeroOK0 && !pawn.IsHeroOwner();

				//Log.Message("show options HeroOK0 " + HeroOK0);
				//Log.Message("show options HeroOK1 " + HeroOK1);
				//Log.Message("show options HeroOK2 " + HeroOK2);

			}
			else if (!RJWSettings.override_control)
				return false;

			//not hero, not override_control - quit
			if (!HeroOK0 && !RJWSettings.override_control)
				return false;

			//not owned hero - quit
			if (HeroOK0 && HeroOK2)
				return false;

			if (pawn.IsPrisoner || xxx.is_slave(pawn))
				return false;

			return true;
		}

		public static List<FloatMenuOption> GenerateRMBOptions(Pawn pawn, LocalTargetInfo target)
		{
			List<FloatMenuOption> opts = new List<FloatMenuOption>();
			FloatMenuOption option = null;

			if (ModsConfig.BiotechActive)
			{
				if (MechanitorUtility.IsMechanitor(pawn))
				{
					if(target.Pawn != null)
					{
						if (target.Pawn.IsPregnant(true))
						{
							Hediff_MechanoidPregnancy pregnancy = (Hediff_MechanoidPregnancy)target.Pawn.health.hediffSet.GetFirstHediffOfDef(HediffDef.Named("RJW_pregnancy_mech"));
							if (pregnancy != null)
							{
								if (pregnancy.is_checked && !pregnancy.is_hacked)
								{
									option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_HackMechPregnancy".Translate(), delegate ()
									{
										Job job22 = JobMaker.MakeJob(DefDatabase<JobDef>.GetNamed("RJW_HackMechPregnancy"), target.Pawn);
										pawn.jobs.TryTakeOrderedJob(job22, JobTag.Misc);
									}, MenuOptionPriority.High), pawn, target);
									opts.AddDistinct(option);
								}
							}
						}
					}
				}
				else
				{
					if(target.Pawn != null)
						if (pawn.IsColonyMechPlayerControlled)
						{
							//mechimplant

							//			option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Masturbate".Translate(), delegate ()
							//			{
							//				FloatMenuUtility.MakeMenu(GenerateSoloSexPoseOptions(pawn, target).Where(x => x.action != null), (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);

							//			}, MenuOptionPriority.High), pawn, target);
							//			opts.AddDistinct(option);
						}
				}
			}

			// Already doing sex.
			// Pose switch
			//if (pawn.jobs.curDriver is JobDriver_Sex)
			//{
			//	// Masturbating
			//	if (target.Pawn == pawn)
			//	{
			//		if (pawn.jobs.curDriver is JobDriver_Masturbate)
			//		{
			//			option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Masturbate".Translate(), delegate ()
			//			{
			//				FloatMenuUtility.MakeMenu(GenerateSoloSexPoseOptions(pawn, target).Where(x => x.action != null), (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);

			//			}, MenuOptionPriority.High), pawn, target);
			//			opts.AddDistinct(option);
			//		}
			//	}
			//	// TODO: Add pose switch
			//	return opts;
			//}

			return opts;
		}

		public static List<FloatMenuOption> GenerateNonSoloSexRoleOptions(Pawn pawn, LocalTargetInfo target, JobDef job, bool rape, bool reverse = false)
		{
			List<FloatMenuOption> opts = new List<FloatMenuOption>();
			FloatMenuOption option = null;

			var partner = target.Pawn;

			if (target.Thing is Corpse)
				partner = (target.Thing as Corpse).InnerPawn;

			var arraytype = 0;      //sex
			if (xxx.is_animal(partner))
			{
				arraytype = 2;      //bestiality/breeding
			}
			else if (rape)
			{
				arraytype = 1;      //rape
			}
			List<InteractionDef> validintdefs = new List<InteractionDef>();
			foreach (InteractionDef d in SexUtility.SexInterractions)
			{
				var interaction = Modules.Interactions.Helpers.InteractionHelper.GetWithExtension(d);
				if (interaction.Extension.rjwSextype == xxx.rjwSextype.None.ToStringSafe())
					continue;
				if (interaction.Extension.rjwSextype == xxx.rjwSextype.Masturbation.ToStringSafe())
					continue;

				if (
					(interaction.HasInteractionTag(InteractionTag.Consensual) && arraytype == 0) ||
					(interaction.HasInteractionTag(InteractionTag.Rape) && arraytype == 1) ||
					(interaction.HasInteractionTag(InteractionTag.Bestiality) && arraytype == 2)
					)
					if (reverse && interaction.HasInteractionTag(InteractionTag.Reverse) || (!reverse && !interaction.HasInteractionTag(InteractionTag.Reverse)))
					{
						ILewdInteractionValidatorService service = LewdInteractionValidatorService.Instance;
						if (service.IsValid(d, pawn, partner))
							validintdefs.Add(d);
					}
			}

			foreach (InteractionDef dictionaryKey in validintdefs)
			{
				var interaction = Modules.Interactions.Helpers.InteractionHelper.GetWithExtension(dictionaryKey);
				var dev = "";
				if (RJWSettings.DevMode)
					dev = " ( defName: " + dictionaryKey.defName + ")";

				var label = interaction.Extension.RMBLabel.CapitalizeFirst() + dev;
				option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption(label, delegate ()
				{
					HaveSex(pawn, job, target, dictionaryKey);
				}, MenuOptionPriority.High), pawn, target);
				opts.AddDistinct(option);
			}

			if (opts.NullOrEmpty())
				opts.AddDistinct(new FloatMenuOption("none", null));

			return opts;
		}

		//multiplayer synch actions

		[SyncMethod]
		public static void HaveSex(Pawn pawn, JobDef jobDef, LocalTargetInfo target, InteractionDef dictionaryKey)
		{
			bool rape;
			Pawn partner = null;

			if (target.Thing is Corpse)
				partner = (target.Thing as Corpse).InnerPawn;
			else if (target.Pawn == null || pawn == target.Pawn) // masturbation
			{
			}
			else
				partner = target.Pawn;

			InteractionWithExtension interaction = Modules.Interactions.Helpers.InteractionHelper.GetWithExtension(dictionaryKey);

			rape = interaction.HasInteractionTag(InteractionTag.Rape);

			Job job;
			if (jobDef == xxx.casual_sex)
				job = new Job(jobDef, target, partner.CurrentBed());
			else if (jobDef == xxx.bestialityForFemale)
				job = new Job(jobDef, target, pawn.ownership.OwnedBed);
			else if (jobDef == xxx.Masturbate)
			{
				job = new Job(jobDef, pawn, null, target.Cell);
				partner = pawn;
			}
			else
				job = new Job(jobDef, target);

			var SP = new SexProps();
			SP.pawn = pawn;
			SP.partner = partner;
			SP.sexType = SexUtility.rjwSextypeGet(dictionaryKey); ;
			SP.isRape = rape;
			SP.isRapist = rape;
			SP.canBeGuilty = false; //pawn.IsHeroOwner();//TODO: fix for MP someday
			SP.dictionaryKey = dictionaryKey;
			SP.rulePack = SexUtility.SexRulePackGet(dictionaryKey);

			pawn.GetRJWPawnData().SexProps = SP;
			pawn.jobs.EndCurrentJob(JobCondition.InterruptForced);
			pawn.jobs.TryTakeOrderedJob(job);
		}
	}
}


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

namespace rjw
{
	[StaticConstructorOnStartup]
	static class RMB_Rape
	{

		static RMB_Rape()
		{
			Harmony harmony = new Harmony("rjw");
			//start sex options
			harmony.Patch(AccessTools.Method(typeof(RMB_Menu), "GenerateRMBOptions"), prefix: null,
				postfix: new HarmonyMethod(typeof(RMB_Rape), nameof(ChoicesAtFor)));
		}

		public static List<FloatMenuOption> ChoicesAtFor(List<FloatMenuOption> __instance, Pawn pawn, LocalTargetInfo target)
		{
			FloatMenuOption(pawn, ref __instance, ref target);
			return __instance;
		}
		public static void FloatMenuOption(Pawn pawn, ref List<FloatMenuOption> opts, ref LocalTargetInfo target)
		{
			opts.AddRange(GenerateRMBOptions(pawn, target).Where(x => x.action != null));
			opts.AddRange(GenerateRMBOptions(pawn, target, true).Where(x => x.action != null));
		}

		public static List<FloatMenuOption> GenerateRMBOptions(Pawn pawn, LocalTargetInfo target, bool reverse = false)
		{
			List<FloatMenuOption> opts = new List<FloatMenuOption>();
			FloatMenuOption option = null;
			bool rape = true;

			if (target.Pawn != pawn)
				if (target.Thing != pawn as Thing)// && !MP.IsInMultiplayer)
				{
					if (target.Pawn != null || target.Thing is Corpse)
						if (xxx.can_rape(pawn, true))
						{
							//Log.Message("targets can_rape " + target.Label);
							string text = null;
							Action action = null;

							if (target.Thing is Corpse && RJWSettings.necrophilia_enabled)
							{
								if (reverse)
								{
									text = "RJW_RMB_RapeCorpse_Reverse".Translate() + ((Corpse)target.Thing).InnerPawn.NameShortColored;
								}
								else
									text = "RJW_RMB_RapeCorpse".Translate() + ((Corpse)target.Thing).InnerPawn.NameShortColored;
								action = delegate ()
								{
									JobDef job = xxx.RapeCorpse;
									var validinteractions = RMB_Menu.GenerateNonSoloSexRoleOptions(pawn, target, job, rape, reverse).Where(x => x.action != null);
									if (validinteractions.Any())
										FloatMenuUtility.MakeMenu(validinteractions, (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
									else
										Messages.Message("No valid interactions found for " + text, MessageTypeDefOf.RejectInput, false);

								};
							}
							else if (target.Pawn != null && xxx.can_be_fucked(target.Pawn) &&
							(xxx.is_human(target.Pawn) || (xxx.is_animal(target.Pawn) && RJWSettings.bestiality_enabled)))
									//(xxx.is_human(target.Pawn)))
							{
								//Log.Message("targets can_rape 1 " + target.Label);
								if (target.Pawn.HostileTo(pawn))
								{
									//Log.Message("targets can_rape HostileTo " + target.Label);
									if (target.Pawn.Downed)
									{
										if (reverse)
										{
											text = "RJW_RMB_Rape_Reverse".Translate() + target.Pawn.NameShortColored;
										}
										else
											text = "RJW_RMB_Rape".Translate() + target.Pawn.NameShortColored;
										action = delegate ()
										{
											JobDef job = xxx.RapeEnemy;
											var validinteractions = RMB_Menu.GenerateNonSoloSexRoleOptions(pawn, target, job, rape, reverse).Where(x => x.action != null);
											if (validinteractions.Any())
												FloatMenuUtility.MakeMenu(validinteractions, (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
											else
												Messages.Message("No valid interactions found for " + text, MessageTypeDefOf.RejectInput, false);
										};
									}
								}
								else if (xxx.is_animal(target.Pawn) && xxx.can_fuck(pawn))
								{
									if (reverse)
									{
										text = "RJW_RMB_RapeAnimal_Reverse".Translate() + target.Pawn.NameShortColored;
									}
									else
										text = "RJW_RMB_RapeAnimal".Translate() + target.Pawn.NameShortColored;
									action = delegate ()
									{
										JobDef job = xxx.bestiality;
										var validinteractions = RMB_Menu.GenerateNonSoloSexRoleOptions(pawn, target, job, rape, reverse).Where(x => x.action != null);
										if (validinteractions.Any())
											FloatMenuUtility.MakeMenu(validinteractions, (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
										else
											Messages.Message("No valid interactions found for " + text, MessageTypeDefOf.RejectInput, false);
									};
								}
								else if (target.Pawn.IsDesignatedComfort())
								{
									//Log.Message("targets can_rape IsDesignatedComfort " + target.Label);
									if (reverse)
									{
										text = "RJW_RMB_Rape_Reverse".Translate() + target.Pawn.NameShortColored;
									}
									else
										text = "RJW_RMB_Rape".Translate() + target.Pawn.NameShortColored;
									action = delegate ()
									{
										JobDef job = xxx.RapeCP;
										var validinteractions = RMB_Menu.GenerateNonSoloSexRoleOptions(pawn, target, job, rape, reverse).Where(x => x.action != null);
										if (validinteractions.Any())
											FloatMenuUtility.MakeMenu(validinteractions, (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
										else
											Messages.Message("No valid interactions found for " + text, MessageTypeDefOf.RejectInput, false);
									};
								}
								else if (xxx.can_get_raped(target.Pawn) && (xxx.get_vulnerability(target.Pawn) >= xxx.get_vulnerability(pawn)))
								{
									//Log.Message("targets can_rape else " + target.Label);
									if (reverse)
									{
										text = "RJW_RMB_Rape_Reverse".Translate() + target.Pawn.NameShortColored;
									}
									else
										text = "RJW_RMB_Rape".Translate() + target.Pawn.NameShortColored;
									action = delegate ()
									{
										JobDef job = xxx.RapeRandom;
										var validinteractions = RMB_Menu.GenerateNonSoloSexRoleOptions(pawn, target, job, rape, reverse).Where(x => x.action != null);
										if (validinteractions.Any())
											FloatMenuUtility.MakeMenu(validinteractions, (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
										else
											Messages.Message("No valid interactions found for " + text, MessageTypeDefOf.RejectInput, false);
									};
								}
							}

							option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption(text, action, MenuOptionPriority.High), pawn, target);
							opts.AddDistinct(option);
						}
				}
			return opts;
		}
	}
}


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

namespace rjw
{
	[StaticConstructorOnStartup]
	static class RMB_Sex
	{

		static RMB_Sex()
		{
			Harmony harmony = new Harmony("rjw");
			//start sex options
			harmony.Patch(AccessTools.Method(typeof(RMB_Menu), "GenerateRMBOptions"), prefix: null,
				postfix: new HarmonyMethod(typeof(RMB_Sex), nameof(ChoicesAtFor)));
		}

		public static List<FloatMenuOption> ChoicesAtFor(List<FloatMenuOption> __instance, Pawn pawn, LocalTargetInfo target)
		{
			FloatMenuOption(pawn, ref __instance, ref target);
			return __instance;
		}
		public static void FloatMenuOption(Pawn pawn, ref List<FloatMenuOption> opts, ref LocalTargetInfo target)
		{
			opts.AddRange(GenerateRMBOptions(pawn, target).Where(x => x.action != null));
			opts.AddRange(GenerateRMBOptions(pawn, target, true).Where(x => x.action != null));
		}

		public static List<FloatMenuOption> GenerateRMBOptions(Pawn pawn, LocalTargetInfo target, bool reverse = false)
		{
			List<FloatMenuOption> opts = new List<FloatMenuOption>();
			FloatMenuOption option = null;
			bool rape = false;

			if (target.Pawn != pawn)
				if (target.Thing != pawn as Thing)// && !MP.IsInMultiplayer)
				{
					if (target.Pawn != null)
						if (!target.Pawn.Downed && !target.Pawn.HostileTo(pawn) && (xxx.can_be_fucked(target.Pawn) || xxx.can_fuck(target.Pawn)))
						{
							string text = null;
							//Action action = null;
							if (xxx.is_human(target.Pawn) && SexAppraiser.would_fuck(target.Pawn, pawn) > 0.1f)
							{
								if (reverse)
								{
									text = "RJW_RMB_Sex_Reverse".Translate() + target.Pawn.NameShortColored;
								}
								else
									text = "RJW_RMB_Sex".Translate() + target.Pawn.NameShortColored;
								option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption(text, delegate ()
								{
									JobDef job = null;
									if (target.Pawn.InBed())
										job = xxx.casual_sex;
									else
										job = xxx.quick_sex;

									var validinteractions = RMB_Menu.GenerateNonSoloSexRoleOptions(pawn, target, job, rape, reverse).Where(x => x.action != null);
									if (validinteractions.Any())
										FloatMenuUtility.MakeMenu(validinteractions, (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
									else
										Messages.Message("No valid interactions found for " + text, MessageTypeDefOf.RejectInput, false);
								}, MenuOptionPriority.High), pawn, target);
								opts.AddDistinct(option);
							}
							else if (xxx.is_animal(target.Pawn) && RJWSettings.bestiality_enabled && target.Pawn.Faction == pawn.Faction)
							{
								if (pawn.ownership.OwnedBed != null && target.Pawn.CanReach(pawn.ownership.OwnedBed, PathEndMode.OnCell, Danger.Some) && !AnimalPenUtility.NeedsToBeManagedByRope(target.Pawn))
								{
									if (reverse)
									{
										text = "RJW_RMB_Bestiality_Reverse".Translate() + target.Pawn.NameShortColored;
									}
									else
										text = "RJW_RMB_Bestiality".Translate() + target.Pawn.NameShortColored;
									option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption(text, delegate ()
									{
										JobDef job = xxx.bestialityForFemale;
										var validinteractions = RMB_Menu.GenerateNonSoloSexRoleOptions(pawn, target, job, rape, reverse).Where(x => x.action != null);
										if (validinteractions.Any())
											FloatMenuUtility.MakeMenu(validinteractions, (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
										else
											Messages.Message("No valid interactions found for " + text, MessageTypeDefOf.RejectInput, false);
									}, MenuOptionPriority.High), pawn, target);
									opts.AddDistinct(option);
								}
							}
						}
				}
			return opts;
		}
	}
}


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

namespace rjw
{
	[StaticConstructorOnStartup]
	static class RMB_Socialize
	{

		static RMB_Socialize()
		{
			Harmony harmony = new Harmony("rjw");
			//start sex options
			harmony.Patch(AccessTools.Method(typeof(RMB_Menu), "GenerateRMBOptions"), prefix: null,
				postfix: new HarmonyMethod(typeof(RMB_Socialize), nameof(ChoicesAtFor)));
		}

		public static List<FloatMenuOption> ChoicesAtFor(List<FloatMenuOption> __instance, Pawn pawn, LocalTargetInfo target)
		{
			FloatMenuOption(pawn, ref __instance, ref target);
			return __instance;
		}
		public static void FloatMenuOption(Pawn pawn, ref List<FloatMenuOption> opts, ref LocalTargetInfo target)
		{
			opts.AddRange(GenerateRMBOptions(pawn, target).Where(x => x.action != null));
		}

		public static List<FloatMenuOption> GenerateRMBOptions(Pawn pawn, LocalTargetInfo target)
		{
			List<FloatMenuOption> opts = new List<FloatMenuOption>();
			FloatMenuOption option = null;

			if (target.Pawn != null && target.Pawn != pawn && !pawn.interactions.InteractedTooRecentlyToInteract() && pawn.interactions.CanInteractNowWith(target.Pawn))
			{
				//if (pawn.jobs.curDriver is JobDriver_Masturbate)
				{
					option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Social".Translate(), delegate ()
					{
						FloatMenuUtility.MakeMenu(GenerateSocialOptions(pawn, target).Where(x => x.action != null), (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
					}, MenuOptionPriority.High), pawn, target);
					opts.AddDistinct(option);
				}
			}
			return opts;
		}

		public static List<FloatMenuOption> GenerateSocialOptions(Pawn pawn, LocalTargetInfo target)
		{
			List<FloatMenuOption> opts = new List<FloatMenuOption>();
			FloatMenuOption option = null;

			option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Insult".Translate(), delegate ()
			{
				Socialize(pawn, target, InteractionDefOf.Insult);
			}, MenuOptionPriority.High), pawn, target);
			opts.AddDistinct(option);

			if (!pawn.HostileTo(target.Pawn))
			{
				option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_SocialFight".Translate(), delegate ()
				{
					SocializeFight(pawn, target);
				}, MenuOptionPriority.High), pawn, target);
				opts.AddDistinct(option);

				option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Chat".Translate(), delegate ()
				{
					Socialize(pawn, target, InteractionDefOf.Chitchat);
				}, MenuOptionPriority.High), pawn, target);
				opts.AddDistinct(option);

				// OP shit +12 relations, enable in cheat menu
				if (RJWSettings.Allow_RMB_DeepTalk)
				{
					option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_DeepTalk".Translate(), delegate ()
					{
						pawn.interactions.TryInteractWith(target.Pawn, InteractionDefOf.DeepTalk);
					}, MenuOptionPriority.High), pawn, target);
					opts.AddDistinct(option);
				}

				if (!LovePartnerRelationUtility.LovePartnerRelationExists(pawn, target.Pawn))
				{
					option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_RomanceAttempt".Translate(), delegate ()
					{
						Socialize(pawn, target, InteractionDefOf.RomanceAttempt);
					}, MenuOptionPriority.High), pawn, target);
					opts.AddDistinct(option);
				}

				if (pawn.relations.DirectRelationExists(PawnRelationDefOf.Lover, target.Pawn) || pawn.relations.DirectRelationExists(PawnRelationDefOf.Fiance, target.Pawn))
				{
					option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_MarriageProposal".Translate(), delegate ()
					{
						Socialize(pawn, target, InteractionDefOf.MarriageProposal);
					}, MenuOptionPriority.High), pawn, target);
					opts.AddDistinct(option);
				}
			}

			return opts;
		}

		//multiplayer synch actions
		[SyncMethod]
		static void SocializeFight(Pawn pawn, LocalTargetInfo target)
		{
			pawn.interactions.StartSocialFight(target.Pawn);
		}
		[SyncMethod]
		static void Socialize(Pawn pawn, LocalTargetInfo target, InteractionDef interaction)
		{
			pawn.interactions.TryInteractWith(target.Pawn, interaction);
		}
	}
}