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

namespace rjw
{
	public class JobDriver_UseItemOn : JobDriver_UseItem
	{
		public static Toil pickup_item(Pawn p, Thing item)
		{
			return new Toil
			{
				initAction = delegate
				{
					p.carryTracker.TryStartCarry(item, 1);
					if (item.Spawned) // If the item is still spawned that means the pawn failed to pick it up
						p.jobs.curDriver.EndJobWith(JobCondition.Incompletable);
				},
				defaultCompleteMode = ToilCompleteMode.Instant
			};
		}

		protected TargetIndex iitem = TargetIndex.A;

		protected TargetIndex itar = TargetIndex.B;

		protected Thing item
		{
			get
			{
				return base.job.GetTarget(iitem).Thing;
			}
		}

		protected Thing tar
		{
			get
			{
				return base.job.GetTarget(itar).Thing;
			}
		}

		protected override IEnumerable<Toil> MakeNewToils()
		{
			if (tar == null)
				foreach (var toil in base.MakeNewToils())
					yield return toil;
			else
			{
				// Find the pawn to use the item on.
				Pawn other;
				{
					var corpse = tar as Corpse;
					other = (corpse == null) ? (Pawn)tar : corpse.InnerPawn;
				}

				this.FailOnDespawnedNullOrForbidden(itar);
				if (!other.Dead)
					this.FailOnAggroMentalState(itar);
				yield return Toils_Reserve.Reserve(itar);

				if ((pawn.inventory != null) && pawn.inventory.Contains(item))
				{
					yield return Toils_Misc.TakeItemFromInventoryToCarrier(pawn, iitem);
				}
				else
				{
					yield return Toils_Reserve.Reserve(iitem);
					yield return Toils_Goto.GotoThing(iitem, PathEndMode.ClosestTouch).FailOnForbidden(iitem);
					yield return pickup_item(pawn, item);
				}

				yield return Toils_Goto.GotoThing(itar, PathEndMode.Touch);

				yield return new Toil
				{
					initAction = delegate
					{
						if (!other.Dead)
							PawnUtility.ForceWait(other, 60);
					},
					defaultCompleteMode = ToilCompleteMode.Delay,
					defaultDuration = 60
				};

				yield return new Toil
				{
					initAction = delegate
					{
						var effective_item = item;

						// Drop the item if it's some kind of apparel. This is because ApparelTracker.Wear only works properly
						// if the apparel to wear is spawned. (I'm just assuming that DoEffect for apparel wears it, which is
						// true for bondage gear)
						if ((effective_item as Apparel) != null)
						{
							Thing dropped_thing;
							if (pawn.carryTracker.TryDropCarriedThing(pawn.Position, ThingPlaceMode.Near, out dropped_thing))
								effective_item = dropped_thing as Apparel;
							else
							{
								ModLog.Error("Unable to drop " + effective_item.Label + " for use on " + xxx.get_pawnname(other) + " (apparel must be dropped before use)");
								effective_item = null;
							}
						}

						if (effective_item != null)
						{
							var eff = effective_item.TryGetComp<CompUseEffect>();
							if (eff != null)
								eff.DoEffect(other);
							else
								ModLog.Error("Unable to get CompUseEffect for use of " + effective_item.Label + " on " + xxx.get_pawnname(other) + " by " + xxx.get_pawnname(pawn));
						}
					},
					defaultCompleteMode = ToilCompleteMode.Instant
				};
			}
		}
	}
}


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

namespace rjw
{
	public class Recipe_InstallChastityBelt : Recipe_InstallImplant
	{
		public override IEnumerable<BodyPartRecord> GetPartsToApplyOn(Pawn p, RecipeDef r)
		{
			return base.GetPartsToApplyOn(p, r);
		}
	}

	public class Recipe_UnlockChastityBelt : Recipe_InstallImplant
	{
		public override IEnumerable<BodyPartRecord> GetPartsToApplyOn(Pawn p, RecipeDef r)
		{
			return base.GetPartsToApplyOn(p, r);
		}
	}
}

repo_name: Euclidean/rjw

license: mit


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

namespace rjw
{
	public class Recipe_ForceOffGear : Recipe_Surgery
	{
		public static bool is_wearing(Pawn p, ThingDef apparel_def)
		{
			if (p.apparel != null)
				foreach (var app in p.apparel.WornApparel)
					if (app.def == apparel_def)
						return true;
			return false;
		}

		public static BodyPartRecord find_part_record(BodyPartDef part_def, Pawn p)
		{
			return p.RaceProps.body.AllParts.Find((BodyPartRecord bpr) => bpr.def == part_def);
		}

		// Puts the recipe in the operations list only if "p" is wearing the relevant apparel. The little trick here is that yielding
		// null causes the game to put the recipe in the list but not actually apply it to a body part.
		public override IEnumerable<BodyPartRecord> GetPartsToApplyOn(Pawn p, RecipeDef generic_def)
		{
			var r = (force_off_gear_def)generic_def;
			if (is_wearing(p, r.removes_apparel))
				yield return null;
		}

		[SyncMethod]
		public static void apply_burns(Pawn p, List<BodyPartDef> parts, float min_severity, float max_severity)
		{
			foreach (var part in parts)
			{
				var rec = find_part_record(part, p);
				if (rec != null)
				{
					//Rand.PopState();
					//Rand.PushState(RJW_Multiplayer.PredictableSeed());
					var to_deal = Rand.Range(min_severity, max_severity) * part.GetMaxHealth(p);
					var dealt = 0.0f;
					var counter = 0;
					while ((counter < 100) && (dealt < to_deal) && (!p.health.hediffSet.PartIsMissing(rec)))
					{
						var dam = Rand.RangeInclusive(3, 5);
						p.TakeDamage(new DamageInfo(DamageDefOf.Burn, dam, 999, -1.0f, null, rec, null));
						++counter;
						dealt += (float)dam;
					}
				}
			}
		}

		[SyncMethod]
		public override void ApplyOnPawn(Pawn p, BodyPartRecord null_part, Pawn surgeon, List<Thing> ingredients,Bill bill)
		{
			var r = (force_off_gear_def)recipe;
			if ((surgeon != null) &&
				(p.apparel != null) &&
				(!CheckSurgeryFail(surgeon, p, ingredients, find_part_record(r.failure_affects, p),bill)))
			{
				// Remove apparel
				foreach (var app in p.apparel.WornApparel)
					if (app.def == r.removes_apparel)
					{
						p.apparel.Remove(app);
						break;
					}

				// Destroy parts
				//Rand.PopState();
				//Rand.PushState(RJW_Multiplayer.PredictableSeed());
				var def_to_destroy = r.destroys_one_of.RandomElement<BodyPartDef>();
				if (def_to_destroy != null)
				{
					var record_to_destroy = find_part_record(def_to_destroy, p);
					if (record_to_destroy != null)
					{
						var dam = (int)(1.5f * def_to_destroy.GetMaxHealth(p));
						p.TakeDamage(new DamageInfo(DamageDefOf.Burn, dam, 999, -1.0f, null, record_to_destroy, null));
					}
				}

				if (r.major_burns_on != null)
					apply_burns(p, r.major_burns_on, 0.30f, 0.60f);
				if (r.minor_burns_on != null)
					apply_burns(p, r.minor_burns_on, 0.15f, 0.35f);
			}
		}
	}
}


code:
using RimWorld;
using Verse;

namespace rjw
{
	public class ThoughtWorker_Bound : ThoughtWorker
	{
		protected override ThoughtState CurrentStateInternal(Pawn p)
		{
			if (p.apparel != null)
			{
				bool bound = false, gagged = false;
				foreach (var app in p.apparel.WornApparel)
				{
					var gear_def = app.def as bondage_gear_def;
					if (gear_def != null)
					{
						bound |= gear_def.gives_bound_moodlet;
						gagged |= gear_def.gives_gagged_moodlet;
					}
				}

				if (bound && gagged)
					return ThoughtState.ActiveAtStage(2);
				else if (gagged)
					return ThoughtState.ActiveAtStage(1);
				else if (bound)
					return ThoughtState.ActiveAtStage(0);
			}
			return ThoughtState.Inactive;
		}
	}
}


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

namespace rjw
{
	//public static class bondage_gear_tradeability
	//{
	//	public static void init()
	//	{
	//		// Allows bondage gear to be selled by traders
	//		if (xxx.config.bondage_gear_enabled)
	//		{
	//			foreach (var def in DefDatabase<bondage_gear_def>.AllDefs)
	//				def.tradeability = Tradeability.Sellable;
	//		}
	//		// Forbids bondage gear to be selled by traders
	//		else
	//		{
	//			foreach (var def in DefDatabase<bondage_gear_def>.AllDefs)
	//				def.tradeability = Tradeability.None;
	//		}
	//	}
	//}

	public static class bondage_gear_extensions
	{
		public static bool has_lock(this Apparel app)
		{
			return (app.TryGetComp<CompHoloCryptoStamped>() != null);
		}

		public static bool is_wearing_locked_apparel(this Pawn p)
		{
			if (p.apparel != null)
				foreach (var app in p.apparel.WornApparel)
					if (app.has_lock())
						return true;
			return false;
		}

		// Tries to get p started on the job of using an item on either another pawn or on themself (if "other" is null).
		// Of course in order for this method to work, the item's useJob has to be able to handle use on another pawn. This
		// is true for the holokey and bondage gear in RJW but not the items in the core game
		public static void start_job(this CompUsable usa, Pawn p, LocalTargetInfo tar)
		{
			if (p.CanReserveAndReach(usa.parent, PathEndMode.Touch, Danger.Some) &&
				((tar == null) || p.CanReserveAndReach(tar, PathEndMode.Touch, Danger.Some)))
			{
				var comfor = usa.parent.GetComp<CompForbiddable>();
				if (comfor != null)
					comfor.Forbidden = false;
				var job = JobMaker.MakeJob(((CompProperties_Usable)usa.props).useJob, usa.parent, tar);
				p.jobs.TryTakeOrderedJob(job);
			}
		}

		// Creates a menu option to use an item. "tar" is expected to be a pawn, corpse or null if it doesn't apply (in which
		// case the pawn will presumably use the item on themself). "required_work" can also be null.
		public static FloatMenuOption make_option(this CompUsable usa, string label, Pawn p, LocalTargetInfo tar, WorkTypeDef required_work)
		{
			if ((tar != null) && (!p.CanReserve(tar)))
			{
				string key = "Reserved";
				string text = TranslatorFormattedStringExtensions.Translate(key);
				return new FloatMenuOption(label + " (" + text + ")", null, MenuOptionPriority.DisabledOption);
			}
			else if ((tar != null) && (!p.CanReach(tar, PathEndMode.Touch, Danger.Some)))
			{
				string key = "NoPath";
				string text = TranslatorFormattedStringExtensions.Translate(key);
				return new FloatMenuOption(label + " (" + text + ")", null, MenuOptionPriority.DisabledOption);
			}
			else if ((required_work != null) && p.WorkTagIsDisabled(required_work.workTags))
			{
				string key = "CannotPrioritizeWorkTypeDisabled";
				string text = TranslatorFormattedStringExtensions.Translate(key, required_work.gerundLabel);
				return new FloatMenuOption(label + " (" + text + ")", null, MenuOptionPriority.DisabledOption);
			}
			else
				return new FloatMenuOption(
					label,
					delegate
					{
						usa.start_job(p, tar);
					},
					MenuOptionPriority.Default);
		}
	}

	public class bondage_gear_def : ThingDef
	{
		public Type soul_type;
		public HediffDef equipped_hediff = null;
		public bool gives_bound_moodlet = false;
		public bool gives_gagged_moodlet = false;
		public bool blocks_hands = false;
		public bool blocks_oral = false;
		public bool blocks_penis = false;
		public bool blocks_vagina = false;
		public bool blocks_anus = false;
		public bool blocks_breasts = false;
		private bondage_gear_soul soul_ins = null;

		public List<BodyPartDef> HediffTargetBodyPartDefs;      //field for optional list of targeted parts for hediff applying
		public List<BodyPartGroupDef> BoundBodyPartGroupDefs;   //field for optional list of groups restrained of verbcasting

		public bondage_gear_soul soul
		{
			get
			{
				if (soul_ins == null)
					soul_ins = (bondage_gear_soul)Activator.CreateInstance(soul_type);
				return soul_ins;
			}
		}
	}

	public class bondage_gear_soul
	{
		// Adds the bondage gear's associated HediffDef and spawns a matching holokey
		public virtual void on_wear(Pawn wearer, Apparel gear)
		{
			var def = (bondage_gear_def)gear.def;

			if (def.equipped_hediff != null && def.HediffTargetBodyPartDefs != null)
			{
				foreach (BodyPartDef partDef in def.HediffTargetBodyPartDefs)                           //getting BodyPartDef, for example "Arm"
				{
					foreach (BodyPartRecord partRec in wearer.RaceProps.body.GetPartsWithDef(partDef))  //applying hediff to every single arm found on pawn
					{
						wearer.health.AddHediff(def.equipped_hediff, partRec);
					}
				}
			}
			else if (def.equipped_hediff != null && def.HediffTargetBodyPartDefs == null)               //backward compatibility/simplified gear define without HediffTargetBodyPartDefs
			{                                                                                //Hediff applyed to whole body
				wearer.health.AddHediff(def.equipped_hediff);
			}

			var gear_stamp = gear.TryGetComp<CompHoloCryptoStamped>();
			if (gear_stamp != null)
			{
				var key = ThingMaker.MakeThing(ThingDef.Named("Holokey"));
				var key_stamp = key.TryGetComp<CompHoloCryptoStamped>();
				key_stamp.copy_stamp_from(gear_stamp);
				if (wearer.Map != null)
					GenSpawn.Spawn(key, wearer.Position, wearer.Map);
				else
					wearer.inventory.TryAddItemNotForSale(key);
			}
		}

		// Removes the gear's HediffDef
		public virtual void on_remove(Apparel gear, Pawn former_wearer)
		{
			var def = (bondage_gear_def)gear.def;
			if (def.equipped_hediff != null && def.HediffTargetBodyPartDefs != null)
			{                                                                                //getting all Hediffs according with equipped_hediff def
				List<Hediff> hediffs = former_wearer.health.hediffSet.hediffs.Where(x => x.def == def.equipped_hediff).ToList();
				foreach (Hediff hedToRemove in hediffs)
				{
					if (def.HediffTargetBodyPartDefs.Contains(hedToRemove.Part.def))            //removing if applyed by this bondage_gear
						former_wearer.health.RemoveHediff(hedToRemove);                         //assuming there can be several different bondages
				}                                                                               //with the same equipped_hediff def
			}
			else if (def.equipped_hediff != null && def.HediffTargetBodyPartDefs == null)       //backward compatibility/simplified gear define without HediffTargetBodyPartDefs
			{
				var hed = former_wearer.health.hediffSet.GetFirstHediffOfDef(def.equipped_hediff);
				if (hed != null)
					former_wearer.health.RemoveHediff(hed);
			}
		}
	}

	// Give bondage gear an extremely low score when it's not being worn so pawns never equip it on themselves and give
	// it an extremely high score when it is being worn so pawns never try to take it off to equip something "better".
	public class bondage_gear : Apparel
	{
		// made this method universal for any bondage_gear, won't affect anything if gear's BoundBodyPartGroupDefs is empty or null
		public override bool AllowVerbCast(Verb verb)
		{
			if ((this.def as bondage_gear_def).BoundBodyPartGroupDefs != null &&
				verb.tool != null &&
				(this.def as bondage_gear_def).BoundBodyPartGroupDefs.Contains(verb.tool.linkedBodyPartsGroup))
			{
				return false;
			}
			return true;
		}

		//needed for save compatibility only
		public override void ExposeData()
		{
			base.ExposeData();
			//if (Scribe.mode == LoadSaveMode.PostLoadInit)
			//	CheckHediffs();
		}

		//save compatibility insurance, will prevent Armbinder hediff with 0part efficiency on whole body
		private void CheckHediffs()
		{
			var def = (bondage_gear_def)this.def;
			if (this.Wearer == null || def.equipped_hediff == null) return;

			bool changedHediff = false;
			void ApplyHediffDirect(HediffDef hedDef, BodyPartRecord partRec)
			{
				Hediff hediff = (Hediff)HediffMaker.MakeHediff(hedDef, Wearer);
				if (partRec != null)
					hediff.Part = partRec;
				Wearer.health.hediffSet.AddDirect(hediff);
				changedHediff = true;
			}

			void RemoveHediffDirect(Hediff hed)
			{
				Wearer.health.hediffSet.hediffs.Remove(hed);
				changedHediff = true;
			}

			List<bondage_gear> wornBondageGear = Wearer.apparel.WornApparel.Where(x => x is bondage_gear).Cast<bondage_gear>().ToList();
			List<Hediff> hediffs = new List<Hediff>();
			foreach (Hediff h in this.Wearer.health.hediffSet.hediffs) hediffs.Add(h);

			//checking current hediffs defined by bondage_gear for being on defined place, cleaning up the misplaced
			bool equippedHediff;
			bool onPlace;
			foreach (Hediff hed in hediffs)
			{
				equippedHediff = false;
				onPlace = false;
				foreach (bondage_gear gear in wornBondageGear)
				{
					if (hed.def == (gear.def as bondage_gear_def).equipped_hediff)
					{
						//if hediff from bondage_gear and on it's defined place then don't touch it else remove
						//assuming there can be several different bondages with the same equipped_hediff def and different hediff target parts, don't know why
						equippedHediff = true;

						if ((hed.Part != null && (gear.def as bondage_gear_def).HediffTargetBodyPartDefs == null))
						{
							//pass
						}
						else if ((hed.Part == null && (gear.def as bondage_gear_def).HediffTargetBodyPartDefs == null))
						{
							onPlace = true;
							break;
						}
						else if (hed.Part != null && (gear.def as bondage_gear_def).HediffTargetBodyPartDefs.Contains(hed.Part.def))
						{
							onPlace = true;
							break;
						}
					}
				}
				if (equippedHediff && !onPlace)
				{
					ModLog.Message("Removing Hediff " + hed.Label + " from " + Wearer + (hed.Part == null ? "'s body" : "'s " + hed.Part));
					RemoveHediffDirect(hed);
				}
			}

			// now iterating every gear for having all hediffs in place, adding missing
			foreach (bondage_gear gear in wornBondageGear)
			{
				if ((gear.def as bondage_gear_def).equipped_hediff == null) continue;
				if ((gear.def as bondage_gear_def).HediffTargetBodyPartDefs == null)                        //handling gear without HediffTargetBodyPartDefs
				{

					Hediff hed = Wearer.health.hediffSet.hediffs.Find(x => (x.def == (gear.def as bondage_gear_def).equipped_hediff && x.Part == null));//checking hediff defined by gear on whole body
					if (hed == null)                                                                        //if no legit hediff, adding
					{
						ModLog.Message("Adding missing Hediff " + (gear.def as bondage_gear_def).equipped_hediff.label + " to " + Wearer + "'s body");
						ApplyHediffDirect((gear.def as bondage_gear_def).equipped_hediff, null);
					}
				}
				else                                                                                //handling gear with defined HediffTargetBodyPartDefs
				{
					foreach (BodyPartDef partDef in (gear.def as bondage_gear_def).HediffTargetBodyPartDefs)    //getting every partDef
					{
						foreach (BodyPartRecord partRec in Wearer.RaceProps.body.GetPartsWithDef(partDef))      //checking all parts of def for applyed hediff
						{
							Hediff hed = Wearer.health.hediffSet.hediffs.Find(x => (x.def == (gear.def as bondage_gear_def).equipped_hediff && x.Part == partRec));//checking hediff defined by gear on defined place
							if (hed == null)                                                                    //if hediff missing, adding
							{
								ModLog.Message("Adding missing Hediff " + (gear.def as bondage_gear_def).equipped_hediff.label + " to " + Wearer + "'s " + partRec.Label);
								ApplyHediffDirect((gear.def as bondage_gear_def).equipped_hediff, partRec);
							}
						}
					}
				}
				//possibility of several different items with THE SAME equipped_hediff def ON THE SAME PART is not considered, that's sick
			}
			if (changedHediff)
			{
				//Possible error in current toil on Notify_HediffChanged() if capacity.Manipulation is involved so making it in the end.
				//Probably harmless, shouldn't happen again after corrected hediffs will be saved
				Wearer.health.Notify_HediffChanged(null);
			}
		}
	}

	public class armbinder : bondage_gear
	{
		// Prevents pawns in armbinders from melee attacking
		//public override bool AllowVerbCast (IntVec3 root, TargetInfo targ)
		//{
		//	return false;
		//}
	}

	public class yoke : bondage_gear
	{
		// Prevents pawns in armbinders from melee attacking
		//public override bool AllowVerbCast (IntVec3 root, TargetInfo targ)
		//{
		//	return false;
		//}
	}

	public class Restraints : bondage_gear
	{
		// Prevents pawns in armbinders from melee attacking
		//public override bool AllowVerbCast (IntVec3 root, TargetInfo targ)
		//{
		//	return false;
		//}
	}

	public class force_off_gear_def : RecipeDef
	{
		public ThingDef removes_apparel;
		public BodyPartDef failure_affects;
		public List<BodyPartDef> destroys_one_of = null;
		public List<BodyPartDef> major_burns_on = null;
		public List<BodyPartDef> minor_burns_on = null;
	}
}


code:
namespace rjw.Modules.Genitals.Enums
{
	public enum Gender
	{
		Male,
		Female,
		Trap,
		Futa,

		MaleOvi,
		FemaleOvi,

		Unknown
	}
}


code:
using rjw.Modules.Genitals.Enums;
using rjw.Modules.Shared.Extensions;
using rjw.Modules.Shared.Logs;
using System.Linq;

namespace rjw.Modules.Genitals.Helpers
{
	public class GenderHelper
	{
		private static ILog _log = LogManager.GetLogger<GenderHelper>();

		/*public static Gender GetGender(InteractionPawn pawn)
		{
			bool hasVagina = pawn.Parts.Vaginas.Any();
			bool hasPenis = pawn.Parts.Penises.Any();
			bool hasOviFemale = pawn.Parts.FemaleOvipositors.Any();
			bool hasOviMale = pawn.Parts.MaleOvipositors.Any();
			bool hasBreasts = pawn.HasBigBreasts();

			Gender result = Gender.Unknown;

			if (hasVagina && !hasPenis)
			{
				result = Gender.Female;
			}
			else
			if (hasPenis && hasVagina)
			{
				result = Gender.Futa;
			}
			else
			if (hasPenis && hasBreasts)
			{
				result = Gender.Trap;
			}
			else
			if (hasPenis)
			{
				result = Gender.Male;
			}
			else
			if (hasOviMale)
			{
				result = Gender.MaleOvi;
			}
			else
			if (hasOviFemale)
			{
				result = Gender.FemaleOvi;
			}
			else
			if (pawn.Pawn.gender == Verse.Gender.Male)
			{
				result = Gender.Male;
			}
			else
			if (pawn.Pawn.gender == Verse.Gender.Female)
			{
				result = Gender.Female;
			}

			_log.Debug($"{pawn.Pawn.GetName()} detected as {result}");

			return result;
		}*/
	}
}


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

using rjw.Modules.Shared.Extensions;
using System.Runtime.InteropServices;

namespace rjw.Modules.Interactions
{
	public enum VanillaPartFamily
	{
		Undefined,
		Hand, Foot, Tail, Mouth, Beak, Tongue
	}
	public enum LewdablePartFamily // Union of GenitalFamily and VanillaPartFamily
	{
		Undefined,
		Vagina, Penis, Breasts, Udders, Anus, FemaleOvipositor, MaleOvipositor,
		Hand, Foot, Tail, Mouth, Beak, Tongue
	}

	public interface ILewdablePart
	{
		public BodyPartRecord BodyPart { get; }
		public LewdablePartFamily Family { get; }

		public Pawn Owner { get; }
		public string Label { get; }
	}

	public class RJWLewdablePart : ILewdablePart
	{
		public ISexPartHediff SexPart;
		public BodyPartRecord BodyPart { get {
				return SexPart.AsHediff.part;
		}}
		public LewdablePartFamily Family { get; set; }

		public Pawn Owner => SexPart.GetOwner();
		public string Label => SexPart.Def.label.CapitalizeFirst();
		public RJWLewdablePart(ISexPartHediff sexPart, LewdablePartFamily family)
		{
			SexPart = sexPart;
			Family = family;
		}
	}

	public class VanillaLewdablePart : ILewdablePart
	{
		public Pawn Owner { get; }
		public BodyPartRecord BodyPart { get; }
		public LewdablePartFamily Family { get; }
		public string Label => BodyPart.def.label.CapitalizeFirst();
		public VanillaLewdablePart(Pawn owner, BodyPartRecord part, LewdablePartFamily family)
		{
			Owner = owner;
			BodyPart = part;
			Family = family;
		}
	}

	public class LewdParts
	{
		public Pawn Owner { get; protected set; }

		private List<RJWLewdablePart> _penises;
		public List<RJWLewdablePart> Penises
		{
			get
			{
				_penises = _penises ?? LewdPartHelper.GetPenises(Owner);
				return _penises;
			}
		}

		private List<RJWLewdablePart> _vaginas;
		public List<RJWLewdablePart> Vaginas
		{
			get
			{
				_vaginas = _vaginas ?? LewdPartHelper.GetVaginas(Owner);
				return _vaginas;
			}
		}

		private List<RJWLewdablePart> _breasts;
		public List<RJWLewdablePart> Breasts
		{
			get
			{
				_breasts = _breasts ?? LewdPartHelper.GetBreasts(Owner);
				return _breasts;
			}
		}

		private List<RJWLewdablePart> _udders;
		public List<RJWLewdablePart> Udders
		{
			get
			{
				_udders = _udders ?? LewdPartHelper.GetUdders(Owner);
				return _udders;
			}
		}

		private List<RJWLewdablePart> _anuses;
		public List<RJWLewdablePart> Anuses
		{
			get
			{
				_anuses = _anuses ?? LewdPartHelper.GetAnuses(Owner);
				return _anuses;
			}
		}

		private List<RJWLewdablePart> _femaleOvipositors;
		public List<RJWLewdablePart> FemaleOvipositors
		{
			get
			{
				_femaleOvipositors = _femaleOvipositors ?? LewdPartHelper.GetFemaleOvipositors(Owner);
				return _femaleOvipositors;
			}
		}

		private List<RJWLewdablePart> _maleOvipositors;
		public List<RJWLewdablePart> MaleOvipositors
		{
			get
			{
				_maleOvipositors = _maleOvipositors ?? LewdPartHelper.GetMaleOvipositors(Owner);
				return _maleOvipositors;
			}
		}

		private List<VanillaLewdablePart> _mouths;
		public List<VanillaLewdablePart> Mouths
		{
			get
			{
				_mouths = _mouths ?? LewdPartHelper.GetMouths(Owner);
				return _mouths;
			}
		}

		private List<VanillaLewdablePart> _beaks;
		public List<VanillaLewdablePart> Beaks
		{
			get
			{
				_beaks = _beaks ?? LewdPartHelper.GetBeaks(Owner);
				return _beaks;
			}
		}

		private List<VanillaLewdablePart> _tongues;
		public List<VanillaLewdablePart> Tongues
		{
			get
			{
				_tongues = _tongues ?? LewdPartHelper.GetTongues(Owner);
				return _tongues;
			}
		}

		private List<VanillaLewdablePart> _feet;
		public List<VanillaLewdablePart> Feet
		{
			get
			{
				_feet = _feet ?? LewdPartHelper.GetFeet(Owner);
				return _feet;
			}
		}

		private List<VanillaLewdablePart> _hands;
		public List<VanillaLewdablePart> Hands
		{
			get
			{
				_hands = _hands ?? LewdPartHelper.GetHands(Owner);
				return _hands;
			}
		}

		private List<VanillaLewdablePart> _tails;
		public List<VanillaLewdablePart> Tails
		{
			get
			{
				_tails = _tails ?? LewdPartHelper.GetTails(Owner);
				return _tails;
			}
		}

		public List<ILewdablePart> AllParts
		{
			get
			{
				return AllRJWParts.Cast<ILewdablePart>()
					.Concat(AllVanillaParts)
					.ToList();
			}
		}

		public List<RJWLewdablePart> AllRJWParts
		{
			get
			{
				if (_penises == null)
					PopulateAll();

				return Penises.Concat(Vaginas).Concat(Breasts).Concat(Udders).Concat(Anuses)
					.Concat(FemaleOvipositors).Concat(MaleOvipositors)
					.ToList();
			}
		}

		public List<VanillaLewdablePart> AllVanillaParts
		{
			get
			{
				return Mouths
					.Concat(Beaks).Concat(Tongues).Concat(Feet).Concat(Hands).Concat(Tails)
					.ToList();
			}
		}

		public LewdParts(Pawn pawn, bool populateAll = false)
		{
			Owner = pawn;
			if (populateAll)
			{
				PopulateAll();
			}
		}

		public void PopulateAll()
		{
			_mouths = LewdPartHelper.GetMouths(Owner);
			_beaks = LewdPartHelper.GetBeaks(Owner);
			_tongues = LewdPartHelper.GetTongues(Owner);
			_hands = LewdPartHelper.GetHands(Owner);
			_feet = LewdPartHelper.GetFeet(Owner);
			_tails = LewdPartHelper.GetTails(Owner);

			IList<ISexPartHediff> cache = Owner.health.hediffSet.hediffs
				.OfType<ISexPartHediff>()
				.ToList();

			_penises = LewdPartHelper.GetPenises(Owner, cache);
			_vaginas = LewdPartHelper.GetVaginas(Owner, cache);
			_breasts = LewdPartHelper.GetBreasts(Owner, cache);
			_udders = LewdPartHelper.GetUdders(Owner, cache);
			_anuses = LewdPartHelper.GetAnuses(Owner, cache);
		}
	}

	public static class LewdPartHelper
	{
		public static LewdParts GetLewdParts(this Pawn self)
		{
			if (self == null)
			{
				return null;
			}

			var lewdParts = new LewdParts(self);

			return lewdParts;
		}

		public static List<RJWLewdablePart> GetPenises(this Pawn pawn, IEnumerable<ISexPartHediff> cache = null)
		{
			return (cache ?? pawn.health.hediffSet.hediffs.OfType<ISexPartHediff>())
				.ByGenitalFamily(GenitalFamily.Penis, LewdablePartFamily.Penis)
				.ToList();
		}

		public static List<RJWLewdablePart> GetVaginas(this Pawn pawn, IEnumerable<ISexPartHediff> cache = null)
		{
			return (cache ?? pawn.health.hediffSet.hediffs.OfType<ISexPartHediff>())
				.ByGenitalFamily(GenitalFamily.Vagina, LewdablePartFamily.Vagina)
				.ToList();
		}

		public static List<RJWLewdablePart> GetBreasts(this Pawn pawn, IEnumerable<ISexPartHediff> cache = null)
		{
			return (cache ?? pawn.health.hediffSet.hediffs.OfType<ISexPartHediff>())
				.ByGenitalFamily(GenitalFamily.Breasts, LewdablePartFamily.Breasts)
				.ToList();
		}

		public static List<RJWLewdablePart> GetUdders(this Pawn pawn, IEnumerable<ISexPartHediff> cache = null)
		{
			return (cache ?? pawn.health.hediffSet.hediffs.OfType<ISexPartHediff>())
				.ByGenitalFamily(GenitalFamily.Udders, LewdablePartFamily.Udders)
				.ToList();
		}

		public static List<RJWLewdablePart> GetAnuses(this Pawn pawn, IEnumerable<ISexPartHediff> cache = null)
		{
			return (cache ?? pawn.health.hediffSet.hediffs.OfType<ISexPartHediff>())
				.ByGenitalFamily(GenitalFamily.Anus, LewdablePartFamily.Anus)
				.ToList();
		}

		public static List<RJWLewdablePart> GetFemaleOvipositors(this Pawn pawn, IEnumerable<ISexPartHediff> cache = null)
		{
			return (cache ?? pawn.health.hediffSet.hediffs.OfType<ISexPartHediff>())
				.ByGenitalFamily(GenitalFamily.FemaleOvipositor, LewdablePartFamily.FemaleOvipositor)
				.ToList();
		}

		public static List<RJWLewdablePart> GetMaleOvipositors(this Pawn pawn, IEnumerable<ISexPartHediff> cache = null)
		{
			return (cache ?? pawn.health.hediffSet.hediffs.OfType<ISexPartHediff>())
				.ByGenitalFamily(GenitalFamily.MaleOvipositor, LewdablePartFamily.MaleOvipositor)
				.ToList();
		}

		public static List<VanillaLewdablePart> GetMouths(this Pawn pawn)
		{
			//EatingSource = mouth
			return pawn.RaceProps.body.GetPartsWithTag(RimWorld.BodyPartTagDefOf.EatingSource)
				.Where(part => part.def.defName?.ToLower().Contains("beak") == false)
				.AsVanillaLewdableParts(pawn, LewdablePartFamily.Mouth).ToList();
		}
		public static List<VanillaLewdablePart> GetBeaks(this Pawn pawn)
		{
			//EatingSource = mouth
			return pawn.RaceProps.body.GetPartsWithTag(RimWorld.BodyPartTagDefOf.EatingSource)
				.Where(part => part.def.defName?.ToLower().Contains("beak") == true)
				.AsVanillaLewdableParts(pawn, LewdablePartFamily.Beak).ToList();
		}
		public static List<VanillaLewdablePart> GetTongues(this Pawn pawn)
		{
			BodyDef body = pawn.RaceProps.body;

			var possibleParts = body.GetPartsWithTag(RimWorld.BodyPartTagDefOf.Tongue)
				.AsVanillaLewdableParts(pawn, LewdablePartFamily.Tongue).ToList();

			if (possibleParts.Count > 0)
			{
				return possibleParts;
			}

			// Vanilla animals don't have tongues
			possibleParts = body.GetPartsWithDef(RJWBodyPartDefOf.AnimalJaw)
				.AsVanillaLewdableParts(pawn, LewdablePartFamily.Tongue).ToList();

			if (possibleParts.Count > 0)
			{
				return possibleParts;
			}

			// Fallback for races with custom tongue def
			possibleParts = GetPartsWhereDefNameContains(body, "tongue")
				.AsVanillaLewdableParts(pawn, LewdablePartFamily.Tongue).ToList();

			return possibleParts;
		}

		public static List<VanillaLewdablePart> GetTails(this Pawn pawn)
		{
			BodyDef body = pawn.RaceProps.body;
			var possibleParts = body.GetPartsWithDef(RJWBodyPartDefOf.Tail)
				.AsVanillaLewdableParts(pawn, LewdablePartFamily.Tail).ToList();
			if (possibleParts.Count > 0)
			{
				return possibleParts;
			}

			// Found at least one race with a custom tail def
			return GetPartsWhereDefNameContains(body, "tail")
				.AsVanillaLewdableParts(pawn, LewdablePartFamily.Tail).ToList();
		}

		public static List<VanillaLewdablePart> GetHands(this Pawn pawn)
		{
			BodyDef body = pawn.RaceProps.body;
			var possibleParts = body.GetPartsWithDef(BodyPartDefOf.Hand)
				.AsVanillaLewdableParts(pawn, LewdablePartFamily.Hand).ToList();
			if (possibleParts.Count > 0)
			{
				return possibleParts;
			}

			// Some races just don't model hands
			possibleParts = body.GetPartsWithDef(BodyPartDefOf.Arm)
				.AsVanillaLewdableParts(pawn, LewdablePartFamily.Hand).ToList();
			if (possibleParts.Count > 0)
			{
				return possibleParts;
			}

			// At least one race only modelled shoulders
			possibleParts = body.GetPartsWithDef(BodyPartDefOf.Shoulder)
				.AsVanillaLewdableParts(pawn, LewdablePartFamily.Hand).ToList();
			if (possibleParts.Count > 0)
			{
				return possibleParts;
			}

			// Fallback for races with custom hand def
			return GetPartsWhereDefNameContains(body, "hand")
				.AsVanillaLewdableParts(pawn, LewdablePartFamily.Hand).ToList();
		}

		public static List<VanillaLewdablePart> GetFeet(this Pawn pawn)
		{
			BodyDef body = pawn.RaceProps.body;
			var possibleParts = body.GetPartsWithDef(RJWBodyPartDefOf.Foot)
				.AsVanillaLewdableParts(pawn, LewdablePartFamily.Foot).ToList();
			if (possibleParts.Count > 0)
			{
				return possibleParts;
			}

			// Animals have paws instead of feet
			possibleParts = body.GetPartsWithDef(RJWBodyPartDefOf.Paw)
				.AsVanillaLewdableParts(pawn, LewdablePartFamily.Foot).ToList();
			if (possibleParts.Count > 0)
			{
				return possibleParts;
			}

			// Don't know if there are races that don't model feet, but I'll add this
			possibleParts = body.GetPartsWithDef(RJWBodyPartDefOf.Leg)
				.AsVanillaLewdableParts(pawn, LewdablePartFamily.Foot).ToList();
			if (possibleParts.Count > 0)
			{
				return possibleParts;
			}

			// Fallback for races with custom foot def
			return GetPartsWhereDefNameContains(body, "foot", "paw")
				.AsVanillaLewdableParts(pawn, LewdablePartFamily.Foot).ToList();
		}

		public static bool HasGenitalTag(this ILewdablePart part, GenitalTag tag)
		{
			if(part is RJWLewdablePart rjwPart)
			{
				return rjwPart.SexPart.Def.genitalTags.Contains(tag);
			}
			return false;
		}

		public static bool HasPartTag(this ILewdablePart part, string tag)
		{
			if(part is RJWLewdablePart rjwPart)
			{
				return rjwPart.SexPart.Def.partTags.Contains(tag);
			}
			return false;
		}

		public static bool CouldBeOrifice(this ILewdablePart part)
		{
			return SatisfiesTag(part.Family, GenitalTag.CanBePenetrated);
		}

		public static bool CouldBePenetrator(this ILewdablePart part)
		{
			return SatisfiesTag(part.Family, GenitalTag.CanPenetrate);
		}

		public static bool IsMissing(Pawn pawn, ILewdablePart part)
		{
			if(pawn.health.hediffSet.PartIsMissing(part.BodyPart))
			{
				// Part can be missing because it has been replaced by an artificial part.
				// In vanilla an artificial part can replace more than one natural part.
				// For example, the bionic arm is actually a shoulder, while arm, hand and fingers are missing
				if(!pawn.health.hediffSet.PartOrAnyAncestorHasDirectlyAddedParts(part.BodyPart))
				{
					return true;
				}
			}
			return false;
		}
		public static bool IsBlocked(Pawn pawn, ILewdablePart part)
		{
			switch (part.Family)
			{
				case LewdablePartFamily.Penis:
					if (Genital_Helper.penis_blocked(pawn) || Genital_Helper.genitals_blocked(pawn))
						return true;
					break;
				case LewdablePartFamily.Vagina:
					if (Genital_Helper.vagina_blocked(pawn) || Genital_Helper.genitals_blocked(pawn))
						return true;
					break;
				case LewdablePartFamily.MaleOvipositor:
				case LewdablePartFamily.FemaleOvipositor:
					if (Genital_Helper.genitals_blocked(pawn))
						return true;
					break;
				case LewdablePartFamily.Anus:
					if (Genital_Helper.anus_blocked(pawn))
						return true;
					break;
				case LewdablePartFamily.Breasts:
					if (Genital_Helper.breasts_blocked(pawn))
						return true;
					break;
				case LewdablePartFamily.Mouth:
				case LewdablePartFamily.Beak:
				case LewdablePartFamily.Tongue:
					if (Genital_Helper.oral_blocked(pawn))
						return true;
					break;
				case LewdablePartFamily.Hand:
					if (Genital_Helper.hands_blocked(pawn))
						return true;
					break;
			}

			return false;
		}

		public static bool IsAvailable(Pawn pawn, ILewdablePart part)
		{
			return !IsMissing(pawn, part) && !IsBlocked(pawn, part);
		}

		// Find all BodyPartRecords where def name contains one of the strings.
		private static IEnumerable<BodyPartRecord> GetPartsWhereDefNameContains(BodyDef body, params string[] name)
		{
			for (int i = 0; i < body.AllParts.Count; i++)
			{
				if (body.AllParts[i].def.defName?.ToLower().ContainsAny(name) == true)
				{
					yield return body.AllParts[i];
				}
			}
		}

		private static IEnumerable<RJWLewdablePart> ByGenitalFamily(this IEnumerable<ISexPartHediff> self, GenitalFamily family, LewdablePartFamily kind)
		{
			return self.Where(e => e.Def.genitalFamily == family)
				.AsRJWLewdableParts(kind);
		}

		private static IEnumerable<VanillaLewdablePart> AsVanillaLewdableParts(this IEnumerable<BodyPartRecord> self, Pawn owner, LewdablePartFamily kind)
		{
			return self.Select(part => new VanillaLewdablePart(owner, part, kind));
		}

		private static IEnumerable<RJWLewdablePart> AsRJWLewdableParts(this IEnumerable<ISexPartHediff> self, LewdablePartFamily kind)
		{
			return self.Select(part => new RJWLewdablePart(part, kind));
		}

		public static List<ISexPartHediff> AsSexPartHediffs(this List<RJWLewdablePart> self)
		{
			return self.Select(e => e.SexPart).ToList();
		}

		public static List<BodyPartRecord> AsBodyPartRecords(this List<ILewdablePart> self)
		{
			return self.Select(e => e.BodyPart).ToList();
		}

		public static bool SatisfiesTag(this LewdablePartFamily part, GenitalTag tag)
		{
			switch(tag)
			{
				case GenitalTag.CanBeFertilized:
					return part == LewdablePartFamily.Vagina;
				case GenitalTag.CanFertilize:
					return part == LewdablePartFamily.Penis;
				case GenitalTag.CanEgg:
					return part == LewdablePartFamily.FemaleOvipositor;
				case GenitalTag.CanFertilizeEgg:
					return part == LewdablePartFamily.MaleOvipositor;
				case GenitalTag.CanLactate:
					return part == LewdablePartFamily.Breasts || part == LewdablePartFamily.Udders;
				case GenitalTag.CanPenetrate:
					return part == LewdablePartFamily.Penis
						|| part == LewdablePartFamily.MaleOvipositor
						|| part == LewdablePartFamily.FemaleOvipositor
						|| part == LewdablePartFamily.Hand;
				case GenitalTag.CanBePenetrated:
					return part == LewdablePartFamily.Vagina
						|| part == LewdablePartFamily.Anus
						|| part == LewdablePartFamily.FemaleOvipositor
						|| part == LewdablePartFamily.Mouth
						|| part == LewdablePartFamily.Beak;
				default:
					return false;
			}
		}

		// GenitalFamily/VanillaPartFamily -> LewdablePartFamily conversions
		public static LewdablePartFamily AsLewdPartFamily(this GenitalFamily genitalFamily)
		{
			return (LewdablePartFamily)Enum.Parse(typeof(LewdablePartFamily), genitalFamily.ToString());
		}

		public static LewdablePartFamily AsLewdPartFamily(this VanillaPartFamily partFamily)
		{
			return (LewdablePartFamily)Enum.Parse(typeof(LewdablePartFamily), partFamily.ToString());
		}

	}
}


code:
using Verse;
using rjw.Modules.Shared;
using System.Collections.Generic;
using System.Linq;

namespace rjw.Modules.Interactions.Preferences
{
	public class AnimalPreferences
	{
		public static IEnumerable<SexPreference> GetPreferences(Pawn pawn, Pawn partner)
		{
			if (xxx.is_animal(pawn))
			{
				if (xxx.is_animal(partner))
				{
					if (pawn.def.defName == partner.def.defName)
					{
						// Same species, they know how to mate, its always vaginal
						yield return new TypePreference(xxx.rjwSextype.Vaginal, Preference.Always)
							.IfDissatisfied(new SexPreference(Preference.LastResort));
					}
					else
					{
						// Different species, always penetrative but try for vaginal.
						yield return new PenetrativePreference(Preference.Neutral)
							.IfDissatisfied(new SexPreference(Preference.LastResort));
						yield return new TypePreference(xxx.rjwSextype.Vaginal, Preference.Love);

						// Never oral.
						yield return new SelfPartPreference(LewdablePartFamily.Mouth, Preference.LastResort);
						yield return new PartnerPartPreference(LewdablePartFamily.Mouth, Preference.LastResort);
					}
				}
				else
				{
					// Humanlike
					yield return new PenetratedPreference(Preference.Neutral)
						.IfSatisfied(new TypePreference(xxx.rjwSextype.Vaginal, Preference.Love));
					yield return new PenetratingPreference(Preference.Neutral)
						.IfSatisfied(new TypePreference(xxx.rjwSextype.Oral, Preference.Hate));
				}
			}
		}
	}
}


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

namespace rjw.Modules.Interactions.Preferences
{
	public static class BestialityPreferences
	{

		public static IEnumerable<SexPreference> GetPreferences(Pawn pawn, Pawn partner)
		{
			if (xxx.is_human(pawn) && xxx.is_animal(partner))
			{
				if (xxx.is_zoophile(pawn))
				{
					yield return new PenetrativePreference(Preference.Love);
				}
				else
				{
					yield return new PenetrativePreference(Preference.Hate);
				}
			}
		}
	}
}


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

namespace rjw.Modules.Interactions.Preferences
{
	public static class BigBreastsPreferences
	{
		public static IEnumerable<SexPreference> GetPreferences(Pawn pawn, Pawn partner)
		{
			var partnerBreasts = LewdPartHelper.GetBreasts(partner);

			if (partnerBreasts.NullOrEmpty())
			{
				yield break;
			}

			var partnerBigBreasts = partnerBreasts.Any(b => b.SexPart.AsHediff.Severity >= 0.8);
			var partnerSmallBreasts = partnerBreasts.All(b => b.SexPart.AsHediff.Severity < 0.4);

			if (partnerBigBreasts)
			{
				yield return new PartnerPartPreference(LewdablePartFamily.Breasts, Preference.Like);
			}
			else if (partnerSmallBreasts)
			{
				yield return new PartnerPartPreference(LewdablePartFamily.Breasts, Preference.Hate);
			}
		}
	}
}


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

namespace rjw.Modules.Interactions.Preferences
{
	public static class BurnoutPreferences
	{
		public static IEnumerable<SexPreference> GetPreferences(Pawn pawn, Pawn partner)
		{
			Need_Sex need = pawn.needs?.TryGetNeed<Need_Sex>();
			if (need != null && need.CurLevel >= need.thresh_neutral())
			{
				yield return new SelfPartPreference(LewdablePartFamily.Vagina, Preference.Dislike);
				yield return new SelfPartPreference(LewdablePartFamily.Anus, Preference.Dislike);
				yield return new SelfPartPreference(LewdablePartFamily.FemaleOvipositor, Preference.Dislike);
				yield return new SelfPartPreference(LewdablePartFamily.Penis, Preference.Hate);
				yield return new SelfPartPreference(LewdablePartFamily.MaleOvipositor, Preference.Hate);
			}
		}
	}
}


code:
using Verse;
using System.Collections.Generic;
using System.Linq;
using rjw.Modules.Shared.Extensions;
using System;

namespace rjw.Modules.Interactions.Preferences
{
	public static class GeneralPreferences
	{
		public static bool IsOrdinary(Pawn pawn, Pawn partner)
		{
			return xxx.is_heterosexual(pawn)
				&& (pawn.gender != partner.gender || pawn == partner);
		}
		public static IEnumerable<SexPreference> GetPreferences(Pawn pawn, Pawn partner)
		{
			// Societal norms. For ordinary pawns.

			if(IsOrdinary(pawn, partner))
			{
				if (pawn.gender == Gender.Male)
				{
					yield return new PenetratedPreference(Preference.Hate);
				}
				if (pawn.gender == Gender.Female)
				{
					yield return new PenetrativePreference(Preference.Neutral)
						.IfSatisfied(new PenetratingPreference(Preference.Hate));
				}
				yield return new SelfPartPreference(LewdablePartFamily.Anus, Preference.LastResort);
			}
		}
	}
}


code:
using Verse;
using rjw.Modules.Shared;
using System.Collections.Generic;
using System.Linq;

namespace rjw.Modules.Interactions.Preferences
{
	public class InsectPreferences
	{
		public static IEnumerable<SexPreference> GetPreferences(Pawn pawn, Pawn partner)
		{
			if (xxx.is_insect(pawn))
			{
				yield return new PenetrativePreference(Preference.Neutral)
					.IfDissatisfied(new SexPreference(Preference.LastResort));
				if(!xxx.is_insect(partner))
				{
					yield return new PenetratingPreference(Preference.Neutral)
						.IfDissatisfied(new SexPreference(Preference.LastResort));
				}
			}
		}
	}
}


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

namespace rjw.Modules.Interactions.Preferences
{
	public static class RapePreferences
	{
		public static IEnumerable<SexPreference> GetPreferences(Pawn pawn, Pawn partner)
		{
			// Dont let them use parts they can use to retaliate
			var score = Preference.Hate; // Maybe should be Preference.LastResort
			yield return new PartnerPartPreference(LewdablePartFamily.Hand, score);
			yield return new PartnerPartPreference(LewdablePartFamily.Mouth, score);
			yield return new PartnerPartPreference(LewdablePartFamily.Beak, score);
			yield return new PartnerPartPreference(LewdablePartFamily.Tail, score);
			yield return new PartnerPartPreference(LewdablePartFamily.Tongue, score);
		}
	}
}


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

namespace rjw.Modules.Interactions.Preferences
{
	public static class TryForBabyPreferences
	{
		public static IEnumerable<SexPreference> GetPreferences(Pawn pawn, Pawn partner)
		{
			if(pawn.relations.GetPregnancyApproachForPartner(partner) == RimWorld.PregnancyApproach.TryForBaby)
			{
				yield return new TypePreference(xxx.rjwSextype.Vaginal, Preference.Need);
			}
		}
	}
}


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

namespace rjw.Modules.Interactions.Preferences
{
	public static class WhoringPreferences
	{
		public static IEnumerable<SexPreference> GetPreferences(Pawn pawn, Pawn partner)
		{
			yield return new TagPreference(SexInteractionTag.Reverse, Preference.Neutral)
				.IfDissatisfied(new SexPreference(Preference.LastResort));

			if(partner.gender == Gender.Male)
			{
				yield return new PenetrativePreference(Preference.Neutral)
					.IfSatisfied(new PenetratingPreference(Preference.LastResort));
			}
		}
	}
}


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

using rjw.Modules.Shared.Extensions;
using rjw.Modules.Shared;
using UnityEngine;

namespace rjw.Modules.Interactions
{
	public class SexInteraction
	{
		public InteractionDef Def { get; private set; }
		public SexInteractionExtension Extension { get; private set; }
		public SexInteraction(InteractionDef def)
		{
			Def = def;
			Extension = def.GetModExtension<SexInteractionExtension>();
		}

		public bool HasInteractionTag(SexInteractionTag tag)
		{
			return Extension.tags.Contains(tag);
		}

		private xxx.rjwSextype _sextype = xxx.rjwSextype.None;
		public xxx.rjwSextype Sextype {
			get {
				if(_sextype == xxx.rjwSextype.None)
				{
					_sextype = ParseHelper.FromString<xxx.rjwSextype>(Extension.rjwSextype);
				}
				return _sextype;

			}
		}
		public bool IsPenetrative()
		{
			switch(Sextype)
			{
				case xxx.rjwSextype.Vaginal:
				case xxx.rjwSextype.Anal:
				case xxx.rjwSextype.Oral:
				case xxx.rjwSextype.Fellatio:
				case xxx.rjwSextype.Fingering:
				case xxx.rjwSextype.Fisting:
				case xxx.rjwSextype.DoublePenetration:
					return true;
				default:
					return false;
			}
		}

		public void AddToPlayLog(Pawn initiator, Pawn recipient)
		{
			PlayLogEntry_Interaction playLogEntry = new PlayLogEntry_Interaction(Def, initiator, recipient, Extension.extraSentencePacks);
			Find.PlayLog.Add(playLogEntry);
		}
	}

	public class SexInteractionResolved
	{
		public SexInteraction Interaction;
		public List<ILewdablePart> InitiatorParts;
		public List<ILewdablePart> RecipientParts;
		public List<Tuple<ILewdablePart, ILewdablePart>> Penetrations; // <Penetrator, Orifice>
		public SexInteractionScorer.SexInteractionScore Score; // Could be Null if forced
	}

	public static class SexInteractionHelper
	{
		public static void LogInteraction(string message)
		{
			if (RJWSettings.DevMode && RJWSettings.DebugInteraction) ModLog.Message(message);
		}
		public static bool ChooseInteraction(SexProps props)
		{
			var preferences = SexInteractionScorer.GetPreferences(props);

			var available = SexInteractionFinder.FindInteractions(props);

			foreach(var i in available)
			{
				i.Score = SexInteractionScorer.ScoreInteraction(i, preferences, props);
			}

			// Only consider the highest scoring variant of each interaction, to not dilute the pool.
			available = available.GroupBy(i => i.Interaction.Def).Select(g => g.MaxBy(i => i.Score.totalScore)).ToList();

			var scored = new List<Weighted<SexInteractionResolved>>();

			LogInteraction($"ChooseInteraction: {props.initiator.GetName()} -> {props.recipient.GetName()}. IsRape: {props.isRape}, IsWhoring: {props.isWhoring}, IsReceiver: {props.isReceiver}");
			foreach(var i in available)
			{
				Internal.WriteInteractionToLog(i);
				scored.Add(new(i.Score.totalScore, i));
			}

			var choice = scored.RandomElementByWeight(i => i.Weight).Element;
			props.resolved = choice;
			props.interaction = choice.Interaction;

			LogInteraction($"Result: {choice.Interaction.Def.defName}");

			return true;
		}

		public static SexInteractionResolved ResolveInteraction(SexProps props)
		{
			// Resolve a specfic InteractionDef
			// If the interaction is not possible, return null

			var cache = new SexInteractionFinder.Internal.FinderCache(props);
			var variants = SexInteractionFinder.Internal.ResolveInteraction(props.interaction, props, cache).ToList();

			if(variants.Count() == 0)
			{
				return null;
			}

			var preferences = SexInteractionScorer.GetPreferences(props);
			foreach (var i in variants)
			{
				i.Score = SexInteractionScorer.ScoreInteraction(i, preferences, props);
			}

			var choice = variants.MaxBy(i => i.Score.totalScore);

			LogInteraction($"ResolveInteraction: {props.initiator.GetName()} -> {props.recipient.GetName()}");
			Internal.WriteInteractionToLog(choice);

			return choice;
		}

		public static class Internal
		{
			public class FinderCache
			{
				public LewdParts initiatorParts;
				public LewdParts recipientParts;

				public FinderCache(SexProps props)
				{
					initiatorParts = new LewdParts(props.initiator, populateAll: true);
					recipientParts = new LewdParts(props.recipient, populateAll: true);
				}
			}

			public static void WriteInteractionToLog(SexInteractionResolved i)
			{
				LogInteraction($"  Interaction: {i.Interaction.Def.defName} ({i.Score.initiatorScore}, {i.Score.recieverScore}, {i.Score.settingsScore}) -> {i.Score.totalScore}");
				LogInteraction($"    Initiator: {i.InitiatorParts.Select(p => p.Label).ToCommaList()}");
				LogInteraction($"    Recipient: {i.RecipientParts.Select(p => p.Label).ToCommaList()}");
				LogInteraction($"    Penetrations: {i.Penetrations.Select(p => $"{p.Item1.Label} -> {p.Item2.Label}").ToCommaList()}");
			}
		}

		// InteractionTag <-> InteractionType conversions
		public static SexInteractionTag AsInteractionTag(this SexInteractionType interactionType)
		{
			return (SexInteractionTag)Enum.Parse(typeof(SexInteractionTag), interactionType.ToString());
		}

		public static SexInteractionType AsInteractionType(this SexInteractionTag interactionTag)
		{
			if (Enum.IsDefined(typeof(SexInteractionType), interactionTag.ToString()))
			{
				return (SexInteractionType)Enum.Parse(typeof(SexInteractionType), interactionTag.ToString());
			}
			return SexInteractionType.Undefined;
		}
	}
}


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

using rjw.Modules.Shared.Extensions;
using rjw.Modules.Interactions.Preferences;

namespace rjw.Modules.Interactions
{
	public static class SexInteractionFinder
	{
		public static List<SexInteractionResolved> FindInteractions(SexProps props)
		{
			var cache = new Internal.FinderCache(props);

			var interactionType = Internal.FindInteractionType(props);

			var interactions = SexUtility.SexInteractions
				.Select(i => new SexInteraction(i))
				.Where(i => i.Extension.tags.Contains(interactionType.AsInteractionTag()));

			var available = new List<SexInteractionResolved>();
			foreach (var interaction in interactions)
			{
				foreach (var resolved in Internal.ResolveInteraction(interaction, props, cache))
				{
					available.Add(resolved);
				}
			}

			return available;
		}

		public static class Internal
		{
			public class FinderCache
			{
				public LewdParts initiatorParts;
				public LewdParts recipientParts;

				public FinderCache(SexProps props)
				{
					initiatorParts = new LewdParts(props.initiator, populateAll: true);
					recipientParts = new LewdParts(props.recipient, populateAll: true);
				}
			}

			static IEnumerable<IEnumerable<T>> GetPermutations<T>(IEnumerable<T> list, int length)
			{
				if (length == 1) return list.Select(t => new T[] { t });
				return GetPermutations(list, length - 1).SelectMany(t => list.Where(o => !t.Contains(o)),	(t1, t2) => t1.Concat(new T[] { t2 }));
			}

			public static IEnumerable<SexInteractionResolved> ResolveInteraction(SexInteraction interaction, SexProps props, FinderCache cache)
			{
				if (!TrySatisfyRequirement(props.initiator, cache.initiatorParts, interaction.Extension.initiatorRequirement, out var initiatorValidParts))
				{
					yield break;
				}
				if (!TrySatisfyRequirement(props.recipient, cache.recipientParts, interaction.Extension.recipientRequirement, out var recipientValidParts))
				{
					yield break;
				}

				// For non-specific interactions there may be multiple way to resolve them.
				// ex: Anus or vagina could be picked in: mutual masturbation, sixty nine, double penetration, bestiality oral

				foreach(var initiatorPermutation in GetPermutations(initiatorValidParts, interaction.Extension.initiatorRequirement.minimumCount ?? 1))
				{
					foreach(var recipientPermutation in GetPermutations(recipientValidParts, interaction.Extension.recipientRequirement.minimumCount ?? 1))
					{
						var resolved = new SexInteractionResolved
						{
							Interaction = interaction,
							InitiatorParts = initiatorPermutation.ToList(),
							RecipientParts = recipientPermutation.ToList(),
							Penetrations = FindPenetrations(interaction, initiatorPermutation.ToList(), recipientPermutation.ToList(), cache)
						};

						yield return resolved;
					}
				}
			}

			public static bool TrySatisfyRequirement(Pawn pawn, LewdParts parts, SexInteractionRequirement requirement, out List<ILewdablePart> validParts)
			{
				validParts = null;

				// if pawnStates is specified, pawn must match atleast one state
				if (!requirement.pawnStates.NullOrEmpty())
				{
					if (!requirement.pawnStates.Any(s => pawn.GetPawnState() == s))
					{
						return false;
					}
				}

				// now its about parts, filter available parts based on requirements

				// first filter out parts that are not available (missing or blocked)
				validParts = parts.AllParts.Where(p => LewdPartHelper.IsAvailable(pawn, p)).ToList();

				// if genitalFamilies is specified, each part must match atleast one
				if(!requirement.genitalFamilies.NullOrEmpty())
				{
					validParts = validParts.Where(p => requirement.genitalFamilies.Any(f => p.Family == f.AsLewdPartFamily())).ToList();
				}

				// if partFamilies is specified, each part must match atleast one
				if(!requirement.partFamilies.NullOrEmpty())
				{
					validParts = validParts.Where(p => requirement.partFamilies.Any(f => p.Family == f.AsLewdPartFamily())).ToList();
				}

				// if genitalTags is specified, each part must match atleast one
				if(!requirement.genitalTags.NullOrEmpty())
				{
					validParts = validParts.Where(p => requirement.genitalTags.Any(t => p.HasGenitalTag(t))).ToList();
				}

				// if partTags is specified, each part must match atleast one
				if(!requirement.partTags.NullOrEmpty())
				{
					validParts = validParts.Where(p => p is RJWLewdablePart part && requirement.partTags.Any(t => part.SexPart.Def.partTags.Contains(t))).ToList();
				}

				// if minimumSeverity is specified, each part must have atleast that severity
				if(requirement.minimumSeverity.HasValue)
				{
					validParts = validParts.Where(p => p is RJWLewdablePart part && part.SexPart.AsHediff.Severity >= requirement.minimumSeverity).ToList();
				}

				// if minimumCount is specified, we must have atleast that many parts
				if(requirement.minimumCount.HasValue)
				{
					if(validParts.Count < requirement.minimumCount)
					{
						return false;
					}
				}

				if(validParts.Count == 0)
				{
					return false;
				}

				// removes extra hand/feet if we have more than needed
				var dedupeParts = new List<ILewdablePart>();
				foreach(var part in validParts)
				{
					if (part is VanillaLewdablePart)
					{
						if(dedupeParts.Count(p => p.Family == part.Family) < (requirement.minimumCount ?? 1))
						{
							dedupeParts.Add(part);
						}
					}
					else
					{
						dedupeParts.Add(part);
					}
				}
				validParts = dedupeParts;

				return true;
			}
			public static List<Tuple<ILewdablePart, ILewdablePart>> FindPenetrations(SexInteraction interaction, List<ILewdablePart> initiatorParts, List<ILewdablePart> recipientParts, FinderCache cache)
			{
				var found = new List<Tuple<ILewdablePart, ILewdablePart>>();

				// Handle NonPenetrative interactions, useful when it appears to be penetrative but it's not
				if (interaction.Extension.tags.Contains(SexInteractionTag.NonPenetrative))
				{
					return found;
				}

				// Handle Sixtynine, it cant specify mouths so we need to handle it manually.
				if (interaction.Def.defName == "Sex_Sixtynine")
				{
					var initiatorMouth = cache.initiatorParts.Mouths
						.Where(p => LewdPartHelper.IsAvailable(cache.initiatorParts.Owner, p)).ToList();

					var recipientMouth = cache.recipientParts.Mouths
						.Where(p => LewdPartHelper.IsAvailable(cache.initiatorParts.Owner, p)).ToList();

					var initiatorPart = initiatorParts.First();
					var recipientPart = recipientParts.First();

					if(initiatorMouth.Count > 0 && recipientPart.CouldBePenetrator())
					{
						found.Add(new(recipientPart, initiatorMouth.First()));
					}

					if(recipientMouth.Count > 0 && initiatorPart.CouldBePenetrator())
					{
						found.Add(new(initiatorPart, recipientMouth.First()));
					}
					return found;
				}

				// Tags tagged with Multiple count as two parts
				foreach(var p in initiatorParts.Where(p => p.HasPartTag("Multiple")).ToList())
				{
					initiatorParts.Add(p);
				}
				foreach(var p in recipientParts.Where(p => p.HasPartTag("Multiple")).ToList())
				{
					recipientParts.Add(p);
				}

				// Just try map penetrators to orifices. Multiple penetrators can be mapped to a single orifice.
				if(initiatorParts.Count > 0 && recipientParts.Count > 0)
				{
					var initiatorPenetrators = initiatorParts.Where(p => p.CouldBePenetrator()).ToList();
					var initiatorOrifices = initiatorParts.Where(p => p.CouldBeOrifice()).ToList();

					var recipientPenetrators = recipientParts.Where(p => p.CouldBePenetrator()).ToList();
					var recipientOrifices = recipientParts.Where(p => p.CouldBeOrifice()).ToList();

					for(var i = 0; i < initiatorPenetrators.Count; i++)
					{
						if(recipientOrifices.Count == 0)
							break;
						found.Add(new(initiatorPenetrators[i], recipientOrifices[i%recipientOrifices.Count]));
					}

					for(var i = 0; i < recipientPenetrators.Count; i++)
					{
						if(initiatorOrifices.Count == 0)
							break;
						found.Add(new(recipientPenetrators[i], initiatorOrifices[i%initiatorOrifices.Count]));
					}
				}

				return found;
			}

			public static SexInteractionType FindInteractionType(SexProps props)
			{
				if (props.initiator == props.recipient || props.recipient == null)
				{
					return SexInteractionType.Masturbation;
				}

				if (props.recipient.health.Dead == true)
				{
					return SexInteractionType.Necrophilia;
				}

				if (xxx.is_mechanoid(props.initiator))
				{
					return SexInteractionType.Mechanoid;
				}

				//Either one or the other but not both
				if (xxx.is_animal(props.initiator) ^ xxx.is_animal(props.recipient))
				{
					return SexInteractionType.Bestiality;
				}

				if (xxx.is_animal(props.initiator) && xxx.is_animal(props.recipient))
				{
					return SexInteractionType.Animal;
				}

				if (props.isWhoring)
				{
					return SexInteractionType.Whoring;
				}

				if (props.isRape)
				{
					return SexInteractionType.Rape;
				}

				return SexInteractionType.Consensual;
			}
		}
	}
}


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

using rjw.Modules.Interactions;

namespace rjw.Modules.Interactions.Preferences
{
	public static class Preference
	{
		public const float LastResort = 0.001f;
		public const float Despise = 0.1f;
		public const float Hate = 0.2f;
		public const float Dislike = 0.5f;
		public const float Neutral = 1f;
		public const float Like = 2f;
		public const float Love = 5f;
		public const float Need = 10f;
		public const float Always = Mathf.Infinity;
	}
	public class SexPreference
	{
		public float score;
		public List<SexPreference> whenSatisfied;
		public List<SexPreference> whenDissatisfied;

		public SexPreference IfSatisfied(SexPreference child)
		{
			whenSatisfied.Add(child);
			return this;
		}

		public SexPreference IfDissatisfied(SexPreference child)
		{
			whenDissatisfied.Add(child);
			return this;
		}

		public virtual bool SatisfiedBy(bool initiator, SexInteraction interaction)
		{
			return true;
		}

		public virtual bool SatisfiedBy(bool initiator, SexInteractionResolved interaction)
		{
			return SatisfiedBy(initiator, interaction.Interaction);
		}

		// NOTE: If WhenSatisfied/WhenDissatisfied return a new score it will OVERWRITE the existing score, not multiply into it.
		private float GetScoreInternal(bool initiator, object interaction, float defaultValue = 1.0f)
		{
			var value = defaultValue;

			var satisfied = false;
			if(interaction is SexInteraction sexInteraction)
			{
				satisfied = SatisfiedBy(initiator, sexInteraction);
			}
			else if(interaction is SexInteractionResolved resolved)
			{
				satisfied = SatisfiedBy(initiator, resolved);
			}

			if(satisfied)
			{
				value = score;
				foreach(var preference in whenSatisfied)
				{
					var newValue = preference.GetScoreInternal(initiator, interaction, -1.0f);
					if(newValue != -1.0f)
					{
						value = newValue;
					}
				}
			}
			else
			{
				foreach(var preference in whenDissatisfied)
				{
					var newValue = preference.GetScoreInternal(initiator, interaction, -1.0f);
					if(newValue != -1.0f)
					{
						value = newValue;
					}
				}
			}
			return value;
		}

		// Can score with the SexInteraction, or the SexInteractionResolved.
		// - SexInteraction will score for the pool of part choices (essentially worst/best case scenario).
		// - SexInteractionResolved will score for the specific parts chosen.
		public float GetScore(bool initiator, SexInteraction interaction, float defaultValue = 1.0f)
		{
			return GetScoreInternal(initiator, interaction, defaultValue);
		}
		public float GetScore(bool initiator, SexInteractionResolved interaction, float defaultValue = 1.0f)
		{
			return GetScoreInternal(initiator, interaction, defaultValue);
		}
		public SexPreference(float score)
		{
			this.score = score;
			whenSatisfied = new List<SexPreference>();
			whenDissatisfied = new List<SexPreference>();
		}
		// Default constructor is required by the rimworld's xml deserializer
		public SexPreference() : this(Preference.Neutral)
		{ }
	}

	public class SelfPartPreference : SexPreference
	{
		public LewdablePartFamily part;
		public override bool SatisfiedBy(bool initiator, SexInteraction interaction)
		{
			var req = initiator ? interaction.Extension.initiatorRequirement : interaction.Extension.recipientRequirement;
			return req.UsesPart(part);
		}

		// If we have the resolved interaction we can check the parts directly.
		public override bool SatisfiedBy(bool initiator, SexInteractionResolved interaction)
		{
			if(initiator)
				return interaction.InitiatorParts.Any(p => p.Family == part);
			else
				return interaction.RecipientParts.Any(p => p.Family == part);
		}

		public SelfPartPreference(LewdablePartFamily part, float score) : base(score)
		{
			this.part = part;
		}

		public SelfPartPreference() : this(LewdablePartFamily.Undefined, Preference.Neutral)
		{ }
	}

	public class PartnerPartPreference : SexPreference
	{
		public LewdablePartFamily part;
		public override bool SatisfiedBy(bool initiator, SexInteraction interaction)
		{
			var req = initiator ? interaction.Extension.recipientRequirement : interaction.Extension.initiatorRequirement;
			return req.UsesPart(part);
		}

		// If we have the resolved interaction we can check the parts directly.
		public override bool SatisfiedBy(bool initiator, SexInteractionResolved interaction)
		{
			if(initiator)
				return interaction.RecipientParts.Any(p => p.Family == part);
			else
				return interaction.InitiatorParts.Any(p => p.Family == part);
		}
		public PartnerPartPreference(LewdablePartFamily part, float score) : base(score)
		{
			this.part = part;
		}
		public PartnerPartPreference() : this(LewdablePartFamily.Undefined, Preference.Neutral)
		{ }
	}

	public class TagPreference : SexPreference
	{
		public SexInteractionTag tag;
		public override bool SatisfiedBy(bool initiator, SexInteraction interaction)
		{
			return interaction.HasInteractionTag(tag);
		}
		public TagPreference(SexInteractionTag tag, float score) : base(score)
		{
			this.tag = tag;
		}
		public TagPreference() : this(SexInteractionTag.Undefined, Preference.Neutral)
		{ }
	}

	public class TypePreference : SexPreference
	{
		public xxx.rjwSextype type;
		public override bool SatisfiedBy(bool initiator, SexInteraction interaction)
		{
			return interaction.Sextype == type;
		}
		public TypePreference(xxx.rjwSextype typ, float score) : base(score)
		{
			type = typ;
		}
		public TypePreference() : this(xxx.rjwSextype.None, Preference.Neutral)
		{ }
	}

	public class PenetratedPreference : SexPreference
	{
		public override bool SatisfiedBy(bool initiator, SexInteraction interaction)
		{
			var req = initiator ? interaction.Extension.initiatorRequirement : interaction.Extension.recipientRequirement;
			return interaction.IsPenetrative() && req.IsOrifice();
		}
		public PenetratedPreference(float score) : base(score)
		{ }
		public PenetratedPreference() : this(Preference.Neutral)
		{ }
	}

	public class PenetratingPreference : SexPreference
	{
		public override bool SatisfiedBy(bool initiator, SexInteraction interaction)
		{
			var req = initiator ? interaction.Extension.initiatorRequirement : interaction.Extension.recipientRequirement;
			return interaction.IsPenetrative() && req.IsPenetrator();
		}
		public PenetratingPreference(float score) : base(score)
		{ }
		public PenetratingPreference() : this(Preference.Neutral)
		{ }
	}

	public class PenetrativePreference : SexPreference
	{
		public override bool SatisfiedBy(bool initiator, SexInteraction interaction)
		{
			return interaction.IsPenetrative();
		}
		public PenetrativePreference(float score) : base(score)
		{ }
		public PenetrativePreference() : this(Preference.Neutral)
		{ }
	}

	public class SexInteractionPreferences
	{
		public List<SexPreference> initiatorPreferences;
		public List<SexPreference> recieverPreferences;

		public SexInteractionPreferences()
		{
			initiatorPreferences = new List<SexPreference>();
			recieverPreferences = new List<SexPreference>();
		}
	}
}


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

using rjw.Modules.Shared;
using rjw.Modules.Interactions.Preferences;

namespace rjw.Modules.Interactions
{
	public static class SexInteractionScorer
	{
		public class SexInteractionScore
		{
			public float initiatorScore;
			public float recieverScore;
			public float settingsScore;
			public float totalScore;
		}

		public static SexInteractionScore ScoreInteraction(SexInteractionResolved resolved, SexInteractionPreferences preferences, SexProps props)
		{
			var score = new SexInteractionScore();

			score.settingsScore = Internal.SettingSexTypeScore(resolved.Interaction.Sextype);
			score.initiatorScore = Internal.GetScore(true, preferences.initiatorPreferences, resolved);
			score.recieverScore = Internal.GetScore(false, preferences.recieverPreferences, resolved);
			score.totalScore = Internal.GetTotalScore(score, props);

			return score;
		}

		public static SexInteractionPreferences GetPreferences(SexProps props)
		{
			var preferences = new SexInteractionPreferences();

			preferences.initiatorPreferences = GetPawnPreferences(true, props);
			preferences.recieverPreferences = GetPawnPreferences(false, props);

			return preferences;
		}

		public static List<SexPreference> GetPawnPreferences(bool isInitiator, SexProps props)
		{
			var pawn = isInitiator ? props.initiator : props.recipient;
			var partner = isInitiator ? props.recipient : props.initiator;

			var preferences = new List<SexPreference>();

			if(pawn == null)
				return preferences;

			if(xxx.is_insect(pawn))
			{
				preferences.AddRange(InsectPreferences.GetPreferences(pawn, partner));
			}
			else
			{
				if(xxx.is_animal(pawn))
				{
					preferences.AddRange(AnimalPreferences.GetPreferences(pawn, partner));
				}
				if(xxx.is_human(pawn))
				{
					preferences.AddRange(BigBreastsPreferences.GetPreferences(pawn, partner));

					if(props.isWhoring && isInitiator)
					{
						preferences.AddRange(WhoringPreferences.GetPreferences(pawn, partner));
					}
					else
					{
						preferences.AddRange(BurnoutPreferences.GetPreferences(pawn, partner));
						preferences.AddRange(GeneralPreferences.GetPreferences(pawn, partner));
					}
				}

				if(props.isRape && isInitiator)
					preferences.AddRange(RapePreferences.GetPreferences(pawn, partner));

				preferences.AddRange(BestialityPreferences.GetPreferences(pawn, partner));
				preferences.AddRange(TryForBabyPreferences.GetPreferences(pawn, partner));
			}

			return preferences;
		}

		public static class Internal
		{
			public static float GetScore(bool isInitiator, List<SexPreference> preferences, SexInteractionResolved resolved)
			{
				var score = 1.0f;
				foreach(var preference in preferences)
				{
					score *= preference.GetScore(isInitiator, resolved);
				}
				return score;
			}

			public static float GetTotalScore(SexInteractionScore score, SexProps props)
			{
				// Average score
				var avg = score.settingsScore * (score.initiatorScore + score.recieverScore) / 2f;

				if (props.isRape)
				{
					return score.settingsScore * score.initiatorScore;
				}

				if(!props.hasPartner()) // Masturbation
				{
					return score.settingsScore * score.initiatorScore * score.recieverScore;
				}

				var mx = Math.Max(score.initiatorScore, score.recieverScore);
				var mn = Math.Min(score.initiatorScore, score.recieverScore);

				var love = mx > 5f && mn > 0.8f;
				var hate = mn < 0.1;

				// Consensual. So consider pawn preferences more empathetically.
				if(props.isWhoring)
				{
					// Whoring. Partner may really want something.
					if(love)
						return score.settingsScore * mx;
					if (hate)
						return Preference.LastResort;
				}
				else
				{
					// Mutual. Someone may really hate something.
					if (hate)
						return Preference.LastResort;
					if(love)
						return score.settingsScore * mx;
				}

				return avg;
			}

			public static float SettingSexTypeScore(xxx.rjwSextype type)
			{
				switch (type)
				{
					case xxx.rjwSextype.Vaginal:
						return RJWPreferenceSettings.vaginal;
					case xxx.rjwSextype.Anal:
						return RJWPreferenceSettings.anal;
					case xxx.rjwSextype.DoublePenetration:
						return RJWPreferenceSettings.double_penetration;
					case xxx.rjwSextype.Boobjob:
						return RJWPreferenceSettings.breastjob;
					case xxx.rjwSextype.Handjob:
						return RJWPreferenceSettings.handjob;
					case xxx.rjwSextype.Footjob:
						return RJWPreferenceSettings.footjob;
					case xxx.rjwSextype.Fingering:
						return RJWPreferenceSettings.fingering;
					case xxx.rjwSextype.Scissoring:
						return RJWPreferenceSettings.scissoring;
					case xxx.rjwSextype.MutualMasturbation:
						return RJWPreferenceSettings.mutual_masturbation;
					case xxx.rjwSextype.Fisting:
						return RJWPreferenceSettings.fisting;
					case xxx.rjwSextype.Rimming:
						return RJWPreferenceSettings.rimming;
					case xxx.rjwSextype.Fellatio:
						return RJWPreferenceSettings.fellatio;
					case xxx.rjwSextype.Cunnilingus:
						return RJWPreferenceSettings.cunnilingus;
					case xxx.rjwSextype.Sixtynine:
						return RJWPreferenceSettings.sixtynine;
					default:
						return 1;
				}
			}
		}
	}
}


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

namespace rjw
{
	[StaticConstructorOnStartup]
	public static class RJW_Multiplayer
	{
		static RJW_Multiplayer()
		{
			if (!MP.enabled) return;

			// This is where the magic happens and your attributes
			// auto register, similar to Harmony's PatchAll.
			MP.RegisterAll();

				/*
				Log.Message("RJW MP compat testing");
				var type = AccessTools.TypeByName("rjw.RJWdesignations");
				//Log.Message("rjw MP compat " + type.Name);
				Log.Message("is host " + MP.IsHosting);
				Log.Message("PlayerName " + MP.PlayerName);
				Log.Message("IsInMultiplayer " + MP.IsInMultiplayer);

				//MP.RegisterSyncMethod(type, "Comfort");
				/*
				MP.RegisterSyncMethod(type, "<GetGizmos>Service");
				MP.RegisterSyncMethod(type, "<GetGizmos>BreedingHuman");
				MP.RegisterSyncMethod(type, "<GetGizmos>BreedingAnimal");
				MP.RegisterSyncMethod(type, "<GetGizmos>Breeder");
				MP.RegisterSyncMethod(type, "<GetGizmos>Milking");
				MP.RegisterSyncMethod(type, "<GetGizmos>Hero");
				*/

			// You can choose to not auto register and do it manually
			// with the MP.Register* methods.

			// Use MP.IsInMultiplayer to act upon it in other places
			// user can have it enabled and not be in session
		}

		//generate PredictableSeed for Verse.Rand
		public static int PredictableSeed()
		{
			int seed = 0;
			try
			{
				Map map = Find.CurrentMap;
				//int seedHourOfDay = GenLocalDate.HourOfDay(map);
				//int seedDayOfYear = GenLocalDate.DayOfYear(map);
				//int seedYear = GenLocalDate.Year(map);
				seed = (GenLocalDate.HourOfDay(map) + GenLocalDate.DayOfYear(map)) * GenLocalDate.Year(map);
				//int seed = (seedHourOfDay + seedDayOfYear) * seedYear;
				//Log.Warning("seedHourOfDay: " + seedHourOfDay + "\nseedDayOfYear: " + seedDayOfYear + "\nseedYear: " + seedYear + "\n" + seed);
			}
			catch
			{
				seed = Rand.Int;
			}
			return seed;
		}

		//generate PredictableSeed for Verse.Rand
		[SyncMethod]
		public static float RJW_MP_RAND()
		{
			return Rand.Value;
		}
	}
}


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

namespace rjw.Modules.Nymphs
{
	public interface INymphService
	{
		Pawn GenerateNymph(Map map, PawnKindDef nymphKind = null, Faction faction = null);
		IEnumerable<Pawn> GenerateNymphs(Map map, int count);

		PawnKindDef RandomNymphKind();
		IEnumerable<PawnKindDef> ListNymphKindDefs();

		void SetManhunter(Pawn nymph);

	}
}


code:
using Multiplayer.API;
using RimWorld;
using rjw.Modules.Shared.Extensions;
using rjw.Modules.Shared.Logs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace rjw.Modules.Nymphs.Implementation
{
	public class NymphService : INymphService
	{
		private static ILog _log = LogManager.GetLogger<NymphService, NymphLogProvider>();

		public static readonly INymphService Instance;

		static NymphService()
		{
			Instance = new NymphService();
		}

		public Pawn GenerateNymph(Map map, PawnKindDef nymphKind = null, Faction faction = null)
		{
			if (nymphKind == null)
			{
				nymphKind = RandomNymphKind();
			}

			PawnGenerationRequest request = new PawnGenerationRequest(
				kind: nymphKind,
				faction: faction,
				tile: map.Tile,
				forceGenerateNewPawn: true,
				canGeneratePawnRelations: true,
				colonistRelationChanceFactor: 0.0f,
				inhabitant: true,
				relationWithExtraPawnChanceFactor: 0
				);

			Pawn nymph = PawnGenerator.GeneratePawn(request);

			_log.Debug($"Generated Nymph {nymph.GetName()}");

			return nymph;
		}

		public IEnumerable<Pawn> GenerateNymphs(Map map, int count)
		{
			if (count <= 0)
			{
				yield break;
			}

			PawnKindDef nymphKind = RandomNymphKind();

			for (int i = 0; i < count; i++)
			{
				yield return GenerateNymph(map, nymphKind);
			}
		}

		[SyncMethod]
		public PawnKindDef RandomNymphKind()
		{
			return ListNymphKindDefs()
				.RandomElement();
		}
		public IEnumerable<PawnKindDef> ListNymphKindDefs()
		{
			return DefDatabase<PawnKindDef>.AllDefs
				.Where(x => x.defName.Contains("Nymph"));
		}

		public void SetManhunter(Pawn nymph)
		{
			if (RJWSettings.NymphPermanentManhunter)
			{
				nymph.mindState.mentalStateHandler.TryStartMentalState(MentalStateDefOf.ManhunterPermanent);
			}
			else
			{
				nymph.mindState.mentalStateHandler.TryStartMentalState(MentalStateDefOf.Manhunter);
			}
		}
	}
}


code:
using Multiplayer.API;
using RimWorld;
using rjw.Modules.Nymphs.Implementation;
using rjw.Modules.Shared.Logs;
using System.Collections.Generic;
using UnityEngine;
using Verse;

namespace rjw.Modules.Nymphs.Incidents
{
	public abstract class IncidentWorker_BaseNymphRaid : IncidentWorker_NeutralGroup
	{
		private static ILog _log = LogManager.GetLogger<IncidentWorker_BaseNymphRaid, NymphLogProvider>();

		protected static readonly INymphService _nymphGeneratorService;

		static IncidentWorker_BaseNymphRaid()
		{
			_nymphGeneratorService = NymphService.Instance;
		}

		protected virtual float ThreatPointMultiplier => 1f;

		[SyncMethod]
		protected override bool CanFireNowSub(IncidentParms parms)
		{
			//No multiplayer support ... I guess
			if (MP.IsInMultiplayer)
			{
				_log.Debug("Could not fire because multiplayer api is on");
				return false;
			}

			//No map
			if (parms.target is Map == false)
			{
				_log.Debug("Could not fire because the incident target is not a map");
				return false;
			}

			return base.CanFireNowSub(parms);
		}

		protected virtual int GetNymphCount(IncidentParms parms, Map map)
		{
			//Calculating nymphs manyness
			int count;
			count = Mathf.RoundToInt(parms.points / parms.pawnKind.combatPower * ThreatPointMultiplier);
			//Cap the min
			count = Mathf.Max(count, 1);

			return count;
		}

		[SyncMethod]
		protected override void ResolveParmsPoints(IncidentParms parms)
		{
			if (parms.points <= 0)
			{
				parms.points = StorytellerUtility.DefaultThreatPointsNow(parms.target) * ThreatPointMultiplier;
			}
			_log.Debug($"Incident generated with {parms.points} points");
		}

		[SyncMethod]
		protected override bool TryExecuteWorker(IncidentParms parms)
		{
			_log.Debug($"Generating incident");

			//Walk from the edge
			parms.raidArrivalMode = PawnsArrivalModeDefOf.EdgeWalkIn;
			if (parms.raidArrivalMode.Worker.TryResolveRaidSpawnCenter(parms) == false)
			{
				_log.Debug($"Incident failed to fire, no spawn points available");
				return false;
			}

			//Find PawnKind
			parms.pawnKind = Nymph_Generator.GetFixedNymphPawnKindDef();

			if(parms.pawnKind == null)
			{
				_log.Debug($"Incident failed to fire, no Pawn Kind for nymphs");
				return false;
			}

			//Calculating nymphs manyness
			int count = GetNymphCount(parms, (Map)parms.target);

			_log.Debug($"Will generate {count} nymphs");

			List<Pawn> nymphs = GenerateNymphs(parms.target as Map, count);

			parms.raidArrivalMode.Worker.Arrive(nymphs, parms);

			SetManhunters(nymphs);

			Find.LetterStack.ReceiveLetter(
				"RJW_nymph_incident_raid_title".Translate(),
				"RJW_nymph_incident_raid_description".Translate(),
				LetterDefOf.ThreatBig,
				nymphs);

			return true;
		}

		protected List<Pawn> GenerateNymphs(Map map, int count)
		{
			List<Pawn> result = new List<Pawn>();

			PawnKindDef nymphKind = _nymphGeneratorService.RandomNymphKind();

			for (int i = 1; i <= count; ++i)
			{
				Pawn nymph = _nymphGeneratorService.GenerateNymph(map, nymphKind);

				//Set it wild
				nymph.ChangeKind(PawnKindDefOf.WildMan);

				result.Add(nymph);
			}

			return result;
		}

		protected void SetManhunters(List<Pawn> nymphs)
		{
			foreach (var nymph in nymphs)
			{
				_nymphGeneratorService.SetManhunter(nymph);
			}
		}
	}
}


code:
using Multiplayer.API;
using RimWorld;
using rjw.Modules.Nymphs.Implementation;
using rjw.Modules.Shared.Extensions;
using rjw.Modules.Shared.Logs;
using System.Collections.Generic;
using Verse;

namespace rjw.Modules.Nymphs.Incidents
{
	public class IncidentWorker_NymphJoin : IncidentWorker
	{
		private static ILog _log = LogManager.GetLogger<IncidentWorker_NymphJoin, NymphLogProvider>();

		protected static readonly INymphService _nymphGeneratorService;

		static IncidentWorker_NymphJoin()
		{
			_nymphGeneratorService = NymphService.Instance;
		}

		[SyncMethod]
		protected override bool CanFireNowSub(IncidentParms parms)
		{
			//The event is disabled, don't fire !
			if (RJWSettings.NymphTamed == false)
			{
				_log.Debug("The incident can't fire as it is disabled in RJW settings");
				return false;
			}

			//No multiplayer support
			if (MP.IsInMultiplayer)
			{
				_log.Debug("Could not fire because multiplayer api is on");
				return false;
			}

			//No map
			if (parms.target is Map == false)
			{
				_log.Debug("Could not fire because the incident target is not a map");
				return false;
			}

			return base.CanFireNowSub(parms);
		}

		[SyncMethod]
		protected override bool TryExecuteWorker(IncidentParms parms)
		{
			_log.Debug($"Generating incident");

			//Walk from the edge
			parms.raidArrivalMode = PawnsArrivalModeDefOf.EdgeWalkIn;
			if (parms.raidArrivalMode.Worker.TryResolveRaidSpawnCenter(parms) == false)
			{
				_log.Debug($"Incident failed to fire, no spawn points available");
				return false;
			}

			Pawn nymph = GenerateNymph(parms.target as Map);

			_log.Debug($"Generated nymph {nymph.GetName()}");

			parms.raidArrivalMode.Worker.Arrive(new List<Pawn>() { nymph }, parms);

			JoinPlayerFaction(nymph);

			Find.LetterStack.ReceiveLetter(
				"RJW_nymph_incident_join_title".Translate(),
				"RJW_nymph_incident_join_description".Translate(),
				LetterDefOf.PositiveEvent,
				nymph);

			return true;
		}

		protected Pawn GenerateNymph(Map map)
		{
			Pawn nymph = _nymphGeneratorService.GenerateNymph(map);

			nymph.ChangeKind(PawnKindDefOf.WildMan);

			return nymph;
		}

		protected void JoinPlayerFaction(Pawn nymph)
		{
			nymph.SetFaction(Faction.OfPlayer);
		}
	}
}


code:
using Multiplayer.API;
using RimWorld;
using rjw.Modules.Shared.Logs;
using UnityEngine;
using Verse;

namespace rjw.Modules.Nymphs.Incidents
{
	public class IncidentWorker_NymphRaidEasy : IncidentWorker_BaseNymphRaid
	{
		private static ILog _log = LogManager.GetLogger<IncidentWorker_NymphRaidEasy, NymphLogProvider>();

		[SyncMethod]
		protected override bool CanFireNowSub(IncidentParms parms)
		{
			//The event is disabled, don't fire !
			if (RJWSettings.NymphRaidEasy == false)
			{
				_log.Debug("The incident can't fire as it is disabled in RJW settings");
				return false;
			}

			return base.CanFireNowSub(parms);
		}
		protected override int GetNymphCount(IncidentParms parms, Map map)
		{
			int count;

			//Calculating nymphs manyness
			if (RJWSettings.NymphRaidRP)
			{
				count = base.GetNymphCount(parms, map);
			}
			else
			{
				count = (Find.World.worldPawns.AllPawnsAlive.Count + map.mapPawns.FreeColonistsAndPrisonersSpawnedCount);
				//Cap the max
				count = Mathf.Min(count, 100);
				//Cap the min
				count = Mathf.Max(count, 1);
			}

			return count;
		}
	}
}


code:
using Multiplayer.API;
using RimWorld;
using rjw.Modules.Shared.Logs;
using UnityEngine;
using Verse;

namespace rjw.Modules.Nymphs.Incidents
{
	public class IncidentWorker_NymphRaidHard : IncidentWorker_BaseNymphRaid
	{
		private static ILog _log = LogManager.GetLogger<IncidentWorker_NymphRaidHard, NymphLogProvider>();

		protected override float ThreatPointMultiplier => 1.5f;

		[SyncMethod]
		protected override bool CanFireNowSub(IncidentParms parms)
		{
			//The event is disabled, don't fire !
			if (RJWSettings.NymphRaidHard == false)
			{
				_log.Debug("The incident can't fire as it is disabled in RJW settings");
				return false;
			}

			return base.CanFireNowSub(parms);
		}
		protected override int GetNymphCount(IncidentParms parms, Map map)
		{
			int count;

			//Calculating nymphs manyness
			if (RJWSettings.NymphRaidRP)
			{
				count = base.GetNymphCount(parms, map);
			}
			else
			{
				count = map.mapPawns.AllPawnsSpawnedCount;
				//Cap the max
				count = Mathf.Min(count, 1000);
				//Cap the min
				count = Mathf.Max(count, 1);
			}

			return count;
		}
	}
}


code:
using Multiplayer.API;
using RimWorld;
using rjw.Modules.Nymphs.Implementation;
using rjw.Modules.Shared.Extensions;
using rjw.Modules.Shared.Logs;
using System.Collections.Generic;
using Verse;

namespace rjw.Modules.Nymphs.Incidents
{
	public class IncidentWorker_NymphVisitor : IncidentWorker
	{
		private static ILog _log = LogManager.GetLogger<IncidentWorker_NymphVisitor, NymphLogProvider>();

		protected static readonly INymphService _nymphService;

		static IncidentWorker_NymphVisitor()
		{
			_nymphService = NymphService.Instance;
		}

		[SyncMethod]
		protected override bool CanFireNowSub(IncidentParms parms)
		{
			//The event is disabled, don't fire !
			if (RJWSettings.NymphWild == false)
			{
				_log.Debug("The incident can't fire as it is disabled in RJW settings");
				return false;
			}

			//No multiplayer support ... I guess
			if (MP.IsInMultiplayer)
			{
				_log.Debug("Could not fire because multiplayer api is on");
				return false;
			}

			//No map
			if (parms.target is Map == false)
			{
				_log.Debug("Could not fire because the incident target is not a map");
				return false;
			}

			return base.CanFireNowSub(parms);
		}

		[SyncMethod]
		protected override bool TryExecuteWorker(IncidentParms parms)
		{
			_log.Debug($"Generating incident");

			//Walk from the edge
			parms.raidArrivalMode = PawnsArrivalModeDefOf.EdgeWalkIn;
			if (parms.raidArrivalMode.Worker.TryResolveRaidSpawnCenter(parms) == false)
			{
				_log.Debug($"Incident failed to fire, no spawn points available");
				return false;
			}

			Pawn nymph = GenerateNymph(parms.target as Map);

			_log.Debug($"Generated nymph {nymph.GetName()}");

			parms.raidArrivalMode.Worker.Arrive(new List<Pawn>() { nymph }, parms);

			SetManhunter(nymph);

			Find.LetterStack.ReceiveLetter(
				"RJW_nymph_incident_wander_title".Translate(),
				"RJW_nymph_incident_wander_description".Translate(),
				LetterDefOf.ThreatSmall,
				nymph);

			return true;
		}

		protected Pawn GenerateNymph(Map map)
		{
			Pawn nymph = _nymphService.GenerateNymph(map);

			nymph.ChangeKind(PawnKindDefOf.WildMan);

			return nymph;
		}

		protected void SetManhunter(Pawn nymph)
		{
			_nymphService.SetManhunter(nymph);
		}
	}
}


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

namespace rjw
{
	// Token: 0x020006B5 RID: 1717
	public class JobGiver_NymphSapper : ThinkNode_JobGiver
	{
		// Token: 0x06002E54 RID: 11860 RVA: 0x001041A7 File Offset: 0x001023A7
		public override ThinkNode DeepCopy(bool resolve = true)
		{
			JobGiver_NymphSapper jobGiver_NymphSapper = (JobGiver_NymphSapper)base.DeepCopy(resolve);
			jobGiver_NymphSapper.canMineMineables = this.canMineMineables;
			jobGiver_NymphSapper.canMineNonMineables = this.canMineNonMineables;
			return jobGiver_NymphSapper;
		}

		// Token: 0x06002E55 RID: 11861 RVA: 0x001041D0 File Offset: 0x001023D0
		[SyncMethod]
		protected override Job TryGiveJob(Pawn pawn)
		{
			if (!RJWSettings.NymphSappers)
				return null;
			if (!xxx.is_nympho(pawn))
				return null;

			IntVec3 intVec;
			{
				IAttackTarget attackTarget;
				if (!(from x in pawn.Map.mapPawns.FreeColonistsAndPrisonersSpawned
				where !x.ThreatDisabled(pawn) && pawn.CanReach(x, PathEndMode.OnCell, Danger.Deadly, false, false, TraverseMode.PassAllDestroyableThings)
				select x).TryRandomElement(out attackTarget))
				{
					return null;
				}
				intVec = attackTarget.Thing.Position;
			}
			using (PawnPath pawnPath = pawn.Map.pathFinder.FindPath(pawn.Position, intVec, TraverseParms.For(pawn, Danger.Deadly, TraverseMode.PassDoors, false), PathEndMode.OnCell))
			{
				IntVec3 cellBeforeBlocker;
				Thing thing = pawnPath.FirstBlockingBuilding(out cellBeforeBlocker, pawn);
				if (thing != null)
				{
					Job job = DigUtility.PassBlockerJob(pawn, thing, cellBeforeBlocker, this.canMineMineables, this.canMineNonMineables);
					if (job != null)
					{
						return job;
					}
				}
			}
			return JobMaker.MakeJob(JobDefOf.Goto, intVec, 500, true);
		}

		// Token: 0x04001A64 RID: 6756
		private bool canMineMineables = false;

		// Token: 0x04001A65 RID: 6757
		private bool canMineNonMineables = false;

		// Token: 0x04001A66 RID: 6758
		private const float ReachDestDist = 10f;

		// Token: 0x04001A67 RID: 6759
		private const int CheckOverrideInterval = 500;
	}
}


code:
using rjw.Modules.Shared.Logs;

namespace rjw.Modules.Nymphs
{
	public class NymphLogProvider : ILogProvider
	{
		public bool IsActive => RJWSettings.DebugNymph;
	}
}


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

namespace rjw
{
	public struct nymph_passion_chances
	{
		public float major;
		public float minor;

		public nymph_passion_chances(float maj, float min)
		{
			major = maj;
			minor = min;
		}
	}

	public static class nymph_backstories
	{

		public static nymph_passion_chances get_passion_chances(BackstoryDef child_bs, BackstoryDef adult_bs, SkillDef skill_def)
		{
			var maj = 0.0f;
			var min = 0.0f;

			if (adult_bs.defName.Contains("feisty"))
			{
				if (skill_def == SkillDefOf.Melee) { maj = 0.50f; min = 1.00f; }
				else if (skill_def == SkillDefOf.Shooting) { maj = 0.25f; min = 0.75f; }
				else if (skill_def == SkillDefOf.Social) { maj = 0.10f; min = 0.67f; }
			}
			else if (adult_bs.defName.Contains("curious"))
			{
				if (skill_def == SkillDefOf.Construction) { maj = 0.15f; min = 0.40f; }
				else if (skill_def == SkillDefOf.Crafting) { maj = 0.50f; min = 1.00f; }
				else if (skill_def == SkillDefOf.Social) { maj = 0.20f; min = 1.00f; }
			}
			else if (adult_bs.defName.Contains("tender"))
			{
				if (skill_def == SkillDefOf.Medicine) { maj = 0.20f; min = 0.60f; }
				else if (skill_def == SkillDefOf.Social) { maj = 0.50f; min = 1.00f; }
			}
			else if (adult_bs.defName.Contains("chatty"))
			{
				if (skill_def == SkillDefOf.Social) { maj = 1.00f; min = 1.00f; }
			}
			else if (adult_bs.defName.Contains("broken"))
			{
				if (skill_def == SkillDefOf.Artistic) { maj = 0.50f; min = 1.00f; }
				else if (skill_def == SkillDefOf.Social) { maj = 0.00f; min = 0.33f; }
			}
			else if (adult_bs.defName.Contains("homekeeper"))
			{
				if (skill_def == SkillDefOf.Cooking) { maj = 0.50f; min = 1.00f; }
				else if (skill_def == SkillDefOf.Social) { maj = 0.00f; min = 0.33f; }
			}

			return new nymph_passion_chances(maj, min);
		}

		// Randomly chooses backstories and traits for a nymph
		[SyncMethod]
		public static void generate(Pawn pawn)
		{
			var tr = pawn.story;
			//reset stories
			tr.Childhood = DefDatabase<BackstoryDef>.AllDefsListForReading.Where(x => x.slot == BackstorySlot.Childhood && x.spawnCategories.Contains("rjw_nymphsCategory")).RandomElement();
			tr.Adulthood = DefDatabase<BackstoryDef>.AllDefsListForReading.Where(x => x.slot == BackstorySlot.Adulthood && x.spawnCategories.Contains("rjw_nymphsCategory")).RandomElement();

			pawn.story.traits.allTraits.AddDistinct(new Trait(xxx.nymphomaniac, 0));

			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			var beauty = 0;
			var rv2 = Rand.Value;

			if (tr.Adulthood.defName.Contains("feisty"))
			{
				beauty = Rand.RangeInclusive(0, 2);
				if (rv2 < 0.33)
					pawn.story.traits.allTraits.AddDistinct(new Trait(TraitDefOf.Brawler));
				else if (rv2 < 0.67)
					pawn.story.traits.allTraits.AddDistinct(new Trait(TraitDefOf.Bloodlust));
				else
					pawn.story.traits.allTraits.AddDistinct(new Trait(xxx.rapist));
			}
			else if (tr.Adulthood.defName.Contains("curious"))
			{
				beauty = Rand.RangeInclusive(0, 2);
				if (rv2 < 0.33)
					pawn.story.traits.allTraits.AddDistinct(new Trait(TraitDefOf.Transhumanist));
			}
			else if (tr.Adulthood.defName.Contains("tender"))
			{
				beauty = Rand.RangeInclusive(1, 2);
				if (rv2 < 0.50)
					pawn.story.traits.allTraits.AddDistinct(new Trait(TraitDefOf.Kind));
			}
			else if (tr.Adulthood.defName.Contains("chatty"))
			{
				beauty = 2;
				if (rv2 < 0.33)
					pawn.story.traits.allTraits.AddDistinct(new Trait(TraitDefOf.Greedy));
			}
			else if (tr.Adulthood.defName.Contains("broken"))
			{
				beauty = Rand.RangeInclusive(0, 2);
				if (rv2 < 0.33)
					pawn.story.traits.allTraits.AddDistinct(new Trait(TraitDefOf.DrugDesire, 1));
				else if (rv2 < 0.67)
					pawn.story.traits.allTraits.AddDistinct(new Trait(TraitDefOf.DrugDesire, 2));
			}
			else if (tr.Adulthood.defName.Contains("homekeeper"))
			{
				beauty = Rand.RangeInclusive(1, 2);
				if (rv2 < 0.33)
					pawn.story.traits.allTraits.AddDistinct(new Trait(TraitDefOf.Kind));
			}

			if (beauty > 0)
				pawn.story.traits.allTraits.AddDistinct(new Trait(VanillaTraitDefOf.Beauty, beauty, false));
		}
	}
}