code:
using System;
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using rjw;
using RJW_Onahole.Data;
using RJW_Onahole.Hediffs;
using RJW_Onahole.Interfaces;
using RJW_Onahole.UI;
using RJW_Onahole.Utilities;
using UnityEngine;
using Verse;
namespace RJW_Onahole.Comps
{
    public class CompCumCollector : CompConditionalBuildingOverlay
    {
        public bool enabled = false;
        public Dictionary<string, float> fluidsToAmounts = new Dictionary<string, float>();
        public Dictionary<string, FluidSetting> fluidsToSettings = new Dictionary<string, FluidSetting>();
        private const float BARWIDTH = 0.3f;
        private const float BARHEIGHT = 0.30f;
        private Vector3 CenterPos { get { Vector3 center = this.parent.DrawPos + Props.graphicDatas[0].drawOffset + new Vector3(0f, 0f, -0.02f); center.y = Props.altitudeLayer.AltitudeFor() + 0.05f; return center; } }
        private Vector3 CirclePos { get { Vector3 center = CenterPos + new Vector3(0, 0, -BARHEIGHT / 2); center.y = Props.altitudeLayer.AltitudeFor() + 0.07f; return center; } }
        private Rot4 BarRotation => Rot4.East;
        public override void PostDraw()
        {
            base.PostDraw();
            if (!RJW_Onahole_Settings.showExtraGraphics) { return; }
            if (this.enabled)
            {
                base.DrawGraphic(0);
                base.DrawGraphic(1);
                if (this.fluidsToAmounts.Values.Sum() > 0)
                {
                    Color color = LastAvailableColor();
                    DrawUtility.DrawFillableBarMultiThingDef(CenterPos, new Vector2(BARHEIGHT, BARWIDTH), BarRotation, fluidsToAmounts, RJW_Onahole_Settings.maxFluidStorage);
                    DrawUtility.DrawFilledCircle(CirclePos + new Vector3(0, 0, BARHEIGHT * fluidsToAmounts.Values.Sum() / RJW_Onahole_Settings.maxFluidStorage), BARWIDTH / 2, SolidColorMaterials.SimpleSolidColorMaterial(color), new Vector3(1f, 1f, 0.4f));
                    base.DrawGraphic(2, color);
                }
            }
        }
        private Color LastAvailableColor()
        {
            foreach (KeyValuePair<string, float> fluidToAmount in fluidsToAmounts.Reverse())
            {
                if (fluidToAmount.Value > 0)
                {
                    Color thingColor = fluidsToSettings[fluidToAmount.Key].SexFluid.filth?.graphicData?.color ?? Color.white;
                    thingColor.a = 1f;
                    return thingColor;
                }
            }
            return Color.white;
        }
        public override void PostExposeData()
        {
            base.PostExposeData();
            Scribe_Values.Look(ref enabled, "cumCollectorEnabled", false);
            Scribe_Collections.Look(ref fluidsToAmounts, "fluidsToAmounts", LookMode.Value, LookMode.Value);
            Scribe_Collections.Look(ref fluidsToSettings, "fluidsToSettings", LookMode.Value, LookMode.Deep);

            if (Scribe.mode == LoadSaveMode.PostLoadInit)
            {
                // Remove malformed data on setting/mod change
                fluidsToAmounts = fluidsToAmounts ?? new Dictionary<string, float>();
                fluidsToSettings = fluidsToSettings ?? new Dictionary<string, FluidSetting>();
                List<string> keysToRemove = new List<string>();
                foreach (string name in fluidsToAmounts.Keys.ToList())
                {
                    if (DefDatabase<SexFluidDef>.GetNamedSilentFail(name) == null)
                    {
                        ("Malformed fluid name in fluidsToAmounts: " + name).LogWarning();
                        keysToRemove.Add(name);
                    }
                    fluidsToAmounts[name] = Mathf.Max(fluidsToAmounts[name], 0f);
                }
                foreach (string key in keysToRemove)
                {
                    fluidsToAmounts.Remove(key);
                }
                keysToRemove.Clear();
                foreach (string sexFluidDefName in fluidsToSettings.Keys.ToList())
                {
                    SexFluidDef sexFluidDef = DefDatabase<SexFluidDef>.GetNamedSilentFail(sexFluidDefName);
                    if (sexFluidDef == null)
                    {
                        ("Malformed fluid name in fluidsToSettings: " + sexFluidDefName).LogWarning();
                        keysToRemove.Add(sexFluidDefName);
                    }
                    else if (fluidsToSettings[sexFluidDefName]?.SexFluid == null)
                    {
                        ("Malformed fluid setting in fluidsToSettings: " + sexFluidDefName).LogWarning();
                        keysToRemove.Add(sexFluidDefName);
                    }
                }
                foreach (string key in keysToRemove)
                {
                    fluidsToSettings.Remove(key);
                }
                InitializeFluidSettings();
                Notify_SettingsChanged();
            }
        }
        public override void PostSpawnSetup(bool respawningAfterLoad)
        {
            base.PostSpawnSetup(respawningAfterLoad);
            InitializeFluidSettings();
        }
        private void InitializeFluidSettings()
        {
            foreach (SexFluidDef sexFluidDef in CacheUtility.sexFluidDefs)
            {
                if (!fluidsToSettings.ContainsKey(sexFluidDef.defName))
                {
                    fluidsToSettings[sexFluidDef.defName] = new FluidSetting(sexFluidDef);
                }
            }
        }
        protected override IEnumerable<Gizmo> GetGizmos()
        {
            yield return ToggleCumCollector();
            yield return new Gizmo_CumStatus(this);
        }
        private Command_Toggle ToggleCumCollector()
        {
            return new Command_Toggle
            {
                defaultLabel = Tr.CumCollector,
                defaultDesc = Tr.CumCollector_Desc,
                isActive = () => this.enabled,
                icon = CacheUtility.Tex.IconCumCollector,
                toggleAction = delegate
                {
                    this.enabled = !this.enabled;
                }
            };
        }

        public float TryStoreCum(float amount, ThingDef filthDef)
        {
            if (!this.enabled) { return amount; }
            FluidSetting fluidSetting = TryMatchFluidSetting(filthDef);
            if (fluidSetting == null) { return amount; }
            float leftOver = AddFluidAmount(filthDef, amount);
            if (fluidSetting.AutoConvert)
            {
                TryConvertFilth(fluidSetting);
            }
            if (fluidSetting.Destroy)
            {
                SetFluidAmount(filthDef, 0f);
            }
            return leftOver;
        }
        public FluidSetting TryMatchFluidSetting(ThingDef filthDef)
        {
            if (filthDef == null) { return null; }
            if (GetFluidSetting(filthDef) != null) { return GetFluidSetting(filthDef); }

            // Fallback for different types of blood
            if (filthDef.defName.ContainsCI("Blood"))
            {
                filthDef = DefDatabase<ThingDef>.GetNamed("Filth_Blood");
            }

            // Fallback for different types of cum
            if (filthDef.defName.ContainsCI("Cum"))
            {
                filthDef = DefDatabase<ThingDef>.GetNamed("FilthCum");
            }

            return GetFluidSetting(filthDef);
        }
        public float TryTakeFluid(float amount, SexFluidDef sexFluidDef)
        {
            if (amount <= 0) { return 0; }
            float took = 0;
            if (sexFluidDef == null) { return took; }
            float takeCum = Mathf.Min(GetFluidAmount(sexFluidDef), amount);
            AddFluidAmount(sexFluidDef, -takeCum);
            took += takeCum;
            return Mathf.Max(took, 0f);
        }
        public bool TryConvertFilth(FluidSetting fluidSetting)
        {
            if (!fluidSetting.AutoConvert) { return false; }
            float num = GetFluidAmount(fluidSetting.SexFluid);
            int converted = fluidSetting.ConvertFromFluid(ref num);
            SetFluidAmount(fluidSetting.SexFluid, num);
            bool isSuccess = true;
            while (converted > 0)
            {
                Thing thing = ThingMaker.MakeThing(fluidSetting.SexFluid.consumable, null);
                int stackCount = Mathf.Min(converted, thing.def.stackLimit);
                thing.stackCount = stackCount;
                converted -= stackCount;
                isSuccess = isSuccess && Bed.TryStoreInBed(thing);
                if (!isSuccess) { break; }
            }
            return isSuccess;
        }
        public void Notify_SettingsChanged()
        {
            foreach (string fluid in fluidsToSettings.Keys)
            {
                if (fluidsToSettings[fluid].AutoConvert)
                {
                    this.TryConvertFilth(fluidsToSettings[fluid]);
                }
                if (fluidsToSettings[fluid].Destroy)
                {
                    fluidsToAmounts[fluid] = 0;
                }

            }
        }

        public float GetFluidAmount(ThingDef filthDef)
        {
            if (CacheUtility.filthDefsToSexFluidDefs.TryGetValue(filthDef, out SexFluidDef sexFluidDef))
            {
                return GetFluidAmount(sexFluidDef);
            }
            return 0;
        }
        public float GetFluidAmount(SexFluidDef sexFluidDef)
        {
            if (sexFluidDef == null) { return 0; }
            if (!fluidsToAmounts.ContainsKey(sexFluidDef.defName) || fluidsToAmounts[sexFluidDef.defName] < 0) { fluidsToAmounts[sexFluidDef.defName] = 0; }
            return fluidsToAmounts[sexFluidDef.defName];
        }
        public float AddFluidAmount(ThingDef filthDef, float amount)
        {
            if (CacheUtility.filthDefsToSexFluidDefs.TryGetValue(filthDef, out SexFluidDef sexFluidDef))
            {
                return AddFluidAmount(sexFluidDef, amount);
            }
            return amount;
        }
        public float AddFluidAmount(SexFluidDef sexFluidDef, float amount)
        {
            if (sexFluidDef == null) { return amount; }
            if (!fluidsToAmounts.ContainsKey(sexFluidDef.defName)) { fluidsToAmounts[sexFluidDef.defName] = 0; }
            float leftOver = Mathf.Max(fluidsToAmounts.Sum(x => x.Value) + amount - RJW_Onahole_Settings.maxFluidStorage, 0);
            fluidsToAmounts[sexFluidDef.defName] += amount - leftOver;
            fluidsToAmounts[sexFluidDef.defName] = Mathf.Max(fluidsToAmounts[sexFluidDef.defName], 0f);
            return leftOver;
        }
        public void SetFluidAmount(ThingDef filthDef, float amount)
        {
            if (CacheUtility.filthDefsToSexFluidDefs.TryGetValue(filthDef, out SexFluidDef sexFluidDef))
            {
                SetFluidAmount(sexFluidDef, amount);
            }
        }
        public void SetFluidAmount(SexFluidDef sexFluidDef, float amount)
        {
            if (sexFluidDef == null) { return; }
            if (!fluidsToAmounts.ContainsKey(sexFluidDef.defName)) { fluidsToAmounts[sexFluidDef.defName] = 0; }
            fluidsToAmounts[sexFluidDef.defName] = Mathf.Max(amount, 0f);
        }
        public FluidSetting GetFluidSetting(ThingDef filthDef)
        {
            if (CacheUtility.filthDefsToSexFluidDefs.TryGetValue(filthDef, out SexFluidDef sexFluidDef))
            {
                return GetFluidSetting(sexFluidDef);
            }
            return null;
        }
        public FluidSetting GetFluidSetting(SexFluidDef sexFluidDef)
        {
            if (sexFluidDef == null) { return null; }
            if (!fluidsToSettings.ContainsKey(sexFluidDef.defName) || fluidsToSettings[sexFluidDef.defName] == null) { fluidsToSettings[sexFluidDef.defName] = new FluidSetting(sexFluidDef); }
            return fluidsToSettings[sexFluidDef.defName];
        }
    }
}


code:
using System;
using System.Collections.Generic;
using RimWorld;
using RJW_Onahole.UI;
using RJW_Onahole.Utilities;
using UnityEngine;
using Verse;
using rjw;
using RJW_Onahole.Hediffs;
using System.Linq;
using RJW_Onahole.Interfaces;
using RJW_Onahole.Data;
namespace RJW_Onahole.Comps
{
    [StaticConstructorOnStartup]
    public class CompDrugInfuser : CompConditionalBuildingOverlay
    {
        public override ResearchProjectDef RequiredResearch => DefDatabase<ResearchProjectDef>.GetNamed("DrugProduction", true);
        public bool Enabled = false;
        public ThingDef DrugThingDef => infuserRefuelable?.FuelThingDef;
        public CompInfuserRefuelable infuserRefuelable;
        private Vector3 CenterPos => (parent.DrawPos + Props.graphicDatas[0].drawOffset).WithY(Props.altitudeLayer.AltitudeFor()) + new Vector3(0.0f, 0.005f, 0.3f);

        public override void PostSpawnSetup(bool respawningAfterLoad)
        {
            base.PostSpawnSetup(respawningAfterLoad);
            infuserRefuelable = parent.GetComp<CompInfuserRefuelable>();
        }

        public override void CompTickRare()
        {
            base.CompTickRare();
            if (infuserRefuelable?.FuelThingDef == null) { return; }
            if (!Enabled) { return; }
            if (Bed.Pawn == null) { return; }
            if (infuserRefuelable.Fuel <= 0) { return; }
            if (Bed.Pawn.health?.hediffSet?.GetFirstHediffOfDef(HediffDefOf.DrugOverdose) != null) { return; } // Don't give drugs if overdose
            if (DrugStatsUtility.GetNeed(DrugThingDef) is NeedDef needDef)
            {
                Need need = Bed.Pawn.needs.TryGetNeed(needDef);
                float needOffset = DrugStatsUtility.GetDrugComp(DrugThingDef).needLevelOffset;
                AddictionUtility.ModifyChemicalEffectForToleranceAndBodySize_NewTemp(Bed.Pawn, DrugStatsUtility.GetChemical(DrugThingDef), ref needOffset, true);
                if (needOffset > 0f)
                {
                    if (need == null || need.CurLevelPercentage < 0.1f || need.MaxLevel - need.CurLevel < needOffset)
                    {
                        this.TakeDrug();
                        return;
                    }
                }
            }
            HediffDef hediffDef = DrugStatsUtility.GetDrugHighGiver(infuserRefuelable.FuelThingDef)?.hediffDef ?? DrugThingDef.ingestible.outcomeDoers?.OfType<IngestionOutcomeDoer_GiveHediff>().FirstOrDefault()?.hediffDef;
            if (hediffDef != null)
            {
                // If drug causes need, Take drug if need is not met
                Hediff hediff = Bed.Pawn.health?.hediffSet?.GetFirstHediffOfDef(hediffDef);
                if (hediff == null || hediff.def.stages?.LastOrDefault()?.minSeverity != null && hediff.Severity < hediff.def.stages.LastOrDefault().minSeverity)
                {
                    this.TakeDrug();
                }
            }
        }
        private void TakeDrug()
        {
            infuserRefuelable.ConsumeFuel(1f);
            Thing thing = ThingMaker.MakeThing(infuserRefuelable.FuelThingDef, null);
            thing.stackCount = 1;
            Bed.Pawn.needs.food.CurLevel += thing.Ingested(Bed.Pawn, 1f);
        }
        public override void PostDraw()
        {
            base.PostDraw();
            if (!RJW_Onahole_Settings.showExtraGraphics) { return; }
            if (infuserRefuelable == null) { return; }
            if (!Enabled) { return; }
            base.DrawGraphic(0);
            base.DrawGraphic(1);
            if (infuserRefuelable.FuelThingDef == null) { return; }
            if (infuserRefuelable.FuelThingDef?.uiIcon != null)
            {
                Mesh mesh = MeshPool.plane10;
                Graphics.DrawMesh(mesh, Matrix4x4.TRS(CenterPos + new Vector3(0f, 0.015f, 0.01f), Quaternion.identity, new Vector3(0.15f, 1f, 0.15f)), CacheUtility.MatFor(infuserRefuelable.FuelThingDef), 0);
            }
            DrawUtility.DrawFillableBar(CenterPos, new Vector2(0.28f, 0.2f), infuserRefuelable.FuelPercentOfTarget, Rot4.East, infuserRefuelable.BarMat());
            if (Bed.Pawn == null) { return; }
            Vector3 PawnMouthPos = PositionUtility.GetPosition(Bed.Pawn, OnaholeDefOf.OnaholeAutoFeeder) ?? Bed.Pawn.DrawPos;
            DrawUtility.DrawPipe(CenterPos + new Vector3(0, 0, -0.15f), (Bed.Pawn.DrawPos + new Vector3(0.2f, 0f, 0f)).WithY(CenterPos.y), PawnMouthPos, 0.02f, GetColor());
        }
        public Color GetColor()
        {
            return infuserRefuelable.Fuel > 0 ? CacheUtility.ColorMat.InfuserColor : Color.white;
        }
        public override void PostExposeData()
        {
            base.PostExposeData();
            Scribe_Values.Look(ref Enabled, "DrugInfuserEnabled", false);
        }
    }
}

repo_name: atainer/rimjobworld

license: mit


code:
using System;
using System.Collections.Generic;
using RimWorld;
using RJW_Onahole.UI;
using RJW_Onahole.Utilities;
using UnityEngine;
using Verse;
using rjw;
using RJW_Onahole.Hediffs;
using System.Linq;
using RJW_Onahole.Interfaces;
namespace RJW_Onahole.Comps
{
    public class CompElectrocutePowerSupply : CompConditionalBuildingOverlay, ICompAddsHediff, IModularPower
    {
        public override ResearchProjectDef RequiredResearch => DefDatabase<ResearchProjectDef>.GetNamed("Electricity", true);
        public List<HediffDef> GetHediffDef() => new List<HediffDef> { OnaholeDefOf.OnaholeBreastElectrodes, OnaholeDefOf.OnaholeGenitalElectrodes };
        public void Notify_PawnGraphicsChange() { }
        public float GetPowerConsumption()
        {
            float mult = Bed.Pawn?.health.hediffSet.HasHediff(OnaholeDefOf.OnaholeBreastElectrodes) == true ? 1f : 0f;
            mult += Bed.Pawn?.health.hediffSet.HasHediff(OnaholeDefOf.OnaholeGenitalElectrodes) == true ? 1f : 0f;
            float basePower = 0f;
            switch (this.BreastElectrocuteMode)
            {
                case ElectrocuteModes.Stimulate:
                    basePower = 5f;
                    break;
                case ElectrocuteModes.Pain:
                    basePower = 10f;
                    break;
                case ElectrocuteModes.Burn:
                    basePower = 20f;
                    break;
            }
            return basePower * mult;
        }
        public void Notify_PowerStateChanged(bool state) { this.Notify_BreastElectrocuteModeChanged(); }
        public int TextureIndex { get => 0; set { } }
        public ElectrocuteModes BreastElectrocuteMode = ElectrocuteModes.Removed;
        public ElectrocuteModes GenitalElectrocuteMode = ElectrocuteModes.Removed;
        public override void PostSpawnSetup(bool respawningAfterLoad)
        {
            base.PostSpawnSetup(respawningAfterLoad);
            this.Notify_BreastElectrocuteModeChanged();
            this.Notify_GenitalElectrocuteModeChanged();
        }
        public void Notify_PawnBind(Pawn pawn)
        {
            if (pawn == null)
            {
                return;
            }
            if (this.BreastElectrocuteMode != ElectrocuteModes.Removed && Genital_Helper.has_breasts(pawn))
            {
                pawn.health.TryAddHediffOfDef(OnaholeDefOf.OnaholeBreastElectrodes);
            }
            if (this.GenitalElectrocuteMode != ElectrocuteModes.Removed && Genital_Helper.has_genitals(pawn))
            {
                pawn.health.TryAddHediffOfDef(OnaholeDefOf.OnaholeGenitalElectrodes);
            }
            this.Notify_BreastElectrocuteModeChanged();
            this.Notify_GenitalElectrocuteModeChanged();
            pawn.Drawer.renderer.SetAllGraphicsDirty();
        }
        public void Notify_PawnUnbind(Pawn pawn)
        {
            pawn.health.TryRemoveHediffOfDef(OnaholeDefOf.OnaholeGenitalElectrodes);
            pawn.health.TryRemoveHediffOfDef(OnaholeDefOf.OnaholeBreastElectrodes);
            pawn.Drawer.renderer.SetAllGraphicsDirty();
        }
        public override void PostDraw()
        {
            base.PostDraw();
            if (!RJW_Onahole_Settings.showExtraGraphics) { return; }
            if (this.BreastElectrocuteMode != ElectrocuteModes.Removed || this.GenitalElectrocuteMode != ElectrocuteModes.Removed)
            {
                if ((int)this.BreastElectrocuteMode <= 1 && (int)this.GenitalElectrocuteMode <= 1 || Bed.GetComp<CompModularPower>()?.PowerOn == false)
                {
                    base.DrawGraphic(0);
                }
                else
                {
                    base.DrawGraphic(1);
                }
            }
        }
        protected override IEnumerable<Gizmo> GetGizmos()
        {
            yield return new ElectrocuteGizmo(this.BreastElectrocuteMode, delegate (ElectrocuteModes mode)
            {
                this.BreastElectrocuteMode = mode;
                this.Notify_BreastElectrocuteModeChanged();
            })
            { defaultLabel = Tr.BreastElectrodes, defaultDesc = Tr.BreastElectrodes_Desc, icon = CacheUtility.Tex.IconBreastElectrodes };

            yield return new ElectrocuteGizmo(this.GenitalElectrocuteMode, delegate (ElectrocuteModes mode)
            {
                this.GenitalElectrocuteMode = mode;
                this.Notify_GenitalElectrocuteModeChanged();
            })
            { defaultLabel = Tr.GenitalElectrodes, defaultDesc = Tr.GenitalElectrodes_Desc, icon = CacheUtility.Tex.IconGenitalElectrodes };
        }
        public override void PostExposeData()
        {
            base.PostExposeData();
            Scribe_Values.Look(ref this.BreastElectrocuteMode, "breastElectrocuteMode", UI.ElectrocuteModes.Removed);
            Scribe_Values.Look(ref this.GenitalElectrocuteMode, "genitalElectrocuteMode", UI.ElectrocuteModes.Removed);
        }

        public void Notify_BreastElectrocuteModeChanged()
        {
            foreach (Pawn pawn in Bed.OwnersForReading)
            {
                if (this.BreastElectrocuteMode == ElectrocuteModes.Removed || !Genital_Helper.has_genitals(pawn))
                {
                    if (pawn.health.TryRemoveHediffOfDef(OnaholeDefOf.OnaholeBreastElectrodes)) { pawn.Drawer.renderer.SetAllGraphicsDirty(); }
                }
                else
                {
                    if (pawn.health.TryAddHediffOfDef(OnaholeDefOf.OnaholeBreastElectrodes)) { pawn.Drawer.renderer.SetAllGraphicsDirty(); }
                }
                foreach (HediffComp_Electrocute hediffComp_Electrocute in pawn.health.hediffSet.GetHediffComps<HediffComp_Electrocute>())
                {
                    if (hediffComp_Electrocute.parent.def == OnaholeDefOf.OnaholeBreastElectrodes)
                    {
                        hediffComp_Electrocute.parent.Severity = (Bed.GetComp<CompModularPower>()?.PowerOn == true ? Mathf.Max((int)this.BreastElectrocuteMode - 1, 0f) : 0f) + 0.5f;
                    }
                }
            }
        }
        public void Notify_GenitalElectrocuteModeChanged()
        {
            foreach (Pawn pawn in Bed.OwnersForReading)
            {
                if (this.GenitalElectrocuteMode == ElectrocuteModes.Removed || !Genital_Helper.has_genitals(pawn))
                {
                    if (pawn.health.TryRemoveHediffOfDef(OnaholeDefOf.OnaholeGenitalElectrodes)) { pawn.Drawer.renderer.SetAllGraphicsDirty(); }
                }
                else
                {
                    if (pawn.health.TryAddHediffOfDef(OnaholeDefOf.OnaholeGenitalElectrodes)) { pawn.Drawer.renderer.SetAllGraphicsDirty(); }
                }
                foreach (HediffComp_Electrocute hediffComp_Electrocute in pawn.health.hediffSet.GetHediffComps<HediffComp_Electrocute>())
                {
                    if (hediffComp_Electrocute.parent.def == OnaholeDefOf.OnaholeGenitalElectrodes)
                    {
                        hediffComp_Electrocute.parent.Severity = (Bed.GetComp<CompModularPower>()?.PowerOn == true ? Mathf.Max((int)this.GenitalElectrocuteMode - 1, 0f) : 0f) + 0.5f;
                    }
                }
            }
        }
    }
}


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

namespace RJW_Onahole.Comps
{
    public class CompProperties_ExtraGraphics : CompProperties
    {
        public GraphicData graphicData;
        public AltitudeLayer altitudeLayer;

        public CompProperties_ExtraGraphics()
        {
            compClass = typeof(CompExtraGraphics);
        }
    }
    public class CompExtraGraphics : ThingComp
    {
        public CompProperties_ExtraGraphics Props => (CompProperties_ExtraGraphics)props;
        private GraphicData graphicData;
        public override void PostSpawnSetup(bool respawningAfterLoad)
        {
            base.PostSpawnSetup(respawningAfterLoad);
            graphicData = new GraphicData();
            graphicData.CopyFrom(Props.graphicData);
            graphicData.color = (parent as Building).DrawColor;
        }
        public override void PostDraw()
        {
            base.PostDraw();
            graphicData.Graphic.Draw(this.parent.DrawPos.WithY(this.Props.altitudeLayer.AltitudeFor()), Rot4.North, parent);
        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using RJW_Onahole.Interfaces;
using RJW_Onahole.UI;
using RJW_Onahole.Utilities;
using UnityEngine;
using Verse;
using rjw;
namespace RJW_Onahole.Comps
{
    [StaticConstructorOnStartup]
    public class CompFeeder : CompConditionalBuildingOverlay, ICompAddsHediff, IModularPower
    {
        public override ResearchProjectDef RequiredResearch => DefDatabase<ResearchProjectDef>.GetNamed("Electricity", true);
        public List<HediffDef> GetHediffDef() => new List<HediffDef> { OnaholeDefOf.OnaholeAutoFeeder };
        public void Notify_PawnGraphicsChange() { }
        public float GetPowerConsumption()
        {
            if (this.Enabled && Bed.Pawn != null && Bed.Pawn.health.hediffSet.HasHediff(OnaholeDefOf.OnaholeAutoFeeder))
            {
                return 10f;
            }
            return 0f;
        }
        public void Notify_PowerStateChanged(bool state) { }
        public int TextureIndex { get => 0; set { } }
        public ThingDef FeedThingDef => feederRefuelable.FuelThingDef;
        private CompFeederRefuelable feederRefuelable;
        private Vector3 CenterPos { get { Vector3 center = this.parent.DrawPos + Props.graphicDatas[0].drawOffset + new Vector3(0f, 0f, 0.5f); center.y = Props.altitudeLayer.AltitudeFor(); return center; } }
        private Vector3 BarPos => CenterPos.WithY(this.parent.DrawPos.y + Props.graphicDatas[1].drawOffset.y + 0.02f) + new Vector3(0, 0, -0.15f);
        private Vector3 TexPos => BarPos + new Vector3(0, 0, 0.22f);
        private Vector3 FeederPortPos => CenterPos + new Vector3(0, 0, -0.32f);
        private Vector3 FeedingPipeSlackPos => CenterPos + new Vector3(0.21f, 0f, -1f);
        private Vector3 InletPos => CenterPos + new Vector3(0.2f, 0, -0.32f);
        private Vector3 InletSlackPos => CenterPos + new Vector3(0.3f, 0f, -2f);
        private Vector3 OutletPos => parent.DrawPos.WithY(CenterPos.y) + new Vector3(-0.04f, 0f, -0.47f);
        private GenDraw.FillableBarRequest request;
        public bool Enabled
        {
            get => _feederEnabled;
            set
            {
                _feederEnabled = value;
                if (_feederEnabled)
                {
                    foreach (Pawn pawn in Bed.OwnersForReading)
                    {
                        if (!Genital_Helper.has_mouth(pawn) || pawn?.needs?.food == null) { continue; }
                        pawn.health.TryAddHediffOfDef(OnaholeDefOf.OnaholeAutoFeeder);
                    }
                }
                else
                {
                    foreach (Pawn pawn in Bed.OwnersForReading)
                    {
                        pawn.health.TryRemoveHediffOfDef(OnaholeDefOf.OnaholeAutoFeeder);
                    }
                }
            }
        }
        public bool _feederEnabled = false;
        public void Notify_PawnBind(Pawn pawn)
        {
            if (this.Enabled && Genital_Helper.has_mouth(pawn) && pawn.needs.food != null)
            {
                pawn.health.TryAddHediffOfDef(OnaholeDefOf.OnaholeAutoFeeder);
            }
        }
        public void Notify_PawnUnbind(Pawn pawn)
        {
            pawn.health.TryRemoveHediffOfDef(OnaholeDefOf.OnaholeAutoFeeder);
        }
        public override void Initialize(CompProperties props)
        {
            base.Initialize(props);
            Props.graphicDatas[1].drawOffset.y = AltitudeLayer.PawnRope.AltitudeFor() - 0.1f - Props.altitudeLayer.AltitudeFor();
        }
        public override void PostSpawnSetup(bool respawningAfterLoad)
        {
            base.PostSpawnSetup(respawningAfterLoad);
            feederRefuelable = parent.GetComp<CompFeederRefuelable>();
        }
        public override void PostDraw()
        {
            base.PostDraw();
            if (!RJW_Onahole_Settings.showExtraGraphics) { return; }
            if (this.Enabled)
            {
                base.DrawGraphic(0);
                base.DrawGraphic(1);
                request = new GenDraw.FillableBarRequest
                {
                    center = BarPos,
                    size = new Vector2(0.25f, 0.07f),
                    fillPercent = feederRefuelable.FuelPercentOfTarget,
                    filledMat = feederRefuelable.BarMat(),
                    unfilledMat = SolidColorMaterials.SimpleSolidColorMaterial(Color.black),
                    margin = 0.04f,
                    rotation = Rot4.East,
                };
                GenDraw.DrawFillableBar(request);
                if (this.feederRefuelable.FuelThingDef?.uiIcon != null)
                {
                    Graphics.DrawMesh(MeshPool.plane10, Matrix4x4.TRS(TexPos, Quaternion.identity, new Vector3(0.2f, 1f, 0.2f)), CacheUtility.MatFor(this.feederRefuelable.FuelThingDef), -1);
                }
                if (Bed.Pawn != null && Bed.Pawn.RaceProps.Humanlike && Bed.Pawn.health.hediffSet.HasHediff(OnaholeDefOf.OnaholeAutoFeeder) && RJW_Onahole_Settings.showFeeder)
                {
                    Vector3 PawnMouthPos = PositionUtility.GetPosition(Bed.Pawn, OnaholeDefOf.OnaholeAutoFeeder) ?? Bed.Pawn.DrawPos;
                    DrawUtility.DrawPipe(FeederPortPos, FeedingPipeSlackPos, PawnMouthPos, 0.05f, Color.white, null, DrawUtility.pipeConnectorMat);
                }
                CompCumCollector cumCollector = Bed.TryGetComp<CompCumCollector>();
                if (cumCollector != null && cumCollector.enabled)
                {
                    DrawUtility.DrawPipe(InletPos, InletSlackPos + RJW_Onahole_Settings.DBG_Offset_1, OutletPos + RJW_Onahole_Settings.DBG_Offset_2, 0.03f, Color.white, null, DrawUtility.pipeConnectorMat);
                }
            }

        }
        public override void PostExposeData()
        {
            base.PostExposeData();
            Scribe_Values.Look(ref _feederEnabled, "feederEnabled", false);
        }
    }
}


code:
using System.Collections.Generic;
using RimWorld;
using RJW_Onahole.UI;
using RJW_Onahole.Utilities;
using UnityEngine;
using Verse;
namespace RJW_Onahole.Comps
{
    public class CompFeederRefuelable : CompIngestibleRefuelable
    {
        private CompFeeder compFeeder;
        public override void PostSpawnSetup(bool respawningAfterLoad)
        {
            base.PostSpawnSetup(respawningAfterLoad);
            defaultBarMat = CacheUtility.ColorMat.FeederDefault;
            compFeeder = parent.GetComp<CompFeeder>();
        }
        public override IEnumerable<ThingDef> AllowedFuel()
        {
            return CacheUtility.foodDefs;
        }

        public override bool Enabled()
        {
            return compFeeder.Enabled;
        }

        public override void SetEnabled(bool enabled)
        {
            compFeeder.Enabled = enabled;
        }

        public override Texture2D ToggleIcon()
        {
            return CacheUtility.Tex.IconFood;
        }
        public override string Title()
        {
            return Tr.AutoFeeder;
        }
        public override string Description()
        {
            return Tr.AutoFeeder_Desc;
        }
    }
}


code:
using System.Collections.Generic;
using RimWorld;
using RJW_Onahole.UI;
using RJW_Onahole.Utilities;
using UnityEngine;
using Verse;
namespace RJW_Onahole.Comps
{
    public class CompInfuserRefuelable : CompIngestibleRefuelable
    {
        private CompDrugInfuser compDrugInfuser;
        public override void PostSpawnSetup(bool respawningAfterLoad)
        {
            base.PostSpawnSetup(respawningAfterLoad);
            defaultBarMat = CacheUtility.ColorMat.InfuserDefault;
            compDrugInfuser = parent.GetComp<CompDrugInfuser>();
        }
        public override IEnumerable<ThingDef> AllowedFuel()
        {
            return CacheUtility.drugDefs;
        }

        public override bool Enabled()
        {
            return compDrugInfuser.Enabled;
        }

        public override void SetEnabled(bool enabled)
        {
            compDrugInfuser.Enabled = enabled;
        }

        public override Texture2D ToggleIcon()
        {
            return CacheUtility.Tex.IconDrug;
        }

        public override string Title()
        {
            return Tr.DrugInfuser;
        }
        public override string Description()
        {
            return Tr.DrugInfuser_Desc;
        }
    }
}


code:
using System.Collections.Generic;
using RimWorld;
using RJW_Onahole.UI;
using RJW_Onahole.Utilities;
using UnityEngine;
using Verse;
namespace RJW_Onahole.Comps
{
    public class CompProperties_IngestibleRefuelable : CompProperties_Refuelable
    {
        public override void PostLoadSpecial(ThingDef parent)
        {
            base.PostLoadSpecial(parent);
            this.initialConfigurableTargetFuelLevel = this.fuelCapacity;
            this.factorByDifficulty = false;
            this.fuelFilter = new ThingFilter();
            foreach (ThingDef thingDef in CacheUtility.foodDefs)
            {
                this.fuelFilter.SetAllow(thingDef, true);
            }
            this.destroyOnNoFuel = false;
            this.consumeFuelOnlyWhenUsed = true;
            this.consumeFuelOnlyWhenPowered = false;
            this.fuelConsumptionRate = 0f;
            this.initialFuelPercent = 0f;
            this.showFuelGizmo = true;
            this.initialAllowAutoRefuel = true;
            this.showAllowAutoRefuelToggle = true;
            this.allowRefuelIfNotEmpty = true;
            this.fuelIsMortarBarrel = false;
            this.targetFuelLevelConfigurable = true;
            this.drawOutOfFuelOverlay = false;
            this.drawFuelGaugeInMap = false;
            this.atomicFueling = false;
            this.fuelIconPath = "things/mote/thoughtsymbol/food";

        }
    }
    [StaticConstructorOnStartup]
    public abstract class CompIngestibleRefuelable : CompRefuelable
    {
        public abstract IEnumerable<ThingDef> AllowedFuel();
        protected Material defaultBarMat = SolidColorMaterials.SimpleSolidColorMaterial(Color.white);
        public Material BarMat() => this.defaultBarMat;
        public abstract bool Enabled();
        public abstract void SetEnabled(bool enabled);
        public abstract Texture2D ToggleIcon();
        public abstract string Title();
        public abstract string Description();
        public ThingDef FuelThingDef
        {
            get { return this.fuelThingDef; }
            set { this.ChangeThingDef(value); }
        }
        public bool allowAutoRefill = true;
        protected ThingDef fuelThingDef;
        public new CompProperties_IngestibleRefuelable Props => (CompProperties_IngestibleRefuelable)this.props;
        private float curFuel;
        private float targetLevel;
        public override void PostDestroy(DestroyMode mode, Map previousMap)
        {
            if (mode != DestroyMode.Vanish && previousMap != null && this.Props.fuelFilter.AllowedDefCount == 1 && this.Props.initialFuelPercent == 0f)
            {
                this.DropFuel(previousMap);
            }
        }
        public override void PostExposeData()
        {
            if (Scribe.mode == LoadSaveMode.Saving)
            {
                // Custom values to properly save and load with 2 refuelable comps on same thing
                this.targetLevel = this.TargetFuelLevel;
                this.curFuel = this.Fuel;
            }
            Scribe_Defs.Look(ref this.fuelThingDef, this.GetType().Name + "fuelThingDef");
            Scribe_Values.Look(ref this.allowAutoRefill, this.GetType().Name + "allowAutoRefill", Props.initialAllowAutoRefuel);
            Scribe_Values.Look(ref this.targetLevel, this.GetType().Name + "targetLevel", Props.initialConfigurableTargetFuelLevel);
            Scribe_Values.Look(ref this.curFuel, this.GetType().Name + "curFuel", 0f);
            if (Scribe.mode == LoadSaveMode.PostLoadInit)
            {
                // set parent fields using custom property
                base.TargetFuelLevel = this.targetLevel;
                Refuel(this.curFuel);
                // if def is null (mod removed), clear fuel
                if (this.fuelThingDef == null)
                {
                    this.ConsumeFuel(this.Fuel);
                }
                this.Notify_CapacityChanged();
            }
        }
        private void ChangeThingDef(ThingDef newDef)
        {
            if (this.fuelThingDef != newDef)
            {
                this.DropFuel();
                this.fuelThingDef = newDef;
            }
        }
        public void Notify_CapacityChanged()
        {
            if (this.Fuel > this.TargetFuelLevel)
            {
                this.DropFuel(null, Mathf.CeilToInt(this.Fuel - this.TargetFuelLevel));
            }
        }
        public override void CompTick() { } // no tick action
        public override void CompTickRare(){} // Hook point for Pipe network and others
        private void DropFuel(Map map = null, int num = 0)
        {
            if (this.fuelThingDef == null) { return; }
            if (map == null)
            {
                map = this.parent.Map;
            }
            float i = num == 0 ? Mathf.FloorToInt(this.Fuel) : num;
            this.ConsumeFuel(i);
            while (i > 0)
            {
                Thing thing = ThingMaker.MakeThing(fuelThingDef, null);
                thing.stackCount = Mathf.Min(Mathf.CeilToInt(i), fuelThingDef.stackLimit);
                i -= thing.stackCount;
                if (this.parent is Building_OnaholeBed building_OnaholeBed)
                {
                    building_OnaholeBed.TryStoreInBed(thing);
                }
                else
                {
                    GenPlace.TryPlaceThing(thing, this.parent.Position, map, ThingPlaceMode.Near, null, null);
                }
            }
        }
        public override string CompInspectStringExtra()
        {
            return string.Empty;
        }
        public override IEnumerable<Gizmo> CompGetGizmosExtra()
        {
            if (this.parent.Faction?.IsPlayer != true) { yield break; }
            yield return new Gizmo_IngestibleFuelStatus(this);
        }
    }
}


code:
using System;
using System.Collections.Generic;
using RimWorld;
using RJW_Onahole.UI;
using RJW_Onahole.Utilities;
using UnityEngine;
using Verse;
using rjw;
using RJW_Onahole.Hediffs;
using System.Linq;
using RJW_Onahole.Interfaces;
using RJW_Onahole.Data;
namespace RJW_Onahole.Comps
{
    [StaticConstructorOnStartup]
    public class CompMilkingMachine : CompConditionalBuildingOverlay, ICompAddsHediff, IModularPower
    {
        public override ResearchProjectDef RequiredResearch => DefDatabase<ResearchProjectDef>.GetNamed("Electricity", true);
        private static readonly ThingDef MilkDef = DefDatabase<ThingDef>.GetNamed("Milk");
        public void Notify_PawnGraphicsChange() { }
        public List<HediffDef> GetHediffDef() => new List<HediffDef> { OnaholeDefOf.OnaholeMilkingMachine };
        public void Notify_PowerStateChanged(bool state) { this.Notify_MilkingModeChanged(); }
        public float GetPowerConsumption()
        {
            float power = 0f;
            if (Bed.Pawn?.health.hediffSet.HasHediff(OnaholeDefOf.OnaholeMilkingMachine) == true)
            {
                switch (this.MilkingMode)
                {
                    case MilkingModes.Normal:
                        power = 5f;
                        break;
                    case MilkingModes.Fast:
                        power = 10f;
                        break;
                    case MilkingModes.VeryFast:
                        power = 15f;
                        break;
                }
            }
            return power;
        }
        public int TextureIndex { get => 0; set { } }
        private Vector3 CenterPos { get { Vector3 center = this.parent.DrawPos + Props.graphicDatas[0].drawOffset + new Vector3(0.175f, 0f, 0.12f); center.y = Props.altitudeLayer.AltitudeFor() - 0.1f; return center; } }
        private Vector3 CirclePos { get { Vector3 center = CenterPos + new Vector3(0, 0, -BARHEIGHT / 2); center.y = Props.altitudeLayer.AltitudeFor() - 0.05f; return center; } }
        private Rot4 Rotation => Rot4.East;
        private const float BARWIDTH = 0.28f;
        private const float BARHEIGHT = 0.4f;
        private Vector3 port1 = new Vector3(0.014f, 0f, -0.505f);
        private Vector3 port2 = new Vector3(0.118f, 0f, -0.505f);
        private Vector3 slack1 = new Vector3(0.32f, 0f, -0.79f);
        private Vector3 slack2 = new Vector3(0.57f, 0f, -0.79f);
        public MilkingModes MilkingMode = MilkingModes.Removed;
        private float overrideFps = 0f;
        public float MilkingProgress = 0f;
        public override void PostSpawnSetup(bool respawningAfterLoad)
        {
            base.PostSpawnSetup(respawningAfterLoad);
            this.Notify_MilkingModeChanged();
        }
        public void Notify_PawnBind(Pawn pawn)
        {
            if (this.MilkingMode != MilkingModes.Removed && pawn.CanBeMilked())
            {
                pawn.health.TryAddHediffOfDef(OnaholeDefOf.OnaholeMilkingMachine);
            }
            pawn.Drawer.renderer.SetAllGraphicsDirty();
        }
        public void Notify_PawnUnbind(Pawn pawn)
        {
            pawn.health.TryRemoveHediffOfDef(OnaholeDefOf.OnaholeMilkingMachine);
        }
        public override void PostDraw()
        {
            base.PostDraw();
            if (!RJW_Onahole_Settings.showExtraGraphics) { return; }
            switch (this.MilkingMode)
            {
                case UI.MilkingModes.Normal:
                    overrideFps = 12f;
                    break;
                case UI.MilkingModes.Fast:
                    overrideFps = 18f;
                    break;
                case UI.MilkingModes.VeryFast:
                    overrideFps = 24f;
                    break;
            }
            if (this.MilkingMode != MilkingModes.Removed)
            {
                DrawUtility.DrawFilledCircle(CirclePos + new Vector3(0, 0, BARHEIGHT * this.MilkingProgress), BARWIDTH / 2, DrawUtility.milkMat, new Vector3(1f, 1f, 0.3f));
                DrawUtility.DrawFillableBar(CenterPos, new Vector2(BARHEIGHT, BARWIDTH), this.MilkingProgress, Rotation, DrawUtility.whiteMat);

                // Draw Machine state
                if (this.MilkingMode == UI.MilkingModes.Off || Bed.GetComp<CompModularPower>()?.PowerOn == false)
                {
                    DrawGraphic(0);
                }
                else
                {
                    if (Bed.Pawn == null)
                    {
                        DrawGraphic(0);
                    }
                    else
                    {
                        DrawInstancedAnimation(OnaholeDefOf.MilkingMachine, overrideInterval: 1f / overrideFps);
                    }

                }
                if (Bed.Pawn?.RaceProps.Humanlike == true && Bed.Pawn?.health.hediffSet.HasHediff(OnaholeDefOf.OnaholeMilkingMachine) == true && RJW_Onahole_Settings.showMilkingMachine)
                {
                    DrawPipe(true);
                    DrawPipe(false);
                }
            }

        }
        private void DrawPipe(bool isWest)
        {
            Vector3 port = isWest ? port1 : port2;
            Vector3 slack = isWest ? slack1 : slack2;
            Vector3 p0 = CenterPos + port;
            Vector3 p1 = CenterPos + slack;
            Vector3 p2 = PositionUtility.GetPosition(Bed.Pawn, OnaholeDefOf.OnaholeMilkingMachine, isWest) ?? Bed.Pawn.DrawPos;
            DrawUtility.DrawPipe(p0, p1, p2, 0.03f, Color.white, DrawUtility.fluidPipeMat, DrawUtility.pipeConnectorMat);
        }
        protected override IEnumerable<Gizmo> GetGizmos()
        {
            yield return MilkingMachineMode();
        }
        public override void PostExposeData()
        {
            base.PostExposeData();
            Scribe_Values.Look(ref MilkingMode, "milkingMode", MilkingModes.Off);
            Scribe_Values.Look(ref MilkingProgress, "milkingProgress", 0f);
        }

        public void Notify_MilkingModeChanged()
        {
            foreach (Pawn pawn in Bed.OwnersForReading)
            {
                if (this.MilkingMode == MilkingModes.Removed || !pawn.CanBeMilked())
                {
                    pawn.health.TryRemoveHediffOfDef(OnaholeDefOf.OnaholeMilkingMachine);
                }
                else if (pawn.CanBeMilked())
                {
                    pawn.health.TryAddHediffOfDef(OnaholeDefOf.OnaholeMilkingMachine);
                }
                foreach (HediffComp_MilkingMachine hediffComp_MilkingMachine in pawn.health.hediffSet.GetAllComps().OfType<HediffComp_MilkingMachine>())
                {
                    hediffComp_MilkingMachine.parent.Severity = (Bed.GetComp<CompModularPower>()?.PowerOn == true ? Mathf.Max((int)this.MilkingMode - 1, 0f) : 0f) + 0.5f;
                }
            }
        }
        private MilkingGizmo MilkingMachineMode()
        {
            return new MilkingGizmo(this.MilkingMode, (x) => { this.MilkingMode = x; this.Notify_MilkingModeChanged(); }, new Func<float>(() => this.MilkingProgress));
        }

        public void PumpMilk(float amount, ThingDef milkDef = null)
        {
            MilkingProgress += amount;
            if (MilkingProgress >= 1f)
            {
                MilkingProgress = 0f;
                Thing thing = ThingMaker.MakeThing(milkDef ?? MilkDef);
                Bed.TryStoreInBed(thing);
            };
        }
    }
}


code:
using RimWorld;
using RJW_Onahole.Interfaces;
using UnityEngine;
using Verse;

namespace RJW_Onahole.Comps
{
    public class CompModularPower : CompPowerTrader
    {
        public override void SetUpPowerVars()
        {
            this.PowerOutput = -TotalPowerDraw;
        }
        private float TotalPowerDraw
        {
            get
            {
                float power = 0f;
                foreach (ThingComp comp in this.parent.AllComps)
                {
                    if (comp is IModularPower powerComp)
                    {
                        power += powerComp.GetPowerConsumption();
                    }
                }
                return power;
            }
        }
        public override void CompTickRare()
        {
            base.CompTickRare();
            this.PowerOutput = -TotalPowerDraw;
            if (this.PowerOutput >= 0f && !this.PowerOn)
            {
                this.PowerOn = true;
                return;
            }
            PowerStateChanged(this.PowerOn);
        }
        private void PowerStateChanged(bool state)
        {
            foreach (ThingComp comp in this.parent.AllComps)
            {
                if (comp is IModularPower powerComp)
                {
                    powerComp.Notify_PowerStateChanged(state);
                }
            }
        }
        public override void LostConnectParent()
        {
            this.PowerOn = this.PowerOutput >= 0f;
            this.connectParent = null;
            if (this.parent.Spawned)
            {
                this.parent.Map.powerNetManager.Notify_ConnectorWantsConnect(this);
            }
        }
    }
}


code:
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using rjw;
using RJW_Onahole.Data;
using RJW_Onahole.Utilities;
using UnityEngine;
using Verse;

namespace RJW_Onahole.Comps
{
    public class CompProperties_MultiAssignable : CompProperties_AssignableToPawn
    {
        public CompProperties_MultiAssignable()
        {
            compClass = typeof(CompMultiAssignable);
        }
        public override void PostLoadSpecial(ThingDef parent)
        {
            this.maxAssignedPawnsCount = 99999;
        }
    }
    public class CompMultiAssignable : CompAssignableToPawn
    {
        Building_OnaholeBed bed => (Building_OnaholeBed)parent;
        public override IEnumerable<Pawn> AssigningCandidates
        {
            get
            {
                if (!parent.Spawned)
                {
                    return Enumerable.Empty<Pawn>();
                }
                return parent.Map.mapPawns.PawnsInFaction(Faction.OfPlayer).Where((Pawn p) => !p.IsOnahole() &&
                ((p.RaceProps.Humanlike && bed.useRestriction.AllowsType(FactionType.Colonist, KindType.Humanlike)) ||
                (p.RaceProps.Animal && RJWSettings.bestiality_enabled && bed.useRestriction.AllowsType(FactionType.Colonist, KindType.Animal)) ||
                (p.RaceProps.IsMechanoid && RJWPregnancySettings.mechanoid_pregnancy_enabled && bed.useRestriction.AllowsType(FactionType.Colonist, KindType.Mechanoid))));
            }
        }

        protected override bool CanDrawOverlayForPawn(Pawn pawn)
        {
            return false;
        }
        protected override bool ShouldShowAssignmentGizmo()
        {
            return false;
        }
        protected override string GetAssignmentGizmoLabel()
        {
            return Tr.DesignateColonyUsers;
        }

        protected override string GetAssignmentGizmoDesc()
        {
            return Tr.DesignateColonyUsers_Desc;
        }
        public override void PostExposeData()
        {
            Scribe_Collections.Look(ref assignedPawns, "multiAssignedPawns", LookMode.Reference);
            Scribe_Collections.Look(ref uninstalledAssignedPawns, "multiUninstalledAssignedPawns", LookMode.Reference);
            if (Scribe.mode == LoadSaveMode.PostLoadInit)
            {
                assignedPawns.RemoveAll((Pawn x) => x == null);
                uninstalledAssignedPawns.RemoveAll((Pawn x) => x == null);
            }
        }

        public override void PostDeSpawn(Map map)
        {
            for (int num = assignedPawns.Count - 1; num >= 0; num--)
            {
                TryUnassignPawn(assignedPawns[num], sort: false, !parent.DestroyedOrNull());
            }
        }

        public override void PostSpawnSetup(bool respawningAfterLoad)
        {
            base.PostSpawnSetup(respawningAfterLoad);
            for (int num = uninstalledAssignedPawns.Count - 1; num >= 0; num--)
            {
                Pawn pawn = uninstalledAssignedPawns[num];
                if (CanSetUninstallAssignedPawn(pawn))
                {
                    TryAssignPawn(pawn);
                }
            }

            uninstalledAssignedPawns.Clear();
        }

        public bool AssignedOrFreeToUse(Pawn pawn)
        {
            return assignedPawns.Contains(pawn) || assignedPawns.Count() == 0;
        }
    }
}


code:
using System.Collections.Generic;
using RimWorld;
using Verse;
using rjw;
using RJW_Onahole.Utilities;
using Verse.AI;
using RJW_Onahole.Interfaces;
using System.Linq;

namespace RJW_Onahole.Comps
{
    public class CompOnaholeBed : ThingComp
    {
        private Building_OnaholeBed bed;
        public override void Initialize(CompProperties props)
        {
            base.Initialize(props);
        }
        public override void PostSpawnSetup(bool respawningAfterLoad)
        {
            base.PostSpawnSetup(respawningAfterLoad);
            bed = (Building_OnaholeBed)parent;
            if (bed.Pawn != null)
            {
                Notify_PawnBind(bed.Pawn);
            }
        }
        public override void CompTickRare()
        {
            base.CompTickRare();
            // Add Deathrest
            if (bed.BoundPawn?.needs?.TryGetNeed<Need_Deathrest>() != null && !bed.OccupiedRect().First().InSunlight(bed.Map))
            {
                bed.Pawn.health.TryAddHediffOfDef(HediffDefOf.Deathrest);
            }
        }
        public void Notify_PawnBind(Pawn pawn)
        {
            if (pawn == null)
            {
                return;
            }
            if (!pawn.IsPlayerPawn())
            {
                pawn.guest.CapturedBy(Faction.OfPlayer);
            }
            if (pawn.ownership.OwnedBed != bed)
            {
                if (pawn.IsFreeNonSlaveColonist) { bed.ForOwnerType = BedOwnerType.Colonist; }
                if (pawn.IsPrisonerOfColony) { bed.ForOwnerType = BedOwnerType.Prisoner; }
                if (ModsConfig.IdeologyActive && pawn.IsSlaveOfColony) { bed.ForOwnerType = BedOwnerType.Slave; }
                pawn.ownership.ClaimBedIfNonMedical(bed);
            }
            pawn.health.AddHediff(OnaholeDefOf.OnaholeBond);
            if (pawn.needs.TryGetNeed<Need_Deathrest>() != null)
            {
                pawn.health.AddHediff(HediffDefOf.Deathrest);
            }
            pawn.Rotation = Rot4.South;
            if (pawn.CurJob?.def != OnaholeDefOf.BeOnahole)
            {
                pawn.jobs.StartJob(JobMaker.MakeJob(OnaholeDefOf.BeOnahole, null, bed), JobCondition.InterruptForced, null, false, true, null, null, false, false, null, true, true);
            }
            foreach (ICompAddsHediff comp in bed.GetComps<ICompAddsHediff>())
            {
                comp.Notify_PawnBind(pawn);
            }
            pawn.Drawer.renderer.SetAllGraphicsDirty();
        }
        public void Notify_PawnUnbind(Pawn pawn)
        {
            pawn.ownership.UnclaimBed();
            pawn.health.TryRemoveHediffOfDef(HediffDefOf.Deathrest);
            CompAssignableToPawn compAssignableToPawn = bed.GetComp<CompAssignableToPawn>();
            if (compAssignableToPawn != null)
            {
                compAssignableToPawn.ForceRemovePawn(pawn);
            }
            foreach (ICompAddsHediff comp in bed.GetComps<ICompAddsHediff>())
            {
                comp.Notify_PawnUnbind(pawn);
            }
        }
    }
}


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

namespace RJW_Onahole.Comps
{
    public class CompProperties_OnaholeChains : CompProperties
    {
        public List<Vector3> starts = new List<Vector3>();
        public List<Vector3> ends = new List<Vector3>();
        public float width;
        public float offset;
        public float endSize;
        public float endOffset;
        public DrawUtility.MaterialType chainMatType;
        public string texPath;
        public DrawUtility.MaterialType connectionMatType;
        public bool symmetric;

        public CompProperties_OnaholeChains()
        {
            compClass = typeof(CompOnaholeChains);
        }
    }
    [StaticConstructorOnStartup]
    public class CompOnaholeChains : ThingComp
    {
        public CompProperties_OnaholeChains Props => (CompProperties_OnaholeChains)props;
        public static Material ChainMat;
        public static Material ConnectionMat;
        public override void Initialize(CompProperties props)
        {
            base.Initialize(props);
        }
        public override void PostSpawnSetup(bool respawningAfterLoad)
        {
            base.PostSpawnSetup(respawningAfterLoad);
            if (Props.starts.Count != Props.ends.Count)
            {
                "Onahole chains starts and ends count mismatch".LogError();
                return;
            }
            ChainMat = Props.chainMatType.GetMaterial();
            ConnectionMat = Props.chainMatType.GetMaterial();
            if (ChainMat == null)
            {
                ("Onahole chains material not found:" + Props.chainMatType).LogError();
            }
        }
        public override void PostDraw()
        {
            base.PostDraw();
            Pawn pawn = ((Building_OnaholeBed)parent)?.BoundPawn;
            if (ChainMat == null || pawn == null)
            {
                return;
            }
            for (int i = 0; i < Props.starts.Count; i++)
            {
                float length = (Props.starts[i]-Props.ends[i]).magnitude;
                Vector3 start = parent.DrawPos + Props.starts[i];
                Vector3 end = parent.DrawPos + PositionUtility.CurAnimationTransform(pawn).MultiplyPoint3x4(Props.ends[i]);
                DrawChain(start, end, length);
                if (Props.symmetric && !(Props.starts[i].x == 0 && Props.ends[i].x == 0))
                {
                    Vector3 start2 = parent.DrawPos + new Vector3(-Props.starts[i].x, Props.starts[i].y, Props.starts[i].z);
                    Vector3 end2 = parent.DrawPos + PositionUtility.CurAnimationTransform(pawn).MultiplyPoint3x4(new Vector3(-Props.ends[i].x, Props.ends[i].y, Props.ends[i].z));
                    DrawChain(start2, end2, length);
                }
            }
        }
        private void DrawChain(Vector3 start, Vector3 end, float? lengthOverride)
        {
            start.y += Props.offset;
            end.y += Props.offset;
            DrawUtility.DrawLine(start, end, ChainMat, Props.width, lengthOverride);
            if (ConnectionMat != null)
            {
                Graphics.DrawMesh(MeshPool.plane10, Matrix4x4.TRS(new Vector3(0, Props.endOffset, 0) + start, Quaternion.identity, new Vector3(Props.endSize, 1, Props.endSize)), ConnectionMat, 0);
                Graphics.DrawMesh(MeshPool.plane10, Matrix4x4.TRS(new Vector3(0, Props.endOffset, 0) + end, Quaternion.identity, new Vector3(Props.endSize, 1, Props.endSize)), ConnectionMat, 0);
            }
        }
    }
}


code:
using System.Collections.Generic;
using RimWorld;
using Verse;
using rjw;
using RJW_Onahole.Utilities;
using Verse.AI;
using RJW_Onahole;
using UnityEngine;
using RJW_Onahole.Data;

namespace RJW_Onahole.Comps
{
    public class CompProperties_PawnTransform : CompProperties
    {
        public CompProperties_PawnTransform()
        {
            compClass = typeof(CompPawnTransform);
        }
    }
    public class CompPawnTransform : ThingComp
    {
        public TransformCollection transformCollection;
        public override void PostExposeData()
        {
            base.PostExposeData();
            Scribe_Deep.Look(ref transformCollection, "transformCollection");
        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using rjw;
using RJW_Onahole.Data;
using RJW_Onahole.HarmonyPatches;
using RJW_Onahole.Hediffs;
using RJW_Onahole.Jobs;
using RJW_Onahole.UI;
using RJW_Onahole.Utilities;
using UnityEngine;
using Verse;
namespace RJW_Onahole.Comps
{
    public class CompVaginaCrossSection : CompCrossSectionBase
    {
        public override int DrawMatchedGraphic()
        {
            int i = -1;
            if (!Genital_Helper.has_vagina(Bed.Pawn)) { return i; }
            Pawn partner = Bed.Pawn?.OnaholeUser();
            SexProps partnerSexProps = (partner?.jobs.curDriver as JobDriver_UseOnahole)?.Sexprops;
            bool isInPosition = partner != null && partner.Position == Bed.Pawn.Position;
            bool isReverse = partnerSexProps?.dictionaryKey.defName.Contains("Reverse") ?? false;
            if (partnerSexProps?.sexType == xxx.rjwSextype.Vaginal && isInPosition && !isReverse)
            {
                // Sex
                i = DrawVaginalSex();
            }
            else if (partnerSexProps?.sexType == xxx.rjwSextype.Fingering && isInPosition && partner.RaceProps.Humanlike)
            {
                // Fingering
                i = DrawFingering();
            }
            else if (partnerSexProps?.sexType == xxx.rjwSextype.Cunnilingus && isInPosition && isReverse)
            {
                // Cunnilingus
                i = DrawCunnilingus();
            }
            else if (partnerSexProps?.sexType == xxx.rjwSextype.Fisting && isInPosition)
            {
                // Fisting
                i = DrawFisting();
            }
            else
            {
                // Machine
                Hediff hediff = null;
                Bed.Pawn?.health.hediffSet.TryGetHediff(OnaholeDefOf.OnaholeVaginalSexMachine, out hediff);
                if (hediff != null)
                {
                    HediffComp_SexMachines sexMachine = hediff.TryGetComp<HediffComp_SexMachines>();
                    if (sexMachine != null)
                    {
                        i = PositionUtility.CalcCurFrame(sexMachine.startTime, 16, 16);
                    }
                    i = DrawInstancedAnimation(OnaholeDefOf.PistonCrossSection, index: i);
                }
            }
            // Pregnancy
            i = DrawPregnancy(i);
            return i;
        }
        public override void DrawComponents(int i)
        {
            DrawGraphic(0);
            DrawInstancedAnimation(OnaholeDefOf.VaginaCrossSection, index: i);
        }
        protected virtual int DrawVaginalSex()
        {
            // Try sync with animation
            int i = TrySyncWithPawnAnimation(Bed.Pawn.OnaholeUser(), OnaholeDefOf.PenisCrossSection.totalFrames);
            if (Bed.Pawn.RaceProps.Insect)
            {
                i = DrawInstancedAnimation(OnaholeDefOf.InsectPenisCrossSection, index: i);
            }
            else if (Bed.Pawn.RaceProps.IsMechanoid)
            {
                i = DrawInstancedAnimation(OnaholeDefOf.MechPenisCrossSection, index: i);
            }
            else
            {
                i = DrawInstancedAnimation(OnaholeDefOf.PenisCrossSection, index: i);
            }
            return i;
        }
        protected virtual int DrawFingering()
        {
            DrawAnimatedTextureGroup("Finger", -1, Bed.Pawn.OnaholeUser().story.SkinColor);
            return 0;
        }
        protected virtual int DrawCunnilingus()
        {
            DrawAnimatedTextureGroup("Tongue", -1);
            return 0;
        }
        protected virtual int DrawFisting()
        {
            int i = TrySyncWithPawnAnimation(Bed.Pawn.OnaholeUser(), OnaholeDefOf.PenisCrossSection.totalFrames);
            i = DrawInstancedAnimation(OnaholeDefOf.FistingCrossSection, index: i, color: Bed.Pawn.OnaholeUser().story.SkinColor);
            return i;
        }
        protected virtual int DrawPregnancy(int i)
        {
            List<Hediff> pregnancies = Bed.Pawn.GetPregnancies();
            float progress = 0f;
            Hediff pregnancy = null;
            foreach (Hediff hediff in pregnancies)
            {
                if (!hediff.Visible) { continue; }
                float pregnancyProgress = hediff.GetPregnancyProgress();
                if (pregnancyProgress > progress)
                {
                    progress = Mathf.Max(pregnancyProgress, progress);
                    pregnancy = hediff;
                }
            }
            if (progress > 0.66f)
            {
                DrawGraphic(3);
                DrawBaby(pregnancy);
            }
            else if (progress > 0.33f)
            {
                DrawGraphic(2);
                DrawBaby(pregnancy);
            }
            else if (i >= 0)
            {
                DrawGraphic(1);
            }
            return progress > 0.33f ? Mathf.Max(0, i) : i;
        }
        protected virtual void DrawBaby(Hediff pregnancy)
        {
            int stage = pregnancy.GetPregnancyProgress() > 0.66f ? 1 : 0;
            if (pregnancy is Hediff_InsectEgg || (pregnancy is Hediff_BasePregnancy basePregnancy && basePregnancy.babies?.Any(x => x.RaceProps.Insect) == true))
            {
                DrawGraphic(6 + stage);
                return;
            }
            if (pregnancy is Hediff_MechanoidPregnancy)
            {
                DrawGraphic(8 + stage);
                return;
            }
            DrawGraphic(4 + stage);
        }
    }
}


code:
using System.Collections.Generic;
using RimWorld;
using Verse;
using rjw;
using RJW_Onahole.Utilities;
using Verse.AI;
using RJW_Onahole.Interfaces;
using UnityEngine;

namespace RJW_Onahole.Comps
{
    public class CompVaginalSexMachine : CompOnaholeBase, ICompAddsHediff, IModularPower
    {
        public override ResearchProjectDef RequiredResearch => DefDatabase<ResearchProjectDef>.GetNamed("Electricity", true);
        public List<HediffDef> GetHediffDef() => new List<HediffDef> { OnaholeDefOf.OnaholeVaginalSexMachine };
        public void Notify_PawnGraphicsChange() { }
        public void Notify_PowerStateChanged(bool state) { }
        public float GetPowerConsumption()
        {
            if (this.Enabled && Bed.Pawn != null)
            {
                return 10f;
            }
            return 0f;
        }
        public int TextureIndex { get => 0; set { } }
        public bool Enabled
        {
            get => _vaginalSexMachineEnabled;
            set
            {
                _vaginalSexMachineEnabled = value;
                if (value)
                {
                    foreach (Pawn pawn in Bed.OwnersForReading)
                    {
                        if (Genital_Helper.has_vagina(pawn))
                        {
                            pawn.health.TryAddHediffOfDef(OnaholeDefOf.OnaholeVaginalSexMachine);
                        }
                    }
                }
                else
                {
                    foreach (Pawn pawn in Bed.OwnersForReading)
                    {
                        pawn.health.TryRemoveHediffOfDef(OnaholeDefOf.OnaholeVaginalSexMachine);
                    }
                }
            }
        }
        private bool _vaginalSexMachineEnabled = false;
        public override void PostExposeData()
        {
            base.PostExposeData();
            Scribe_Values.Look(ref _vaginalSexMachineEnabled, "vaginalSexMachineEnabled", false);
        }
        public void Notify_PawnBind(Pawn pawn)
        {
            if (Enabled && Genital_Helper.has_vagina(pawn))
            {
                pawn.health.TryAddHediffOfDef(OnaholeDefOf.OnaholeVaginalSexMachine);
            }
        }
        public void Notify_PawnUnbind(Pawn pawn)
        {
            pawn.health.TryRemoveHediffOfDef(OnaholeDefOf.OnaholeVaginalSexMachine);
        }
        protected override IEnumerable<Gizmo> GetGizmos()
        {
            yield return ToggleVaginalSexMachine();
        }

        private Command_Toggle ToggleVaginalSexMachine()
        {
            return new Command_Toggle
            {
                defaultLabel = Tr.VaginalSexMachine,
                defaultDesc = Tr.VaginalSexMachine_Desc,
                isActive = () => this.Enabled,
                icon = CacheUtility.Tex.IconVaginalSexMachine,
                toggleAction = delegate
                {
                    this.Enabled = !this.Enabled;
                }
            };
        }

    }
}


code:
using System.Collections.Generic;
using RJW_Onahole.Utilities;
using UnityEngine;
using Verse;

namespace RJW_Onahole.Data
{
    public enum AnimateMode
    {
        Off,
        Loop,
        BackAndForth,
        Random
    }

    public class AnimatedTextureGroupData
    {
        public string label;
        public AnimateMode animateMode;
        public AltitudeLayer? altitudeLayer;
        public Vector3 groupOffset;
        public float fps = 5;
        public float Interval => 1f / fps;
        public List<GraphicData> animatedTextures;
    }
    public class AnimatedTextureGroup
    {
        public AnimatedTextureGroupData animatedTextureGroupData;
        private int cursor = 1;
        private int _index = 0;
        private float lastUpdateTime = 0;
        public float overrideFps = -1f;
        public float overrideInterval => overrideFps == -1f ? animatedTextureGroupData.Interval : 1f / overrideFps;
        public AnimatedTextureGroup(AnimatedTextureGroupData animatedTextureGroupData)
        {
            this.animatedTextureGroupData = animatedTextureGroupData;
        }
        public int CurIndex
        {
            get
            {
                if (animatedTextureGroupData.animateMode == AnimateMode.Off || Find.TickManager.Paused )
                {
                    return _index;
                }
                if ((overrideInterval <= 0 && Time.time - lastUpdateTime >= animatedTextureGroupData.Interval) || (overrideInterval > 0 && Time.time - lastUpdateTime >= overrideInterval))
                {
                    lastUpdateTime = Time.time;
                    if (animatedTextureGroupData.animateMode == AnimateMode.Loop)
                    {
                        _index = (_index + 1) % animatedTextureGroupData.animatedTextures.Count;
                    }
                    else if (animatedTextureGroupData.animateMode == AnimateMode.Random)
                    {
                        _index = Rand.Range(0, animatedTextureGroupData.animatedTextures.Count);
                    }
                    else if (animatedTextureGroupData.animateMode == AnimateMode.BackAndForth)
                    {
                        _index += cursor;
                        if (_index <= 0 || _index >= animatedTextureGroupData.animatedTextures.Count - 1)
                        {
                            cursor *= -1;
                        }
                    }
                }
                return _index;
            }
        }
        public GraphicData CurGraphic
        {
            get
            {
                return animatedTextureGroupData.animatedTextures[CurIndex];
            }
        }
        public GraphicData GetGraphic(int i = -1)
        {
            if (i == -1)
            {
                i = CurIndex;
            }
            return animatedTextureGroupData.animatedTextures[i];
        }
        public void DrawAt(Vector3 pos, Thing thing, int i = -1, Color? color = null)
        {
            if (i == -1)
            {
                i = CurIndex;
            }
            Graphic graphic = GetGraphic(i).Graphic;
            DrawGraphicAt(pos, thing, graphic, color);
        }
        public void DrawGraphicAt(Vector3 pos, Thing thing, Graphic graphic, Color? color = null)
        {
            if (color != null)
            {
                graphic = graphic.GetColoredVersion(graphic.Shader, color.Value, color.Value);
            }
            graphic.Draw(pos.WithY(animatedTextureGroupData.altitudeLayer == null ? pos.y : animatedTextureGroupData.altitudeLayer.Value.AltitudeFor()) + animatedTextureGroupData.groupOffset, Rot4.North, thing);
        }
    }
    public class AnimatedTextureGroupCollection
    {
        public Dictionary<string, AnimatedTextureGroup> LabelsToAnimatedTextureGroups;
        public AnimatedTextureGroupCollection(List<AnimatedTextureGroupData> animatedTextureGroupDatas)
        {
            LabelsToAnimatedTextureGroups = new Dictionary<string, AnimatedTextureGroup>();
            if (animatedTextureGroupDatas == null)
            {
                return;
            }
            foreach (AnimatedTextureGroupData animatedTextureGroupData in animatedTextureGroupDatas)
            {
                AnimatedTextureGroup animatedTextureGroup = new AnimatedTextureGroup(animatedTextureGroupData);
                LabelsToAnimatedTextureGroups[animatedTextureGroupData.label] = animatedTextureGroup;
            }

        }

        public AnimatedTextureGroup GetGroup(string label)
        {
            if (LabelsToAnimatedTextureGroups.ContainsKey(label))
            {
                return LabelsToAnimatedTextureGroups[label];
            }
            return null;
        }
        public void DrawGroup(string label, Vector3 pos, Thing thing, int i = -1, Color? color = null)
        {
            GetGroup(label)?.DrawAt(pos, thing, i, color);
        }
    }
}


code:
using System;
using UnityEngine;
using Verse;

namespace RJW_Onahole.Data
{
    public class AnimationData
    {
        public int frame;
        public int totalFrames;
        public float interval;
        public int cursor = 1;
        public float lastFrameTime;
        public Color color;
        public AnimateMode mode;
        private Action<float> update;

        public AnimationData(AnimateMode mode, int totalFrames,float interval, float curTime = 0, int startFrame = 0, Color? color = null)
        {
            this.frame = startFrame;
            this.interval = interval;
            this.totalFrames = totalFrames;
            this.lastFrameTime = curTime;
            this.color = color ?? Color.white;
            switch (mode)
            {
                case AnimateMode.Random:
                    this.mode = AnimateMode.Random;
                    update = UpdateRandom;
                    break;
                case AnimateMode.BackAndForth:
                    this.mode = AnimateMode.BackAndForth;
                    update = UpdateBackAndForth;
                    break;
                default:
                    this.mode = AnimateMode.Loop;
                    update = UpdateLoop;
                    break;
            }
        }
        public AnimationData Update(float curTime)
        {
            this.update.Invoke(curTime);
            return this;
        }
        private void UpdateLoop(float curTime)
        {
            if (curTime - lastFrameTime > interval)
            {
                lastFrameTime = curTime;
                frame++;
                if (frame >= totalFrames)
                {
                    frame = 0;
                }
            }
        }
        private void UpdateRandom(float curTime)
        {
            if (curTime - lastFrameTime > interval)
            {
                lastFrameTime = curTime;
                frame = new IntRange(0, totalFrames - 1).RandomInRange;
            }
        }
        private void UpdateBackAndForth(float curTime)
        {
            if (curTime - lastFrameTime > interval)
            {
                lastFrameTime = curTime;
                if (frame <=0 || frame >= totalFrames - 1)
                {
                    cursor *= -1;
                }
                frame += cursor;
            }
        }

    }
}


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

namespace RJW_Onahole.Data
{
    /// <summary>
    /// Maps filth to sex fluid
    /// </summary>
    public class FilthToSexFluidDef : Def
    {
        public SexFluidDef sexFluid;
    }
}


code:
using System.Linq;
using RimWorld;
using rjw;
using RJW_Onahole.Utilities;
using UnityEngine;
using Verse;

namespace RJW_Onahole.Data
{
    public class FluidSetting : IExposable
    {
        public bool AllowFeed
        {
            get
            {
                if (_sexFluid != null)
                {
                    _allowFeed = _allowFeed && CanFeed();
                }
                return _allowFeed;
            }
            set
            {
                _allowFeed = value && CanFeed();
                if (value)
                {
                    Destroy = false;
                }
            }
        }
        public bool AutoConvert
        {
            get
            {
                _autoConvert = _autoConvert && CanAutoConvert();
                return _autoConvert;
            }
            set
            {
                _autoConvert = value && CanAutoConvert();
                if (value)
                {
                    Destroy = false;
                }
            }
        }
        public bool Destroy
        {
            get => _destroy;
            set
            {
                _destroy = value;
                if (value)
                {
                    AllowFeed = false;
                    AutoConvert = false;
                }
            }
        }
        private bool _allowFeed;
        private bool _autoConvert;
        private bool _destroy;
        private bool isInitialized;
        private SexFluidDef _sexFluid;
        public SexFluidDef SexFluid => _sexFluid;
        public ThingDef FilthDef => _sexFluid?.filth;
        public const string UnitStr = "mL";
        public float NutritionPerUnit => SexFluid?.consumable?.ingestible?.CachedNutrition * SexFluid.consumableFluidRatio ?? SexFluid.baseNutrition;
        public void ExposeData()
        {
            Scribe_Values.Look(ref _allowFeed, "allowFeed", false);
            Scribe_Values.Look(ref _autoConvert, "autoConvert", false);
            Scribe_Values.Look(ref _destroy, "destroy", false);
            Scribe_Defs.Look(ref _sexFluid, "sexFluidDef");
            isInitialized = true;
        }
        public FluidSetting()
        {
            InitializeDefaultOption();
        }
        public FluidSetting(SexFluidDef sexFluidDef)
        {
            _sexFluid = sexFluidDef;
            InitializeDefaultOption();
        }
        private void InitializeDefaultOption()
        {
            if (!CanFeed() && !CanAutoConvert())
            {
                Destroy = true;
                return;
            }
            if (SexFluid?.consumable == null)
            {
                // Disable auto convert if there is no conversion
                _autoConvert = false;
                // Disable feed if no default nutrition value
                AllowFeed = (AllowFeed || !isInitialized) && CanFeed();
                return;
            }
            AutoConvert = (AutoConvert || !isInitialized) && CanAutoConvert();
            if (!AutoConvert)
            {
                AllowFeed = (AllowFeed || !isInitialized) && CanFeed();
            }
        }
        public void CopyFrom(FluidSetting other)
        {
            _sexFluid = other.SexFluid;
            AllowFeed = other.AllowFeed;
            AutoConvert = other.AutoConvert;
            Destroy = other.Destroy;
        }
        public bool CanFeed() => SexFluid.CanFeed();
        public bool CanAutoConvert() => SexFluid.CanAutoConvert();
        public int ConvertFromFluid(ref float fluidAmount)
        {
            int numConverted = Mathf.FloorToInt(fluidAmount * SexFluid.consumableFluidRatio);
            fluidAmount -= numConverted / SexFluid.consumableFluidRatio;
            return numConverted;
        }
        public string AutoConvertTipString()
        {
            return CanAutoConvert() ? Tr.AutoConvert + "\n" + (1f / SexFluid.consumableFluidRatio).ToStringByStyle(ToStringStyle.FloatMaxThree) + UnitStr + "=" + SexFluid.consumable.LabelCap.ToString() + "x1" : "";
        }
        public string FeedTipString()
        {
            return Tr.Feed + "\n" + OTr.Nutrition + ":" + NutritionPerUnit.ToStringByStyle(ToStringStyle.FloatMaxThree) + "/" + UnitStr;
        }
    }
}


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

namespace RJW_Onahole.Data
{
    public class GuestSilverReservationMapComponent : MapComponent
    {
        public Dictionary<Pawn, int> PawnsToReservedAmounts = new Dictionary<Pawn, int>();
        // Prepare separate lists for keys and values
        private List<Pawn> PawnsToReservedAmounts_keys = new List<Pawn>();
        private List<int> PawnsToReservedAmounts_values = new List<int>();

        public GuestSilverReservationMapComponent(Map map) : base(map)
        {
        }

        public override void ExposeData()
        {
            base.ExposeData();
            // Save the dictionary data if we're saving
            if (Scribe.mode == LoadSaveMode.Saving && PawnsToReservedAmounts != null)
            {
                PawnsToReservedAmounts_keys.Clear();
                PawnsToReservedAmounts_values.Clear();
                foreach (KeyValuePair<Pawn, int> kvp in PawnsToReservedAmounts)
                {
                    PawnsToReservedAmounts_keys.Add(kvp.Key);
                    PawnsToReservedAmounts_values.Add(kvp.Value);
                }
            }

            // Use Scribe_Collections.Look to handle the data
            Scribe_Collections.Look(ref PawnsToReservedAmounts_keys, "PawnsToReservedAmounts_keys", LookMode.Reference);
            Scribe_Collections.Look(ref PawnsToReservedAmounts_values, "PawnsToReservedAmounts_values", LookMode.Value);

            // Reconstruct the dictionary if we're loading
            if (Scribe.mode == LoadSaveMode.PostLoadInit)
            {
                for (int i = 0; i < PawnsToReservedAmounts_keys.Count; i++)
                {
                    if (PawnsToReservedAmounts_keys[i] == null) continue;
                    try
                    {
                        PawnsToReservedAmounts[PawnsToReservedAmounts_keys[i]] = PawnsToReservedAmounts_values[i];
                    }
                    catch (System.Exception e)
                    {
                        Log.Warning("RJW_Onahole: " + e);
                    }
                }
                CleanUp();
            }
        }
        public void CleanUp()
        {
            List<Pawn> pawnsToRemove = new List<Pawn>();
            foreach (Pawn pawn in PawnsToReservedAmounts.Keys)
            {
                if (pawn.Map != map)
                {
                    pawnsToRemove.Add(pawn);
                }
            }
            foreach (Pawn pawn in pawnsToRemove)
            {
                PawnsToReservedAmounts.Remove(pawn);
            }
        }
    }
}


code:
using System;
using System.Collections.Generic;
using RimWorld;
using Verse;
using UnityEngine;
using RJW_Onahole.Render;

namespace RJW_Onahole.Data
{
    [StaticConstructorOnStartup]
    public class InstancedAnimationDef : Def
    {
        public string texPath;
        public int totalFrames;
        public float framePerSecond;
        public AnimateMode animateMode;
        public ShaderTypeDef shaderTypeDef;
        public Vector3 drawOffset;
        public Vector2 drawSize = Vector2.one;
        public List<Vector2> framePositions;
        public Type controllerClass;
        public Type ControllerClass => controllerClass ?? typeof(InstancedAnimationController);
        [Unsaved]
        private Texture2D tex;
        public Texture2D Tex => tex ?? (tex = ContentFinder<Texture2D>.Get(texPath));
        public ShaderTypeDef ShaderTypeDef => shaderTypeDef ?? OnaholeDefOf.InstancedAnimation;

        private InstancedAnimationController controller;
        public InstancedAnimationController Controller
        {
            get
            {
                if (controller == null)
                {
                    controller = (InstancedAnimationController)Activator.CreateInstance(ControllerClass, this);
                }
                return controller;
            }
        }
        [Unsaved]
        private Material mat;
        public Material Mat
        {
            get
            {
                if (mat == null)
                {
                    mat = new Material(ShaderTypeDef.Shader);
                    mat.mainTexture = Tex;
                    mat.SetFloat("_GridSize", CalculateGridSize(totalFrames));
                    mat.enableInstancing = true;
                }
                return mat;
            }
        }
        protected static int CalculateGridSize(int totalFrames)
        {
            int gridSize = 1;
            while (gridSize * gridSize < totalFrames)
            {
                gridSize *= 2;
            }
            return gridSize;
        }

    }
}


code:
using System;
using System.Collections.Generic;
using RimWorld;
using Verse;
using UnityEngine;
using RJW_Onahole.Utilities;

namespace RJW_Onahole.Data
{
    public class OnaholeRaceTransformDef : Def
    {
        public List<HediffToTransform> transforms;
        [Unsaved(false)]
        private Dictionary<string, TransformData> __hediffToTransforms;
        public Dictionary<string, TransformData> HediffToTransforms
        {
            get
            {
                if (__hediffToTransforms == null)
                {
                    __hediffToTransforms = new Dictionary<string, TransformData>();
                    foreach (HediffToTransform transform in transforms)
                    {
                        __hediffToTransforms.TryAdd(transform.hediffDefName, new TransformData(transform.hediffDefName, transform.Scale, transform.Offset));
                    }
                }
                return __hediffToTransforms;
            }
        }

    }
    public class HediffToTransform
    {
        public string hediffDefName;
        public Vector2 Scale;
        public Vector2 Offset;
    }
}


code:
using System.Collections.Generic;
using Verse;
using System.Collections;
using UnityEngine;
using System;
using System.Linq;
using RJW_Onahole.Utilities;

namespace RJW_Onahole.Data
{
    public class TransformCollection : IExposable
    {
        public Dictionary<string, TransformData> hediffsToTransforms = new Dictionary<string, TransformData>();
        public void ExposeData()
        {
            Scribe_Collections.Look(ref hediffsToTransforms, "hediffsToTransforms", LookMode.Value, LookMode.Deep);
        }
        public TransformCollection() { }
        public TransformCollection(TransformCollection transformCollection)
        {
            this.hediffsToTransforms = transformCollection.hediffsToTransforms;
        }
        public TransformCollection(Dictionary<string, TransformData> hediffsToTransforms)
        {
            foreach (string key in hediffsToTransforms.Keys.ToList())
            {
                this.hediffsToTransforms.SetOrAdd(key, hediffsToTransforms[key].MakeCopy());
            }
        }
        public void CopyFrom(TransformCollection transformCollection)
        {
            foreach (string key in transformCollection.hediffsToTransforms.Keys.ToList())
            {
                if (this.hediffsToTransforms.ContainsKey(key))
                {
                    this.hediffsToTransforms[key].CopyFrom(transformCollection.hediffsToTransforms[key]);
                }
                else
                {
                    this.hediffsToTransforms.Add(key, transformCollection.hediffsToTransforms[key].MakeCopy());
                }
            }
        }
    }
    public class TransformData : IExposable
    {
        public string hediffDefName;
        public Vector3 Scale;
        public Vector3 Offset;
        // Somehow Vector3 is not properly supported by Scribe_Values, and Vector2 somehow only allow 1 decimal place, so we need to store it as a Tuple
        // WTF rimworld?
        private Tuple<float, float> _scale;
        private Tuple<float, float> _offset;
        private Vector2 _v2scale;
        private Vector2 _v2offset;
        public float Separation;
        public void ExposeData()
        {
            Scribe_Values.Look(ref hediffDefName, "hediffDefName");
            // Save the scale and offset as Tuple to get 2 decimal places
            if (Scribe.mode == LoadSaveMode.Saving)
            {
                _scale = new Tuple<float, float>(Scale.x, Scale.z);
                _offset = new Tuple<float, float>(Separation, Offset.z);
                Scribe_Values.Look(ref _scale, "Scale");
                Scribe_Values.Look(ref _offset, "Offset");
            }
            // Load using Vector2, tuple cannot be parsed by Scribe_Values
            if (Scribe.mode == LoadSaveMode.LoadingVars)
            {
                Scribe_Values.Look(ref _v2scale, "Scale");
                Scribe_Values.Look(ref _v2offset, "Offset");
            }
            // Convert the Vector2 back to Vector3
            if (Scribe.mode == LoadSaveMode.PostLoadInit)
            {
                Scale = new Vector3(_v2scale.x, 1, _v2scale.y);
                Offset = new Vector3(0, 0, _v2offset.y);
                Separation = _v2offset.x;
            }
        }
        public TransformData() { }
        public TransformData(string hediffDefName, Vector2 scale, Vector2 offset)
        {
            this.hediffDefName = hediffDefName;
            this.Scale = new Vector3(scale.x, 1, scale.y);
            this.Offset = new Vector3(0, 0, offset.y);
            this.Separation = offset.x;
        }
        public TransformData(string hediffDefName, float separation, float z_offset, float x_scale, float z_scale)
        {
            this.hediffDefName = hediffDefName;
            this.Scale = new Vector3(x_scale, 1, z_scale);
            this.Offset = new Vector3(0, 0, z_offset);
            this.Separation = separation;
        }
        public void CopyFrom(TransformData transformData)
        {
            this.hediffDefName = transformData.hediffDefName;
            this.Scale = transformData.Scale;
            this.Offset = transformData.Offset;
            this.Separation = transformData.Separation;
        }
        public TransformData MakeCopy()
        {
            return new TransformData(this.hediffDefName, this.Separation, this.Offset.z, this.Scale.x, this.Scale.z);
        }

    }
}


code:
using Verse;

namespace RJW_Onahole.Data
{
    public class SA0ScaleDataDef : Def, IExposable
    {
        public float x;
        public float z;
        public SA0ScaleDataDef()
        {
        }
        public void ExposeData()
        {
            Scribe_Values.Look(ref defName, "defName");
            Scribe_Values.Look(ref x, "x");
            Scribe_Values.Look(ref z, "z");
        }
    }
}


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

namespace RJW_Onahole.Data
{
    /// <summary>
    /// Removes certain sex fluids from collectable list
    /// </summary>
    public class SexFluidBlacklistDef : Def
    {
    }
}


code:
using System;
using System.Collections.Generic;
using RimWorld;
using RJW_Onahole.Utilities;
using Verse;
using rjw;
using System.Collections;
using System.Linq;
using Verse.AI.Group;

namespace RJW_Onahole.Data
{
    public enum FactionType
    {
        Colonist,
        Prisoner,
        Slave,
        Guest,
        Factionless
    }
    public enum KindType
    {
        Humanlike,
        Animal,
        Insect,
        Mechanoid
    }
    public class UseRestriction : IExposable
    {
        public List<RowElements> restrictions = new List<RowElements>();
        public UseRestriction()
        {
            restrictions = new List<RowElements>();
            for (int i = 0; i < Enum.GetValues(typeof(FactionType)).Length; i++)
            {
                restrictions.Add(new RowElements());
            }
        }
        public void ExposeData()
        {
            Scribe_Collections.Look(ref restrictions, "restrictions", LookMode.Deep);
            if (Scribe.mode == LoadSaveMode.PostLoadInit)
            {
                if (restrictions.Count != Enum.GetValues(typeof(FactionType)).Length)
                {
                    "Restrictions count mismatch, resetting".LogWarning();
                    restrictions.Clear();
                    for (int i = 0; i < Enum.GetValues(typeof(FactionType)).Length; i++)
                    {
                        restrictions.Add(new RowElements());
                    }

                }
            }
        }
        public bool Allows(Pawn pawn, out string reason)
        {
            reason = "";
            if (pawn.InAggroMentalState) { return true; } //Allow random rape
            if (pawn.IsEnemy())
            {
                if (pawn.TryGetLord(out Lord lord)
                && (lord.CurLordToil is LordToil_AssaultColony
                || lord.CurLordToil is LordToil_AssaultColonyBreaching
                || lord.CurLordToil is LordToil_AssaultColonySappers))
                {
                    return true;
                }
                reason = "Lord is not actively assaulting";
                return false;
            }
            bool allowed = restrictions[(int)GetFactionType(pawn)][(int)GetKindType(pawn)];
            if (!RJWSettings.bestiality_enabled && (GetKindType(pawn) == KindType.Animal || GetKindType(pawn) == KindType.Insect))
            {
                allowed = false;
            }
            if (!RJWPregnancySettings.mechanoid_pregnancy_enabled && GetKindType(pawn) == KindType.Mechanoid)
            {
                allowed = false;
            }
            if (!allowed)
            {
                reason = "Use is restricted for pawn {0} with faction type " + GetFactionType(pawn).ToString() + " and kind type " + GetKindType(pawn).ToString();
            }
            return allowed;
        }
        public bool AllowsType(FactionType factionType, KindType kindType)
        {
            return restrictions[(int)factionType][(int)kindType];
        }
        public bool AllowsAnyFromFaction(FactionType type)
        {
            return restrictions[(int)type].Any(x => x);
        }
        public bool AllowsAnyOfKind(KindType type)
        {
            return restrictions.Any(x => x[(int)type]);
        }
        public void CopyFrom(UseRestriction other)
        {
            for (int i = 0; i < Enum.GetValues(typeof(FactionType)).Length; i++)
            {
                for (int j = 0; j < Enum.GetValues(typeof(KindType)).Length; j++)
                {
                    restrictions[i][j] = other.restrictions[i][j];
                }
            }
        }
        public FactionType GetFactionType(Pawn pawn)
        {
            if (pawn.Faction == null)
            {
                return FactionType.Factionless;
            }
            if (pawn.IsPrisonerOfColony)
            {
                return FactionType.Prisoner;
            }
            if (pawn.IsSlaveOfColony)
            {
                return FactionType.Slave;
            }
            if (pawn.Faction == Faction.OfPlayer)
            {
                return FactionType.Colonist;
            }
            if (pawn.IsGuest())
            {
                return FactionType.Guest;
            }
            "Unknown faction type for pawn {0}, using Factionless".LogWarning(pawn.LabelShort);
            return FactionType.Factionless;
        }
        public KindType GetKindType(Pawn pawn)
        {
            if (pawn.RaceProps.Humanlike)
            {
                return KindType.Humanlike;
            }
            if (pawn.RaceProps.Animal && !pawn.RaceProps.Insect)
            {
                return KindType.Animal;
            }
            if (pawn.RaceProps.Insect)
            {
                return KindType.Insect;
            }
            if (pawn.RaceProps.IsMechanoid)
            {
                return KindType.Mechanoid;
            }
            "Unknown kind type for pawn {0}, using humanlike".LogWarning(pawn.LabelShort);
            return KindType.Humanlike;
        }
        public class RowElements : IExposable, IEnumerable<bool>
        {
            List<bool> elements = new List<bool>();
            public RowElements()
            {
                for (int i = 0; i < Enum.GetValues(typeof(KindType)).Length; i++)
                {
                    elements.Add(false);
                }
            }
            public bool this[int index]
            {
                get
                {
                    return elements[index];
                }
                set
                {
                    elements[index] = value;
                }
            }
            public void ExposeData()
            {
                Scribe_Collections.Look(ref elements, "elements", LookMode.Value);
            }

            public IEnumerator<bool> GetEnumerator()
            {
                return elements.GetEnumerator();
            }

            IEnumerator IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }
        }
    }
}