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

using ShowParts = rjw.RJWSettings.ShowParts;

namespace rjw
{
	/// <summary>
	/// Comp for rjw hediff parts
	/// </summary>
	public class HediffComp_SexPart : HediffComp
	{
		public string SizeLabel => parent.CurStage?.label ?? "";

		public string originalOwnerRace = "";      //base race when part created
		public string previousOwner = "";            //base race when part created

		private float? forcedSize;
		public float baseSize;                              	// Absolute base size when part created, someday alter by operation
		public float originalOwnerSize = 0;                         // Body size of this part's first owner. Not used in calculations.
		public float bodySizeOverride = -1;                // For modders, used to overwrite the (displayed) size instead of pawn.body.size

		private SexFluidDef fluidOverride;                   //cummies/milk - insectjelly/honey etc
		public float partFluidFactor;                       //amount of Milk/Ejaculation/Wetness

		public string possibleEggsTipString;                         //for ovi eggs, maybe
		public bool isTransplant = false;                   //transplanted from someone else

		// Whether this part could ever have been seen by the player based on mod settings and apparel.
		public bool discovered;

		private bool initialised;

		public new HediffDef_SexPart Def => (HediffDef_SexPart)base.Def;

		public HediffCompProperties_SexPart Props => (HediffCompProperties_SexPart)props;

		public float FluidAmount => partFluidFactor * Def.fluidMultiplier;

		public SexFluidDef Fluid
		{
			get => fluidOverride ?? Def.fluid;
			set
			{
				if (value == Def.fluid)
				{
					fluidOverride = null;
				}
				else
				{
					fluidOverride = value;
				}
			}
		}

		private List<string> DefaultPositionNames
		{
			get
			{
				List<string> result = Def.defaultBodyPartList ?? new();
				string singleDefaultPart = Def.defaultBodyPart;
				if (!singleDefaultPart.NullOrEmpty() && !result.Contains(singleDefaultPart))
				{
					result.Add(singleDefaultPart);
				}
				return result;
			}
		}

		public float Size
		{
			get
			{
				if (forcedSize.HasValue)
				{
					return forcedSize.Value;
				}
				return baseSize * BodySizeFactor;
			}
		}

		// Scales with square of body size so that severity is not constant. May be worth using relBodySize * Growth instead.
		private float BodySizeFactor => Pawn.BodySize * Pawn.BodySize
									/ (Pawn.RaceProps.baseBodySize * Pawn.RaceProps.baseBodySize);

		public bool HasForcedSize => forcedSize.HasValue;

		/// <summary>
		/// save data
		/// </summary>
		public override void CompExposeData()
		{
			base.CompExposeData();

			Scribe_Values.Look(ref baseSize, nameof(baseSize));
			Scribe_Values.Look(ref originalOwnerSize, nameof(originalOwnerSize));
			Scribe_Values.Look(ref originalOwnerRace, nameof(originalOwnerRace));
			Scribe_Values.Look(ref previousOwner, nameof(previousOwner));
			Scribe_Defs.Look(ref fluidOverride, nameof(fluidOverride));
			Scribe_Values.Look(ref partFluidFactor, nameof(partFluidFactor));
			Scribe_Values.Look(ref possibleEggsTipString, "possibleEggs");
			Scribe_Values.Look(ref isTransplant, nameof(isTransplant));
			Scribe_Values.Look(ref forcedSize, nameof(forcedSize));
			Scribe_Values.Look(ref discovered, nameof(discovered));
			Scribe_Values.Look(ref initialised, nameof(initialised), defaultValue: true);
		}

		public override void CompPostPostAdd(DamageInfo? dinfo)
		{
			base.CompPostPostAdd(dinfo);
			try//error at world gen
			{
				Pawn.GetRJWPawnData().genitals = new List<Hediff>();
				Pawn.GetRJWPawnData().breasts = new List<Hediff>();
				Pawn.GetRJWPawnData().anus = new List<Hediff>();
			}
			catch
			{
			}
			if (!initialised)
			{
				Init(reroll: false);
			}
		}

		public override void CompPostMake()
		{
			base.CompPostMake();
			// A null pawn is only possible in vanilla in verb-related debug outputs, but better safe than sorry.
			if (Pawn != null)
			{
				Init();
			}
		}

		public override void CompPostPostRemoved()
		{
			base.CompPostPostRemoved();
			try
			{
				Pawn.GetRJWPawnData().genitals = new List<Hediff>();
				Pawn.GetRJWPawnData().breasts = new List<Hediff>();
				Pawn.GetRJWPawnData().anus = new List<Hediff>();
			}
			catch
			{
			}
		}

		public override bool CompDisallowVisible()
		{
			if (base.CompDisallowVisible())
			{
				return true;
			}

			if (RJWSettings.ShowRjwParts == ShowParts.Hide)
			{
				return true;
			}

			if (discovered)
			{
				return false;
			}

			// Show if required by settings, or at game start if in dev mode
			if (RJWSettings.ShowRjwParts != ShowParts.Known || (Current.ProgramState != ProgramState.Playing && Prefs.DevMode))
			{
				discovered = true;
				return false;
			}

			// Show for hero and apparel-less pawns
			if (Pawn.IsDesignatedHero() && Pawn.IsHeroOwner() || Pawn.apparel == null)
			{
				discovered = true;
			}
			else
			{
				var partGroups = parent.Part.groups;
				if (!partGroups.Any(group => Pawn.apparel.BodyPartGroupIsCovered(group)))
				{
					discovered = true;
				}
			}
			return !discovered;
		}

		/// <summary>
		/// show part info in healthtab
		/// </summary>
		public override string CompTipStringExtra
		{
			get
			{
				if (!initialised)
				{
					ModLog.Warning(" " + xxx.get_pawnname(Pawn) + " part " + parent.def.defName + " is broken, resetting");
					Init();
					UpdateSeverity();
					updatepartposition();
				}
				// In theory the tip should be a condensed form and the longer form should be elsewhere, but where?
				return GetTipString(RJWSettings.ShowRjwParts == RJWSettings.ShowParts.Extended).Join("\n");
			}
		}

		IEnumerable<string> GetTipString(bool extended)
		{
			//ModLog.Message(" CompTipStringExtra " + xxx.get_pawnname(Pawn) + " " + parent.def.defName);
			// TODO: Part type should be a property in the extension.
			//StringBuilder defstringBuilder = new StringBuilder();
			if (parent.Part != null)
				if (Def.genitalFamily == GenitalFamily.Breasts)
				{
					return GetBreastTip(extended);
				}
				else if (Def.genitalFamily == GenitalFamily.Penis)
				{
					return GetPenisTip(extended);
				}
				else if (Def.genitalFamily == GenitalFamily.Anus || Def.genitalFamily == GenitalFamily.Vagina)
				{
					return GetOrificeTip(extended);
				}
				else if (Def.genitalFamily == GenitalFamily.FemaleOvipositor || Def.genitalFamily == GenitalFamily.MaleOvipositor)
				{
					return GetEggTip();
				}

			return Enumerable.Empty<string>();
		}

		IEnumerable<string> GetEggTip()
		{
			if (!possibleEggsTipString.NullOrEmpty())
			{
				yield return "RJW_PartInfo_eggs".Translate(possibleEggsTipString);
			}
		}

		IEnumerable<string> GetPenisTip(bool extended)
		{
			if (PartSizeCalculator.TryGetLength(parent, out float length))
			{
				yield return "RJW_PartInfo_length".Translate(length.ToString("F1"));
			}

			if (!extended)
			{
				yield break;
			}

			if (PartSizeCalculator.TryGetGirth(parent, out float girth))
			{
				yield return "RJW_PartInfo_girth".Translate(girth.ToString("F1"));
			}

			if (PartSizeCalculator.TryGetPenisWeight(parent, out float weight))
			{
				yield return "RJW_PartInfo_weight".Translate(weight.ToString("F1"));
			}

			foreach (var line in GetFluidTip())
			{
				yield return line;
			}

			if (Def.partTags.Any())
			{
				yield return "RJW_PartInfo_properties".Translate(Def.partTags.ToCommaList());
			}
		}

		IEnumerable<string> GetOrificeTip(bool extended)
		{
			if (!extended)
			{
				yield break;
			}
			//defstringBuilder.Append(this.Def.description);
			//defstringBuilder.AppendLine("Cum: " + FluidType);
			//defstringBuilder.AppendLine("Ejaculation: " + (FluidAmmount * FluidModifier).ToString("F2") + "ml");

			// TODO: Should be scaled bigger than penis size either in xml or here.
			if (PartSizeCalculator.TryGetLength(parent, out float length))
			{
				yield return "RJW_PartInfo_maxLength".Translate(length.ToString("F1"));
			}

			if (PartSizeCalculator.TryGetGirth(parent, out float girth))
			{
				yield return "RJW_PartInfo_maxGirth".Translate(girth.ToString("F1"));
			}

			foreach (var line in GetFluidTip())
			{
				yield return line;
			}

			if (Def.partTags.Any())
			{
				yield return "Properties: " + Def.partTags.ToCommaList();
			}
		}

		IEnumerable<string> GetBreastTip(bool extended)
		{
			if (parent.Part.def == xxx.breastsDef)
			{
				foreach (var line in GetBraCupTip())
				{
					yield return line;
				}

				if (!extended)
				{
					yield break;
				}

				if (PartSizeCalculator.TryGetCupSize(parent, out float size))
				{
					var cupSize = (int)size;

					if (cupSize > 0)
					{
						var bandSize = PartSizeCalculator.GetBandSize(parent);
						yield return "RJW_PartInfo_braSize".Translate(
							bandSize,
							cupSize);

						if (PartSizeCalculator.TryGetOverbustSize(parent, out float overbust))
						{
							var underbust = PartSizeCalculator.GetUnderbustSize(parent);
							yield return "RJW_PartInfo_underbust".Translate(underbust.ToString("F1"));
							yield return "RJW_PartInfo_overbust".Translate(overbust.ToString("F1"));
						}

						if (PartSizeCalculator.TryGetBreastWeight(parent, out float weight))
						{
							yield return "RJW_PartInfo_weight".Translate(weight.ToString("F1"));
						}
					}
					else
					{
						yield return "RJW_PartInfo_braSizeNone".Translate();
					}
				}
			}

			foreach (var line in GetFluidTip())
			{
				yield return line;
			}

			if (Def.partTags.Any())
			{
				yield return "Properties: " + Def.partTags.ToCommaList();
			}
		}

		IEnumerable<string> GetBraCupTip()
		{
			if (PartSizeCalculator.TryGetCupSize(parent, out float size))
			{
				var cupSize = (int)size;

				if (cupSize > 0)
				{
					yield return "RJW_PartInfo_braCup".Translate(
						BraSizeConfigDef.GetCupSizeLabel(cupSize));
				}
			}
		}

		IEnumerable<string> GetFluidTip()
		{
			if (partFluidFactor != 0 && Fluid != null)
			{
				yield return "RJW_PartInfo_fluidTypeFluidAmount".Translate(
					Fluid.LabelCap,
					partFluidFactor.ToString("F1"));
			}
		}

		//TODO: someday part(SizeOwner) change operations
		public void UpdateSeverity(float newSeverity = -1f)
		{
			if (newSeverity != -1f)
			{
				baseSize = newSeverity * Pawn.BodySize / BodySizeFactor;
			}
			parent.Severity = Size / Pawn.BodySize;
		}

		public void UpdatePartFluidFactor()
		{
			if (Def.fluidMultiplier > 0)
			{
				partFluidFactor = Def.GetRandomFluidAmount(Pawn.RaceProps.baseBodySize, baseSize);
			}
		}

		public void ForceSize(float size)
		{
			forcedSize = size;
			UpdateSeverity();
		}

		public void UnforceSize()
		{
			forcedSize = null;
			UpdateSeverity();
		}

		[SyncMethod]
		public void updatepartposition()
		{
			if (parent.def is not HediffDef_SexPart def)
			{
				return;
			}

			//Log.Message("0 " + (parent.Part == null));
			//Log.Message("1 " +parent.Part);
			//Log.Message("2 " +parent.Part.def);
			//Log.Message("3 " +parent.Part.def.defName);
			//Log.Message("4 " +partBase);
			//Log.Message("5 " +partBase.DefaultBodyPart);
			//ModLog.Message("pawn " + xxx.get_pawnname(Pawn) + ", part: " + parent.def.defName);
			if (parent.Part == null)
			{
				var bp = Pawn.RaceProps.body.AllParts.Find(x => DefaultPositionNames.Any(x.def.defName.Contains));
				if (bp != null)
				{
					ModLog.Warning(" " + xxx.get_pawnname(Pawn) + " part is null/wholebody? is in wrong BodyPart position, resetting to default: " + def.defaultBodyPart);
					parent.Part = bp;
					Pawn.GetRJWPawnData().genitals = new List<Hediff>();
					Pawn.GetRJWPawnData().breasts = new List<Hediff>();
					Pawn.GetRJWPawnData().anus = new List<Hediff>();
				}
				else
				{
					ModLog.Message(" " + xxx.get_pawnname(Pawn) + " default bodypart not found: " + def.defaultBodyPart + " -skip.");
				}
			}
			else if (def.defaultBodyPart != "" && !parent.Part.def.defName.Contains(def.defaultBodyPart))
			{
				//Log.Message("f");
				//Log.Message(partBase.DefaultBodyPartList.ToLineList());
				if (def.defaultBodyPartList.Any() && def.defaultBodyPartList.Any(parent.Part.def.defName.Contains))
				{
					return;
				}
				var bp = Pawn.RaceProps.body.AllParts.Find(x => x.def.defName.Contains(def.defaultBodyPart));
				if (bp != null)
				{
					ModLog.Warning(" " + xxx.get_pawnname(Pawn) + " part " + parent.def.defName + " is in wrong BodyPart position, resetting to default: " + def.defaultBodyPart);
					parent.Part = bp;
					Pawn.GetRJWPawnData().genitals = new List<Hediff>();
					Pawn.GetRJWPawnData().breasts = new List<Hediff>();
					Pawn.GetRJWPawnData().anus = new List<Hediff>();
				}
				else
				{
					ModLog.Message(" " + xxx.get_pawnname(Pawn) + " default bodypart not found: " + def.defaultBodyPart + " -skip.");
				}
				//if (pawn.IsColonist)
				//{
				//	Log.Message(xxx.get_pawnname(pawn) + " has broken hediffs, removing " + this.ToString());
				//	Log.Message(Part.ToString());
				//	Log.Message(bp.def.defName);
				//	Log.Message(partBase.DefaultBodyPart.ToString());
				//}
			}
		}

		/// <summary>
		/// fill comp data
		/// </summary>
		[SyncMethod]
		public void Init(Pawn pawn = null, bool reroll = true)
		{
			pawn ??= Pawn;

			if (originalOwnerSize == 0)
			{
				originalOwnerSize = pawn.BodySize;
			}

			if (reroll || baseSize == 0)
			{
				//var s = (parent is Hediff_NaturalSexPart) ? originalOwnerSize : Pawn.RaceProps.baseBodySize;
				baseSize = CalculateSize(pawn, originalOwnerSize);
				UpdatePartFluidFactor();
			}

			if (Def.genitalTags.Contains(GenitalTag.CanFertilizeEgg) || Def.HasComp(typeof(HediffComp_Ovipositor)))
			{
				var possibleEggNames = HediffComp_Ovipositor.PossibleEggs(pawn.kindDef?.defName)
					.SelectMany(eggDef => eggDef.childrenDefs)
					.Select(defName => DefDatabase<PawnKindDef>.GetNamedSilentFail(defName)?.label)
					.Where(label => label != null);

				if (possibleEggNames.Any())
				{
					possibleEggsTipString = possibleEggNames.ToCommaList();
				}
				else
				{
					possibleEggsTipString = pawn?.kindDef?.label ?? "";
				}
			}

			if (originalOwnerRace.NullOrEmpty())
			{
				originalOwnerRace = pawn?.kindDef.race.LabelCap ?? "RJW_PartInfo_unknownSpecies".Translate();
			}
			previousOwner = pawn?.LabelNoCount ?? "RJW_PartInfo_unknownOwner".Translate();

			UpdateSeverity();
			initialised = true;
		}

		[SyncMethod]
		public float CalculateSize(Pawn pawn, float bodySize)
		{
			float relativeSize;
			if (Def.genitalFamily == GenitalFamily.Breasts && pawn?.gender == Gender.Male && !Rand.Chance(TrapChance(pawn)))
			{
				relativeSize = .01f;
			}
			else
			{
				relativeSize = Def.GetRandomSize();
			}
			return relativeSize * bodySize;
		}

		private static float TrapChance(Pawn pawn)
		{
			if (RJWSettings.MaleTrap && !xxx.is_animal(pawn))
			{
				if (xxx.is_nympho(pawn))
				{
					return RJWSettings.futa_nymph_chance;
				}
				if (pawn.Faction != null && (int)pawn.Faction.def.techLevel < 5)
				{
					return RJWSettings.futa_natives_chance;
				}
				return RJWSettings.futa_spacers_chance;
			}
			return 0f;
		}
	}
}


code:
using Verse;
using RimWorld;

namespace rjw
{
	/// <summary>
	/// FeelingBroken raise/lower severity
	/// </summary>
	public class HediffCompProperties_FeelingBrokenSeverityReduce : HediffCompProperties
	{
		public HediffCompProperties_FeelingBrokenSeverityReduce()
		{
			this.compClass = typeof(HediffComp_FeelingBrokenSeverityReduce);
		}

		public SimpleCurve severityPerDayReduce;
	}
	class HediffComp_FeelingBrokenSeverityReduce : HediffComp_SeverityModifierBase
	{
		private HediffCompProperties_FeelingBrokenSeverityReduce Props
		{
			get
			{
				return (HediffCompProperties_FeelingBrokenSeverityReduce)this.props;
			}
		}
		public override void CompPostTick(ref float severityAdjustment)
		{
			base.CompPostTick(ref severityAdjustment);
			if (base.Pawn.IsHashIntervalTick(SeverityUpdateInterval))
			{
				float num = this.SeverityChangePerDay();
				num *= 0.00333333341f;
				if (xxx.has_traits(Pawn))
				{
					if (xxx.RoMIsActive)
						if (Pawn.story.traits.HasTrait(xxx.Succubus))
							num *= 4.0f;
					if (Pawn.story.traits.HasTrait(VanillaTraitDefOf.Tough))
					{
						num *= 2.0f;
					}
					if (Pawn.story.traits.HasTrait(VanillaTraitDefOf.Tough))
					{
						num *= 2.0f;
					}
					if (Pawn.story.traits.HasTrait(TraitDefOf.Wimp))
					{
						num *= 0.5f;
					}
					if (Pawn.story.traits.HasTrait(VanillaTraitDefOf.Nerves))
					{
						int td = Pawn.story.traits.DegreeOfTrait(VanillaTraitDefOf.Nerves);
						switch (td)
						{
							case -2:
								num *= 2.0f;
								break;
							case -1:
								num *= 1.5f;
								break;
							case 1:
								num *= 0.5f;
								break;
							case 2:
								num *= 0.25f;
								break;
						}
					}
				}
				severityAdjustment += num;
			}
		}
		public override float SeverityChangePerDay()
		{
			return this.Props.severityPerDayReduce.Evaluate(this.parent.ageTicks / GenDate.TicksPerDay);
		}
	}

	public class HediffCompProperties_FeelingBrokenSeverityIncrease : HediffCompProperties
	{
		public HediffCompProperties_FeelingBrokenSeverityIncrease()
		{
			this.compClass = typeof(HediffComp_FeelingBrokenSeverityIncrease);
		}

		public SimpleCurve severityPerDayIncrease;
	}
	class HediffComp_FeelingBrokenSeverityIncrease : AdvancedHediffComp
	{
		private HediffCompProperties_FeelingBrokenSeverityIncrease Props
		{
			get
			{
				return (HediffCompProperties_FeelingBrokenSeverityIncrease)this.props;
			}
		}
		public override void CompPostMerged(Hediff other)
		{
			float num = Props.severityPerDayIncrease.Evaluate(this.parent.ageTicks / GenDate.TicksPerDay);
			if (xxx.has_traits(Pawn))
			{
				if (xxx.RoMIsActive)
					if (Pawn.story.traits.HasTrait(xxx.Succubus))
						num *= 0.25f;

				if (Pawn.story.traits.HasTrait(VanillaTraitDefOf.Tough))
				{
					num *= 0.5f;
				}
				if (Pawn.story.traits.HasTrait(TraitDefOf.Wimp))
				{
					num *= 2.0f;
				}
				if (Pawn.story.traits.HasTrait(VanillaTraitDefOf.Nerves))
				{
					int td = Pawn.story.traits.DegreeOfTrait(VanillaTraitDefOf.Nerves);
					switch (td)
					{
						case -2:
							num *= 0.25f;
							break;
						case -1:
							num *= 0.5f;
							break;
						case 1:
							num *= 1.5f;
							break;
						case 2:
							num *= 2.0f;
							break;
					}
				}
			}
			other.Severity *= num;
		}
	}

	public class AdvancedHediffWithComps : HediffWithComps
	{
		public override bool TryMergeWith(Hediff other)
		{
			for (int i = 0; i < this.comps.Count; i++)
			{
				if(this.comps[i] is AdvancedHediffComp) ((AdvancedHediffComp)this.comps[i]).CompBeforeMerged(other);
			}
			return base.TryMergeWith(other);
		}
	}
	public class AdvancedHediffComp : HediffComp
	{
		public virtual void CompBeforeMerged(Hediff other)
		{
		}
	}
}

repo_name: aelina/rjw

license: mit


code:
using System.Linq;
using Verse;
using RimWorld;
using System.Text;
using Multiplayer.API;
using UnityEngine;
using System.Collections.Generic;
using rjw.Modules.Genitals.Enums;
using RimWorld.QuestGen;

namespace rjw
{
//TODO figure out how this thing works and move eggs to comps

	[StaticConstructorOnStartup]
	public class HediffDef_SexPart : HediffDef
	{
		public const float BaseFluidAmount = 20f;
		private static readonly FloatRange FluidAmountVariation = new(.75f, 1.25f);

		public SexFluidDef fluid;					//cummies/milk - insectjelly/honey etc
		public float fluidMultiplier = 1;           //amount of Milk/Ejaculation/Wetness
		public bool produceFluidOnOrgasm;
		public string defaultBodyPart = "";			//Bodypart to move this part to, after fucking up with pc or other mod
		public List<string> defaultBodyPartList;    //Bodypart list to move this part to, after fucking up with pc or other mod

		public float sizeMultiplier = 1f;

		public PartSizeConfigDef sizeProfile;
		public List<string> partTags = new();
		public GenitalFamily genitalFamily;
		public List<GenitalTag> genitalTags = new();

		public override IEnumerable<string> ConfigErrors()
		{
			foreach (string err in base.ConfigErrors())
			{
				yield return err;
			}
			if (sizeProfile == null && genitalFamily != GenitalFamily.Undefined)
			{
				yield return $"Missing required <{nameof(sizeProfile)}> field. Pick a default from "
				+ "<RJW Defs folder>/ConfigDefs/PartSizeConfigs.xml.";
			}
		}

		[SyncMethod]
		public float GetRandomFluidAmount(float raceSize, float partSize)
		{
			return fluidMultiplier * partSize * BaseFluidAmount * FluidAmountVariation.RandomInRange * raceSize * raceSize;
		}

		[SyncMethod]
		public float GetRandomSize()
		{
			float sample = Rand.Gaussian(0.5f, 0.125f);
			return Mathf.Clamp(sample, 0.01f, 1f) * sizeMultiplier;
		}

		public string GetStandardSizeLabel(float size)
		{
			return stages.FindLast(stage => stage.minSeverity < size)?.label;
		}
	}
}


code:
using Verse;
using RimWorld;

namespace rjw
{
	public class Hediff_ArtificialSexPart : Hediff_AddedPart, ISexPartHediff
	{
		public override bool ShouldRemove => false;

		public HediffDef_SexPart Def => def as HediffDef_SexPart;

		public HediffWithComps AsHediff => this;

		public override string LabelBase => def.label;

		/// <summary>
		/// stack hediff in health tab?
		/// </summary>
		public override int UIGroupKey
		{
			get
			{
				return loadID;
			}
		}

		/// <summary>
		/// do not merge same rjw parts into one
		/// </summary>
		public override bool TryMergeWith(Hediff other)
		{
			return false;
		}
	}
}


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

namespace rjw
{
	public class Cocoon : HediffWithComps
	{
		public int tickNext;

		public override void PostMake()
		{
			Severity = 1.0f;
			SetNextTick();
		}

		public override void ExposeData()
		{
			base.ExposeData();
			Scribe_Values.Look(ref tickNext, "tickNext", 1000, true);
		}

		public override void Tick()
		{
			if (Find.TickManager.TicksGame >= tickNext)
			{
				//Log.Message("Cocoon::Tick() " + base.xxx.get_pawnname(pawn));
				HealWounds();
				SatisfyHunger();
				SatisfyThirst();
				SetNextTick();
			}
		}

		public void HealWounds()
		{
			IEnumerable<Hediff> enumerable = from hd in pawn.health.hediffSet.hediffs
											 where !hd.IsTended() && hd.TendableNow()
											 select hd;
			if (enumerable != null)
			{
				foreach (Hediff item in enumerable)
				{
					HediffWithComps val = item as HediffWithComps;
					if (val != null)
						if (val.Bleeding)
						{
							//Log.Message("TrySealWounds " + xxx.get_pawnname(pawn) + ", Bleeding " + item.Label);
							//HediffComp_TendDuration val2 = HediffUtility.TryGetComp<HediffComp_TendDuration>(val);
							val.Heal(0.5f);
							//val2.tendQuality = 1f;
							//val2.tendTicksLeft = 10000;
							//pawn.health.Notify_HediffChanged(item);
						}
						// tend infections
						// tend lifeThreatening chronic
						else if ((!val.def.chronic && val.def.lethalSeverity > 0f) || (val.CurStage?.lifeThreatening ?? false))
						{
							//Log.Message("TryHeal " + xxx.get_pawnname(pawn) + ", infection(?) " + item.Label);
							HediffComp_TendDuration val2 = HediffUtility.TryGetComp<HediffComp_TendDuration>(val);
							val2.tendQuality = 1f;
							val2.tendTicksLeft = 10000;
							pawn.health.Notify_HediffChanged(item);
						}
				}
			}
		}

		public void SatisfyHunger()
		{
			Need_Food need = pawn.needs.TryGetNeed<Need_Food>();
			if (need == null)
			{
				return;
			}
			//pawn.PositionHeld.IsInPrisonCell(pawn.Map)
			//Log.Message("Cocoon::SatisfyHunger() " + xxx.get_pawnname(pawn) + " IsInPrisonCell " + pawn.PositionHeld.IsInPrisonCell(pawn.Map));
			//Log.Message("Cocoon::SatisfyHunger() " + xxx.get_pawnname(pawn) + " GetRoom " + pawn.PositionHeld.GetRoom(pawn.Map));
			//Log.Message("Cocoon::SatisfyHunger() " + xxx.get_pawnname(pawn) + " GetRoom " + pawn.PositionHeld.GetZone(pawn.Map));

			if (need.CurLevel < 0.15f)
			{
				//Log.Message("Cocoon::SatisfyHunger() " + xxx.get_pawnname(pawn) + " need to eat");
				float nutrition_amount = need.MaxLevel / 5f;
				pawn.needs.food.CurLevel += nutrition_amount;
			}
		}

		public void SatisfyThirst()
		{
			if (!xxx.DubsBadHygieneIsActive)
				return;

			Need need = pawn.needs.AllNeeds.Find(x => x.def == xxx.DBHThirst);
			if (need == null)
			{
				return;
			}

			if (need.CurLevel < 0.15f)
			{
				//Log.Message("Cocoon::SatisfyThirst() " + xxx.get_pawnname(pawn) + " need to drink");
				float nutrition_amount = need.MaxLevel / 5f;
				pawn.needs.TryGetNeed(need.def).CurLevel += nutrition_amount;
			}
		}

		public void SetNextTick()
		{
			//make actual tick every 16.6 sec
			tickNext = Find.TickManager.TicksGame + 1000;
			//Log.Message("Cocoon::SetNextTick() " + tickNext);
		}
	}
}


code:
using Verse;
using RimWorld;

namespace rjw
{
	public class Hediff_NaturalSexPart : HediffWithComps, ISexPartHediff
	{
		public override bool ShouldRemove => false;

		public HediffDef_SexPart Def => def as HediffDef_SexPart;

		public HediffWithComps AsHediff => this;

		public override string LabelBase
		{
			get
			{
				/*
				 * make patch to make/save capmods?
				if (CapMods.Count < 5)
				{
					PawnCapacityModifier pawnCapacityModifier = new PawnCapacityModifier();
					pawnCapacityModifier.capacity = PawnCapacityDefOf.Moving;
					pawnCapacityModifier.offset += 0.5f;
					CapMods.Add(pawnCapacityModifier);
				}
				*/

				//name/kind
				return def.label;
			}
		}

		/// <summary>
		/// stack hediff in health tab?
		/// </summary>
		public override int UIGroupKey
		{
			get
			{
				return loadID;
			}
		}

		/// <summary>
		/// do not merge same rjw parts into one
		/// </summary>
		public override bool TryMergeWith(Hediff other)
		{
			return false;
		}
	}
}


code:
using System.Linq;
using Verse;

namespace rjw
{
	public class Hediff_PartsSizeChangerPC : HediffWithComps
	{
		public override void PostAdd(DamageInfo? dinfo)
		{
			if (Part != null)
			{
				foreach (var sexPart in pawn.health.hediffSet.hediffs.Where(x => x.Part == Part).OfType<ISexPartHediff>().ToList())
				{
					HediffComp_SexPart partComp = sexPart.GetPartComp();
					if (partComp != null)
					{
						//Log.Message("  Hediff_PartsSizeChanger: " + hed.Label);
						//Log.Message("  Hediff_PartsSizeChanger: " + hed.Severity);
						//Log.Message("  Hediff_PartsSizeChanger: " + CompHediff.SizeBase);
						//Log.Message("  Hediff_PartsSizeChanger: " + "-----");
						//Log.Message("  Hediff_PartsSizeChanger: " + this.Label);
						//Log.Message("  Hediff_PartsSizeChanger: " + this.Severity);
						partComp.UpdateSeverity(CurStage.minSeverity);
						//Log.Message("  Hediff_PartsSizeChanger: " + "-----");
						//Log.Message("  Hediff_PartsSizeChanger: " + hed.Label);
						//Log.Message("  Hediff_PartsSizeChanger: " + hed.Severity);
						//Log.Message("  Hediff_PartsSizeChanger: " + CompHediff.SizeBase);
					}
				}
			}

			pawn.health.RemoveHediff(this);
		}
	}

	public class Hediff_PartsSizeChangerCE : HediffWithComps
	{
		public override void PostAdd(DamageInfo? dinfo)
		{
			if (Part != null)
			{
				foreach (var sexPart in pawn.health.hediffSet.hediffs.Where(x => x.Part == Part).OfType<ISexPartHediff>().ToList())
				{
					HediffComp_SexPart partComp = sexPart.GetPartComp();
					if (partComp != null)
					{
						partComp.UpdateSeverity(def.initialSeverity);
					}
				}
			}

			pawn.health.RemoveHediff(this);
		}
	}
}


code:
using Verse;

//Hediff worker for pawns' "lay down and submit" button
namespace rjw
{
	public class Hediff_Submitting: HediffWithComps
	{
		public override bool ShouldRemove {
			get
			{
				Pawn daddy = pawn.CarriedBy;
				if (daddy != null && daddy.Faction == pawn.Faction)
				{
					return true;
				}
				else
					return base.ShouldRemove;
			}
		}
	}
}


code:
using Verse;
using RimWorld;
using rjw.Modules.Interactions.Objects.Parts;

namespace rjw
{
	// Would be nice to call this just 'ISexPart', but ILewdablePart already exists and represents something similar but different.
	public interface ISexPartHediff
	{
		HediffDef_SexPart Def { get; }

		T GetComp<T>() where T : HediffComp;

		HediffWithComps AsHediff { get; }
	}

	public static class ISexPartHediffExtensions
	{
		public static HediffComp_SexPart GetPartComp(this ISexPartHediff partHediff) => partHediff.GetComp<HediffComp_SexPart>();

		public static Pawn GetOwner(this ISexPartHediff part) => part.AsHediff.pawn;
	}
}


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

namespace rjw
{
	public class PartAdder
	{
		public float chance = 0f;
		public string rjwPart;
		public List<string> bodyParts;
	}
}


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

namespace rjw;

public static class PartSizeCalculator
{
	public static bool TryGetLength(Hediff hediff, out float size)
	{
		return TryGetMeasurementFromCurve(hediff, GetSizeConfig(hediff)?.lengths, true, out size);
	}

	public static bool TryGetLength(HediffDef def, float baseSize, out float size)
	{
		return TryGetMeasurementFromCurve(def, baseSize, 1f, GetSizeConfig(def)?.lengths, out size);
	}

	public static bool TryGetGirth(Hediff hediff, out float size)
	{
		return TryGetMeasurementFromCurve(hediff, GetSizeConfig(hediff)?.girths, true, out size);
	}

	public static bool TryGetGirth(HediffDef def, float baseSize, out float size)
	{
		return TryGetMeasurementFromCurve(def, baseSize, 1f, GetSizeConfig(def)?.girths, out size);
	}

	public static bool TryGetCupSize(Hediff hediff, out float size)
	{
		// Cup size is already "scaled" because the same breast volume has a smaller cup size on a larger band size.
		return TryGetMeasurementFromCurve(hediff, GetSizeConfig(hediff)?.cupSizes, false, out size);
	}

	public static bool TryGetCupSize(HediffDef def, float baseSize, out float size)
	{
		return TryGetMeasurementFromCurve(def, baseSize, 1f, GetSizeConfig(def)?.cupSizes, out size);
	}

	public static float GetBandSize(Hediff hediff)
	{
		var size = GetUnderbustSize(hediff);
		size /= BraSizeConfigDef.Instance.bandSizeInterval;
		size = (float)Math.Round(size, MidpointRounding.AwayFromZero);
		size *= BraSizeConfigDef.Instance.bandSizeInterval;
		return size;
	}

	public static float GetUnderbustSize(Hediff hediff)
	{
		return BraSizeConfigDef.Instance.bandSizeBase * GetScale(hediff);
	}

	static float GetScale(Hediff hediff)
	{
		if (!hediff.TryGetComp<HediffComp_SexPart>(out var comp))
		{
			// Added as a Option for Modders to change Size above max-severity.
			if (comp.bodySizeOverride >= 0.001)
				return comp.bodySizeOverride;
		}
		return hediff.pawn.BodySize;
	}

	public static bool TryGetOverbustSize(Hediff hediff, out float size)
	{
		if (!TryGetCupSize(hediff, out var cupSize))
		{
			size = 0f;
			return false;
		}

		// Cup size is rounded up, so to do the math backwards subtract .9
		size = GetUnderbustSize(hediff) + ((cupSize - .9f) * BraSizeConfigDef.Instance.cupSizeInterval);
		return true;
	}

	static bool TryGetMeasurementFromCurve(
		Hediff hediff,
		List<float> stageSizes,
		bool shouldScale,
		out float measurement)
	{
		var scaleFactor = shouldScale ? GetScale(hediff) : 1.0f;
		if (!hediff.TryGetComp<HediffComp_SexPart>(out var comp))
		{
			measurement = 0;
			return false;
		}
		return TryGetMeasurementFromCurve(hediff.def, comp.Size, scaleFactor, stageSizes, out measurement);
	}

	static bool TryGetMeasurementFromCurve(HediffDef hediffDef, float baseSize, float scale, List<float> stageSizes, out float measurement)
	{
		if (hediffDef is not HediffDef_SexPart || stageSizes.NullOrEmpty())
		{
			measurement = 0f;
			return false;
		}
		var curve = new SimpleCurve(hediffDef.stages.Zip(stageSizes, (stage, stageSize) => new CurvePoint(stage.minSeverity, stageSize)));
		measurement = curve.Evaluate(baseSize) * scale;
		return true;
	}

	public static bool TryGetPenisWeight(Hediff hediff, out float weight)
	{
		if (!TryGetLength(hediff, out float length) ||
			!TryGetGirth(hediff, out float girth))
		{
			weight = 0f;
			return false;
		}

		var density = GetSizeConfig(hediff).density;
		if (density == null)
		{
			weight = 0f;
			return false;
		}

		var r = girth / (2.0 * Math.PI);
		var volume = r * r * Math.PI * length;

		weight = (float)(volume * density.Value / 1000f);
		return true;
	}

	public static bool TryGetBreastWeight(Hediff hediff, out float weight)
	{
		if (!TryGetCupSize(hediff, out float rawSize))
		{
			weight = 0f;
			return false;
		}

		var density = GetSizeConfig(hediff).density;
		if (density == null)
		{
			weight = 0f;
			return false;
		}

		// Up a band size and down a cup size is about the same volume.
		var extraBandSize =  BraSizeConfigDef.Instance.bandSizeBase * (1.0f - GetScale(hediff));
		var extraCupSizes = extraBandSize / BraSizeConfigDef.Instance.bandSizeInterval;
		var size = rawSize + extraCupSizes;

		var pounds = 0.765f
			+ 0.415f * size
			+ -0.0168f * size * size
			+ 2.47E-03f * size * size * size;
		var kg = Math.Max(0, pounds * 0.45359237f);
		weight = kg * density.Value;
		return true;
	}

	private static PartSizeConfigDef GetSizeConfig(Hediff hediff)
	{
		return GetSizeConfig(hediff.def);
	}

	private static PartSizeConfigDef GetSizeConfig(HediffDef def)
	{
		return (def as HediffDef_SexPart)?.sizeProfile;
	}
}


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

namespace rjw;

public class PartSizeConfigDef : Def
{
		public bool bodysizescale = false; // rescales parts sizes based on bodysize of initial owner race

		/// <summary>
		/// Human standard would be 1.0. Null for no weight display.
		/// </summary>
		public float? density = null;

		public List<float> lengths;
		public List<float> girths;
		public List<float> cupSizes;
}


code:
using Verse;
using RimWorld;
using System.Collections.Generic;
using System.Linq;
using rjw.Modules.Interactions.Enums;
using rjw.Modules.Interactions.Objects.Parts;

namespace rjw;

public class SexFluidDef : Def
{
	public ThingDef filth;

	public ThingDef consumable;

	public float consumableFluidRatio = 1;

	public bool ingestThroughAnyOrifice;

	public bool alwaysProduceFilth;

	public float baseNutrition;

	public float baseNutritionCost = 1;

	public bool quenchesThirst = true;

	public List<string> tags = [];

	public List<SexFluidIngestionDoer> ingestionDoers;

	public override IEnumerable<string> ConfigErrors()
	{
		foreach (string err in base.ConfigErrors())
		{
			yield return err;
		}
		if (filth != null && filth.filth == null)
		{
			yield return $"{nameof(filth)} must be a ThingDef with a <filth> field.";
		}
		if (consumable != null && baseNutrition != 0)
		{
			yield return $"{nameof(consumable)} is set, so {nameof(baseNutrition)} is meaningless.";
		}
	}

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

		if (consumable != null)
		{
			descriptionHyperlinks ??= new();
			descriptionHyperlinks.AddDistinct(consumable);
		}
	}

}


code:
using Verse;

namespace rjw;

public abstract class SexFluidIngestionDoer
{
	public abstract void Ingested(Pawn pawn, SexFluidDef fluid, float amount, ISexPartHediff fromPart, ISexPartHediff toPart = null);
}


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

namespace rjw
{
	public class InteractionExtension : DefModExtension
	{
		/// <summary>
		/// </summary>
		public string RMBLabel = ""; // rmb menu
		public string rjwSextype = ""; // xxx.rjwSextype

		public List<string> rulepack_defs = new List<string>(); //rulepack(s) for this interaction
	}
}


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

using RimWorld;
using Verse;

namespace rjw
{
	internal class InteractionWorker_AnalSexAttempt : InteractionWorker
	{
		//initiator - rapist
		//recipient - victim
		public static bool AttemptAnalSex(Pawn initiator, Pawn recipient)
		{
			//--Log.Message(xxx.get_pawnname(initiator) + " is attempting to anally rape " + xxx.get_pawnname(recipient));
			return true;
		}

		public override float RandomSelectionWeight(Pawn initiator, Pawn recipient)
		{
			// this interaction is triggered by the jobdriver
			if (initiator == null || recipient == null)
				return 0.0f;

			return 0.0f; // base.RandomSelectionWeight(initiator, recipient);
		}

		public override void Interacted(Pawn initiator, Pawn recipient, List<RulePackDef> extraSentencePacks, out string letterText, out string letterLabel, out LetterDef letterDef, out LookTargets lookTargets)
		{
			//add something fancy here later?
			letterText = null;
			letterLabel = null;
			letterDef = null;
			lookTargets = recipient;
			//Find.LetterStack.ReceiveLetter("Rape attempt", "A wandering nymph has decided to join your colony.", LetterDefOf.NegativeEvent, recipient);

			if (initiator == null || recipient == null)
				return;
			//--ModLog.Message(" InteractionWorker_AnalRapeAttempt::Interacted( " + xxx.get_pawnname(initiator) + ", " + xxx.get_pawnname(recipient) + " ) called");
			AttemptAnalSex(initiator, recipient);
		}
	}

	internal class InteractionWorker_VaginalSexAttempt : InteractionWorker
	{
		//initiator - rapist
		//recipient - victim
		public static bool AttemptAnalSex(Pawn initiator, Pawn recipient)
		{
			//--Log.Message(xxx.get_pawnname(initiator) + " is attempting to anally rape " + xxx.get_pawnname(recipient));
			return false;
		}

		public override float RandomSelectionWeight(Pawn initiator, Pawn recipient)
		{
			// this interaction is triggered by the jobdriver
			if (initiator == null || recipient == null)
				return 0.0f;

			return 0.0f; // base.RandomSelectionWeight(initiator, recipient);
		}

		public override void Interacted(Pawn initiator, Pawn recipient, List<RulePackDef> extraSentencePacks, out string letterText, out string letterLabel, out LetterDef letterDef, out LookTargets lookTargets)
		{
			//add something fancy here later?
			letterText = null;
			letterLabel = null;
			letterDef = null;
			lookTargets = recipient;
			//Find.LetterStack.ReceiveLetter("Rape attempt", "A wandering nymph has decided to join your colony.", LetterDefOf.NegativeEvent, recipient);

			if (initiator == null || recipient == null)
				return;
			//--ModLog.Message(" InteractionWorker_AnalRapeAttempt::Interacted( " + xxx.get_pawnname(initiator) + ", " + xxx.get_pawnname(recipient) + " ) called");
			AttemptAnalSex(initiator, recipient);
		}
	}

	internal class InteractionWorker_OtherSexAttempt : InteractionWorker
	{
		//initiator - rapist
		//recipient - victim
		public static bool AttemptAnalSex(Pawn initiator, Pawn recipient)
		{
			//--Log.Message(xxx.get_pawnname(initiator) + " is attempting to anally rape " + xxx.get_pawnname(recipient));
			return false;
		}

		public override float RandomSelectionWeight(Pawn initiator, Pawn recipient)
		{
			// this interaction is triggered by the jobdriver
			if (initiator == null || recipient == null)
				return 0.0f;

			return 0.0f; // base.RandomSelectionWeight(initiator, recipient);
		}

		public override void Interacted(Pawn initiator, Pawn recipient, List<RulePackDef> extraSentencePacks, out string letterText, out string letterLabel, out LetterDef letterDef, out LookTargets lookTargets)
		{
			//add something fancy here later?
			letterText = null;
			letterLabel = null;
			letterDef = null;
			lookTargets = recipient;
			//Find.LetterStack.ReceiveLetter("Rape attempt", "A wandering nymph has decided to join your colony.", LetterDefOf.NegativeEvent, recipient);

			if (initiator == null || recipient == null)
				return;
			//--ModLog.Message(" InteractionWorker_AnalRapeAttempt::Interacted( " + xxx.get_pawnname(initiator) + ", " + xxx.get_pawnname(recipient) + " ) called");
			AttemptAnalSex(initiator, recipient);
		}
	}
}


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

namespace rjw;

//biotech JobDriver_ControlMech
public class JobDriver_HackMechPregnancy : JobDriver
{
	private const TargetIndex MechInd = TargetIndex.A;

	private Pawn Mech => (Pawn)job.GetTarget(TargetIndex.A).Thing;

	private int MechControlTime => Mathf.RoundToInt(Mech.GetStatValue(StatDefOf.ControlTakingTime) * 60f);

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

	protected override IEnumerable<Toil> MakeNewToils()
	{
		if (ModsConfig.BiotechActive)
		{
			this.FailOnDestroyedNullOrForbidden(TargetIndex.A);
			yield return Toils_Goto.GotoThing(TargetIndex.A, PathEndMode.Touch);
			yield return Toils_General.WaitWith(TargetIndex.A, MechControlTime, useProgressBar: true, maintainPosture: true, maintainSleep: false, TargetIndex.A).WithEffect(EffecterDefOf.ControlMech, TargetIndex.A);
			Toil toil = ToilMaker.MakeToil("MakeNewToils");
			toil.initAction = delegate
			{
				Hediff_MechanoidPregnancy pregnancy = (Hediff_MechanoidPregnancy)Mech.health.hediffSet.GetFirstHediffOfDef(HediffDef.Named("RJW_pregnancy_mech"));
				pregnancy.Hack();
			};
			toil.PlaySoundAtEnd(SoundDefOf.ControlMech_Complete);
			yield return toil;
		}
	}
}


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

namespace rjw
{
	public class JobDriver_BestialityForFemale : JobDriver_SexBaseInitiator
	{
		public IntVec3 SleepSpot => Bed.SleepPosOfAssignedPawn(pawn);
		public override bool TryMakePreToilReservations(bool errorOnFailed)
		{
			return pawn.Reserve(Target, job, 1, 0, null, errorOnFailed);
		}

		[SyncMethod]
		protected override IEnumerable<Toil> MakeNewToils()
		{
			setup_ticks();
			var PartnerJob = xxx.gettin_loved;

			this.FailOnDespawnedOrNull(iTarget);
			this.FailOnDespawnedNullOrForbidden(iBed);
			this.FailOn(() => !pawn.CanReserveAndReach(Partner, PathEndMode.Touch, Danger.Deadly));
			this.FailOn(() => pawn.Drafted);
			this.FailOn(() => Partner.IsFighting());
			this.FailOn(() => !Partner.CanReach(SleepSpot, PathEndMode.Touch, Danger.Deadly)); //changed to SleepSpot as partner will only move to bed

            yield return Toils_Reserve.Reserve(iTarget, 1, 0);
			yield return Toils_Goto.GotoThing(iTarget, PathEndMode.Touch);

			var gotoBed = new Toil();
			gotoBed.defaultCompleteMode = ToilCompleteMode.PatherArrival;
			gotoBed.initAction = delegate
			{
				pawn.pather.StartPath(SleepSpot, PathEndMode.OnCell);
				Partner.jobs.StopAll();
				Job job = JobMaker.MakeJob(JobDefOf.GotoMindControlled, SleepSpot);
				Partner.jobs.StartJob(job, JobCondition.InterruptForced);
			};
			gotoBed.FailOnDespawnedOrNull(iBed); //Changed from FailOnBedNoLongerUsable(iBed) due to pawn unable to use not owned bed.
			gotoBed.AddFailCondition(() => Partner.Downed);
			yield return gotoBed;

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

			var StartPartnerJob = new Toil();
			StartPartnerJob.defaultCompleteMode = ToilCompleteMode.Instant;
			StartPartnerJob.socialMode = RandomSocialMode.Off;
			StartPartnerJob.initAction = delegate
			{
				var gettin_loved = JobMaker.MakeJob(PartnerJob, pawn, Bed);
				Partner.jobs.StartJob(gettin_loved, JobCondition.InterruptForced);
			};
			yield return StartPartnerJob;

			var SexToil = new Toil();
			SexToil.defaultCompleteMode = ToilCompleteMode.Never;
			SexToil.socialMode = RandomSocialMode.Off;
			SexToil.handlingFacing = true;
			SexToil.initAction = delegate
			{
				Start();

				// TODO: replace this quick n dirty way
				CondomUtility.GetCondomFromRoom(pawn);
				// Try to use whore's condom first, then client's
				Sexprops.usedCondom = CondomUtility.TryUseCondom(pawn) || CondomUtility.TryUseCondom(Partner);
				if (Sexprops.usedCondom)
				{
					var receiverDriver = Partner?.jobs?.curDriver as JobDriver_SexBaseReciever;
					if (receiverDriver is not null && receiverDriver.Sexprops != null)
						receiverDriver.Sexprops.usedCondom = true;
				}
			};
			SexToil.FailOn(() => Partner.Dead);
			SexToil.FailOn(() => Partner.CurJob?.def != PartnerJob);
			SexToil.AddPreTickAction(delegate
			{
				if (pawn.IsHashIntervalTick(ticks_between_hearts))
					if (xxx.is_zoophile(pawn))
						ThrowMetaIconF(pawn.Position, pawn.Map, FleckDefOf.Heart);
					else
						ThrowMetaIconF(pawn.Position, pawn.Map, xxx.mote_noheart);
				SexTick(pawn, Partner);
				SexUtility.reduce_rest(Partner, 2);
				SexUtility.reduce_rest(pawn, 1);
				if (ticks_left <= 0)
					ReadyForNextToil();
			});
			SexToil.AddFinishAction(End);
			yield return SexToil;

			yield return new Toil
			{
				initAction = () => SexUtility.ProcessSex(Sexprops),
				defaultCompleteMode = ToilCompleteMode.Instant
			};
		}
	}
}


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

namespace rjw
{
	public class JobDriver_BestialityForMale : JobDriver_Rape
	{
		public override bool TryMakePreToilReservations(bool errorOnFailed)
		{
			return pawn.Reserve(Target, job, 1, 0, null, errorOnFailed);
		}

		[SyncMethod]
		protected override IEnumerable<Toil> MakeNewToils()
		{
			//--ModLog.Message(" JobDriver_BestialityForMale::MakeNewToils() called");

			setup_ticks();
			var PartnerJob = xxx.gettin_bred;

			//this.FailOn (() => (!Partner.health.capacities.CanBeAwake) || (!comfort_prisoners.is_designated (Partner)));
			// Fail if someone else reserves the prisoner before the pawn arrives or colonist can't reach animal
			this.FailOn(() => !pawn.CanReserveAndReach(Partner, PathEndMode.Touch, Danger.Deadly));
			this.FailOn(() => Partner.HostileTo(pawn));
			this.FailOnDespawnedNullOrForbidden(iTarget);
			this.FailOn(() => pawn.Drafted);

			yield return Toils_Reserve.Reserve(iTarget, 1, 0);
			//ModLog.Message(" JobDriver_BestialityForMale::MakeNewToils() - moving towards animal");
			yield return Toils_Goto.GotoThing(iTarget, PathEndMode.Touch);
			yield return Toils_Interpersonal.WaitToBeAbleToInteract(pawn);
			yield return Toils_Interpersonal.GotoInteractablePosition(iTarget);

			if (xxx.is_kind(pawn)
				|| (xxx.CTIsActive && xxx.has_traits(pawn) && pawn.story.traits.HasTrait(xxx.RCT_AnimalLover)))
			{
				yield return TalkToAnimal(pawn, Partner);
				yield return TalkToAnimal(pawn, Partner);
			}

			if (Rand.Chance(0.6f))
				yield return TalkToAnimal(pawn, Partner);

			yield return Toils_Goto.GotoThing(iTarget, PathEndMode.OnCell);

			SexUtility.RapeTargetAlert(pawn, Partner);

			Toil StartPartnerJob = new Toil();
			StartPartnerJob.defaultCompleteMode = ToilCompleteMode.Instant;
			StartPartnerJob.socialMode = RandomSocialMode.Off;
			StartPartnerJob.initAction = delegate
			{
				//--ModLog.Message(" JobDriver_BestialityForMale::MakeNewToils() - Setting animal job driver");
				var dri = Partner.jobs.curDriver as JobDriver_SexBaseRecieverRaped;
				if (dri == null)
				{
					//wild animals may flee or attack
					if (pawn.Faction != Partner.Faction && Partner.RaceProps.wildness > Rand.Range(0.22f, 1.0f)
						&& !(pawn.TicksPerMoveCardinal < (Partner.TicksPerMoveCardinal / 2) && !Partner.Downed && xxx.is_not_dying(Partner)))
					{
						Partner.jobs.StopAll(); // Wake up animal if sleeping.

						float aggro = Partner.kindDef.RaceProps.manhunterOnTameFailChance;
						if (Partner.kindDef.RaceProps.predator)
							aggro += 0.2f;
						else
							aggro -= 0.1f;

						//wild animals may attack
						if (Rand.Chance(aggro) && Partner.CanSee(pawn))
						{
							Partner.rotationTracker.FaceTarget(pawn);
							LifeStageUtility.PlayNearestLifestageSound(Partner, (ls) => ls.soundAngry, null,null, 1.4f);
							ThrowMetaIconF(Partner.Position, Partner.Map, FleckDefOf.IncapIcon);
							ThrowMetaIcon(pawn.Position, pawn.Map, ThingDefOf.Mote_ColonistFleeing); //red '!'
							Partner.mindState.mentalStateHandler.TryStartMentalState(MentalStateDefOf.Manhunter);
							if (Partner.kindDef.RaceProps.herdAnimal && Rand.Chance(0.2f))
							{ // 20% chance of turning the whole herd hostile...
								List<Pawn> packmates = Partner.Map.mapPawns.AllPawnsSpawned.Where(x =>
									x != Partner && x.def == Partner.def && x.Faction == Partner.Faction &&
									x.Position.InHorDistOf(Partner.Position, 24f) && x.CanSee(Partner)).ToList();

								foreach (Pawn packmate in packmates)
								{
									packmate.mindState.mentalStateHandler.TryStartMentalState(MentalStateDefOf.Manhunter);
								}
							}
							Messages.Message(pawn.Name.ToStringShort + " is being attacked by " + xxx.get_pawnname(Partner) + ".", pawn, MessageTypeDefOf.ThreatSmall);
						}
						//wild animals may flee
						else
						{
							ThrowMetaIcon(Partner.Position, Partner.Map, ThingDefOf.Mote_ColonistFleeing);
							LifeStageUtility.PlayNearestLifestageSound(Partner, (ls) => ls.soundCall, g => g.soundCall, null);
							Partner.mindState.StartFleeingBecauseOfPawnAction(pawn);
							Partner.mindState.mentalStateHandler.TryStartMentalState(MentalStateDefOf.PanicFlee);
						}
						pawn.jobs.EndCurrentJob(JobCondition.Incompletable);
					}
					else
					{
						Job gettin_bred = JobMaker.MakeJob(PartnerJob, pawn, Partner);
						Partner.jobs.StartJob(gettin_bred, JobCondition.InterruptForced, null, true);
					}
				}
			};
			yield return StartPartnerJob;

			Toil SexToil = new Toil();
			SexToil.defaultCompleteMode = ToilCompleteMode.Never;
			SexToil.defaultDuration = duration;
			SexToil.handlingFacing = true;
			SexToil.FailOn(() => Partner.CurJob?.def != PartnerJob);
			SexToil.initAction = delegate
			{
				Partner.pather.StopDead();
				Partner.jobs.curDriver.asleep = false;
				Start();
			};
			SexToil.tickAction = delegate
			{
				if (pawn.IsHashIntervalTick(ticks_between_hearts))
					if (xxx.is_zoophile(pawn))
						ThrowMetaIconF(pawn.Position, pawn.Map, FleckDefOf.Heart);
					else
						ThrowMetaIconF(pawn.Position, pawn.Map, xxx.mote_noheart);
				SexTick(pawn, Partner);
				//no hitting wild animals, and getting rect by their Manhunter
				/*
				if (pawn.IsHashIntervalTick (ticks_between_hits))
					roll_to_hit (pawn, Partner);
					*/
				SexUtility.reduce_rest(Partner, 1);
				SexUtility.reduce_rest(pawn, 2);
				if (ticks_left <= 0)
					ReadyForNextToil();
			};
			SexToil.AddFinishAction(delegate
			{
				End();
			});
			yield return SexToil;

			yield return new Toil
			{
				initAction = delegate
				{
					//ModLog.Message(" JobDriver_BestialityForMale::MakeNewToils() - creating aftersex toil");
					SexUtility.ProcessSex(Sexprops);
				},
				defaultCompleteMode = ToilCompleteMode.Instant
			};
		}

		[SyncMethod]
		private Toil TalkToAnimal(Pawn pawn, Pawn animal)
		{
			Toil toil = new Toil();
			toil.initAction = delegate
			{
				pawn.interactions.TryInteractWith(animal, SexUtility.AnimalSexChat);
			};
			//Rand.PopState();
			//Rand.PushState(RJW_Multiplayer.PredictableSeed());
			toil.defaultCompleteMode = ToilCompleteMode.Delay;
			toil.defaultDuration = Rand.Range(120, 220);
			return toil;
		}
	}
}


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

namespace rjw
{
	/// <summary>
	/// This is the driver for animals mounting breeders.
	/// </summary>
	public class JobDriver_Breeding : JobDriver_Rape
	{
		public override bool TryMakePreToilReservations(bool errorOnFailed)
		{
			return pawn.Reserve(Target, job, BreederHelper.max_animals_at_once, 0);
		}

		[SyncMethod]
		protected override IEnumerable<Toil> MakeNewToils()
		{
			setup_ticks();
			var PartnerJob = xxx.gettin_raped;

			//--Log.Message("JobDriver_Breeding::MakeNewToils() - setting fail conditions");
			this.FailOnDespawnedNullOrForbidden(iTarget);
			this.FailOn(() => !pawn.CanReserve(Partner, BreederHelper.max_animals_at_once, 0)); // Fail if someone else reserves the target before the animal arrives.
			this.FailOn(() => !pawn.CanReach(Partner, PathEndMode.Touch, Danger.Some)); // Fail if animal cannot reach target.
			this.FailOn(() => pawn.Drafted);

			// Path to target
			yield return Toils_Goto.GotoThing(iTarget, PathEndMode.OnCell);

			//if (!(pawn.IsDesignatedBreedingAnimal() && Partner.IsDesignatedBreeding()));
			if (!(pawn.IsAnimal() && Partner.IsAnimal()))
				SexUtility.RapeTargetAlert(pawn, Partner);

			var StartPartnerJob = new Toil();
			StartPartnerJob.defaultCompleteMode = ToilCompleteMode.Instant;
			StartPartnerJob.socialMode = RandomSocialMode.Off;
			StartPartnerJob.initAction = delegate
			{
				if (Partner.jobs.curDriver is JobDriver_SexBaseRecieverRaped) return;

				var bed = Partner.CurrentBed();

				Partner.jobs.StartJob(
					JobMaker.MakeJob(PartnerJob, pawn),
					lastJobEndCondition: JobCondition.InterruptForced
				);

				if (bed is not null)
					if (Partner.jobs.curDriver is JobDriver_SexBaseRecieverRaped driver)
						driver.Set_bed(bed);
			};
			yield return StartPartnerJob;

			// Use happy heart instead of bad-touch heart when accepting of breeding.
			var fleckDef = xxx.is_zoophile(pawn) || xxx.is_animal(pawn) ? FleckDefOf.Heart : xxx.mote_noheart;

			// Breed target
			var SexToil = new Toil();
			SexToil.defaultCompleteMode = ToilCompleteMode.Never;
			SexToil.defaultDuration = duration;
			SexToil.handlingFacing = true;
			SexToil.initAction = delegate
			{
				Partner.pather.StopDead();
				Partner.jobs.curDriver.asleep = false;
				Start();
			};
			SexToil.tickAction = delegate
			{
				if (pawn.IsHashIntervalTick(ticks_between_hearts))
					ThrowMetaIconF(pawn.Position, pawn.Map, fleckDef);
				SexTick(pawn, Partner);
				if (!Partner.Dead)
					SexUtility.reduce_rest(Partner, 1);
				SexUtility.reduce_rest(pawn, 2);
				if (ticks_left <= 0)
					ReadyForNextToil();
			};
			SexToil.FailOn(() => Partner.CurJob?.def != PartnerJob);
			SexToil.AddFinishAction(End);
			yield return SexToil;

			yield return new Toil
			{
				initAction = delegate
				{
					//Log.Message("JobDriver_Breeding::MakeNewToils() - Calling aftersex");
					//// Trying to add some interactions and social logs
					Sexprops.isRape = !(pawn.relations.DirectRelationExists(PawnRelationDefOf.Bond, Partner) ||
					 	(xxx.is_animal(pawn) && (pawn.RaceProps.wildness - pawn.RaceProps.petness + 0.18f) > Rand.Range(0.36f, 1.8f)));
					SexUtility.ProcessSex(Sexprops);
				},
				defaultCompleteMode = ToilCompleteMode.Instant
			};
		}
	}
}


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

namespace rjw
{
	//biotech JobDriver_ControlMech
	public class JobDriver_HackMechPregnancy : JobDriver
	{
		private const TargetIndex MechInd = TargetIndex.A;

		private Pawn Mech => (Pawn)job.GetTarget(TargetIndex.A).Thing;

		private int MechControlTime => Mathf.RoundToInt(Mech.GetStatValue(StatDefOf.ControlTakingTime) * 60f);

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

		protected override IEnumerable<Toil> MakeNewToils()
		{
			if (ModsConfig.BiotechActive)
			{
				this.FailOnDestroyedNullOrForbidden(TargetIndex.A);
				yield return Toils_Goto.GotoThing(TargetIndex.A, PathEndMode.Touch);
				yield return Toils_General.WaitWith(TargetIndex.A, MechControlTime, useProgressBar: true, maintainPosture: true, maintainSleep: false, TargetIndex.A).WithEffect(EffecterDefOf.ControlMech, TargetIndex.A);
				Toil toil = ToilMaker.MakeToil("MakeNewToils");
				toil.initAction = delegate
				{
					Hediff_MechanoidPregnancy pregnancy = (Hediff_MechanoidPregnancy)Mech.health.hediffSet.GetFirstHediffOfDef(HediffDef.Named("RJW_pregnancy_mech"));
					pregnancy.Hack();
				};
				toil.PlaySoundAtEnd(SoundDefOf.ControlMech_Complete);
				yield return toil;
			}
		}
	}
}


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

namespace rjw
{
	public class JobDriver_Knotted : JobDriver_Goto
	{
		/// <summary>
		/// Number of ticks to smooth out the start and end transitions.
		/// </summary>
		const int TweenTicks = 10;

		/// <summary>
		/// The pawn's partner.
		/// </summary>
		public Pawn Partner => job.GetTarget(TargetIndex.A).Pawn;

		/// <summary>
		/// Set this to apply a positional offset to this pawn.
		/// </summary>
		private Vector3 forcedBodyOffset = Vector3.zero;
		public override Vector3 ForcedBodyOffset => forcedBodyOffset;

		protected override IEnumerable<Toil> MakeNewToils()
		{
			if (job.def.waitAfterArriving <= 0)
				yield break;

			var waiter = Toils_General.Wait(job.def.waitAfterArriving);

			// TODO: Deal with clothing better.  If the pawn was fucking without
			// clothes, they should still be drawn nude.  Since tying is a separate
			// job and we don't know if this was something like an animal "breeding"
			// someone in their clothing, it isn't straightforward to tell when
			// they should remain nude.  This is probably a sign this should not
			// have been a separate job but actually an extra toil of sex jobs.

			// Just in case.  Animals may only pant, whine, or tug during a tie...
			if (xxx.is_animal(pawn))
				waiter.socialMode = RandomSocialMode.Off;

			// We should have a partner set.  If we don't, use fallback behavior
			// of just waiting (which is how knotting used to work), with none
			// of the partner related enhancements.
			if (Partner is not Pawn partner)
			{
				ModLog.Warning($"Knotting job for {xxx.get_pawnname(pawn)} did not receive a partner.");
				yield return waiter;
				yield break;
			}

			this.FailOnDespawnedOrNull(TargetIndex.A);
			this.FailOnDowned(TargetIndex.A);

			// Detect when the tie breaks early.  This can happen for a number of
			// reasons, but I often saw it when an animal starts being trained
			// during the tie.
			this.FailOn(() => !IsCouplingValid());

			// If another mod patches this method, they can use it to add more
			// complex animation or positioning to the waiter.
			AddAnimation(waiter);

			yield return waiter;
		}

		/// <summary>
		/// <para>Applies some special animaion handling during the tie.</para>
		/// <para>Animation systems can patch this to attach their own actions to
		/// enable more complex animation.</para>
		/// </summary>
		/// <param name="waiter">The wait toil.</param>
		void AddAnimation(Toil waiter)
		{
			if (Partner is not Pawn partner) return;

			// Only if the tie involves an animal will we turn the tie butt-to-butt.
			// Human-likes with knots will just hold their partner close.  <3
			if (!InvolvesAnimal()) return;

			var initTicks = waiter.defaultDuration;
			var remTicks = initTicks;

			waiter.handlingFacing = true;
			waiter.AddPreTickAction(delegate ()
			{
				pawn.rotationTracker.Face(partner.DrawPos);
				pawn.Rotation = pawn.Rotation.Opposite;

				var newOffset = GetOffset();
				if (newOffset == Vector3.zero) return;

				// Tween the offset at the start and end.
				// Can't really smooth it out for an interrupted tie, though.
				var tweenTicks = Math.Min(TweenTicks, Math.Min(initTicks - remTicks, remTicks));
				var curTween = (float)tweenTicks / TweenTicks;
				forcedBodyOffset = newOffset * curTween;

				remTicks -= 1;
			});
		}

		/// <summary>
		/// Gets an offset for the current pawn to improve the visuals.
		/// </summary>
		/// <returns>An offset to apply.</returns>
		public Vector3 GetOffset()
		{
			// Must be an animal.
			if (!xxx.is_animal(pawn))
				return Vector3.zero;

			// Partner must exist and be human.
			if (Partner is not Pawn partner || xxx.is_animal(partner))
				return Vector3.zero;

			// Only applicable for east or west facings.
			if (pawn.Rotation != Rot4.East && pawn.Rotation != Rot4.West)
				return Vector3.zero;

			// Nudge the animal for a better lineup.
			var offset = partner.BodySize * 0.15f;
			var sign = pawn.Rotation == Rot4.West ? -1f : 1f;
			return new Vector3(sign * offset, 0f, -0.2f);
		}

		/// <summary>
		/// Whether this tie involves at least one animal.
		/// </summary>
		/// <returns>Whether the partner and/or pawn is an animal.</returns>
		public bool InvolvesAnimal() => xxx.is_animal(pawn) || xxx.is_animal(Partner);

		/// <summary>
		/// <para>Makes sure that the designated partner is still coupled to this pawn.</para>
		/// <para>For hardiness, this actually checks the partner pawn to see if they're
		/// using either `JobDriver_Sex` or `JobDriver_Knotted`, and the partner is still
		/// this pawn.</para>
		/// </summary>
		/// <returns>Whether the coupling is valid.</returns>
		public bool IsCouplingValid()
		{
			if (Partner is not Pawn partner) return false;

			var result = partner.jobs.curDriver switch
			{
				JobDriver_Knotted knottedDriver => knottedDriver.Partner == pawn,
				JobDriver_Sex sexDriver => sexDriver.Partner == pawn,
				_ => false
			};
			if (result) return true;

			// It's possible our partner is queued to tie, but between jobs.
			if (partner.jobs.jobQueue.FirstOrDefault()?.job is not { } job) return false;
			if (job.def != xxx.knotted) return false;
			return job.AnyTargetIs(pawn);
		}
	}
}


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

namespace rjw
{
	public class JobDriver_Masturbate : JobDriver_SexBaseInitiator
	{
		public override bool TryMakePreToilReservations(bool errorOnFailed)
		{
			return true; // No reservations needed.
		}
		public virtual IntVec3 cell => (IntVec3)job.GetTarget(iCell);

		[SyncMethod]
		protected override void SetupDurationTicks()
		{
			// Faster fapping when frustrated.
			duration = (int)(xxx.is_frustrated(pawn) ? 2500.0f * Rand.Range(0.2f, 0.7f) : 2500.0f * Rand.Range(0.2f, 0.4f));
			ticks_left = duration;
		}

		protected override void SetupOrgasmTicks()
		{
			orgasmstick = 180;
			sex_ticks = duration;
			orgasmStartTick = sex_ticks;
		}

		protected override IEnumerable<Toil> MakeNewToils()
		{
			setup_ticks();

			//this.FailOn(() => PawnUtility.PlayerForcedJobNowOrSoon(pawn));
			this.FailOn(() => pawn.health.Downed);
			this.FailOn(() => pawn.IsBurning());
			this.FailOn(() => pawn.IsFighting());
			this.FailOn(() => pawn.Drafted);

			Toil findfapspot = new Toil
			{
				initAction = delegate
				{
					pawn.pather.StartPath(cell, PathEndMode.OnCell);
				},
				defaultCompleteMode = ToilCompleteMode.PatherArrival
			};
			yield return findfapspot;

			//ModLog.Message(" Making new toil for QuickFap.");

			Toil SexToil = Toils_General.Wait(duration);
			SexToil.handlingFacing = true;
			SexToil.initAction = delegate
			{
				Start();
			};
			SexToil.tickAction = delegate
			{
				if (pawn.IsHashIntervalTick(ticks_between_hearts))
					ThrowMetaIconF(pawn.Position, pawn.Map, FleckDefOf.Heart);
				SexTick(pawn, null);
				SexUtility.reduce_rest(pawn, 1);
				if (ticks_left <= 0)
					ReadyForNextToil();
			};
			SexToil.AddFinishAction(delegate
			{
				End();
			});
			yield return SexToil;

			yield return new Toil
			{
				initAction = delegate
				{
					SexUtility.Aftersex(Sexprops);
					if (!SexUtility.ConsiderCleaning(pawn)) return;

					LocalTargetInfo own_cum = pawn.PositionHeld.GetFirstThing<Filth>(pawn.Map);

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

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


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

namespace rjw
{
	/// <summary>
	/// This is the driver for animals mating.
	/// </summary>
	public class JobDriver_Mating : JobDriver_Rape
	{
		public override bool TryMakePreToilReservations(bool errorOnFailed)
		{
			return pawn.Reserve(Target, job, BreederHelper.max_animals_at_once, 0, null, errorOnFailed);
		}

		[SyncMethod]
		protected override IEnumerable<Toil> MakeNewToils()
		{
			setup_ticks();
			var partnerJob = xxx.gettin_loved;

			//--Log.Message("JobDriver_Mating::MakeNewToils() - setting fail conditions");
			this.FailOnDespawnedNullOrForbidden(iTarget);
			this.FailOn(() => !pawn.CanReserve(Partner, BreederHelper.max_animals_at_once, 0)); // Fail if someone else reserves the target before the animal arrives.
			this.FailOn(() => !pawn.CanReach(Partner, PathEndMode.Touch, Danger.Some)); // Fail if animal cannot reach target.
			this.FailOn(() => pawn.Drafted);

			// Path to target
			yield return Toils_Goto.GotoThing(iTarget, PathEndMode.OnCell);

			var startPartnerJob = new Toil();
			startPartnerJob.defaultCompleteMode = ToilCompleteMode.Instant;
			startPartnerJob.socialMode = RandomSocialMode.Off;
			startPartnerJob.initAction = delegate
			{
				if (Partner.jobs.curDriver is JobDriver_SexBaseRecieverLoved) return;

				var bed = Partner.CurrentBed();

				Partner.jobs.StartJob(
					JobMaker.MakeJob(partnerJob, pawn),
					JobCondition.InterruptForced
				);

				if (bed is not null)
					if (Partner.jobs.curDriver is JobDriver_SexBaseRecieverLoved driver)
						driver.Set_bed(bed);
			};
			yield return startPartnerJob;

			// Mate target
			var sexToil = new Toil();
			sexToil.defaultCompleteMode = ToilCompleteMode.Never;
			sexToil.defaultDuration = duration;
			sexToil.handlingFacing = true;
			sexToil.initAction = delegate
			{
				Partner.pather.StopDead();
				Partner.jobs.curDriver.asleep = false;

				Start();
				if (xxx.is_human(Partner))
					Sexprops.usedCondom = CondomUtility.TryUseCondom(Partner);
				if (Sexprops.usedCondom)
				{
					var receiverDriver = Partner?.jobs?.curDriver as JobDriver_SexBaseReciever;
					if (receiverDriver is not null && receiverDriver.Sexprops != null)
						receiverDriver.Sexprops.usedCondom = true;
				}
			};
			sexToil.tickAction = delegate
			{
				if (pawn.IsHashIntervalTick(ticks_between_hearts))
					ThrowMetaIconF(pawn.Position, pawn.Map, FleckDefOf.Heart);
				SexTick(pawn, Partner);
				if (!Partner.Dead)
					SexUtility.reduce_rest(Partner, 1);
				SexUtility.reduce_rest(pawn, 2);
				if (ticks_left <= 0)
					ReadyForNextToil();
			};
			sexToil.FailOn(() => Partner.CurJob?.def != partnerJob);
			sexToil.AddFinishAction(End);
			yield return sexToil;

			yield return new Toil
			{
				initAction = () => SexUtility.ProcessSex(Sexprops),
				defaultCompleteMode = ToilCompleteMode.Instant
			};
		}
	}
}


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

namespace rjw
{
	public class JobDriver_Rape : JobDriver_SexBaseInitiator
	{
		public override bool TryMakePreToilReservations(bool errorOnFailed)
		{
			return pawn.Reserve(Target, job, xxx.max_rapists_per_prisoner, 0, null, errorOnFailed);
		}

		protected override IEnumerable<Toil> MakeNewToils()
		{
			if (RJWSettings.DebugRape) ModLog.Message("" + this.GetType().ToString() + "::MakeNewToils() called");
			setup_ticks();
			var PartnerJob = xxx.gettin_raped;

			this.FailOnDespawnedNullOrForbidden(iTarget);
			this.FailOn(() => !pawn.CanReserve(Partner, xxx.max_rapists_per_prisoner, 0)); // Fail if someone else reserves the prisoner before the pawn arrives
			this.FailOn(() => pawn.IsFighting());
			this.FailOn(() => Partner.IsFighting());
			this.FailOn(() => pawn.Drafted);
			yield return Toils_Goto.GotoThing(iTarget, PathEndMode.OnCell);

			SexUtility.RapeTargetAlert(pawn, Partner);

			var StartPartnerJob = new Toil();
			StartPartnerJob.defaultCompleteMode = ToilCompleteMode.Instant;
			StartPartnerJob.socialMode = RandomSocialMode.Off;
			StartPartnerJob.initAction = delegate
			{
				if (Partner.jobs.curDriver is JobDriver_SexBaseRecieverRaped) return;

				var bed = Partner.CurrentBed();

				Partner.jobs.StartJob(
					JobMaker.MakeJob(PartnerJob, pawn),
					lastJobEndCondition: JobCondition.InterruptForced
				);

				if (bed is not null)
					if (Partner.jobs.curDriver is JobDriver_SexBaseRecieverRaped driver)
						driver.Set_bed(bed);
			};
			yield return StartPartnerJob;

			var SexToil = new Toil();
			SexToil.defaultCompleteMode = ToilCompleteMode.Never;
			SexToil.defaultDuration = duration;
			SexToil.handlingFacing = true;
			SexToil.initAction = delegate
			{
				Partner.pather.StopDead();
				Partner.jobs.curDriver.asleep = false;
				if (RJWSettings.rape_stripping && (Partner.IsColonist || pawn.IsColonist))
					Partner.Strip();
				Start();
			};
			SexToil.tickAction = delegate
			{
				if (pawn.IsHashIntervalTick(ticks_between_hearts))
					ThrowMetaIconF(pawn.Position, pawn.Map, FleckDefOf.Heart);
				SexTick(pawn, Partner);
				SexUtility.reduce_rest(Partner, 1);
				SexUtility.reduce_rest(pawn, 2);
				if (ticks_left <= 0)
					ReadyForNextToil();
			};
			SexToil.FailOn(() => Partner.CurJob?.def != PartnerJob);
			SexToil.AddFinishAction(End);
			yield return SexToil;

			yield return new Toil
			{
				initAction = () => SexUtility.ProcessSex(Sexprops),
				defaultCompleteMode = ToilCompleteMode.Instant
			};
		}
	}
}


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

namespace rjw
{
	public class JobDriver_RapeComfortPawn : JobDriver_Rape
	{
		protected override IEnumerable<Toil> MakeNewToils()
		{
			if (RJWSettings.DebugRape) ModLog.Message("" + this.GetType().ToString() + "::MakeNewToils() called");
			setup_ticks();
			var PartnerJob = xxx.gettin_raped;

			this.FailOnDespawnedNullOrForbidden(iTarget);
			//this.FailOn(() => (!Partner.health.capacities.CanBeAwake) || (!comfort_prisoners.is_designated(Partner)));//this is wrong
			this.FailOn(() => (!Partner.IsDesignatedComfort()));
			this.FailOn(() => !pawn.CanReserve(Partner, xxx.max_rapists_per_prisoner, 0)); // Fail if someone else reserves the prisoner before the pawn arrives
			this.FailOn(() => pawn.Drafted);
			yield return Toils_Goto.GotoThing(iTarget, PathEndMode.OnCell);

			SexUtility.RapeTargetAlert(pawn, Partner);

			Toil StartPartnerJob = new Toil();
			StartPartnerJob.defaultCompleteMode = ToilCompleteMode.Instant;
			StartPartnerJob.socialMode = RandomSocialMode.Off;
			StartPartnerJob.initAction = delegate
			{
				if (Partner.jobs.curDriver is JobDriver_SexBaseRecieverRaped) return;

				var bed = Partner.CurrentBed();

				Partner.jobs.StartJob(
					JobMaker.MakeJob(PartnerJob, pawn),
					lastJobEndCondition: JobCondition.InterruptForced
				);

				if (bed is not null)
					if (Partner.jobs.curDriver is JobDriver_SexBaseRecieverRaped driver)
						driver.Set_bed(bed);
			};
			yield return StartPartnerJob;

			Toil SexToil = new Toil();
			SexToil.defaultCompleteMode = ToilCompleteMode.Never;
			SexToil.defaultDuration = duration;
			SexToil.handlingFacing = true;
			SexToil.initAction = delegate
			{
				Partner.pather.StopDead();
				Partner.jobs.curDriver.asleep = false;

				Start();
				// Unlike normal rape try use comfort prisoner condom
				CondomUtility.GetCondomFromRoom(Partner);
				Sexprops.usedCondom = CondomUtility.TryUseCondom(Partner);
				if (Sexprops.usedCondom)
				{
					var receiverDriver = Partner?.jobs?.curDriver as JobDriver_SexBaseReciever;
					if (receiverDriver is not null && receiverDriver.Sexprops != null)
						receiverDriver.Sexprops.usedCondom = true;
				}

				if (RJWSettings.DebugRape) ModLog.Message("JobDriver_RapeComfortPawn::MakeNewToils() - reserving prisoner");
				//pawn.Reserve(Partner, xxx.max_rapists_per_prisoner, 0);
			};
			SexToil.tickAction = delegate
			{
				if (pawn.IsHashIntervalTick(ticks_between_hearts))
					ThrowMetaIconF(pawn.Position, pawn.Map, FleckDefOf.Heart);
				SexTick(pawn, Partner);
				SexUtility.reduce_rest(Partner, 1);
				SexUtility.reduce_rest(pawn, 2);
				if (ticks_left <= 0)
					ReadyForNextToil();
			};
			SexToil.FailOn(() => Partner.CurJob?.def != PartnerJob);
			SexToil.AddFinishAction(End);
			yield return SexToil;

			yield return new Toil
			{
				initAction = delegate
				{
					// Trying to add some interactions and social logs
					SexUtility.ProcessSex(Sexprops);
					Partner.records.Increment(xxx.GetRapedAsComfortPawn);
				},
				defaultCompleteMode = ToilCompleteMode.Instant
			};
		}
	}
}


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

namespace rjw
{
	public class JobDriver_ViolateCorpse : JobDriver_Rape
	{
		public override bool TryMakePreToilReservations(bool errorOnFailed)
		{
			return pawn.Reserve(Target, job, 1, -1, null, errorOnFailed);
		}

		protected override IEnumerable<Toil> MakeNewToils()
		{
			if (RJWSettings.DebugRape) ModLog.Message(" JobDriver_ViolateCorpse::MakeNewToils() called");
			setup_ticks();

			this.FailOnDespawnedNullOrForbidden(iTarget);
			this.FailOn(() => !pawn.CanReserve(Target, 1, 0));  // Fail if someone else reserves the prisoner before the pawn arrives
			this.FailOn(() => pawn.IsFighting());
			this.FailOn(() => pawn.Drafted);
			this.FailOn(Target.IsBurning);

			if (RJWSettings.DebugRape) ModLog.Message(" JobDriver_ViolateCorpse::MakeNewToils() - moving towards Target");
			yield return Toils_Goto.GotoThing(iTarget, PathEndMode.OnCell);

			var alert = RJWPreferenceSettings.rape_attempt_alert == RJWPreferenceSettings.RapeAlert.Disabled ?
				MessageTypeDefOf.SilentInput : MessageTypeDefOf.NeutralEvent;
			Messages.Message(xxx.get_pawnname(pawn) + " is trying to rape a corpse of " + xxx.get_pawnname(Partner), pawn, alert);

			setup_ticks();// re-setup ticks on arrival

			var SexToil = new Toil();
			SexToil.defaultCompleteMode = ToilCompleteMode.Never;
			SexToil.defaultDuration = duration;
			SexToil.handlingFacing = true;
			SexToil.initAction = delegate
			{
				if (RJWSettings.DebugRape) ModLog.Message(" JobDriver_ViolateCorpse::MakeNewToils() - stripping Target");
				(Target as Corpse).Strip();
				Start();
			};
			SexToil.tickAction = delegate
			{
				if (pawn.IsHashIntervalTick(ticks_between_hearts))
					if (xxx.is_necrophiliac(pawn))
						ThrowMetaIconF(pawn.Position, pawn.Map, FleckDefOf.Heart);
					else
						ThrowMetaIconF(pawn.Position, pawn.Map, xxx.mote_noheart);
				//if (pawn.IsHashIntervalTick (ticks_between_hits))
				//	roll_to_hit (pawn, Target);
				SexTick(pawn, Target);
				SexUtility.reduce_rest(pawn, 2);
				if (ticks_left <= 0)
					ReadyForNextToil();
			};
			SexToil.AddFinishAction(delegate
			{
				End();
			});
			yield return SexToil;

			yield return new Toil
			{
				initAction = delegate
				{
					if (RJWSettings.DebugRape) ModLog.Message(" JobDriver_ViolateCorpse::MakeNewToils() - creating aftersex toil");
					SexUtility.ProcessSex(Sexprops);
				},
				defaultCompleteMode = ToilCompleteMode.Instant
			};
		}
	}
}


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

namespace rjw
{
	internal class JobDef_RapeEnemy : JobDef
	{
		public List<JobDef> interruptJobs;
		public List<string> TargetDefNames = new List<string>();
		public int priority = 0;

		protected JobDriver_RapeEnemy instance
		{
			get
			{
				if (_tmpInstance == null)
				{
					_tmpInstance = (JobDriver_RapeEnemy)Activator.CreateInstance(driverClass);
				}
				return _tmpInstance;
			}
		}

		private JobDriver_RapeEnemy _tmpInstance;

		public override void ResolveReferences()
		{
			base.ResolveReferences();
			interruptJobs = new List<JobDef> { null, JobDefOf.LayDown, JobDefOf.Wait_Wander, JobDefOf.GotoWander, JobDefOf.AttackMelee };
		}

		public virtual bool CanUseThisJobForPawn(Pawn rapist)
		{
			bool busy = !interruptJobs.Contains(rapist.CurJob?.def);
			if (RJWSettings.DebugRape) ModLog.Message(" JobDef_RapeEnemy::CanUseThisJobForPawn( " + xxx.get_pawnname(rapist) + " ) - busy:" + busy + " with current job: " + rapist.CurJob?.def?.ToString());
			if (busy) return false;

			return instance.CanUseThisJobForPawn(rapist);// || TargetDefNames.Contains(rapist.def.defName);
		}

		public virtual Pawn FindVictim(Pawn rapist, Map m)
		{
			return instance.FindVictim(rapist, m);
		}
	}

	public class JobDriver_RapeEnemy : JobDriver_Rape
	{
		//override can_rape mechanics
		protected bool requireCanRape = true;

		public virtual bool CanUseThisJobForPawn(Pawn rapist)
		{
			return xxx.is_human(rapist);
		}

		// this is probably useseless, maybe there be something in future
		public virtual bool considerStillAliveEnemies => true;

		[SyncMethod]
		public virtual Pawn FindVictim(Pawn rapist, Map m)
		{
			if (RJWSettings.DebugRape) ModLog.Message($"{this.GetType().ToString()}::TryGiveJob({xxx.get_pawnname(rapist)}) map {m?.ToString()}");
			if (rapist == null || m == null) return null;

			if (RJWSettings.DebugRape) ModLog.Message($" can rape = {xxx.can_rape(rapist)}");
			if (requireCanRape && !xxx.can_rape(rapist)) return null;

			List<Pawn> validTargets = new List<Pawn>();
			float min_fuckability = 0.10f;                          // Don't rape pawns with <10% fuckability
			float avg_fuckability = 0f;                             // Average targets fuckability, choose target higher than that
			var valid_targets = new Dictionary<Pawn, float>();      // Valid pawns and their fuckability
			Pawn chosentarget = null;                               // Final target pawn

			IEnumerable<Pawn> targets = m.mapPawns.AllPawnsSpawned.Where(x
				=> !x.IsForbidden(rapist) && x != rapist && x.HostileTo(rapist)
				&& IsValidTarget(rapist, x))
				.ToList();

			if (RJWSettings.DebugRape) ModLog.Message($" targets {targets.Count()}");

			if (targets.Any(x => IsBlocking(rapist, x)))	//If any of the targets is not downed and visible - don't proceed with rape (you have more pressing things to do).
			{												//This is a bit whacky bearing in mind target selection. For example vulnerable pawns will block, but non-vulnearable will not
				return null;
			}

			foreach (var target in targets)
			{
				if (!Pather_Utility.cells_to_target_rape(rapist, target.Position))
				{
					//if (RJWSettings.DebugRape) ModLog.Message($" {xxx.get_pawnname(target)} too far (cells) = {rapist.Position.DistanceToSquared(target.Position)}, skipping");
					if (RJWSettings.DebugRape) ModLog.Message($" {xxx.get_pawnname(target)} too far (cells) = {rapist.Position.DistanceTo(target.Position)}, skipping");
					continue;// too far
				}

				float fuc = GetFuckability(rapist, target);

				if (fuc > min_fuckability)
				{
					if (Pather_Utility.can_path_to_target(rapist, target.Position))
						valid_targets.Add(target, fuc);
					else
						if (RJWSettings.DebugRape) ModLog.Message($" {xxx.get_pawnname(target)} too far (path), skipping");
				}
				else
					if (RJWSettings.DebugRape) ModLog.Message($" {xxx.get_pawnname(target)} fuckability too low = {fuc}, skipping");

			}
			if (RJWSettings.DebugRape) ModLog.Message($" fuckable targets {valid_targets.Count()}");

			if (valid_targets.Any())
			{
				avg_fuckability = valid_targets.Average(x => x.Value);
				if (RJWSettings.DebugRape) ModLog.Message($" avg_fuckability {avg_fuckability}");

				// choose pawns to fuck with above average fuckability
				var valid_targetsFiltered = valid_targets.Where(x => x.Value >= avg_fuckability);
				if (RJWSettings.DebugRape) ModLog.Message($" targets above avg_fuckability {valid_targetsFiltered.Count()}");

				if (valid_targetsFiltered.Any())
					chosentarget = valid_targetsFiltered.RandomElement().Key;
			}

			return chosentarget;
		}

		bool IsBlocking(Pawn rapist, Pawn target)
		{
			return considerStillAliveEnemies && !target.Downed && rapist.CanSee(target);
		}

		bool IsValidTarget(Pawn rapist, Pawn target)
		{

			if (!RJWSettings.bestiality_enabled)
			{
				if (xxx.is_animal(target) && xxx.is_human(rapist))
				{
					//bestiality disabled, skip.
					return false;
				}
				if (xxx.is_animal(rapist) && xxx.is_human(target))
				{
					//bestiality disabled, skip.
					return false;
				}
			}

			if (!RJWSettings.animal_on_animal_enabled)
				if ((xxx.is_animal(target) && xxx.is_animal(rapist)))
				{
					//animal_on_animal disabled, skip.
					return false;
				}

			if ((xxx.is_mechanoid(rapist) && xxx.is_animal(target)) || (xxx.is_animal(rapist) && xxx.is_mechanoid(target)))
				return false; //no Mech on Animal action, ref JobDriver_RapeEnemyByMech::GetFuckability()

			if (target.CurJob?.def == xxx.gettin_raped || target.CurJob?.def == xxx.gettin_loved)
			{
				//already having sex with someone, skip, give chance to other victims.
				return false;
			}

			return Can_rape_Easily(target) &&
				(xxx.is_human(target) || xxx.is_animal(target)) &&
				rapist.CanReserveAndReach(target, PathEndMode.OnCell, Danger.Some, xxx.max_rapists_per_prisoner, 0);
		}

		public virtual float GetFuckability(Pawn rapist, Pawn target)
		{
			float fuckability = 0;
			if (target.health.hediffSet.HasHediff(xxx.submitting)) // it's not about attractiveness anymore, it's about showing who's whos bitch
			{
				fuckability = 2 * SexAppraiser.would_fuck(rapist, target, invert_opinion: true, ignore_bleeding: true, ignore_gender: true);
			}
			else if (SexAppraiser.would_rape(rapist, target))
			{
				fuckability = SexAppraiser.would_fuck(rapist, target, invert_opinion: true, ignore_bleeding: true, ignore_gender: true);
			}

			if (RJWSettings.DebugRape) ModLog.Message($"JobDriver_RapeEnemy::GetFuckability({xxx.get_pawnname(rapist)}, {xxx.get_pawnname(target)})");

			return fuckability;
		}

		protected bool Can_rape_Easily(Pawn pawn)
		{
			return xxx.can_get_raped(pawn) && !pawn.IsBurning();
		}
	}
}


code:
using RimWorld;
using Verse;

namespace rjw
{
	internal class JobDriver_RapeEnemyByAnimal : JobDriver_RapeEnemy
	{
		public override bool CanUseThisJobForPawn(Pawn rapist)
		{
			if (rapist.CurJob != null && (rapist.CurJob.def != JobDefOf.LayDown || rapist.CurJob.def != JobDefOf.Wait_Wander || rapist.CurJob.def != JobDefOf.GotoWander))
				return false;

			return xxx.is_animal(rapist) && !xxx.is_insect(rapist);
		}
	}
}


code:
using RimWorld;
using Verse;

namespace rjw
{
	internal class JobDriver_RapeEnemyByHumanlike : JobDriver_RapeEnemy
	{
		public override bool CanUseThisJobForPawn(Pawn rapist)
		{
			if (rapist.CurJob != null && (rapist.CurJob.def != JobDefOf.LayDown || rapist.CurJob.def != JobDefOf.Wait_Wander || rapist.CurJob.def != JobDefOf.GotoWander))
				return false;

			return xxx.is_human(rapist);
		}
	}
}


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

namespace rjw
{
	internal class JobDriver_RapeEnemyByInsect : JobDriver_RapeEnemy
	{
		public override bool CanUseThisJobForPawn(Pawn rapist)
		{
			if (rapist.CurJob != null && (rapist.CurJob.def != JobDefOf.LayDown || rapist.CurJob.def != JobDefOf.Wait_Wander || rapist.CurJob.def != JobDefOf.GotoWander))
				return false;

			return xxx.is_insect(rapist);
		}

		public override float GetFuckability(Pawn rapist, Pawn target)
		{
			//Female plant Eggs to everyone.
			//if (rapist.gender == Gender.Female) //Genital_Helper.has_ovipositorF(rapist);
			//{
			//	//only rape when target dont have eggs yet
			//	//if ((from x in target.health.hediffSet.GetHediffs<Hediff_InsectEgg>() where (x.IsParent(rapist)) select x).Count() > 0)
			//	{
			//		return 1f;
			//	}
			//}
			////Male rape to everyone.
			////Feritlize eggs to target with planted eggs.
			//else //Genital_Helper.has_ovipositorM(rapist);
			//{
			//	//only rape target when can fertilize
			//	//if ((from x in target.health.hediffSet.GetHediffs<Hediff_InsectEgg>() where (x.IsParent(rapist) && !x.fertilized) select x).Count() > 0)
			//	if ((from x in target.health.hediffSet.GetHediffs<Hediff_InsectEgg>() where x.IsParent(rapist) select x).Count() > 0)
			//	{
			//		return 1f;
			//	}
			//}
			return 1f;
		}
	}
}


code:
using RimWorld;
using Verse;

namespace rjw
{
	internal class JobDriver_RapeEnemyByMech : JobDriver_RapeEnemy
	{
		public override bool CanUseThisJobForPawn(Pawn rapist)
		{
			if (rapist.CurJob != null && (rapist.CurJob.def != JobDefOf.LayDown || rapist.CurJob.def != JobDefOf.Wait_Wander || rapist.CurJob.def != JobDefOf.GotoWander))
				return false;

			return xxx.is_mechanoid(rapist);
		}

		public override float GetFuckability(Pawn rapist, Pawn target)
		{
			//Plant stuff into humanlikes.
			if (xxx.is_human(target))
				return 1f;
			else
				return 0f;
		}

	}
}


code:
using RimWorld;
using Verse;

namespace rjw
{
	internal class JobDriver_RapeEnemyToParasite : JobDriver_RapeEnemy
	{
		//not implemented
		public JobDriver_RapeEnemyToParasite()
		{
			this.requireCanRape = false;
		}

		public override bool CanUseThisJobForPawn(Pawn rapist)
		{
			if (rapist.CurJob != null && (rapist.CurJob.def != JobDefOf.LayDown || rapist.CurJob.def != JobDefOf.Wait_Wander || rapist.CurJob.def != JobDefOf.GotoWander))
				return false;

			return false;
		}
	}
}


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

namespace rjw
{
	public class JobDriver_RandomRape : JobDriver_Rape
	{
		//Add some stuff. planning became bersek when failed to rape.
	}
}