code:
using HarmonyLib;
using RimWorld;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;
using Verse;
using Verse.AI;

namespace rjw
{
	[StaticConstructorOnStartup]
	static class RMB_Menu
	{

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

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

		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 void SexFloatMenuOption(Vector3 clickPos, Pawn pawn, List<FloatMenuOption> opts)
		{
			// If the pawn in question cannot take jobs, don't bother.
			if (pawn.jobs == null)
				return;

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

			// No menu for you - quit.
			//if (!SaveStorage.DataStore.GetPawnData(pawn).ShowRMB_Menu)
			//	return;

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

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

			//TODO: move checks below into ShowRMB_Menu
			//not heromode or override_control - quit
			if (!(RJWSettings.RPG_hero_control || RJWSettings.override_control))
				return;

			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();
			}
			else if (!RJWSettings.override_control)
				return;

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

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

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

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

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

			//var t = DefDatabase<InteractionDef>.AllDefs;
			//Log.Message("count 1 " + t.Count());
			//var t1 = t.Where(x => !x.modExtensions.NullOrEmpty());
			//Log.Message("count 2 " + t1.Count());
			//foreach (var tt in t1)
			//{
			//	Log.Message("def " + tt.defName);
			//	if (tt.HasModExtension<InteractionExtension>())
			//	{
			//		Log.Message("rjw InteractionExtension def ");
			//		var ttt = tt.GetModExtension<InteractionExtension>();
			//		var tags = ttt.tags.ToCommaList();
			//		Log.Message("tags " + tags);
			//	}
			//}

			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(GenerateSexOptions(pawn, target).Where(x => x.action != null));
				//sex-role?-pose ?
				//rjw?-sex(do fuck/rape checks)-role?-pose ?

			}
		}

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

			// social stuff
			//if (pawn.jobs.curDriver is JobDriver_Sex)
			{
				// Masturbating
				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);
					}
				}
			}

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

			// Start
			// Add menu options to masturbate, if self
			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);
			}

			// Add menu options for con sex, if not self
			if (target.Thing != pawn as Thing)
			{
				if (target.Pawn != null)
					if (!target.Pawn.Downed && !target.Pawn.HostileTo(pawn) && (xxx.can_be_fucked(target.Pawn) || xxx.can_fuck(target.Pawn)))
					{
						//TODO change whore JD to work manually
						// Add menu option to whore sex with target
						//option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("Solicit " + target.Pawn.Name, delegate ()
						//{
						//	if (!target.Pawn.IsColonist && !target.Pawn.IsPrisoner && pawn.CurrentBed() != null)
						//		pawn.jobs.TryTakeOrderedJob(new Job(xxx.whore_is_serving_visitors, target.Pawn, pawn.CurrentBed()));
						//}, MenuOptionPriority.High), pawn, target);
						//opts.AddDistinct(option);

						// Add menu option to have sex with target, if target finds you sexy enough
						if (xxx.is_human(target.Pawn) && SexAppraiser.would_fuck(target.Pawn, pawn) > 0.1f)
						{
							option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Sex".Translate() + target.Pawn.NameShortColored, delegate ()
							{
								Job job = null;
								if (target.Pawn.InBed())
									job = new Job(xxx.casual_sex, target, target.Pawn.CurrentBed());
								else
									job = new Job(xxx.quick_sex, target);

								FloatMenuUtility.MakeMenu(GenerateSexRoleOptions(pawn, target, false, job).Where(x => x.action != null), (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
							}, 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))
							{
								// Add menu option to have sex with animal(invite to bed)
								option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Sex".Translate() + target.Pawn.NameShortColored, delegate ()
								{
									Job job = new Job(xxx.bestialityForFemale, target, pawn.ownership.OwnedBed);
									FloatMenuUtility.MakeMenu(GenerateSexRoleOptions(pawn, target, false, job).Where(x => x.action != null), (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
									//pawn.jobs.TryTakeOrderedJob(new Job(xxx.quick_sex, target.Pawn));
								}, MenuOptionPriority.High), pawn, target);
								opts.AddDistinct(option);
							}
							//	// Add menu option to have sex with animal(service animal on spot)
							//	option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Sex".Translate() + target.Pawn.NameShortColored, delegate ()
							//	{
							//		pawn.jobs.TryTakeOrderedJob(new Job(xxx.bestialityForFemale, target.Pawn, pawn.ownership.OwnedBed));
							//		//pawn.jobs.TryTakeOrderedJob(new Job(xxx.quick_sex, target.Pawn));
							//	}, MenuOptionPriority.High), pawn, target);
							//	opts.AddDistinct(option);

						}
					}

					// Add menu options for non con sex, if not self
					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)
						{
							text = "RJW_RMB_RapeCorpse".Translate() + ((Corpse)target.Thing).InnerPawn.NameShortColored;
							action = delegate ()
							{
								Job job = new Job(xxx.RapeCorpse, target);
								FloatMenuUtility.MakeMenu(GenerateSexRoleOptions(pawn, target, true, job).Where(x => x.action != null), (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
							};
						}
						else if (target.Pawn != null && xxx.can_be_fucked(target.Pawn) && (xxx.is_human(target.Pawn) || (xxx.is_animal(target.Pawn) && RJWSettings.bestiality_enabled)))
						{
							//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)
								{
									text = "RJW_RMB_RapeEnemy".Translate() + target.Pawn.NameShortColored;
									action = delegate ()
									{
										Job job = new Job(xxx.RapeEnemy, target);
										FloatMenuUtility.MakeMenu(GenerateSexRoleOptions(pawn, target, true, job).Where(x => x.action != null), (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
									};
								}
							}
							else if (xxx.is_animal(target.Pawn) && xxx.can_fuck(pawn))
							{
								text = "RJW_RMB_RapeAnimal".Translate() + target.Pawn.NameShortColored;
								action = delegate ()
								{
									Job job = new Job(xxx.bestiality, target);
									FloatMenuUtility.MakeMenu(GenerateSexRoleOptions(pawn, target, true, job).Where(x => x.action != null), (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
								};
							}
							else if (target.Pawn.IsDesignatedComfort())
							{
								//Log.Message("targets can_rape IsDesignatedComfort " + target.Label);
								text = "RJW_RMB_RapeCP".Translate() + target.Pawn.NameShortColored;
								action = delegate ()
								{
									Job job = new Job(xxx.RapeCP, target);
									FloatMenuUtility.MakeMenu(GenerateSexRoleOptions(pawn, target, true, job).Where(x => x.action != null), (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
								};
							}
							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);
								text = "RJW_RMB_Rape".Translate() + target.Pawn.NameShortColored;
								action = delegate ()
								{
									Job job = new Job(xxx.RapeRandom, target);
									FloatMenuUtility.MakeMenu(GenerateSexRoleOptions(pawn, target, true, job).Where(x => x.action != null), (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
									//pawn.jobs.TryTakeOrderedJob(new Job(xxx.RapeRandom, target.Pawn));
								};
							}

						}

						option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption(text, action, MenuOptionPriority.High), pawn, target);
						opts.AddDistinct(option);
					}
				}
			return opts;
		}
		public static List<FloatMenuOption> GenerateSexRoleOptions(Pawn pawn, LocalTargetInfo target, bool rape, Job job)
		{
			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 sexScores = SexUtility.DetermineSexScores(pawn, partner, rape, false, pawn);
			var arraytype = 0;
			if (xxx.is_animal(partner) && !rape)
			{
				arraytype = 2;
			}
			else if (rape)
			{
				arraytype = 1;
			}

			for (int i = 0; i < SexUtility.rmb_dictionarykeys.GetLength(0); i++)
			{
				InteractionDef dictionaryKey = null;
				if (sexScores[i] > 0)
					dictionaryKey = SexUtility.rmb_dictionarykeys[i, arraytype];
				if (dictionaryKey != null)
				{
					//Log.Message("dictionarykeys id: " + i);
					//Log.Message("dictionaryKey " + dictionaryKey);

					var prefix = "Do ";
					var postfix = "";
					//if (rape)
					//	prefix = " - Rape";
					var t2 = i;
					var giving = pawn;
					var receiving = partner;
					if (t2 % 2 != 0 && !rape)
					{
						giving = partner;
						receiving = pawn;
						prefix = "Ask for ";
						if(arraytype == 2)
							prefix = "Provide for ";

						postfix = "";
					}
					option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption(prefix + dictionaryKey.label + postfix, delegate ()
					//option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("Commit " + dictionaryKey.LabelCap + t1, delegate ()
					{
						pawn.jobs.TryTakeOrderedJob(job);
						//if (pawn.jobs.TryTakeOrderedJob(new Job(xxx.quick_sex, partner)))
						//{
						//if (t1 == " - Receive")
						//{
						//Log.Message("i: " + t2 + " arraytype: " + arraytype);
						//Log.Message("job: " + pawn.jobs.curDriver);
						var rulepack = SexUtility.rulepacks[t2, arraytype];
						//Log.Message("rulepack: " + rulepack);
						var props = new SexProps(pawn, partner, SexUtility.sexActs[dictionaryKey], rape, giving, receiving, rulepack, dictionaryKey);
						//Log.Message("props: " + props);
						(pawn.jobs.curDriver as JobDriver_Sex).Sexprops = props;
						//Log.Message("fin: " + pawn.jobs.curDriver);
							//}
						//}

						//FloatMenuUtility.MakeMenu(GenerateSexPoseOptions(pawn, target).Where(x => x.action != null), (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);
					}, MenuOptionPriority.High), pawn, target);
					opts.AddDistinct(option);
				}
			}

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

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

			option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("give".Translate(), delegate ()
			{
				pawn.jobs.TryTakeOrderedJob(new Job(xxx.Masturbate, null, null, target.Cell));
			}, MenuOptionPriority.High), pawn, target);
			opts.AddDistinct(option);

			option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("receive".Translate(), delegate ()
			{
				pawn.jobs.TryTakeOrderedJob(new Job(xxx.Masturbate, null, null, target.Cell));
			}, 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 ()
			{
				pawn.interactions.TryInteractWith(target.Pawn, InteractionDefOf.Insult);
			}, MenuOptionPriority.High), pawn, target);
			opts.AddDistinct(option);

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

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

				// OP shit +12 relations
				//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 ()
					{
						pawn.interactions.TryInteractWith(target.Pawn, 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 ()
					{
						pawn.interactions.TryInteractWith(target.Pawn, InteractionDefOf.MarriageProposal);
					}, 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 List<FloatMenuOption> GenerateSoloSexPoseOptions(Pawn pawn, LocalTargetInfo target)
		{
			List<FloatMenuOption> opts = new List<FloatMenuOption>();
			FloatMenuOption option = null;

			var pawnDic = SexUtility.DetermineSexParts(pawn);

			bool pawnHasMouth = pawnDic.TryGetValue("HasMouth");
			bool pawnHasAnus = pawnDic.TryGetValue("HasAnus");
			bool pawnHasBigBreasts = pawnDic.TryGetValue("HasBigBreasts");
			bool pawnHasVagina = pawnDic.TryGetValue("HasVagina");
			bool pawnHasPenis = pawnDic.TryGetValue("HasPenis");
			bool pawnHasBigPenis = pawnDic.TryGetValue("HasBigPenis");
			bool pawnHasMultiPenis = pawnDic.TryGetValue("HasMultiPenis");
			bool pawnHasHands = pawnDic.TryGetValue("HasHands");
			bool pawnHasTail = pawnDic.TryGetValue("HasTail");

			if (pawnHasPenis && pawnHasHands)
			{
				option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Masturbate_Fap".Translate(), delegate ()
				{
					pawn.jobs.TryTakeOrderedJob(new Job(xxx.Masturbate, null, null, target.Cell));
				}, MenuOptionPriority.High), pawn, target);
				opts.AddDistinct(option);
			}

			if (pawnHasPenis && pawnHasMouth && pawnHasBigPenis)
			{
				option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Masturbate_AF".Translate(), delegate ()
				{
					pawn.jobs.TryTakeOrderedJob(new Job(xxx.Masturbate, null, null, target.Cell));
				}, MenuOptionPriority.High), pawn, target);
				opts.AddDistinct(option);
			}

			if (pawnHasVagina && pawnHasHands)
			{
				option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Masturbate_Schlic".Translate(), delegate ()
				{
					pawn.jobs.TryTakeOrderedJob(new Job(xxx.Masturbate, null, null, target.Cell));
				}, MenuOptionPriority.High), pawn, target);
				opts.AddDistinct(option);
			}

			if (pawnHasVagina && pawnHasTail)
				option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Masturbate_TJV".Translate(), delegate ()
				{
					pawn.jobs.TryTakeOrderedJob(new Job(xxx.Masturbate, null, null, target.Cell));
				}, MenuOptionPriority.High), pawn, target);
			opts.AddDistinct(option);

			if (pawnHasBigBreasts)
			{
				option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Masturbate_Breasts".Translate(), delegate ()
				{
					pawn.jobs.TryTakeOrderedJob(new Job(xxx.Masturbate, null, null, target.Cell));
				}, MenuOptionPriority.High), pawn, target);
				opts.AddDistinct(option);
			}

			if (pawnHasBigBreasts && pawnHasBigPenis)
			{
				option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Masturbate_BJ".Translate(), delegate ()
				{
					pawn.jobs.TryTakeOrderedJob(new Job(xxx.Masturbate, null, null, target.Cell));
				}, MenuOptionPriority.High), pawn, target);
				opts.AddDistinct(option);
			}

			if (pawnHasAnus && pawnHasHands)
			{
				option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Masturbate_Anal".Translate(), delegate ()
				{
					pawn.jobs.TryTakeOrderedJob(new Job(xxx.Masturbate, null, null, target.Cell));
				}, MenuOptionPriority.High), pawn, target);
				opts.AddDistinct(option);
			}

			if (pawnHasAnus && pawnHasTail)
				option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("RJW_RMB_Masturbate_TJA".Translate(), delegate ()
				{
					pawn.jobs.TryTakeOrderedJob(new Job(xxx.Masturbate, null, null, target.Cell));
				}, MenuOptionPriority.High), pawn, target);
			opts.AddDistinct(option);

			return opts;
		}
	}
}


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

namespace rjw
{
	[StaticConstructorOnStartup]
	static class RMB_Menu2
	{
		//Widgets.InfoCardButton
		//Find.WindowStack.Add(new Dialog_InfoCard(thing));
		//Find.WindowStack.Add(new Dialog_InfoCard(def));
		static RMB_Menu2()
		{
			Harmony harmony = new Harmony("rjw");
			//start sex options
			//harmony.Patch(AccessTools.Method(typeof(FloatMenuMakerMap), "AddHumanlikeOrders"), prefix: null,
			//	postfix: new HarmonyMethod(typeof(RMB_Menu), nameof(SexFloatMenuOption)));
			//change sex options
			//harmony.Patch(AccessTools.Method(typeof(FloatMenuMakerMap), "AddHumanlikeOrders"), prefix: null,
			//	postfix: new HarmonyMethod(typeof(RMB_Menu), nameof(SexFloatMenuOption)));
		}

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

		private static TargetingParameters targetParameters = null;

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

		private static void SexFloatMenuOption(Vector3 clickPos, Pawn pawn, List<FloatMenuOption> opts)
		{
			// If the pawn in question cannot take jobs, don't bother.
			if (pawn.jobs == null)
				return;

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

			// No menu for you - quit.
			//if (!SaveStorage.DataStore.GetPawnData(pawn).ShowRMB_Menu)
			//	return;

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

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

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

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

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

			// Find valid targets for sex.
			var validtargets = GenUI.TargetsAt(clickPos, TargetParameters);
			//Log.Message("targets count " + validtargets.Count());
			foreach (LocalTargetInfo target in validtargets)
			{
				// 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);
				// Add menu options to masturbate
				FloatMenuOption option = null;

				if (target.Pawn == pawn)
				{
					option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("Masturbate".Translate(), delegate ()
					{
						List<FloatMenuOption> opts1 = new List<FloatMenuOption>();
						FloatMenuOption option1 = null;

						option1 = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("Masturbate on chair or bed".Translate(), delegate ()
						{
							Find.Targeter.BeginTargeting(TargetParemetersMasturbationChairOrBed(target), (LocalTargetInfo targetThing) =>
							{
								pawn.jobs.TryTakeOrderedJob(new Job(xxx.Masturbate_Quick, null, null, targetThing.Thing.PositionHeld));
							});
						}, MenuOptionPriority.High), pawn, target);
						opts1.AddDistinct(option1);

						option1 = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("Masturbate at".Translate(), delegate ()
						{
							Find.Targeter.BeginTargeting(TargetParemetersMasturbationLoc(target), (LocalTargetInfo targetThing) =>
							{
								pawn.jobs.TryTakeOrderedJob(new Job(xxx.Masturbate_Quick, null, null, targetThing.Cell));
							});
						}, MenuOptionPriority.High), pawn, target);
						opts1.AddDistinct(option1);

						option1 = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("Masturbate here".Translate(), delegate ()
						{
							pawn.jobs.TryTakeOrderedJob(new Job(xxx.Masturbate_Quick, null, null, target.Pawn.PositionHeld));
						}, MenuOptionPriority.High), pawn, target);
						opts1.AddDistinct(option1);

						FloatMenuUtility.MakeMenu(opts1, (FloatMenuOption opt) => opt.Label, (FloatMenuOption opt) => opt.action);

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

				//wip
				if (target.Thing != pawn as Thing)
				{
					if (target.Pawn != null)
						if (!target.Pawn.HostileTo(pawn) && xxx.is_human(target.Pawn))
						{
							// Add menu option to whore sex with target
							//option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("Solicit " + target.Pawn.Name, delegate ()
							//{
							//	if (!target.Pawn.IsColonist && !target.Pawn.IsPrisoner && pawn.CurrentBed() != null)
							//		pawn.jobs.TryTakeOrderedJob(new Job(xxx.whore_is_serving_visitors, target.Pawn, pawn.CurrentBed()));
							//}, MenuOptionPriority.High), pawn, target);
							//opts.AddDistinct(option);

							// Add menu option to have sex with target
							option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("Have sex with " + target.Pawn.NameShortColored, delegate ()
							{
								if (target.Pawn.InBed())
									pawn.jobs.TryTakeOrderedJob(new Job(xxx.casual_sex, target.Pawn, target.Pawn.CurrentBed()));
								else
									pawn.jobs.TryTakeOrderedJob(new Job(xxx.quick_sex, target.Pawn));
							}, MenuOptionPriority.High), pawn, target);
							opts.AddDistinct(option);
						}

					if (xxx.can_rape(pawn))
					{
						if (target.Thing is Corpse)
						{
							// Add menu option to rape target.
							option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("Rape " + (((Corpse)target.Thing).InnerPawn).NameShortColored, delegate ()
							{
								if (RJWSettings.necrophilia_enabled && target.Thing is Corpse)
									pawn.jobs.TryTakeOrderedJob(new Job(xxx.RapeCorpse, target.Thing));
							}, MenuOptionPriority.High), pawn, target);
							opts.AddDistinct(option);
						}
						else if (target.Pawn != null)
						{
							// Add menu option to rape target.
							option = FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("Rape " + target.Pawn.NameShortColored, delegate ()
							{
								if (target.Pawn.IsDesignatedComfort())
									pawn.jobs.TryTakeOrderedJob(new Job(xxx.RapeCP, target.Pawn));
								else if (target.Pawn.HostileTo(pawn))
									pawn.jobs.TryTakeOrderedJob(new Job(xxx.RapeEnemy, target.Pawn));
								else
									pawn.jobs.TryTakeOrderedJob(new Job(xxx.RapeRandom, target.Pawn));
							}, MenuOptionPriority.High), pawn, target);
							opts.AddDistinct(option);
						}
					}
				}
			}
		}
	}
}

repo_name: Euclidean/rjw

license: mit


code:
using Verse;

namespace rjw
{
	/* TO BE FINISHED
	public enum RandomDialogueJobType
	{
		Rape = 0,
		Love = 1,
		Other = 2
	}

	public sealed class RandomDialoguesDef : Def
	{
		public RandomDialogueJobType JobType;
		public string RD0;
		public string RD1;
		public string RD2;
		public string RD3;
		public string RD4;
		public string RD5;
		public string RD6;
		public string RD7;
		public string RD8;
		public string RD9;
		public string RD10;
		public string RD11;
		public string RD12;
		public string RD13;
		public string RD14;
		public string RD15;
		public string RD16;
		public string RD17;
		public string RD18;
		public string RD19;
	}

	[DefOf]
	public static class RandomDialoguesDefOf
	{
		//0 means before, 1 means during, 2 means after
		//public readonly static RandomDialoguesDef rape0;
		//public readonly static RandomDialoguesDef rape1;
		//public readonly static RandomDialoguesDef rape2;

		//public readonly static RandomDialoguesDef love0;
		//public readonly static RandomDialoguesDef love1;
		//public readonly static RandomDialoguesDef love2;

		//public readonly static RandomDialoguesDef other0;
		//public readonly static RandomDialoguesDef other1;
		//public readonly static RandomDialoguesDef other2;
	}
	*/

	// TO BE FINISHED
	/*
	public static class RandomDialogues
	{
		public static void processBeforeDialogue(Pawn psubject,Pawn pobject,int Type = 4)
		{
			switch (Type)
			{
				case 0:
					string traitDefName;
					if (xxx.has_traits(psubject)&&Rand.Value<0.25){
						traitDefName = psubject.random_pick_a_trait();
						switch (traitDefName)
						{
							case "Nymphomaniac":
								break;

							case "Rapist":
								break;

							case "Necrophiliac":
								break;

							case "Zoophile":
								break;

							case "Bloodlust":
								break;

							case "Psychopath":
								break;

							case "Cannibal":
								break;

							case "Masochist":
								break;

							case "Prosthophile":
								break;

							case "Prosthophobe":
								break;

							case "Jealous":
								break;

							case "Gay":
								break;

							default:
								break;
						}
					}
					if (xxx.has_traits(pobject))
					{
					}
					return;

				case 1:
					return;

				case 2:
					return;

				default:
					Log.Error("[RJW]RandomDialogues::processBeforeDialogue - Type not found");
					return;
			}
		}

		public static void processDuringDialogue(Pawn psubject, Pawn pobject, int Type = 4)
		{
			switch (Type)
			{
				case 0:
					return;

				case 1:
					return;

				case 2:
					return;

				default:
					Log.Error("[RJW]RandomDialogues::processBeforeDialogue - Type not found");
					return;
			}
		}

		public static void processAfterDialogue(Pawn psubject, Pawn pobject, int Type = 4)
		{
			switch (Type)
			{
				case 0:
					return;

				case 1:
					return;

				case 2:
					return;

				default:
					Log.Error("[RJW]RandomDialogues::processBeforeDialogue - Type not found");
					return;
			}
		}
	}
	*/
}


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

namespace rjw
{
	/// <summary>
	/// Responsible for judging the sexiness of potential partners.
	/// </summary>
	public static class SexAppraiser
	{
		public const float base_sat_per_fuck = 0.40f;
		public const float base_attraction = 0.60f;
		public const float no_partner_ability = 0.8f;

		static readonly SimpleCurve fuckability_per_reserved = new SimpleCurve
		{
			new CurvePoint(0f, 1.0f),
			new CurvePoint(0.3f, 0.4f),
			new CurvePoint(1f, 0.2f)
		};

		static readonly SimpleCurve attractiveness_from_age_male = new SimpleCurve
		{
			new CurvePoint(0f,  0.0f),
			new CurvePoint(4f,  0.1f),
			new CurvePoint(5f,  0.2f),
			new CurvePoint(15f, 0.8f),
			new CurvePoint(20f, 1.0f),
			new CurvePoint(32f, 1.0f),
			new CurvePoint(40f, 0.9f),
			new CurvePoint(45f, 0.77f),
			new CurvePoint(50f, 0.7f),
			new CurvePoint(55f, 0.5f),
			new CurvePoint(75f, 0.1f),
			new CurvePoint(100f, 0f)
		};

		//These were way too low and could be increased further. Anything under 0.7f pretty much stops sex from happening.
		static readonly SimpleCurve attractiveness_from_age_female = new SimpleCurve
		{
			new CurvePoint(0f,  0.0f),
			new CurvePoint(4f,  0.1f),
			new CurvePoint(5f,  0.2f),
			new CurvePoint(14f, 0.8f),
			new CurvePoint(28f, 1.0f),
			new CurvePoint(30f, 1.0f),
			new CurvePoint(45f, 0.7f),
			new CurvePoint(55f, 0.3f),
			new CurvePoint(75f, 0.1f),
			new CurvePoint(100f, 0f)
		};

		/// <summary>
		///	Returns boolean, no more messing around with floats.
		/// Just a simple 'Would rape? True/false'.
		/// </summary>
		[SyncMethod]
		public static bool would_rape(Pawn rapist, Pawn rapee)
		{
			float rape_factor = 0.3f; // start at 30%

			float vulnerabilityFucker = xxx.get_vulnerability(rapist); //0 to 3
			float vulnerabilityPartner = xxx.get_vulnerability(rapee); //0 to 3

			// More inclined to rape someone from another faction.
			if (rapist.HostileTo(rapee) || rapist.Faction != rapee.Faction)
				rape_factor += 0.25f;

			// More inclined to rape if the target is designated as CP.
			if (rapee.IsDesignatedComfort())
				rape_factor += 0.25f;

			// More inclined to rape when horny.
			Need_Sex horniness = rapist.needs.TryGetNeed<Need_Sex>();
			if (!xxx.is_animal(rapist) && horniness?.CurLevel <= horniness?.thresh_horny())
			{
				rape_factor += 0.25f;
			}

			if (xxx.is_animal(rapist))
			{
				if (vulnerabilityFucker < vulnerabilityPartner)
					rape_factor -= 0.1f;
				else
					rape_factor += 0.25f;
			}
			else if (xxx.is_animal(rapee))
			{
				if (xxx.is_zoophile(rapist))
					rape_factor += 0.5f;
				else
					rape_factor -= 0.2f;
			}
			else
			{
				rape_factor *= 0.5f + Mathf.InverseLerp(vulnerabilityFucker, 3f, vulnerabilityPartner);
			}

			if (rapist.health.hediffSet.HasHediff(HediffDef.Named("AlcoholHigh")))
				rape_factor *= 1.25f; //too drunk to care

			// Increase factor from traits.
			if (xxx.is_rapist(rapist))
				rape_factor *= 1.5f;
			if (xxx.is_nympho(rapist))
				rape_factor *= 1.25f;
			if (xxx.is_bloodlust(rapist))
				rape_factor *= 1.2f;
			if (xxx.is_psychopath(rapist))
				rape_factor *= 1.2f;
			if (xxx.is_masochist(rapee))
				rape_factor *= 1.2f;

			// Lower factor from traits.
			if (xxx.is_masochist(rapist))
				rape_factor *= 0.8f;

			if (rapist.needs?.joy != null && rapist.needs.joy.CurLevel < 0.1f) // The rapist is really bored...
				rape_factor *= 1.2f;

			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			if (rapist.relations == null || xxx.is_animal(rapist)) return Rand.Chance(rape_factor);
			float opinion = rapist.relations.OpinionOf(rapee);

			//remove Pretty trait bonus
			//ThoughtWorker_Pretty
			float beauty = rapee.GetStatValue(StatDefOf.PawnBeauty, true) * 20f;

			opinion -= beauty;

			// Won't rape friends, unless rapist or psychopath.
			if (xxx.is_kind(rapist))
			{   //<-80: 1f /-40: 0.5f / 0+: 0f
				rape_factor *= 1f - Mathf.Pow(GenMath.InverseLerp(-80, 0, opinion), 2);
			}
			else if (xxx.is_rapist(rapist) || xxx.is_psychopath(rapist))
			{   //<40: 1f /80: 0.5f / 120+: 0f
				opinion -= beauty;
				rape_factor *= 1f - Mathf.Pow(GenMath.InverseLerp(40, 120, opinion), 2); // This can never be 0, since opinion caps at 100.
			}
			else
			{   //<-60: 1f /-20: 0.5f / 40+: 0f
				opinion -= beauty;
				rape_factor *= 1f - Mathf.Pow(GenMath.InverseLerp(-60, 40, opinion), 2);
			}

			//Log.Message("rjw::xxx rape_factor for " + get_pawnname(rapee) + " is " + rape_factor);

			return Rand.Chance(rape_factor);
		}

		public static float would_fuck(Pawn fucker, Corpse fucked, bool invert_opinion = false, bool ignore_bleeding = false, bool ignore_gender = false)
		{
			CompRottable comp = fucked.GetComp<CompRottable>();

			//--Log.Message("rotFactor:" + rotFactor);

			// Things that don't rot, such as mechanoids and weird mod-added stuff such as Rimworld of Magic's elementals.
			if (comp == null)
			{
				// Trying to necro the weird mod-added stuff causes an error, so skipping those for now.
				return 0.0f;
			}

			float maxRot = ((CompProperties_Rottable)comp.props).TicksToDessicated;
			float rotFactor = (maxRot - comp.RotProgress) / maxRot;
			//--Log.Message("rotFactor:" + rotFactor);
			return would_fuck(fucker, fucked.InnerPawn, invert_opinion, ignore_bleeding, ignore_gender) * rotFactor;
		}

		public static float would_fuck_animal(Pawn pawn, Pawn target, bool invert_opinion = false, bool ignore_bleeding = false, bool ignore_gender = false)
		{
			float wildness_modifier = 1.0f;
			List<float> size_preference = new List<float>() { pawn.BodySize * 0.75f, pawn.BodySize * 1.6f };
			float fuc = would_fuck(pawn, target, invert_opinion, ignore_bleeding, ignore_gender); // 0.0 to ~3.0, orientation checks etc.

			if (fuc < 0.1f)
			{   // Would not fuck
				return 0;
			}

			if (xxx.has_quirk(pawn, "Teratophile"))
			{   // Teratophiles prefer more 'monstrous' partners.
				size_preference[0] = pawn.BodySize * 0.8f;
				size_preference[1] = pawn.BodySize * 2.0f;
				wildness_modifier = 0.3f;
			}
			if (pawn.health.hediffSet.HasHediff(HediffDef.Named("AlcoholHigh")))
			{
				wildness_modifier = 0.5f; //Drunk and making poor judgments.
				size_preference[1] *= 1.5f;
			}
			else if (pawn.health.hediffSet.HasHediff(HediffDef.Named("YayoHigh")))
			{
				wildness_modifier = 0.2f; //This won't end well.
				size_preference[1] *= 2.5f;
			}
			var partBPR = Genital_Helper.get_genitalsBPR(pawn);
			var parts = Genital_Helper.get_PartsHediffList(pawn, partBPR);
			if (Genital_Helper.has_vagina(pawn, parts) || Genital_Helper.has_anus(pawn))
			{
				if (!(Genital_Helper.has_penis_fertile(pawn, parts) || Genital_Helper.has_penis_infertile(pawn, parts)))
				{
					size_preference[1] = pawn.BodySize * 1.3f;
				}
			}
			if (xxx.is_animal(pawn))
			{
				size_preference[1] = pawn.BodySize * 1.3f;
				wildness_modifier = 0.4f;
			}
			else if (xxx.has_traits(pawn))
			{
				if (pawn.story.traits.HasTrait(TraitDefOf.Tough) || pawn.story.traits.HasTrait(TraitDefOf.Brawler))
				{
					size_preference[1] += 0.2f;
					wildness_modifier -= 0.2f;
				}
				else if (pawn.story.traits.HasTrait(TraitDef.Named("Wimp")))
				{
					size_preference[0] -= 0.2f;
					size_preference[1] -= 0.2f;
					wildness_modifier += 0.25f;
				}
			}

			float wildness = target.RaceProps.wildness; // 0.0 to 1.0
			float petness = target.RaceProps.petness; // 0.0 to 1.0
			float distance = pawn.Position.DistanceTo(target.Position);

			//ModLog.Message("would_fuck_animal:: base: " + fuc + ", wildness: " + wildness + ", petness: " + petness + ", distance: " + distance);

			fuc = fuc + fuc * petness - fuc * wildness * wildness_modifier;

			if (fuc < 0.1f)
			{   // Would not fuck
				return 0;
			}

			// Adjust by distance, nearby targets preferred.
			fuc *= 1.0f - Mathf.Max(distance / 10000, 0.1f);

			// Adjust by size difference.
			if (target.BodySize < size_preference[0])
			{
				fuc *= Mathf.Lerp(0.1f, size_preference[0], target.BodySize);
			}
			else if (target.BodySize > size_preference[1])
			{
				fuc *= Mathf.Lerp(size_preference[1] * 10, size_preference[1], target.BodySize);
			}

			if (target.Faction != pawn.Faction)
			{
				//ModLog.Message("would_fuck_animal(NT):: base: " + fuc + ", bound1: " + fuc * 0.75f);
				//ModLog.Message("would_fuck_animal(NT):: base: " + fuc + ", bound2: " + fuc + 0.25f);
				fuc *= 0.75f; // Less likely to target wild animals.
			}
			else if (pawn.relations.DirectRelationExists(PawnRelationDefOf.Bond, target))
			{
				//ModLog.Message("would_fuck_animal(T):: base: " + fuc + ", bound1: " + fuc * 1.25f);
				//ModLog.Message("would_fuck_animal(T):: base: " + fuc + ", bound2: " + fuc + 0.25f);
				fuc *= 1.25f; // Bonded animals preferred.
			}

			return fuc;
		}

		// Returns how fuckable 'fucker' thinks 'p' is on a scale from 0.0 to 1.0
		public static float would_fuck(Pawn fucker, Pawn fucked, bool invert_opinion = false, bool ignore_bleeding = false, bool ignore_gender = false)
		{
			//--ModLog.Message("would_fuck("+xxx.get_pawnname(fucker)+","+xxx.get_pawnname(fucked)+","+invert_opinion.ToString()+") is called");
			if (!xxx.is_healthy_enough(fucker) && !xxx.is_psychopath(fucker)) // Not healthy enough to have sex, shouldn't have got this far.
				return 0f;
			if ((xxx.is_animal(fucker) || xxx.is_animal(fucked)) && (!xxx.is_animal(fucker) || !xxx.is_animal(fucked)) && !RJWSettings.bestiality_enabled)
				return 0f; // Animals disabled.
			if (fucked.Dead && !RJWSettings.necrophilia_enabled)
				return 0f; // Necrophilia disabled.
			if (fucker.Dead || fucker.Suspended || fucked.Suspended)
				return 0f; // Target unreachable. Shouldn't have got this far, but doesn't hurt to double-check.
			if (xxx.is_starved(fucked) && fucked.Faction == fucker.Faction && !xxx.is_psychopath(fucker) && !xxx.is_rapist(fucker))
				return 0f;
			if (!ignore_bleeding && !xxx.is_not_dying(fucked) && !xxx.is_psychopath(fucker) && !xxx.is_rapist(fucker) && !xxx.is_bloodlust(fucker))
				return 0f; // Most people wouldn't fuck someone who's dying.

			if (!IsGenderOk(fucker, fucked))
			{
				return 0.0f;
			}

			int fucker_age = fucker.ageTracker.AgeBiologicalYears;
			string fucker_quirks = CompRJW.Comp(fucker).quirks.ToString();
			int p_age = fucked.ageTracker.AgeBiologicalYears;

			// --- Age checks ---
			if (!IsAgeOk(fucker, fucked, fucker_age, p_age))
			{
				return 0.0f;
			}
			float age_factor = GetAgeFactor(fucker, fucked, p_age);

			// --- Orientation checks ---
			float orientation_factor = GetOrientationFactor(fucker, fucked, ignore_gender);
			if (orientation_factor == 0.0f)
			{
				orientation_factor = fucker.relations.SecondaryLovinChanceFactor(fucked);
				//Log.Message("would_fuck() SecondaryLovinChanceFactor:" + orientation_factor);
				if (orientation_factor <= 0)
					return 0.0f;
			}

			// --- Body and appearance checks ---
			float body_factor = GetBodyFactor(fucker, fucked);

			// --- Opinion checks ---
			float opinion_factor = GetOpinionFactor(fucker, fucked, invert_opinion);
			float horniness_factor = GetHorninessFactor(fucker);

			float reservedPercentage = (fucked.Dead ? 1f : fucked.ReservedCount()) / xxx.max_rapists_per_prisoner;
			//Log.Message("would_fuck() reservedPercentage:" + reservedPercentage + "fuckability_per_reserved"+ fuckability_per_reserved.Evaluate(reservedPercentage));
			//Log.Message("would_fuck() - horniness_factor = " + horniness_factor.ToString());

			float prenymph_att = Mathf.InverseLerp(0f, 2.8f, base_attraction * orientation_factor * horniness_factor * age_factor * body_factor * opinion_factor);
			float final_att = !(xxx.is_nympho(fucker) || fucker.health.hediffSet.HasHediff(HediffDef.Named("HumpShroomEffect"))) ? prenymph_att : 0.2f + 0.8f * prenymph_att;
			//Log.Message("would_fuck( " + xxx.get_pawnname(fucker) + ", " + xxx.get_pawnname(fucked) + " ) - prenymph_att = " + prenymph_att.ToString() + ", final_att = " + final_att.ToString());

			return Mathf.Min(final_att, fuckability_per_reserved.Evaluate(reservedPercentage));
		}

		private static bool IsGenderOk(Pawn fucker, Pawn fucked)
		{
			if (fucker.gender == Gender.Male)
			{
				switch (RJWPreferenceSettings.Malesex)
				{
					case RJWPreferenceSettings.AllowedSex.All:
						break;
					case RJWPreferenceSettings.AllowedSex.Homo:
						if (fucked.gender != Gender.Male)
							return false;
						break;
					case RJWPreferenceSettings.AllowedSex.Nohomo:
						if (fucked.gender == Gender.Male)
							return false;
						break;
				}
			}
			if (fucker.gender == Gender.Female)
			{
				switch (RJWPreferenceSettings.FeMalesex)
				{
					case RJWPreferenceSettings.AllowedSex.All:
						break;
					case RJWPreferenceSettings.AllowedSex.Homo:
						if (fucked.gender != Gender.Female)
							return false;
						break;
					case RJWPreferenceSettings.AllowedSex.Nohomo:
						if (fucked.gender == Gender.Female)
							return false;
						break;
				}
			}
			return true;
		}

		private static float GetHorninessFactor(Pawn fucker)
		{
			float horniness_factor; // 1 to 1.6
			{
				float need_sex = xxx.need_some_sex(fucker);
				switch (need_sex)
				{
					case 3:
						horniness_factor = 1.6f;
						break;

					case 2:
						horniness_factor = 1.3f;
						break;

					case 1:
						horniness_factor = 1.1f;
						break;

					default:
						horniness_factor = 1f;
						break;
				}
			}
			//Log.Message("would_fuck() - horniness_factor = " + horniness_factor.ToString());
			return horniness_factor;
		}

		private static float GetOpinionFactor(Pawn fucker, Pawn fucked, bool invert_opinion)
		{
			float opinion_factor;
			{
				if (fucked.relations != null && fucker.relations != null && !xxx.is_animal(fucker) && !xxx.is_animal(fucked))
				{
					float opi = !invert_opinion ? fucker.relations.OpinionOf(fucked) : 100 - fucker.relations.OpinionOf(fucked); // -100 to 100
					opinion_factor = 0.8f + (opi + 100.0f) * (.45f / 200.0f); // 0.8 to 1.25
				}
				else if ((xxx.is_animal(fucker) || xxx.is_animal(fucked)) && fucker.relations.DirectRelationExists(PawnRelationDefOf.Bond, fucked))
				{
					opinion_factor = 1.3f;
				}
				else
				{
					opinion_factor = 1.0f;
				}

				// More likely to take advantege of CP.
				if (fucked.IsDesignatedComfort() || (fucked.IsDesignatedBreeding() && xxx.is_animal(fucker)))
					opinion_factor += 0.25f;
				else if (fucked.IsDesignatedService())
					opinion_factor += 0.1f;

				// Less picky if designated for whorin'.
				if (fucker.IsDesignatedService())
					opinion_factor += 0.1f;

				if (Quirk.Sapiosexual.IsSatisfiedBy(fucker, fucked))
				{
					opinion_factor *= 1.4f;
				}
			}
			//Log.Message("would_fuck() - opinion_factor = " + opinion_factor.ToString());
			return opinion_factor;
		}

		private static float GetBodyFactor(Pawn fucker, Pawn fucked)
		{
			float body_factor; //0.4 to 1.6
			{
				if (fucker.health.hediffSet.HasHediff(HediffDef.Named("AlcoholHigh")))
				{
					if (!xxx.is_zoophile(fucker) && xxx.is_animal(fucked))
						body_factor = 0.8f;
					else
						body_factor = 1.25f; //beer lens
				}
				else if (xxx.is_zoophile(fucker) && !xxx.is_animal(fucked))
				{
					body_factor = 0.7f;
				}
				else if (!xxx.is_zoophile(fucker) && xxx.is_animal(fucked))
				{
					body_factor = 0.45f;
				}
				else if (Quirk.Teratophile.IsSatisfiedBy(fucker, fucked))
				{
					body_factor = 1.4f;
				}
				else if (fucked.story != null)
				{
					if (fucked.story.bodyType == BodyTypeDefOf.Female) body_factor = 1.25f;
					else if (fucked.story.bodyType == BodyTypeDefOf.Fat) body_factor = 1.0f;
					else body_factor = 1.1f;

					if (fucked.story.traits.HasTrait(TraitDefOf.CreepyBreathing))
						body_factor *= 0.9f;

					//if (fucked.story.traits.HasTrait(TraitDefOf.Beauty))
					//{
					//	switch (fucked.story.traits.DegreeOfTrait(TraitDefOf.Beauty))
					//	{
					//		case 2: // Beautiful
					//			body_factor *= 1.25f;
					//			break;
					//		case 1: // Pretty
					//			body_factor *= 1.1f;
					//			break;
					//		case -1: // Ugly
					//			body_factor *= 0.8f;
					//			break;
					//		case -2: // Staggeringly Ugly
					//			body_factor *= 0.5f;
					//			break;
					//	}
					//}
					if (fucked.GetStatValue(StatDefOf.PawnBeauty) >= 2)
						body_factor *= 1.25f;
					else if (fucked.GetStatValue(StatDefOf.PawnBeauty) >= 1)
						body_factor *= 1.1f;
					else if (fucked.GetStatValue(StatDefOf.PawnBeauty) < 0)
						if (fucked.GetStatValue(StatDefOf.PawnBeauty) >= -1)
							body_factor *= 0.8f;
						else
							body_factor *= 0.5f;

					if (RelationsUtility.IsDisfigured(fucked))
					{
						body_factor *= 0.8f;
					}

					// Nude target is more tempting.
					if (!fucked.Dead && fucked.apparel.PsychologicallyNude && fucker.CanSee(fucked))
						body_factor *= 1.1f;
				}
				else
				{
					body_factor = 1.1f;
				}

				if (Quirk.Somnophile.IsSatisfiedBy(fucker, fucked))
				{
					body_factor *= 1.25f;
				}

				if (Quirk.PregnancyFetish.IsSatisfiedBy(fucker, fucked))
				{
					body_factor *= 1.25f;
				}

				if (Quirk.ImpregnationFetish.IsSatisfiedBy(fucker, fucked))
				{
					body_factor *= 1.25f;
				}

				if (xxx.AlienFrameworkIsActive && !xxx.is_animal(fucker))
				{
					if (xxx.is_xenophile(fucker))
					{
						if (fucker.def.defName == fucked.def.defName)
							body_factor *= 0.5f; // Same species, xenophile less interested.
					}
					else if (xxx.is_xenophobe(fucker))
					{
						if (fucker.def.defName != fucked.def.defName)
							body_factor *= 0.25f; // Different species, xenophobe less interested.
					}
				}

				if (fucked.Dead && !xxx.is_necrophiliac(fucker))
				{
					body_factor *= 0.5f;
				}
			}
			//Log.Message("would_fuck() - body_factor = " + body_factor.ToString());
			return body_factor;
		}

		private static float GetOrientationFactor(Pawn fucker, Pawn fucked, bool ignore_gender)
		{
			float orientation_factor; //0 or 1
			{
				orientation_factor = 1.0f;

				if (!ignore_gender && !xxx.is_animal(fucker))
				{
					if (!CompRJW.CheckPreference(fucker, fucked))
					{
						//Log.Message("would_fuck( " + xxx.get_pawnname(fucker) + ", " + xxx.get_pawnname(fucked) + " )");
						//Log.Message("would_fuck() - preference fail");
						orientation_factor = 0.0f;
					}
				}
			}
			//Log.Message("would_fuck() - orientation_factor = " + orientation_factor.ToString());
			return orientation_factor;
		}

		private static float GetAgeFactor(Pawn fucker, Pawn fucked, int p_age)
		{
			float age_factor;

			//The human age curve needs work. Currently pawns refuse to have sex with anyone over age of ~50 no matter what the other factors are, which is just silly...
			age_factor = fucked.gender == Gender.Male ? attractiveness_from_age_male.Evaluate(SexUtility.ScaleToHumanAge(fucked)) : attractiveness_from_age_female.Evaluate(SexUtility.ScaleToHumanAge(fucked));
			//--Log.Message("would_fuck() - age_factor = " + age_factor.ToString());

			if (xxx.is_animal(fucker))
			{
				age_factor = 1.0f;  //using flat factors, since human age is not comparable to animal ages
			}
			else if (xxx.is_animal(fucked))
			{
				if (p_age <= 1 && fucked.RaceProps.lifeExpectancy > 8)
					age_factor = 0.5f;
				else
					age_factor = 1.0f;
				//--Log.Message("would_fuck() - animal age_factor = " + age_factor.ToString());
			}
			if (Quirk.Gerontophile.IsSatisfiedBy(fucker, fucked))
			{
				age_factor = 1.0f;
			}

			return age_factor;
		}

		private static bool IsAgeOk(Pawn fucker, Pawn fucked, int fucker_age, int p_age)
		{
			bool age_ok;
			{
				if (xxx.is_animal(fucker) && p_age >= RJWSettings.sex_minimum_age)
				{
					age_ok = true;
				}
				else if (xxx.is_animal(fucked) && fucker_age >= RJWSettings.sex_minimum_age)
				{
					// don't check the age of animals when they are the victim
					age_ok = true;
				}
				else if (fucker_age >= RJWSettings.sex_free_for_all_age && p_age >= RJWSettings.sex_free_for_all_age)
				{
					age_ok = true;
				}
				else if (fucker_age < RJWSettings.sex_minimum_age || p_age < RJWSettings.sex_minimum_age)
				{
					age_ok = false;
				}
				else
				{
					age_ok = Math.Abs(fucker.ageTracker.AgeBiologicalYearsFloat - fucked.ageTracker.AgeBiologicalYearsFloat) < 2.05f;
				}
			}
			//Log.Message("would_fuck() - age_ok = " + age_ok.ToString());

			return age_ok;
		}

		static int ReservedCount(this Thing pawn)
		{
			int ret = 0;
			if (pawn == null) return 0;
			try
			{
				ReservationManager reserver = pawn.Map.reservationManager;
				IList reservations = (IList)AccessTools.Field(typeof(ReservationManager), "reservations").GetValue(reserver);

				if (reservations.Count == 0) return 0;
				Type reserveType = reservations[0].GetType();
				ret += (from object t in reservations
						where t != null
						let target = (LocalTargetInfo)AccessTools.Field(reserveType, "target").GetValue(t)
						let claimant = (Pawn)AccessTools.Field(reserveType, "claimant").GetValue(t)
						where target != null
						where target.Thing != null
						where target.Thing.ThingID == pawn.ThingID
						select (int)AccessTools.Field(reserveType, "stackCount").GetValue(t)).Count();
			}
			catch (Exception e)
			{
				ModLog.Warning(e.ToString());
			}
			return ret;
		}
	}
}


code:
namespace rjw
{
	public enum SexPartType : byte
	{
		Anus = 0,
		FemaleBreast = 1,
		FemaleGenital = 2,
		MaleBreast = 3,
		MaleGenital = 4
	}
}


code:
using System;
using Verse;

namespace rjw
{
	public static class SexPartTypeExtensions
	{
		public static BodyPartDef GetBodyPartDef(this SexPartType sexPartType)
		{
			return sexPartType switch
			{
				SexPartType.Anus => xxx.anusDef,
				SexPartType.FemaleBreast => xxx.breastsDef,
				SexPartType.FemaleGenital => xxx.genitalsDef,
				SexPartType.MaleBreast => xxx.breastsDef,
				SexPartType.MaleGenital => xxx.genitalsDef,
				_ => throw new ArgumentException($"Unrecognized sexPartType: {sexPartType}")
			};
		}
	}
}


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

namespace rjw
{
	/// <summary>
	/// Compares traits for equality but ignores forsed flag
	/// </summary>
	class TraitComparer : IEqualityComparer<Trait>
	{
		bool ignoreForced;
		bool ignoreDegree;

		public TraitComparer(bool ignoreDegree = false, bool ignoreForced = true)
		{
			this.ignoreDegree = ignoreDegree;
			this.ignoreForced = ignoreForced;
		}

		public bool Equals(Trait x, Trait y)
		{
			return
				x.def == y.def &&
				(ignoreDegree || (x.Degree == y.Degree)) &&
				(ignoreForced || (x.ScenForced == y.ScenForced));
		}

		public int GetHashCode(Trait obj)
		{
			return
				(obj.def.GetHashCode() << 5) +
				(ignoreDegree ? 0 : obj.Degree) +
				((ignoreForced || obj.ScenForced) ? 0 : 0x10);
		}
	}
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
namespace rjw
{

	public static class Unprivater
	{
		internal const BindingFlags flags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
		/// <summary>
		/// T is for returned type. For instance-class fields
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="fieldName"></param>
		/// <param name="obj"></param>
		/// <returns></returns>
		public static T GetProtectedValue<T>(string fieldName, object obj)
		{
			var fieldinfo = obj.GetType().GetField(fieldName, flags);
			var readData = fieldinfo.GetValue(obj);
			if (readData is T)
			{
				return (T)readData;
			}
			else
			{
				try
				{
					return (T)Convert.ChangeType(readData, typeof(T));
				}
				catch (InvalidCastException)
				{
					return default(T);
				}
			}
		}

		public static bool SetProtectedValue(string fieldName, object obj, object value)
		{
			var fieldinfo = obj.GetType().GetField(fieldName, flags);

			try
				{
					fieldinfo.SetValue(obj, value);
					return true;
				}
				catch (Exception)
				{
					return false;
			}

		}

		/// <summary>
		/// T is for returned type. For instance-class properties ( [ get; thingies ] )
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="fieldName"></param>
		/// <param name="obj"></param>
		/// <returns></returns>
		public static T GetProtectedProperty<T>(string propertyName, object obj)
		{
			var propinfo = obj.GetType().GetProperty(propertyName, flags);

			var getter = propinfo.GetGetMethod(nonPublic: true);
			var readData = getter.Invoke(obj, null);
			if (readData is T)
			{
				return (T)readData;
			}
			else
			{
				try
				{
					return (T)Convert.ChangeType(readData, typeof(T));
				}
				catch (InvalidCastException)
				{
					return default(T);
				}
			}
		}

		public static bool SetProtectedProperty(string propertyName, object obj, object value)
		{
			var propinfo = obj.GetType().GetProperty(propertyName, flags);

			try
			{
				propinfo.SetValue(obj, value, null);
				return true;
			}
				catch (Exception)
				{
				return false;
				}

		}

		/// <summary>
		/// T is for returned type. For static-class fields
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="fieldName"></param>
		/// <param name="t"></param>
		/// <returns></returns>
		public static T GetProtectedValue<T>(string fieldName, Type t)
		{
			var fieldinfo = t.GetField(fieldName, flags);
			var readData = fieldinfo.GetValue(null);
			if (readData is T)
			{
				return (T)readData;
			}
			else
			{
				try
				{
					return (T)Convert.ChangeType(readData, typeof(T));
				}
				catch (InvalidCastException)
				{
					return default(T);
				}
			}
		}

		public static bool SetProtectedValue(string fieldName, Type t, object value)
		{
			var fieldinfo = t.GetField(fieldName, flags);

			try
			{
				fieldinfo.SetValue(null, value);
				return true;
			}
			catch (Exception)
			{
				return false;
			}

		}

		// needs testing, dunno if it works for statics
		public static T GetProtectedProperty<T>(string propertyName, Type t)
		{
			var propinfo = t.GetProperty(propertyName, flags);

			var getter = propinfo.GetGetMethod(nonPublic: true);
			var readData = getter.Invoke(null, null);
			if (readData is T)
			{
				return (T)readData;
			}
			else
			{
				try
				{
					return (T)Convert.ChangeType(readData, typeof(T));
				}
				catch (InvalidCastException)
				{
					return default(T);
				}
			}
		}

	}

}


code:
namespace rjw
{
	/*This is useless for now.
	public class Verb_Fuck : Verb
	{
		private const int TargetCooldown = 40;

		protected override bool TryCastShot()
		{
			Pawn casterPawn = base.CasterPawn;

			Thing thing = this.currentTarget.Thing;
			casterpawn.rotationTracker.Face(thing.DrawPos);

			this.SoundJuicy().PlayOneShot(new TargetInfo(thing.Position, casterPawn.Map, false));
			casterPawn.Drawer.Notify_MeleeAttackOn(thing);

			casterpawn.rotationTracker.FaceCell(thing.Position);
			if (casterPawn.caller != null) {
				casterPawn.caller.Notify_DidMeleeAttack();
			}
			return true;
		}

		private SoundDef SoundJuicy()
		{
			return SoundDef.Named ("Sex");
		}
	}
	*/
}