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

namespace RJW_Onahole
{
    public class RJW_Onahole_Settings : ModSettings
    {
        public static float OnaholeAppraiseMultiplier = 1.0f;
        public static float OnaholeAppraiseOffset = 0.0f;
        public static int OnaholePriceFuckableOne = 100;
        public static int OnaholePriceFuckableZero = 0;
        public static int maxFluidStorage = 1000;
        public static int HaulCoolDownTicks = 20000;
        public static bool stripOnBind = true;
        public static bool useReduceResistance = true;
        public static bool orgasmReduceWill = true;
        public static bool orgasmReduceCertainty = true;
        public static bool machineBlocksParts = true;
        public static bool showCrossSections = true;
        public static bool showExtraGraphics = true;
        public static bool showMilkingMachine = true;
        public static bool showVaginalSexMachine = true;
        public static bool showAnalSexMachine = true;
        public static bool showBreastElectrodes = true;
        public static bool showGenitalElectrodes = true;
        public static bool showFeeder = true;
        public static bool showBindColonistMenu = true;
        public static bool showBindSlaveMenu = true;
        public static bool showBindPrisonerMenu = true;
        public static bool showBindAnimalMenu = false;
        public static bool showBindMechanoidMenu = false;
        public static bool showBindDownedEnemyMenu = false;
        public static bool showBindMiscMenu = false;
        public static Dictionary<string, TransformCollection> racesToTransforms = new Dictionary<string, TransformCollection>();
        private static TransformCollection defaultTransform = new TransformCollection();
        public static UseRestriction defaultUseRestriction = new UseRestriction();
        public static Vector3 DBG_Offset_1;
        public static Vector3 DBG_Offset_2;
        public static Vector3 DBG_Offset_3;

        public static Vector3 DBG_Scale_1 = Vector3.one;
        public static Vector3 DBG_Scale_2 = Vector3.one;
        public static Vector3 DBG_Scale_3 = Vector3.one;
        public static TabManager tabManager;
        public static List<SA0ScaleDataDef> SA0Scales = new List<SA0ScaleDataDef>();
        private static bool updated;

        //Brothel
        public static bool UseBrothelPrice;
        public static bool OverrideBedScore;
        public static float BedScore;
        public static TransformCollection DefaultTransform
        {
            get
            {
                TransformCollection transform = ConfigUtility.GetNewDefaultTransform();
                if (defaultTransform == null)
                {
                    defaultTransform = transform;
                    return defaultTransform;
                }
                // Avoid null object reference when new hediff added
                if (updated) { return defaultTransform; }
                foreach (string key in transform.hediffsToTransforms.Keys)
                {
                    if (!defaultTransform.hediffsToTransforms.ContainsKey(key) || defaultTransform.hediffsToTransforms[key] == null)
                    {
                        defaultTransform.hediffsToTransforms[key] = transform.hediffsToTransforms[key];
                    }
                    updated = true;
                }
                return defaultTransform;
            }
        }
        public static bool isDevMode = false;
        public static List<TabRecord> tabRecords = new List<TabRecord>();
        public override void ExposeData()
        {
            base.ExposeData();
            Scribe_Values.Look(ref HaulCoolDownTicks, "HaulCoolDownTicks", 20000);
            Scribe_Values.Look(ref maxFluidStorage, "MaxCumStorage", 1000);
            Scribe_Values.Look(ref OnaholeAppraiseMultiplier, "OnaholeAppraiseMultiplier", 1.0f);
            Scribe_Values.Look(ref OnaholeAppraiseOffset, "OnaholeAppraiseOffset", 0.0f);
            Scribe_Values.Look(ref OnaholePriceFuckableZero, "OnaholePriceFuckableZero", 50);
            Scribe_Values.Look(ref OnaholePriceFuckableOne, "OnaholePriceFuckableOne", 100);
            Scribe_Values.Look(ref stripOnBind, "stripOnBind", true);
            Scribe_Values.Look(ref useReduceResistance, "useReduceResistance", true);
            Scribe_Values.Look(ref orgasmReduceWill, "orgasmReduceWill", true);
            Scribe_Values.Look(ref orgasmReduceCertainty, "orgasmReduceCertainty", true);
            Scribe_Values.Look(ref machineBlocksParts, "machineBlocksParts", true);
            Scribe_Values.Look(ref showCrossSections, "showCrossSections", true);
            Scribe_Values.Look(ref showExtraGraphics, "showExtraGraphics", true);
            Scribe_Values.Look(ref showMilkingMachine, "showMilkingMachine", true);
            Scribe_Values.Look(ref showVaginalSexMachine, "showVaginalSexMachine", true);
            Scribe_Values.Look(ref showAnalSexMachine, "showAnalSexMachine", true);
            Scribe_Values.Look(ref showBreastElectrodes, "showBreastElectrodes", true);
            Scribe_Values.Look(ref showGenitalElectrodes, "showGenitalElectrodes", true);
            Scribe_Values.Look(ref showFeeder, "showFeeder", true);
            Scribe_Values.Look(ref showBindColonistMenu, "showBindColonistMenu", false);
            Scribe_Values.Look(ref showBindSlaveMenu, "showBindSlaveMenu", true);
            Scribe_Values.Look(ref showBindPrisonerMenu, "showBindPrisonerMenu", true);
            Scribe_Values.Look(ref showBindAnimalMenu, "showBindAnimalMenu", false);
            Scribe_Values.Look(ref showBindMechanoidMenu, "showBindMechanoidMenu", false);
            Scribe_Values.Look(ref showBindDownedEnemyMenu, "showBindDownedEnemyMenu", true);
            Scribe_Values.Look(ref showBindMiscMenu, "showBindMiscMenu", false);
            Scribe_Collections.Look(ref racesToTransforms, "RacesToTransforms", LookMode.Value, LookMode.Deep);
            Scribe_Deep.Look(ref defaultTransform, "DefaultTransform");
            Scribe_Deep.Look(ref defaultUseRestriction, "DefaultUseRestriction");
            Scribe_Values.Look(ref isDevMode, "isDevMode", false);
            Scribe_Collections.Look(ref SA0Scales, "SA0Scales", LookMode.Deep);
            Scribe_Values.Look(ref UseBrothelPrice, "UseBrothelPrice", true);
            Scribe_Values.Look(ref OverrideBedScore, "OverrideBedScore", true);
            Scribe_Values.Look(ref BedScore, "BedScore", 1f);
            if (Scribe.mode == LoadSaveMode.PostLoadInit)
            {
                if (defaultUseRestriction == null) { defaultUseRestriction = new UseRestriction(); }
            }
        }
        public void UpdateSettings()
        {
            // Update graphics
            if (Find.CurrentMap is Map map)
            {
                foreach (Pawn pawn in map.mapPawns.AllHumanlikeSpawned)
                {
                    if (pawn.IsOnahole())
                    {
                        pawn.Drawer.renderer.SetAllGraphicsDirty();
                    }
                }
            }
        }
        public RJW_Onahole_Settings()
        {
            if (tabManager == null)
            {
                tabManager = new TabManager();
            }
        }
        private static void AddTabs()
        {
            tabManager.AddTab(Tr.Settings, GeneralSettings);
            tabManager.AddTab(OTr.Menu, MenuSettings);
            tabManager.AddTab(Tr.ShowExtraGraphics, GraphicsSettings);
            tabManager.AddTab(Tr.EditTransform, PawnGraphicsSettings);
            if (Prefs.DevMode)
            {
                tabManager.AddTab(OTr.DevelopmentMode, DevSettings);
            }
            else
            {
                isDevMode = false;
            }
        }
        public void DoWindowContents(Rect inRect)
        {
            // Add tabs here to avoid translation not loaded
            if (tabManager.IsEmpty) { AddTabs(); }
            tabManager.Draw(inRect);
        }
        public static void GeneralSettings(Rect inRect)
        {
            Listing_Standard listing_Standard = new Listing_Standard();
            listing_Standard.Begin(inRect);
            string HaulCoolDownTicksBuffer = HaulCoolDownTicks.ToString();
            listing_Standard.TextFieldNumericLabeled<int>(Tr.HaulCoolDownTicks, ref HaulCoolDownTicks, ref HaulCoolDownTicksBuffer);
            string MaxCumStorageBuffer = maxFluidStorage.ToString();
            listing_Standard.TextFieldNumericLabeled<int>(Tr.MaxFluidStorage, ref maxFluidStorage, ref MaxCumStorageBuffer, 0, 9999999);
            string OnaholePriceMultiplierBuffer = OnaholeAppraiseMultiplier.ToString();
            listing_Standard.TextFieldNumericLabeled<float>(Tr.AppraiseMultiplier, ref OnaholeAppraiseMultiplier, ref OnaholePriceMultiplierBuffer, 0.0f, 10.0f);
            string OnaholePriceOffsetBuffer = OnaholeAppraiseOffset.ToString();
            listing_Standard.TextFieldNumericLabeled<float>(Tr.AppraiseOffset, ref OnaholeAppraiseOffset, ref OnaholePriceOffsetBuffer, -10.0f, 10.0f);
            string OnaholePriceFuckableZeroBuffer = OnaholePriceFuckableZero.ToString();
            listing_Standard.TextFieldNumericLabeled<int>(Tr.PriceAppraiseZero, ref OnaholePriceFuckableZero, ref OnaholePriceFuckableZeroBuffer);
            string OnaholePriceFuckableOneBuffer = OnaholePriceFuckableOne.ToString();
            listing_Standard.TextFieldNumericLabeled<int>(Tr.PriceAppraiseOne, ref OnaholePriceFuckableOne, ref OnaholePriceFuckableOneBuffer);
            listing_Standard.CheckboxLabeled(Tr.StripOnBind, ref stripOnBind);
            listing_Standard.CheckboxLabeled(Tr.UseReduceResistance, ref useReduceResistance);
            listing_Standard.CheckboxLabeled(Tr.OrgasmReduceWill, ref orgasmReduceWill);
            listing_Standard.CheckboxLabeled(Tr.OrgasmReduceCertainty, ref orgasmReduceCertainty);
            listing_Standard.CheckboxLabeled(Tr.MachineBlocksParts, ref machineBlocksParts);

            listing_Standard.End();
        }
        public static void MenuSettings(Rect inRect)
        {
            Listing_Standard listing_Standard = new Listing_Standard();
            listing_Standard.Begin(inRect);
            listing_Standard.CheckboxLabeled(Tr.Bind + ": " + OTr.Colonist, ref showBindColonistMenu);
            listing_Standard.CheckboxLabeled(Tr.Bind + ": " + OTr.Slave, ref showBindSlaveMenu);
            listing_Standard.CheckboxLabeled(Tr.Bind + ": " + OTr.Prisoner, ref showBindPrisonerMenu);
            listing_Standard.CheckboxLabeled(Tr.Bind + ": " + OTr.Animal, ref showBindAnimalMenu);
            listing_Standard.CheckboxLabeled(Tr.Bind + ": " + OTr.Mechanoid, ref showBindMechanoidMenu);
            listing_Standard.CheckboxLabeled(Tr.Bind + ": " + OTr.Enemy + OTr.Downed.InBrackets(), ref showBindDownedEnemyMenu);
            listing_Standard.CheckboxLabeled(Tr.Bind + ": " + OTr.Misc, ref showBindMiscMenu);
            listing_Standard.End();
        }
        public static void GraphicsSettings(Rect inRect)
        {
            Listing_Standard listing_Standard = new Listing_Standard();
            listing_Standard.Begin(inRect);
            listing_Standard.CheckboxLabeled(Tr.ShowCrossSections, ref showCrossSections);
            listing_Standard.CheckboxLabeled(Tr.ShowExtraGraphics, ref showExtraGraphics);
            if (showExtraGraphics)
            {
                listing_Standard.Indent(12f);
                listing_Standard.ColumnWidth -= 12f;
                listing_Standard.CheckboxLabeled(Tr.Show + ": " + OnaholeDefOf.OnaholeMilkingMachine.label, ref showMilkingMachine);
                listing_Standard.CheckboxLabeled(Tr.Show + ": " + OnaholeDefOf.OnaholeVaginalSexMachine.label, ref showVaginalSexMachine);
                listing_Standard.CheckboxLabeled(Tr.Show + ": " + OnaholeDefOf.OnaholeAnalSexMachine.label, ref showAnalSexMachine);
                listing_Standard.CheckboxLabeled(Tr.Show + ": " + OnaholeDefOf.OnaholeBreastElectrodes.label, ref showBreastElectrodes);
                listing_Standard.CheckboxLabeled(Tr.Show + ": " + OnaholeDefOf.OnaholeGenitalElectrodes.label, ref showGenitalElectrodes);
                listing_Standard.CheckboxLabeled(Tr.Show + ": " + OnaholeDefOf.OnaholeAutoFeeder.label, ref showFeeder);
            }
            listing_Standard.End();
        }
        public static void PawnGraphicsSettings(Rect inRect)
        {
            Listing_Standard listing_Standard = new Listing_Standard();
            listing_Standard.Begin(inRect);
            bool pressed = listing_Standard.ButtonTextLabeled(OTr.Default, OTr.ClickToEdit);
            if (pressed) { Find.WindowStack.Add(new Window_SetTransform(OTr.Default)); }
            bool pressed2 = listing_Standard.ButtonTextLabeled(OTr.Race, OTr.ClickToEdit);
            if (pressed2) { Find.WindowStack.Add(new FloatMenu(DropDownOptions())); }
            listing_Standard.Label(Tr.CustomTransforms);
            listing_Standard.End();
            List<string> keysToRemove = new List<string>();
            float curY = listing_Standard.CurHeight + inRect.y;
            if (racesToTransforms != null)
            {
                foreach (string key in racesToTransforms.Keys)
                {
                    if (DefDatabase<ThingDef>.GetNamedSilentFail(key) == null) { keysToRemove.Add(key); continue; }
                    Rect rect = new Rect(inRect.x, curY, inRect.width, 24f);
                    Widgets.Label(rect.LeftHalf(), key.ThingFullLabel());
                    if (Widgets.ButtonText(rect.RightHalf().LeftHalf(), OTr.Edit))
                    {
                        Find.WindowStack.Add(new Window_SetTransform(key.DefLabel<ThingDef>(), key));
                        break;
                    }
                    if (Widgets.ButtonText(rect.RightHalf().RightHalf(), OTr.Delete))
                    {
                        keysToRemove.Add(key);
                    }
                    curY += 24f;
                }
                if (keysToRemove.Count > 0)
                {
                    foreach (string key in keysToRemove) { racesToTransforms.Remove(key); }
                }
            }
        }
        public static void DevSettings(Rect inRect)
        {
            Listing_Standard listing_Standard = new Listing_Standard();
            listing_Standard.Begin(inRect);
            listing_Standard.CheckboxLabeled(OTr.DevelopmentMode, ref isDevMode);
            listing_Standard.End();
            if (isDevMode)
            {
                float y = listing_Standard.CurHeight + inRect.y;
                float LineHeight = 40f;
                UIWidgets.LabeledSliderNumericDisplay(new Rect(0, y, inRect.width, LineHeight), "X " + Tr.Offset, ref DBG_Offset_1.x, -1, 1, "F3");
                y += LineHeight;
                UIWidgets.LabeledSliderNumericDisplay(new Rect(0, y, inRect.width, LineHeight), "Z " + Tr.Offset, ref DBG_Offset_1.z, -1, 1, "F3");
                y += LineHeight;
                UIWidgets.LabeledSliderNumericDisplay(new Rect(0, y, inRect.width, LineHeight), "X " + Tr.Offset, ref DBG_Offset_2.x, -1, 1, "F3");
                y += LineHeight;
                UIWidgets.LabeledSliderNumericDisplay(new Rect(0, y, inRect.width, LineHeight), "Z " + Tr.Offset, ref DBG_Offset_2.z, -1, 1, "F3");
                y += LineHeight;
                UIWidgets.LabeledSliderNumericDisplay(new Rect(0, y, inRect.width, LineHeight), "X " + Tr.Offset, ref DBG_Offset_3.x, -1, 1, "F3");
                y += LineHeight;
                UIWidgets.LabeledSliderNumericDisplay(new Rect(0, y, inRect.width, LineHeight), "Z " + Tr.Offset, ref DBG_Offset_3.z, -1, 1, "F3");
                y += LineHeight;
                UIWidgets.LabeledSliderNumericDisplay(new Rect(0, y, inRect.width, LineHeight), "X " + Tr.Scale, ref DBG_Scale_1.x, 0, 10, "F3");
                y += LineHeight;
                UIWidgets.LabeledSliderNumericDisplay(new Rect(0, y, inRect.width, LineHeight), "Z " + Tr.Scale, ref DBG_Scale_1.z, 0, 10, "F3");
                y += LineHeight;
                UIWidgets.LabeledSliderNumericDisplay(new Rect(0, y, inRect.width, LineHeight), "X " + Tr.Scale, ref DBG_Scale_2.x, 0, 10, "F3");
                y += LineHeight;
                UIWidgets.LabeledSliderNumericDisplay(new Rect(0, y, inRect.width, LineHeight), "Z " + Tr.Scale, ref DBG_Scale_2.z, 0, 10, "F3");
                y += LineHeight;
                UIWidgets.LabeledSliderNumericDisplay(new Rect(0, y, inRect.width, LineHeight), "X " + Tr.Scale, ref DBG_Scale_3.x, 0, 10, "F3");
                y += LineHeight;
                UIWidgets.LabeledSliderNumericDisplay(new Rect(0, y, inRect.width, LineHeight), "Z " + Tr.Scale, ref DBG_Scale_3.z, 0, 10, "F3");
            }
        }
        public static SimpleCurve PriceCurve
        {
            get
            {
                SimpleCurve curve = new SimpleCurve
                {
                    { 0, OnaholePriceFuckableZero },
                    { 1, OnaholePriceFuckableOne }
                };
                return curve;
            }
        }
        private static List<FloatMenuOption> DropDownOptions()
        {
            List<FloatMenuOption> options = new List<FloatMenuOption>();
            foreach (ThingDef humanlikeDef in CacheUtility.humanLikeDefs)
            {
                options.Add(new FloatMenuOption(humanlikeDef.defName.ThingFullLabel(), delegate
                {
                    Find.WindowStack.Add(new Window_SetTransform(humanlikeDef.label, humanlikeDef.defName));
                }));
            }
            return options;
        }
    }
}


code:
using RJW_Onahole.Data;
using UnityEngine;
using Verse;

namespace RJW_Onahole.Render
{
    public class AnimatedRenderNodeProperties: PawnRenderNodeProperties
    {
        public InstancedAnimationDef animationDef;
        public AnimatedRenderNodeProperties()
        {
            this.workerClass = typeof(PawnRenderNodeWorker_Animated);
        }
    }
}

repo_name: atainer/rimjobworld

license: mit


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

namespace RJW_Onahole.Render
{
    [StaticConstructorOnStartup]
    public class InstancedAnimationController
    {
        public InstancedAnimationDef def;
        public float FramePerSecond => def.framePerSecond;
        public float TimePerFrame => 1f / FramePerSecond;
        public Material Material => def.Mat;
        protected Dictionary<string, AnimationData> dataBuffer = new Dictionary<string, AnimationData>();
        protected List<string> keys = new List<string>();
        protected MaterialPropertyBlock propertyBlock = new MaterialPropertyBlock();
        private float curTime = 0f;
        protected int drawCount = 0; // draw count on this frame
        protected bool shouldCleanBuffer = false;
        public InstancedAnimationController(InstancedAnimationDef def)
        {
            this.def = def;
            EventUtility.OnPreDraw += PreDraw;
            EventUtility.OnPostDraw += PostDraw;
        }
        protected virtual Mesh GetMesh()
        {
            return MeshPool.plane10;
        }
        /// <summary>
        /// Called at beginning of each frame
        /// </summary>
        protected virtual void PreDraw()
        {
            if (shouldCleanBuffer)
            {
                HashSet<string> drawnKeys = new HashSet<string>(keys.GetRange(0, drawCount));
                dataBuffer.RemoveAll((x) => !drawnKeys.Contains(x.Key));
                keys.Clear();
                shouldCleanBuffer = false;
            }
            drawCount = 0;
            curTime = Time.time;
        }
        public virtual string Draw(Matrix4x4 matrix, string key = null, int startingIndex = 0, float overrideInterval = -1f, Color? color = null)
        {
            if (Find.TickManager.Paused) { return DrawUnchanged(matrix, key, startingIndex, overrideInterval, color); }
            AnimationData data = NewAnimationData(ref key, startingIndex, color) ?? dataBuffer[key].Update(curTime);
            if (overrideInterval > 0) { data.interval = overrideInterval; }
            DrawMesh(matrix, data, key);
            return key;
        }
        public virtual string DrawFrame(Matrix4x4 matrix, string key = null, int frame = 0, Color? color = null)
        {
            AnimationData data = NewAnimationData(ref key, frame, color) ?? dataBuffer[key];
            data.frame = frame;
            DrawMesh(matrix, data, key);
            return key;
        }
        public virtual string DrawUnchanged(Matrix4x4 matrix, string key, int startingIndex = 0, float overrideInterval = -1f, Color? color = null)
        {
            AnimationData data = NewAnimationData(ref key, startingIndex, color) ?? dataBuffer[key];
            if (overrideInterval > 0) { data.interval = overrideInterval; }
            DrawMesh(matrix, data, key);
            return key;
        }
        protected virtual AnimationData NewAnimationData(ref string key, int startingIndex = 0, Color? color = null)
        {
            AnimationData data = null;
            if (key.NullOrEmpty() || !dataBuffer.ContainsKey(key))
            {
                key = Guid.NewGuid().ToString();
                data = new AnimationData(def.animateMode, def.totalFrames, TimePerFrame, curTime, startingIndex, color);
                dataBuffer.Add(key, data);
            }
            return data;
        }
        protected virtual void DrawMesh(Matrix4x4 matrix, AnimationData data, string key)
        {
            propertyBlock.Clear();
            propertyBlock.SetFloat("_CurrentFrame", data.frame);
            propertyBlock.SetColor("_Color", data.color);
            Graphics.DrawMesh(GetMesh(), matrix, Material, 0, null, 0, propertyBlock, false, false);
            keys.AddOrReplace(drawCount, key);
            drawCount++;
        }
        public virtual string GetMaterialPropertyBlock(string key, MaterialPropertyBlock propertyBlock, int startingIndex = 0, float overrideInterval = -1f, Color? color = null)
        {
            AnimationData data = NewAnimationData(ref key, startingIndex, color) ?? dataBuffer[key];
            propertyBlock.Clear();
            propertyBlock.SetFloat("_CurrentFrame", data.frame);
            propertyBlock.SetColor("_Color", data.color);
            return key;
        }
        /// <summary>
        /// Called at the end of each frame
        /// </summary>
        /// <param name="matrix"></param>
        /// <param name="key"></param>
        /// <param name="startingIndex"></param>
        /// <param name="color"></param>
        /// <returns></returns>
        protected virtual void PostDraw()
        {
            if (dataBuffer.Count - drawCount >= 30) { shouldCleanBuffer = true; } // Cleanup on next frame
        }

        /// <summary>
        /// Return current frame of animation. Note: if animation in cache but in fact stopped, a frame may still be returned. Use other ways to check if animation is stopped
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public virtual int GetCurFrame(string key)
        {
            if (dataBuffer.TryGetValue(key, out AnimationData data)) { return data.frame; }
            return -1;
        }

    }
}


code:
using UnityEngine;
using Verse;

namespace RJW_Onahole.Render
{
    public class MaskedRenderNodeProperties: PawnRenderNodeProperties
    {
        public Vector3 textureOffset;
        public Vector2 textureScale;
        public MaskedRenderNodeProperties()
        {
            this.workerClass = typeof(PawnRenderNodeWorker_MaskedTexture);
            this.shaderTypeDef = OnaholeDefOf.InheritMaskAlpha;
        }
    }
}


code:
using System;
using System.Collections.Generic;
using RimWorld;
using UnityEngine;
using Verse;
using RJW_Onahole.Utilities;
using RJW_Onahole.Comps;
using RJW_Onahole.Hediffs;
using static RimWorld.PawnOverlayDrawer;
using RJW_Onahole.Data;
using System.Linq;

namespace RJW_Onahole.Render
{
	public class PawnRenderNodeWorker_Animated : PawnRenderNodeWorker_CustomTransform
	{
		private readonly MaterialPropertyBlock materialPropertyBlock = new MaterialPropertyBlock();
		public override MaterialPropertyBlock GetMaterialPropertyBlock(PawnRenderNode node, Material material, PawnDrawParms parms)
		{
			materialPropertyBlock.Clear();
			materialPropertyBlock.SetFloat("_StartTime", node.hediff?.TryGetComp<HediffComp_OnaholeBase>()?.startTime ?? 0f);
			return materialPropertyBlock;
		}
		public override Material GetFinalizedMaterial(PawnRenderNode node, PawnDrawParms parms)
		{
			return (node.Props as AnimatedRenderNodeProperties)?.animationDef?.Mat ?? base.GetFinalizedMaterial(node, parms);
		}
	}
}


code:
using System;
using System.Collections.Generic;
using RimWorld;
using UnityEngine;
using Verse;
using RJW_Onahole.Utilities;
using RJW_Onahole.Comps;
using RJW_Onahole.Hediffs;
using static RimWorld.PawnOverlayDrawer;
using RJW_Onahole.Data;
using System.Linq;

namespace RJW_Onahole.Render
{
	public class PawnRenderNodeWorker_CustomTransform : PawnRenderNodeWorker
	{
		protected TransformData GetTransformData(PawnRenderNode node, PawnDrawParms parms)
		{
			return ConfigUtility.GetPawnTransformData(parms.pawn, node.hediff.def);
		}
		public override Vector3 OffsetFor(PawnRenderNode node, PawnDrawParms parms, out Vector3 pivot)
		{
			Vector3 offset = base.OffsetFor(node, parms, out pivot);
			TransformData transformData = GetTransformData(node, parms);
			offset += transformData.Offset;
			offset.x += transformData.Separation;
			return offset;
		}
		public override Vector3 ScaleFor(PawnRenderNode node, PawnDrawParms parms)
		{
			return base.ScaleFor(node, parms).MultipliedBy(GetTransformData(node, parms).Scale);
		}
		public override void PostDraw(PawnRenderNode node, PawnDrawParms parms, Mesh mesh, Matrix4x4 matrix)
		{
			RegisterPosition(node, parms, mesh, matrix);
			base.PostDraw(node, parms, mesh, matrix);
		}
		protected virtual void RegisterPosition(PawnRenderNode node, PawnDrawParms parms, Mesh mesh, Matrix4x4 matrix, bool flipped = false)
		{
			Vector3 pos = matrix.GetColumn(3);
			PositionUtility.RegisterPosition(parms.pawn, node.hediff.def, pos, flipped);
		}

		public override bool CanDrawNow(PawnRenderNode node, PawnDrawParms parms)
		{
			return parms.pawn.IsOnahole() && RJW_Onahole_Settings.showExtraGraphics && node.hediff.def.ShouldDraw();
		}
	}
	public class PawnRenderNodeWorker_CustomTransform_Flipped : PawnRenderNodeWorker_CustomTransform
	{
		public override Vector3 OffsetFor(PawnRenderNode node, PawnDrawParms parms, out Vector3 pivot)
		{
			Vector3 offset = base.OffsetFor(node, parms, out pivot);
			TransformData transformData = base.GetTransformData(node, parms);
			offset.x -= transformData.Separation * 2;
			return offset;
		}
		protected override void RegisterPosition(PawnRenderNode node, PawnDrawParms parms, Mesh mesh, Matrix4x4 matrix, bool flipped = false)
		{
			base.RegisterPosition(node, parms, mesh, matrix, true);
		}
	}
}


code:
using System;
using System.Collections.Generic;
using RimWorld;
using UnityEngine;
using Verse;
using RJW_Onahole.Utilities;
using RJW_Onahole.Comps;
using RJW_Onahole.Hediffs;
using static RimWorld.PawnOverlayDrawer;
using RJW_Onahole.Data;
using System.Linq;

namespace RJW_Onahole.Render
{
	public class PawnRenderNodeWorker_MaskedTexture : PawnRenderNodeWorker
	{
		protected Graphic transparentGraphic = GraphicDatabase.Get<Graphic_Single>(CacheUtility.Tex.Transparent, ShaderDatabase.Cutout, Vector2.one, Color.white, 0);
		protected MaterialPropertyBlock materialPropertyBlock = new MaterialPropertyBlock();

		protected TransformData GetTransformData(PawnRenderNode node, PawnDrawParms parms)
		{
			return ConfigUtility.GetPawnTransformData(parms.pawn, node.hediff.def);
		}
		public override void PreDraw(PawnRenderNode node, Material mat, PawnDrawParms parms)
		{
			base.PreDraw(node, mat, parms);
		}
		public override MaterialPropertyBlock GetMaterialPropertyBlock(PawnRenderNode node, Material material, PawnDrawParms parms)
		{
			MaskedRenderNodeProperties props = node.Props as MaskedRenderNodeProperties;
			TransformData transformData = ConfigUtility.GetPawnTransformData(parms.pawn, node.hediff.def);
			materialPropertyBlock.Clear();
			Texture texture = (props.overlayLayer == OverlayLayer.Head) ? parms.pawn.Drawer.renderer.HeadGraphic.MatAt(parms.pawn.Rotation).mainTexture : parms.pawn.Drawer.renderer.BodyGraphic.MatAt(parms.pawn.Rotation).mainTexture;
			materialPropertyBlock.SetTexture("_MaskTex", texture);
			Vector3 offset = base.OffsetFor(node, parms, out _) + props.textureOffset + transformData.Offset;
			Vector3 scale = this.ScaleFor(node, parms);
			Vector3 transformScale = transformData.Scale;
			Mesh mesh = node.GetMesh(parms);
			offset.x += transformData.Separation;
			offset.x /= scale.x * props.textureScale.x * mesh.bounds.size.x * transformScale.x;
			offset.z /= scale.z * props.textureScale.y * mesh.bounds.size.z * transformScale.z;
			if (props.overlayLayer == OverlayLayer.Head)
			{
				parms.pawn.Drawer.renderer.renderTree.rootNode.GetTransform(parms, out _, out _, out _, out Vector3 globalScale);
				offset.z /= globalScale.z; // This is done specially for Vivi
			}
			materialPropertyBlock.SetVector("_Main_TexOffset", new Vector4(offset.x, -offset.z, 0, 0));
			materialPropertyBlock.SetVector("_Main_TexScale", new Vector4(scale.x * props.textureScale.x * transformScale.x, scale.z * props.textureScale.y * transformScale.z, 0, 0));
			materialPropertyBlock.SetVector("_Color", node.ColorFor(parms.pawn));
			return materialPropertyBlock;
		}
		protected override Graphic GetGraphic(PawnRenderNode node, PawnDrawParms parms)
		{
			if (node.Props.texPaths.NullOrEmpty() || !(parms.bed is Building_OnaholeBed bed))
			{
				return base.GetGraphic(node, parms);
			}
			int i = bed.GetControlComp(node.hediff.def)?.TextureIndex ?? 0;
			if (i == 0)
			{
				return transparentGraphic;
			}
			return CacheUtility.Graphic.forPath[node.Props.texPaths[i - 1]];
		}
		public override Vector3 OffsetFor(PawnRenderNode node, PawnDrawParms parms, out Vector3 pivot)
		{
			Vector3 offset = base.OffsetFor(node, parms, out pivot);
			if (node.Props.overlayLayer == OverlayLayer.Head)
			{
				offset += parms.pawn.Drawer.renderer.BaseHeadOffsetAt(parms.facing);
			}
			return offset;
		}
		public override Vector3 ScaleFor(PawnRenderNode node, PawnDrawParms parms)
		{
			Vector3 vector = Vector3.one;
			vector.x *= node.Props.drawSize.x * node.debugScale;
			vector.z *= node.Props.drawSize.y * node.debugScale;
			if (node.AnimationWorker != null && node.AnimationWorker.Enabled() && !parms.flags.FlagSet(PawnRenderFlags.Portrait))
			{
				vector = vector.MultipliedBy(node.AnimationWorker.ScaleAtTick(node.tree.AnimationTick, parms));
			}

			if (node.Props.drawData != null)
			{
				vector *= node.Props.drawData.ScaleFor(parms.pawn);
			}
			return vector;
		}

		public override bool CanDrawNow(PawnRenderNode node, PawnDrawParms parms)
		{
			return parms.pawn.IsOnahole() && RJW_Onahole_Settings.showExtraGraphics && node.hediff.def.ShouldDraw();
		}
		public override void PostDraw(PawnRenderNode node, PawnDrawParms parms, Mesh mesh, Matrix4x4 matrix)
		{
			TransformData transformData = ConfigUtility.GetPawnTransformData(parms.pawn, node.hediff.def);
			Vector3 offset = matrix.GetColumn(3);
			offset.x += transformData.Separation;
			offset += transformData.Offset;
			Vector3 diff = offset - parms.pawn.DrawPos;
			// Cap distance to pawn to avoid incorrect offset being registered
			if (new Vector2(diff.x, diff.z).sqrMagnitude <= 8f)
			{
				// TODO check what is causing transform to be registered way off
				PositionUtility.RegisterPosition(parms.pawn, node.hediff.def, offset);
			}
		}
	}
}


code:
using RJW_Onahole.Data;
using RJW_Onahole.Utilities;
using UnityEngine;
using Verse;

namespace RJW_Onahole.Render
{
    [StaticConstructorOnStartup]
    public static class ShaderTimer
    {
        public static float curTime;
        static ShaderTimer()
        {
            curTime = Time.time;
            EventUtility.OnPreDraw += UpdateTime;
        }
        public static void UpdateTime()
        {
            if (Find.TickManager?.Paused != true)
            {
                curTime = Time.time;
                Shader.SetGlobalFloat("_CurTime", curTime);
            }
        }
    }
}


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

namespace RJW_Onahole.UI
{
    [StaticConstructorOnStartup]
    public class Command_FloatMenu_ExtraIcon<T> : Command where T : Enum
    {
        public T mode;
        public Action<T> setMode;
        protected List<Gizmo> gizmos;
        public Command_FloatMenu_ExtraIcon(T mode, Action<T> setMode) : base()
        {
            this.mode = mode;
            this.setMode = setMode;
        }
        public override GizmoResult GizmoOnGUI(Vector2 loc, float maxWidth, GizmoRenderParms parms)
        {
            GizmoResult result = base.GizmoOnGUI(loc, maxWidth, parms);
            GUI.color = Color.white;
            if (GetIcon(mode) != null)
            {
                Rect rect2 = new Rect(loc.x + 75f - 32f, loc.y, 32f, 32f);
                Widgets.DrawTextureFitted(rect2, GetIcon(mode), 1f);
                GUI.color = Color.white;
            }
            return result;
        }
        protected virtual Texture2D GetIcon(T mode)
        {
            return null;
        }
        protected virtual string GetLabel(T mode)
        {
            return mode.ToString();
        }
        protected FloatMenu GetFloatMenu()
        {
            List<FloatMenuOption> list = new List<FloatMenuOption>();
            foreach (T mode in Enum.GetValues(typeof(T)))
            {
                list.Add(FloatMenuOption(mode));
            }
            return new FloatMenu(list);
        }
        protected FloatMenuOption FloatMenuOption(T mode)
        {
            return new FloatMenuOption(GetLabel(mode), delegate
            {
                this.setMode(mode);
                for (int i = 0; i < gizmos.Count; i++)
                {
                    (gizmos[i] as Command_FloatMenu_ExtraIcon<T>).setMode(mode);
                }
            }, GetIcon(mode), Color.white);
        }

        public override bool GroupsWith(Gizmo other)
        {
            return base.GroupsWith(other) && EqualityComparer<T>.Default.Equals((other as Command_FloatMenu_ExtraIcon<T>).mode, mode);
        }
        public override void ProcessGroupInput(Event ev, List<Gizmo> group)
        {
            // Store the gizmos locally in the group to avoid being cleared
            this.gizmos = new List<Gizmo>(group);
            base.ProcessGroupInput(ev, group);
            Find.WindowStack.Add(GetFloatMenu());
        }
    }
    [StaticConstructorOnStartup]
    public class Command_FloatMenuWithCooldown_ExtraIcon<T> : Command_FloatMenu_ExtraIcon<T> where T : Enum
    {
        private static readonly Texture2D CooldownBarTex = SolidColorMaterials.NewSolidColorTexture(new Color32(9, 203, 4, 64));
        protected Func<float> cooldownPercentGetter;
        public Command_FloatMenuWithCooldown_ExtraIcon(T mode, Action<T> setMode, Func<float> func) : base(mode, setMode)
        {
            this.cooldownPercentGetter = func;
        }
        public override GizmoResult GizmoOnGUI(Vector2 loc, float maxWidth, GizmoRenderParms parms)
        {
            GizmoResult result = base.GizmoOnGUI(loc, maxWidth, parms);
            Rect rect = new Rect(loc.x, loc.y, this.GetWidth(maxWidth), 75f);
            if (this.cooldownPercentGetter != null)
            {
                float num = this.cooldownPercentGetter();
                if (num < 1f)
                {
                    Widgets.FillableBar(rect, Mathf.Clamp01(num), CooldownBarTex, null, false);
                    Text.Font = GameFont.Tiny;
                    Text.Anchor = TextAnchor.UpperCenter;
                    Widgets.Label(rect, num.ToStringPercent("F0"));
                    Text.Anchor = TextAnchor.UpperLeft;
                }
            }
            return result;
        }
    }
}


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

namespace RJW_Onahole.UI
{
    public enum ElectrocuteModes
    {
        Removed,
        Off,
        Stimulate,
        Pain,
        Burn
    }
    public class ElectrocuteGizmo : Command_FloatMenu_ExtraIcon<ElectrocuteModes>
    {
        public ElectrocuteGizmo(ElectrocuteModes mode, Action<ElectrocuteModes> setMode) : base(mode, setMode)
        {
        }

        protected override string GetLabel(ElectrocuteModes mode) => mode.Label();
        protected override Texture2D GetIcon(ElectrocuteModes mode)
        {
            switch (mode)
            {
                case ElectrocuteModes.Removed:
                    return CacheUtility.Tex.Cancel;
                case ElectrocuteModes.Off:
                    return CacheUtility.Tex.Off;
                case ElectrocuteModes.Stimulate:
                    return CacheUtility.Tex.Low;
                case ElectrocuteModes.Pain:
                    return CacheUtility.Tex.Medium;
                case ElectrocuteModes.Burn:
                    return CacheUtility.Tex.High;
                default:
                    return CacheUtility.Tex.Cancel;
            }
        }
    }
    public enum MilkingModes
    {
        Removed,
        Off,
        Normal,
        Fast,
        VeryFast
    }
    public class MilkingGizmo : Command_FloatMenuWithCooldown_ExtraIcon<MilkingModes>
    {
        public MilkingGizmo(MilkingModes mode, Action<MilkingModes> setMode, Func<float> func) : base(mode, setMode, func)
        {
            this.icon = CacheUtility.Tex.IconMilkingMachine;
            this.defaultLabel = Tr.MilkingMachine;
            this.defaultDesc = Tr.MilkingMachine_Desc;
        }
        protected override string GetLabel(MilkingModes mode) => mode.Label();
        protected override Texture2D GetIcon(MilkingModes mode)
        {
            switch (mode)
            {
                case MilkingModes.Removed:
                    return CacheUtility.Tex.Cancel;
                case MilkingModes.Off:
                    return CacheUtility.Tex.Off;
                case MilkingModes.Normal:
                    return CacheUtility.Tex.Low;
                case MilkingModes.Fast:
                    return CacheUtility.Tex.Medium;
                case MilkingModes.VeryFast:
                    return CacheUtility.Tex.High;
                default:
                    return CacheUtility.Tex.Cancel;
            }
        }
    }
    public class Gizmo_CumStatus : Gizmo
    {
        private CompCumCollector cumCollector;
        private HashSet<CompCumCollector> cumCollectors;
        private Dictionary<string, Texture2D> legends = new Dictionary<string, Texture2D>();
        public Gizmo_CumStatus(CompCumCollector cumCollector)
        {
            this.cumCollector = cumCollector;
            this.Order = -100f;
            this.cumCollectors = new HashSet<CompCumCollector>
            {
                cumCollector
            };
            foreach (object obj in Find.Selector.SelectedObjects)
            {
                if (obj is Building_OnaholeBed bed)
                {
                    foreach (CompCumCollector comp in bed.AllComps.OfType<CompCumCollector>())
                    {
                        if (comp.GetType() == typeof(CompCumCollector))
                        {
                            this.cumCollectors.Add(comp);
                        }
                    }
                }
            }
        }
        public override float GetWidth(float maxWidth)
        {
            return 280f;
        }

        public override GizmoResult GizmoOnGUI(Vector2 topLeft, float maxWidth, GizmoRenderParms parms)
        {
            Rect rect = new Rect(topLeft.x, topLeft.y, this.GetWidth(maxWidth), 75f);
            Widgets.DrawWindowBackground(rect);
            this.DrawOpen(rect.LeftPart(0.25f));
            this.DrawBar(rect.RightPart(0.75f));
            return new GizmoResult(GizmoState.Clear);
        }
        private void DrawOpen(Rect buttonRect)
        {
            Rect rect = buttonRect.ContractedBy(6f);
            rect.height = Mathf.Min(rect.height, rect.width);
            rect.width = rect.height;
            if (Widgets.ButtonImage(rect, CacheUtility.Tex.IconOpen, false))
            {
                Find.WindowStack.Add(new Window_ConfigureStorage());
            }
            Rect textRect = buttonRect.BottomHalf().BottomHalf();
            textRect.height *= 2f;
            Text.Font = GameFont.Tiny;
            Text.Anchor = TextAnchor.MiddleCenter;
            Widgets.Label(textRect, Tr.OnaholeBedStorage + (this.cumCollectors.Count <= 1 ? "" : " (x" + this.cumCollectors.Count + ")"));
            Text.Font = GameFont.Small;
            Text.Anchor = TextAnchor.UpperLeft;
        }
        private void DrawBar(Rect rect)
        {
            Rect innerRect = rect.ContractedBy(6f);
            Text.Font = GameFont.Small;
            Text.Anchor = TextAnchor.MiddleLeft;
            innerRect.SplitHorizontally(innerRect.height / 2, out Rect topRect, out Rect bottomRect);
            Widgets.Label(topRect, Tr.FluidStorage + (this.cumCollectors.Count <= 1 ? "" : " (x" + this.cumCollectors.Count + ")"));
            Dictionary<string, float> dict = new Dictionary<string, float>();
            foreach (CompCumCollector comp in this.cumCollectors)
            {
                foreach (KeyValuePair<string, float> kvp in comp.fluidsToAmounts)
                {
                    if (!dict.ContainsKey(kvp.Key))
                    {
                        dict.Add(kvp.Key, kvp.Value);
                    }
                    else
                    {
                        dict[kvp.Key] += kvp.Value;
                    }
                }
            }
            UIWidgets.FillableBarMultiThingDef(bottomRect, dict, RJW_Onahole_Settings.maxFluidStorage * cumCollectors.Count, ref legends);
            Text.Anchor = TextAnchor.MiddleCenter;
            Text.Font = GameFont.Tiny;
            Widgets.Label(bottomRect, (int)dict.Sum(x => x.Value) + "/" + RJW_Onahole_Settings.maxFluidStorage * cumCollectors.Count + " ml");

            //ToolTip
            if (Mouse.IsOver(rect))
            {
                TipSignal tipSignal = new TipSignal(() => GetTipString(cumCollector), rect.GetHashCode());
                TooltipHandler.TipRegion(rect, tipSignal);
            }
        }
        private static string GetTipString(CompCumCollector cumCollector)
        {
            string text = "";
            if (cumCollector.fluidsToAmounts.Count == 0) { return text; }
            foreach (KeyValuePair<string, float> kvp in cumCollector.fluidsToAmounts)
            {
                text += kvp.Key.DefLabel<SexFluidDef>() + ": " + kvp.Value + "ml" + "\n";
            };
            text = text.Remove(text.Length - 1);
            return text;
        }
        public override bool GroupsWith(Gizmo other)
        {
            return other is Gizmo_CumStatus;
        }
    }
    public class Gizmo_SelectTexture : Command_Action
    {
        public ICompAddsHediff comp;
        public PawnRenderNodeProperties renderNodeProperties;
        public Gizmo_SelectTexture(ICompAddsHediff comp, PawnRenderNodeProperties renderNodeProperties)
        {
            this.comp = comp;
            this.icon = DrawUtility.HediffTex(renderNodeProperties, comp.TextureIndex);
            this.defaultLabel = Tr.ChangeBindStyle;
            this.action = delegate
            {
                comp.TextureIndex++;
                comp.TextureIndex %= comp.GetHediffDef().FirstOrDefault().RenderNodeProperties.Find((x) => x.texPaths.Count > 0).texPaths.Count + 1;
                comp.Notify_PawnGraphicsChange();
            };
        }
    }
    [StaticConstructorOnStartup]
    public class Gizmo_IngestibleFuelStatus : Gizmo
    {
        public CompIngestibleRefuelable compIngestibleRefuelable;
        public HashSet<CompIngestibleRefuelable> allSelectedComps;
        public Type compType;

        private readonly Texture2D fullBarTex;

        private static readonly Texture2D EmptyBarTex = SolidColorMaterials.NewSolidColorTexture(Color.black);

        private static readonly Texture2D TargetLevelArrow = CacheUtility.Tex.TargetArrow;
        private const float ArrowScale = 0.5f;
        public Gizmo_IngestibleFuelStatus(CompIngestibleRefuelable compFeederRefuelable)
        {
            this.Order = -100f;
            this.compIngestibleRefuelable = compFeederRefuelable;
            this.compType = compFeederRefuelable.GetType();
            this.allSelectedComps = new HashSet<CompIngestibleRefuelable> { compFeederRefuelable };
            this.fullBarTex = CacheUtility.ColorMat.ColorTex(compIngestibleRefuelable.BarMat().color);
            foreach (object obj in Find.Selector.SelectedObjects)
            {
                if (obj is ThingWithComps thing)
                {
                    foreach (CompIngestibleRefuelable comp in thing.AllComps.OfType<CompIngestibleRefuelable>())
                    {
                        if (comp.GetType() == this.compType)
                        {
                            this.allSelectedComps.Add(comp);
                        }
                    }
                }
            }
        }
        public override float GetWidth(float maxWidth)
        {
            return 280f;
        }

        public override GizmoResult GizmoOnGUI(Vector2 topLeft, float maxWidth, GizmoRenderParms parms)
        {
            Rect overRect = new Rect(topLeft.x, topLeft.y, this.GetWidth(maxWidth), 75f);
            Widgets.DrawWindowBackground(overRect);
            this.DrawToggle(overRect.LeftPart(1f / 3f));
            this.DrawFuelBar(overRect.RightPart(2f / 3f).LeftPart(0.75f));
            this.DrawGizmos(overRect.RightPart(2f / 3f).RightPart(0.25f));
            return new GizmoResult(GizmoState.Clear);
        }
        private void DrawToggle(Rect toggleRect)
        {
            Rect rect = toggleRect.ContractedBy(6f);
            rect.height = Mathf.Min(rect.height, rect.width);
            rect.width = rect.height;
            if (Widgets.ButtonImage(rect, this.compIngestibleRefuelable.ToggleIcon(), false, this.compIngestibleRefuelable.Description()))
            {
                this.compIngestibleRefuelable.SetEnabled(!this.compIngestibleRefuelable.Enabled());
                foreach (CompIngestibleRefuelable comp in this.allSelectedComps)
                {
                    comp.SetEnabled(this.compIngestibleRefuelable.Enabled());
                }
            }
            Rect textRect = toggleRect.BottomHalf().BottomHalf();
            textRect.height *= 2f;
            Text.Font = GameFont.Tiny;
            Text.Anchor = TextAnchor.MiddleCenter;
            Widgets.Label(textRect, this.compIngestibleRefuelable.Title() + (this.allSelectedComps.Count <= 1 ? "" : " (x" + this.allSelectedComps.Count + ")"));
            Text.Font = GameFont.Small;
            Text.Anchor = TextAnchor.UpperLeft;
            Rect checkboxRect = rect.TopPart(0.3f).RightPart(0.3f);
            Widgets.DrawTextureFitted(checkboxRect, this.compIngestibleRefuelable.Enabled() ? Widgets.CheckboxOnTex : Widgets.CheckboxOffTex, 1f);
        }
        private void DrawFuelBar(Rect barInfoRect)
        {
            Rect rect = barInfoRect.ContractedBy(6f);
            Text.Font = GameFont.Tiny;
            if (Widgets.ButtonImage(barInfoRect.LeftPart(0.25f).TopHalf(), compIngestibleRefuelable.FuelThingDef?.uiIcon ?? CacheUtility.Tex.IconSelection, false))
            {
                Find.WindowStack.Add(new FloatMenu(DropDownOptions()));
            }
            Text.Anchor = TextAnchor.MiddleLeft;
            Widgets.Label(barInfoRect.RightPart(0.75f).TopHalf(), compIngestibleRefuelable.FuelThingDef?.label ?? OTr.ClickToSelect);
            Rect rect3 = rect.BottomHalf();
            float fuel = allSelectedComps.Count <= 1 ? this.compIngestibleRefuelable.Fuel : this.allSelectedComps.Sum(x => x.Fuel);
            float maxFuel = allSelectedComps.Count <= 1 ? this.compIngestibleRefuelable.Props.fuelCapacity : this.allSelectedComps.Sum(x => x.Props.fuelCapacity);
            float fractionFilled = fuel / maxFuel;
            Widgets.FillableBar(rect3, fractionFilled, fullBarTex, EmptyBarTex, false);
            if (this.compIngestibleRefuelable.Props.targetFuelLevelConfigurable && allSelectedComps.Count <= 1)
            {
                float fractionTarget = this.compIngestibleRefuelable.TargetFuelLevel / this.compIngestibleRefuelable.Props.fuelCapacity;
                float targetX = rect3.x + fractionTarget * rect3.width - (float)TargetLevelArrow.width * ArrowScale / 2f;
                float targetY = rect3.y - (float)TargetLevelArrow.height * ArrowScale;
                GUI.DrawTexture(new Rect(targetX, targetY, (float)TargetLevelArrow.width * ArrowScale, (float)TargetLevelArrow.height * ArrowScale), TargetLevelArrow);
            }
            Text.Font = GameFont.Small;
            Text.Anchor = TextAnchor.MiddleCenter;
            Widgets.Label(rect3, fuel.ToString("F0") + " / " + maxFuel.ToString("F0"));
            Text.Anchor = TextAnchor.UpperLeft;
        }
        private void DrawGizmos(Rect gizmoRect)
        {
            gizmoRect = gizmoRect.ContractedBy(6f);
            if (Widgets.ButtonImage(gizmoRect.TopHalf(), compIngestibleRefuelable.allowAutoRefill ? TexCommand.ForbidOff : TexCommand.ForbidOn, false, Tr.AllowAutoRefill))
            {
                compIngestibleRefuelable.allowAutoRefill = !compIngestibleRefuelable.allowAutoRefill;
                foreach (CompIngestibleRefuelable refuelable in allSelectedComps)
                {
                    refuelable.allowAutoRefill = compIngestibleRefuelable.allowAutoRefill;
                }
            }
            if (Widgets.ButtonImage(gizmoRect.BottomHalf(), CacheUtility.Tex.IconSetLevel, false, Tr.SetTargetLevel))
            {
                TargetLevelWindow();
            }
        }
        private void TargetLevelWindow()
        {
            int maxValue = allSelectedComps.Min(x => (int)x.Props.fuelCapacity);
            int targetValue = Mathf.Min((int)compIngestibleRefuelable.TargetFuelLevel, maxValue);
            string textGetter(int x) => Tr.SetTargetLevel + x.ToString();
            Dialog_Slider dialog_Slider = new Dialog_Slider(textGetter, 0, maxValue, delegate (int value)
            {
                foreach (CompIngestibleRefuelable refuelable in allSelectedComps)
                {
                    refuelable.TargetFuelLevel = value;
                    refuelable.Notify_CapacityChanged();
                }
            }, targetValue);
            Find.WindowStack.Add(dialog_Slider);
        }
        private List<FloatMenuOption> DropDownOptions()
        {
            List<FloatMenuOption> options = new List<FloatMenuOption>
            {
                new FloatMenuOption(OTr.Off, delegate
                {
                    foreach (CompIngestibleRefuelable refuelable in allSelectedComps)
                    {
                        refuelable.FuelThingDef = null;
                    }
                })
            };
            foreach (ThingDef thingDef in compIngestibleRefuelable.AllowedFuel())
            {

                if (!IsAvailable(thingDef)) { continue; }
                options.Add(new FloatMenuOption(thingDef.label, delegate
                {
                    foreach (CompIngestibleRefuelable refuelable in allSelectedComps)
                    {
                        refuelable.FuelThingDef = thingDef;
                    }
                }, thingDef.uiIcon, Color.white));
            }
            return options;
        }
        public bool IsAvailable(ThingDef thingDef)
        {
            ListerThings listerThings = compIngestibleRefuelable.parent.Map.listerThings;
            if (listerThings != null)
            {
                return listerThings.AllThings.Any(t => t.def == thingDef);
            }
            return false;
        }
        public override bool GroupsWith(Gizmo other)
        {
            return other is Gizmo_IngestibleFuelStatus gizmo_IngestibleFuelStatus && gizmo_IngestibleFuelStatus.compType == this.compType;
        }
    }
}


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

public class TabManager
{
    private readonly List<TabRecord> tabs;
    private readonly List<Action<Rect>> tabActions;
    private int selectedTabIndex;
    private Rect contentRect;
    private Rect tabRect;
    public TabManager()
    {
        tabs = new List<TabRecord>();
        tabActions = new List<Action<Rect>>();
        selectedTabIndex = 0;
    }
    public bool IsEmpty => tabs.Count == 0;
    public void AddTab(string label, Action<Rect> onTabSelected)
    {
        int index = tabs.Count;
        tabs.Add(new TabRecord(label, () => SelectTab(index), selectedTabIndex == index));
        tabActions.Add(onTabSelected);
    }

    private void SelectTab(int index)
    {
        selectedTabIndex = index;
        RefreshTabs();
    }

    public void Draw(Rect inRect, float tabHeight = 30f, float padding = 5f)
    {
        tabRect = new Rect(inRect.x, inRect.y + 48f, inRect.width, tabHeight);
        contentRect = new Rect(inRect.x, inRect.y + tabHeight + 48f, inRect.width, inRect.height - tabHeight - 72f).ContractedBy(padding);
        TabDrawer.DrawTabs(tabRect, tabs);
        tabActions[selectedTabIndex].Invoke(contentRect);

    }
    private void RefreshTabs()
    {
        for (int i = 0; i < tabs.Count; i++)
        {
            tabs[i].selected = i == selectedTabIndex;
        }
    }
}


code:
using RimWorld;
using System;
using Verse;
using UnityEngine;
using RJW_Onahole.Utilities;
using System.Collections.Generic;
using System.Linq;
using RJW_Onahole.Data;
using rjw;
using Verse.Sound;
using RJW_Onahole.Comps;
namespace RJW_Onahole.UI
{
    public abstract class Widget_BulkConfig<T> where T : class // Ensure reference type
    {
        public readonly T primarySelection;
        protected readonly List<T> allSelections;
        public bool isChanged;
        public virtual T Default => null;
        public Widget_BulkConfig(T primarySelection, List<T> allSelections)
        {
            this.primarySelection = primarySelection;
            this.allSelections = allSelections;
        }
        public virtual void Draw(Rect inRect)
        {
            Widgets.DrawBox(inRect);
            inRect = inRect.ContractedBy(10f);
            // Draw default save/load widgets
            if (Default != null)
            {
                Rect buttonRect = new Rect(inRect.xMax - 20f, inRect.y, 20f, 20f);
                if (Widgets.ButtonImage(buttonRect, TexButton.Reload, false, OTr.RestoreDefaults))
                {
                    SoundDefOf.Click.PlayOneShotOnCamera();
                    PropagateChange(Default, primarySelection);
                    isChanged = true;

                }
                buttonRect.x -= 20f;
                if (Widgets.ButtonImage(buttonRect, TexButton.Save, false, OTr.ChangeDefaults))
                {
                    PropagateChange(primarySelection, Default);
                    RJW_Onahole_Mod.Settings.Write();
                }
            }
            DrawContents(inRect);
        }
        public abstract void DrawContents(Rect inRect);
        protected virtual void SetDefaults() {}
        public abstract void PropagateChange(T source, T target);
        public virtual void OnClose()
        {
            if (isChanged)
            {
                foreach (T t in allSelections)
                {
                    if (t != primarySelection)
                    {
                        PropagateChange(primarySelection, t);
                    }
                }
            }
        }
    }
}


code:
using RimWorld;
using System;
using Verse;
using UnityEngine;
using RJW_Onahole.Utilities;
using System.Collections.Generic;
using System.Linq;
using RJW_Onahole.Data;
using rjw;
using Verse.Sound;
using RJW_Onahole.Comps;
namespace RJW_Onahole.UI
{
    public class Widget_MultiAssignable : Widget_BulkConfig<CompMultiAssignable>
    {

        private Vector2 scrollPosition;

        private const float EntryHeight = 35f;

        private const int AssignButtonWidth = 165;

        private const int SeparatorHeight = 7;

        private static readonly List<Pawn> tmpPawnSorted = new List<Pawn>(16);

        public Widget_MultiAssignable(CompMultiAssignable primarySelection, List<CompMultiAssignable> allSelections) : base(primarySelection, allSelections)
        {
        }

        public override void PropagateChange(CompMultiAssignable source, CompMultiAssignable target)
        {
            if (target.parent.Faction != Faction.OfPlayer) { return; } // Don't propagate changes to AI controlled ones
            foreach (Pawn pawn in target.AssignedPawnsForReading)
            {
                target.TryUnassignPawn(pawn);
            }
            foreach (Pawn pawn in source.AssignedPawnsForReading)
            {
                target.TryAssignPawn(pawn);
            }
        }
        public override void DrawContents(Rect inRect)
        {
            Text.Font = GameFont.Medium;
            Widgets.Label(new Rect(inRect.x, inRect.y, inRect.width, 40f), Tr.SetRestriction + OTr.Colonist.InBrackets());
            inRect.y += 20f;
            inRect.height -= 20f;
            Text.Font = GameFont.Small;
            Rect outRect = new Rect(inRect);
            outRect.yMin += 20f;
            outRect.yMax -= 40f;
            float num = 0f;
            num += (float)primarySelection.AssignedPawnsForReading.Count * EntryHeight;
            num += (float)primarySelection.AssigningCandidates.Count() * EntryHeight;
            num += SeparatorHeight;
            num += EntryHeight;
            num += primarySelection.AssignedPawnsForReading.Count > 0 ? SeparatorHeight : 0f;
            Rect viewRect = new Rect(0f, 0f, outRect.width, num);
            Widgets.AdjustRectsForScrollView(inRect, ref outRect, ref viewRect);
            Widgets.BeginScrollView(outRect, ref scrollPosition, viewRect);
            SortTmpList(primarySelection.AssignedPawnsForReading);
            float y = 0f;
            Widgets.Label(new Rect(0f, y, viewRect.width, EntryHeight), OTr.Allow + ":");
            y += EntryHeight;
            for (int i = 0; i < tmpPawnSorted.Count; i++)
            {
                Pawn pawn = tmpPawnSorted[i];
                DrawAssignedRow(pawn, ref y, viewRect, i);
            }

            if (primarySelection.AssignedPawnsForReading.Count > 0)
            {
                Rect r = new Rect(0f, y, viewRect.width, SeparatorHeight);
                y += SeparatorHeight;
                using (new TextBlock(Widgets.SeparatorLineColor))
                {
                    Widgets.DrawLineHorizontal(r.x, r.y + r.height / 2f, r.width);
                }
                Widgets.Label(new Rect(0f, y, viewRect.width, EntryHeight), OTr.Forbid + ":");
                y += EntryHeight;
            }

            SortTmpList(primarySelection.AssigningCandidates);
            for (int j = 0; j < tmpPawnSorted.Count; j++)
            {
                Pawn pawn2 = tmpPawnSorted[j];
                DrawUnassignedRow(pawn2, ref y, viewRect, j);
            }

            tmpPawnSorted.Clear();
            Widgets.EndScrollView();
        }

        private void SortTmpList(IEnumerable<Pawn> collection)
        {
            tmpPawnSorted.Clear();
            tmpPawnSorted.AddRange(collection);
            tmpPawnSorted.SortBy((Pawn x) => x.LabelShort);
        }

        private void DrawAssignedRow(Pawn pawn, ref float y, Rect viewRect, int i)
        {
            Rect rect = new Rect(0f, y, viewRect.width, EntryHeight);
            y += EntryHeight;
            if (i % 2 == 1)
            {
                Widgets.DrawLightHighlight(rect);
            }

            Rect rect2 = rect;
            rect2.width = rect.height;
            Widgets.ThingIcon(rect2, pawn);
            Rect rect3 = rect;
            rect3.xMin = rect.xMax - AssignButtonWidth - 10f;
            rect3 = rect3.ContractedBy(2f);
            if (Widgets.ButtonText(rect3, "BuildingUnassign".Translate()))
            {
                primarySelection.TryUnassignPawn(pawn);
                SoundDefOf.Click.PlayOneShotOnCamera();
                isChanged = true;
            }

            Rect rect4 = rect;
            rect4.xMin = rect2.xMax + 10f;
            rect4.xMax = rect3.xMin - 10f;
            using (new TextBlock(TextAnchor.MiddleLeft))
            {
                Widgets.LabelEllipses(rect4, pawn.LabelCap);
            }
        }

        private void DrawUnassignedRow(Pawn pawn, ref float y, Rect viewRect, int i)
        {
            if (primarySelection.AssignedPawnsForReading.Contains(pawn))
            {
                return;
            }

            AcceptanceReport acceptanceReport = primarySelection.CanAssignTo(pawn);
            bool accepted = acceptanceReport.Accepted;
            Rect rect = new Rect(0f, y, viewRect.width, EntryHeight);
            y += EntryHeight;
            if (i % 2 == 1)
            {
                Widgets.DrawLightHighlight(rect);
            }

            if (!accepted)
            {
                GUI.color = Color.gray;
            }

            Rect rect2 = rect;
            rect2.width = rect.height;
            Widgets.ThingIcon(rect2, pawn);
            Rect rect3 = rect;
            rect3.xMin = rect.xMax - AssignButtonWidth - 10f;
            rect3 = rect3.ContractedBy(2f);
            if (accepted)
            {
                TaggedString taggedString = primarySelection.AssignedAnything(pawn) ? "BuildingReassign".Translate() : "BuildingAssign".Translate();
                if (Widgets.ButtonText(rect3, taggedString))
                {
                    primarySelection.TryAssignPawn(pawn);
                    SoundDefOf.Click.PlayOneShotOnCamera();
                    isChanged = true;
                }
            }

            Rect rect5 = rect;
            rect5.xMin = rect2.xMax + 10f;
            rect5.xMax = rect3.xMin - 10f;
            string label = pawn.LabelCap + (accepted ? "" : (" (" + acceptanceReport.Reason.StripTags() + ")"));
            using (new TextBlock(TextAnchor.MiddleLeft))
            {
                Widgets.LabelEllipses(rect5, label);
            }
        }

    }
}


code:
using RimWorld;
using System;
using Verse;
using UnityEngine;
using RJW_Onahole.Utilities;
using System.Collections.Generic;
using System.Linq;
namespace RJW_Onahole.UI
{
    public class Widget_SetPrice : Widget_BulkConfig<Building_OnaholeBed>
    {

        public Widget_SetPrice(Building_OnaholeBed primarySelection, List<Building_OnaholeBed> allSelections) : base(primarySelection, allSelections)
        {
        }

        public override void DrawContents(Rect inRect)
        {
            int oldPrice = primarySelection._price;
            Rect rect = new Rect(inRect.x, inRect.y, inRect.width, 30);
            Text.Font = GameFont.Medium;
            Widgets.Label(rect, OTr.MarketValue);
            rect.y += 40;
            Text.Font = GameFont.Small;
            string _price = primarySelection._price.ToString();
            Widgets.TextFieldNumeric(rect, ref primarySelection._price, ref _price, 0, 100000);
            if (primarySelection._price != oldPrice)
            {
                isChanged = true;
            }
        }

        public override void PropagateChange(Building_OnaholeBed source, Building_OnaholeBed target)
        {
            target._price = source._price;
        }
    }
}


code:
using RimWorld;
using System;
using Verse;
using UnityEngine;
using RJW_Onahole.Utilities;
using System.Collections.Generic;
using System.Linq;
using RJW_Onahole.Data;
using rjw;
namespace RJW_Onahole.UI
{
    public class Widget_SetRestriction : Widget_BulkConfig<UseRestriction>
    {
        private static int VisibleRows
        {
            get
            {
                int length = Enum.GetValues(typeof(KindType)).Length;
                if (!RJWSettings.bestiality_enabled) { length -= 2; }
                if (!RJWPregnancySettings.mechanoid_pregnancy_enabled) { length--; }
                return length;
            }
        }
        private static int VisibleColumns
        {
            get
            {
                int length = Enum.GetValues(typeof(FactionType)).Length;
                if (!ModsConfig.IdeologyActive) { length--; }
                return length;
            }
        }
        private static int TotalRows => Enum.GetValues(typeof(KindType)).Length;
        private static int TotalColumns => Enum.GetValues(typeof(FactionType)).Length;
        private static readonly string[] rowTitles = Enum.GetValues(typeof(KindType)).Cast<KindType>().Select(k => k.Label()).ToArray();
        private readonly string[] columnTitles = Enum.GetValues(typeof(FactionType)).Cast<FactionType>().Select(f => f.Label()).ToArray();
        public override UseRestriction Default => RJW_Onahole_Settings.defaultUseRestriction;

        public Widget_SetRestriction(UseRestriction primarySelection, List<UseRestriction> allSelections) : base(primarySelection, allSelections)
        {
        }

        public override void PropagateChange(UseRestriction source, UseRestriction target)
        {
            target.CopyFrom(source);
        }
        public override void DrawContents(Rect inRect)
        {
            Text.Font = GameFont.Medium;
            Widgets.Label(new Rect(inRect.x, inRect.y, inRect.width, 40f), Tr.SetRestriction);
            Text.Font = GameFont.Small;
            Text.Anchor = TextAnchor.MiddleCenter;
            // Calculate dynamic widths based on title lengths
            float rowLabelWidth = rowTitles.Max(t => Text.CalcSize(t.Translate()).x) + 10f;
            float rowLabelHeight = rowTitles.Max(t => Text.CalcSize(t.Translate()).y) + 10f;
            float columnLabelWidth = columnTitles.Max(t => Text.CalcSize(t.Translate()).x) + 10f;
            float columnLabelHeight = columnTitles.Max(t => Text.CalcSize(t.Translate()).y) + 10f;

            float startX = rowLabelWidth + 10f;  // Start of the checkbox columns
            float startY = columnLabelHeight + 50f;  // Start of the rows, below the column titles

            DrawColumnTitles(rowLabelHeight, startX, startY, columnLabelWidth, columnLabelHeight);
            DrawRowTitles(inRect, rowLabelHeight, rowLabelWidth, startX, startY, columnLabelWidth);
            Text.Anchor = TextAnchor.UpperLeft;
            DrawCheckboxes(rowLabelHeight, columnLabelWidth, startX, startY);

        }
        private void DrawColumnTitles(float rowLabelHeight, float startX, float startY, float columnLabelWidth, float columnLabelHeight)
        {
            // Draw column titles and their corresponding check/uncheck functionality
            float curX = startX;
            for (int col = 0; col < TotalColumns; col++)
            {
                if ((FactionType)col == FactionType.Slave && !ModsConfig.IdeologyActive) { continue; }
                Rect columnTitleRect = new Rect(curX, startY - columnLabelHeight, columnLabelWidth, rowLabelHeight);
                if (Mouse.IsOver(columnTitleRect))
                {
                    Rect columnRect = new Rect(curX, startY - columnLabelHeight, columnLabelWidth, VisibleRows * rowLabelHeight + columnLabelHeight + 10f);
                    Widgets.DrawHighlight(columnRect);

                    if ((FactionType)col == FactionType.Guest)
                    {
                        TipSignal tip = new TipSignal(Tr.ForGuestUse_Desc, columnTitleRect.GetHashCode());
                        TooltipHandler.TipRegion(columnTitleRect, tip);

                    }
                }
                Widgets.Label(columnTitleRect, columnTitles[col]);
                curX += columnLabelWidth;

                // Check/uncheck all checkboxes in this column
                if (Widgets.ButtonInvisible(columnTitleRect))
                {
                    bool isChecked = true;
                    // Set all checkboxes in this column based on column title state
                    for (int row = 0; row < TotalRows; row++)
                    {
                        if (!primarySelection.restrictions[col][row])
                        {
                            isChecked = false;
                            break;
                        }
                    }
                    for (int row = 0; row < TotalRows; row++)
                    {
                        primarySelection.restrictions[col][row] = !isChecked;
                    }
                    isChanged = true;
                }
            }
        }
        private void DrawRowTitles(Rect inRect, float rowLabelHeight, float rowLabelWidth, float startX, float startY, float columnLabelWidth)
        {
            // Draw row titles and their corresponding check/uncheck functionality
            float curY = startY;
            for (int row = 0; row < TotalRows; row++)
            {
                if (((KindType)row == KindType.Animal || (KindType)row == KindType.Insect) && !RJWSettings.bestiality_enabled) { continue; }
                if ((KindType)row == KindType.Mechanoid && !RJWPregnancySettings.mechanoid_pregnancy_enabled) { continue; }
                Rect rowTitleRect = new Rect(inRect.x, curY, rowLabelWidth, rowLabelHeight);
                if (Mouse.IsOver(rowTitleRect))
                {
                    Rect rowRect = new Rect(inRect.x, curY, VisibleColumns * columnLabelWidth + startX, rowLabelHeight);
                    Widgets.DrawHighlight(rowRect);

                    if ((KindType)row == KindType.Animal)
                    {
                        TipSignal tip = new TipSignal(Tr.ForAnimalUse_Desc, rowTitleRect.GetHashCode());
                        TooltipHandler.TipRegion(rowTitleRect, tip);
                    }
                    else if ((KindType)row == KindType.Insect)
                    {
                        TipSignal tip = new TipSignal(Tr.ForInsectUse_Desc, rowTitleRect.GetHashCode());
                        TooltipHandler.TipRegion(rowTitleRect, tip);
                    }
                    else if ((KindType)row == KindType.Mechanoid)
                    {
                        TipSignal tip = new TipSignal(Tr.ForMechanoidUse_Desc, rowTitleRect.GetHashCode());
                        TooltipHandler.TipRegion(rowTitleRect, tip);
                    }
                }
                Widgets.Label(rowTitleRect, rowTitles[row]);
                curY += rowLabelHeight;

                // Check/uncheck all checkboxes in this row
                if (Widgets.ButtonInvisible(rowTitleRect))
                {
                    bool isChecked = true;
                    // Set all checkboxes in this row based on row title state
                    for (int col = 0; col < TotalColumns; col++)
                    {
                        if (!primarySelection.restrictions[col][row])
                        {
                            isChecked = false;
                            break;
                        }
                    }
                    for (int col = 0; col < TotalColumns; col++)
                    {
                        primarySelection.restrictions[col][row] = !isChecked;
                    }
                    isChanged = true;
                }
            }
        }
        private void DrawCheckboxes(float rowLabelHeight, float columnLabelWidth, float startX, float startY)
        {
            // Draw checkboxes
            float checkboxSize = Mathf.Min(rowLabelHeight, columnLabelWidth);
            float curY = startY;
            for (int row = 0; row < TotalRows; row++)
            {
                float curX = startX;
                if (((KindType)row == KindType.Animal || (KindType)row == KindType.Insect) && !RJWSettings.bestiality_enabled) { continue; }
                if ((KindType)row == KindType.Mechanoid && !RJWPregnancySettings.mechanoid_pregnancy_enabled) { continue; }
                for (int col = 0; col < TotalColumns; col++)
                {
                    if ((FactionType)col == FactionType.Slave && !ModsConfig.IdeologyActive) { continue; }
                    bool isChecked = primarySelection.restrictions[col][row];
                    Texture2D texture = isChecked ? Widgets.CheckboxOnTex : Widgets.CheckboxOffTex;
                    Rect checkboxRect = new Rect(curX + ((columnLabelWidth - checkboxSize) / 2), curY + ((rowLabelHeight - checkboxSize) / 2), checkboxSize, checkboxSize);
                    if (Widgets.ButtonImage(checkboxRect, texture))
                    {
                        primarySelection.restrictions[col][row] = !isChecked;
                        isChanged = true;
                    }
                    curX += columnLabelWidth;
                }
                curY += rowLabelHeight;
            }
        }
    }
}


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

namespace RJW_Onahole.UI
{
    [StaticConstructorOnStartup]
    public static class UIWidgets
    {
        public static void LabeledSliderNumericDisplay(Rect rect, string label, ref float value, float min, float max, string format = "F2")
        {
            Widgets.Label(rect.LeftHalf(), label.Translate());
            Widgets.HorizontalSlider(rect.RightHalf().BottomHalf(), ref value, new FloatRange(min, max));
            string buffer = value.ToString(format);
            Rect topRect = rect.RightHalf().TopHalf();
            topRect.x += topRect.width / 4;
            topRect.width /= 2;
            Widgets.TextFieldNumeric(topRect, ref value, ref buffer, min, max);
        }
        public static List<Texture2D> texturePool = new List<Texture2D>()
        {
            SolidColorMaterials.NewSolidColorTexture(new Color(0f, 0f, 0f)),
            SolidColorMaterials.NewSolidColorTexture(new Color(0.1f, 0.3f, 0.1f)),
            SolidColorMaterials.NewSolidColorTexture(new Color(0.3f, 0.1f, 0.3f)),
            SolidColorMaterials.NewSolidColorTexture(new Color(0.1f, 0.1f, 0.3f)),
            SolidColorMaterials.NewSolidColorTexture(new Color(0.3f, 0.3f, 0.1f)),
            SolidColorMaterials.NewSolidColorTexture(new Color(0.2f, 0.4f, 0.2f)),
            SolidColorMaterials.NewSolidColorTexture(new Color(0.4f, 0.2f, 0.4f)),
            SolidColorMaterials.NewSolidColorTexture(new Color(0.2f, 0.2f, 0.4f)),
            SolidColorMaterials.NewSolidColorTexture(new Color(0.4f, 0.4f, 0.2f)),
            SolidColorMaterials.NewSolidColorTexture(new Color(0.2f, 0.4f, 0.4f)),
            SolidColorMaterials.NewSolidColorTexture(new Color(0.4f, 0.2f, 0.2f)),
        };
        public static Dictionary<Color, Texture2D> bufferedTexture = new Dictionary<Color, Texture2D>();
        public static void FillableBarMultiThingDef(Rect rect, Dictionary<string, float> fluidsToAmounts, int max, ref Dictionary<string, Texture2D> legends)
        {
            GUI.DrawTexture(rect, texturePool[0]);
            float x = rect.x;
            int poolIndex = 1;
            foreach (KeyValuePair<string, float> kvp in fluidsToAmounts)
            {
                if (kvp.Value == 0) { continue; }
                float width = kvp.Value / max * rect.width;
                Rect subRect = new Rect(x, rect.y, width, rect.height);
                ThingDef thingDef = DefDatabase<SexFluidDef>.GetNamed(kvp.Key)?.filth;
                Color? color = thingDef?.graphicData?.color;
                if (color != null)
                {
                    if (!bufferedTexture.ContainsKey((Color)color))
                    {
                        bufferedTexture[(Color)color] = SolidColorMaterials.NewSolidColorTexture((Color)color);
                    }
                    GUI.DrawTexture(subRect, bufferedTexture[(Color)color]);
                    legends[kvp.Key] = bufferedTexture[(Color)color];
                }
                else
                {
                    GUI.DrawTexture(subRect, texturePool[poolIndex]);
                    legends[kvp.Key] = texturePool[poolIndex];
                    poolIndex++;
                    if (poolIndex >= texturePool.Count)
                    {
                        poolIndex = 1;
                    }

                }
                x += width;
            }
        }
        private static readonly Texture2D Food = CacheUtility.Tex.IconFood;
        public static void FilthConversionWidget(Rect rect, SexFluidDef sexFluidDef, CompCumCollector compCumCollector, HashSet<CompCumCollector> cumCollectors)
        {
            Text.Anchor = TextAnchor.MiddleLeft;
            bool changed = false;
            float lineHeight = rect.height / 2f;
            FluidSetting fluidSetting = compCumCollector.GetFluidSetting(sexFluidDef);
            ThingDef convertibleThingDef = fluidSetting?.SexFluid?.consumable;
            string label = sexFluidDef.LabelCap + ": " + cumCollectors.Sum(x => x.GetFluidAmount(sexFluidDef)).ToStringByStyle(ToStringStyle.Integer) + "ml";
            Rect fluidLabelRect = rect.LeftPart(0.3f);
            Widgets.Label(fluidLabelRect, label);

            Rect convertThingRect = rect.RightPart(0.7f);
            convertThingRect = new Rect(convertThingRect.x, convertThingRect.y, convertThingRect.width - (3 * lineHeight), lineHeight * 2);
            Rect iconRect = new Rect(convertThingRect.x, convertThingRect.y + (lineHeight / 2f), lineHeight, lineHeight);

            // Draw selected item
            if (convertibleThingDef != null) { Widgets.ThingIcon(iconRect, convertibleThingDef); }
            Rect labelRect = new Rect(convertThingRect.x + lineHeight, rect.y, convertThingRect.width - lineHeight, lineHeight * 2);
            bool isConvertible = fluidSetting.CanAutoConvert();
            if (!fluidSetting.CanAutoConvert())
            {
                Widgets.Label(labelRect, Tr.NoConvertibleItem);
            }
            else
            {
                Widgets.Label(labelRect, convertibleThingDef.LabelCap);
            }
            if (Mouse.IsOver(convertThingRect) && fluidSetting.CanAutoConvert()) { Widgets.DrawHighlight(convertThingRect); }
            if (Widgets.ButtonInvisible(convertThingRect) && fluidSetting.CanAutoConvert())
            {
                Find.WindowStack.Add(new Dialog_InfoCard(convertibleThingDef));
            }
            //Feeder Button
            if (fluidSetting.CanFeed())
            {
                Rect feedLabelRect = new Rect(rect.x + rect.width - 3 * lineHeight, rect.y, lineHeight, lineHeight);
                Text.Font = GameFont.Tiny;
                Widgets.Label(feedLabelRect, fluidSetting.NutritionPerUnit.ToStringByStyle(ToStringStyle.FloatMaxThree));
                Text.Font = GameFont.Small;
                Rect feedIconRect = new Rect(rect.x + rect.width - 3 * lineHeight, rect.y + lineHeight, lineHeight, lineHeight);
                if (Widgets.ButtonImage(feedIconRect, Food, true, fluidSetting.FeedTipString()))
                {
                    changed = true;
                    fluidSetting.AllowFeed = !fluidSetting.AllowFeed;
                }
                Texture2D textureFeed = fluidSetting.AllowFeed ? CacheUtility.Tex.CheckOn : CacheUtility.Tex.CheckOff;
                Widgets.DrawTextureFitted(new Rect(feedIconRect.x, feedIconRect.y, feedIconRect.width / 2, feedIconRect.height / 2), textureFeed, 1);
            }
            //AutoConvert Button
            Rect convertLabelRect = new Rect(rect.x + rect.width - 2 * lineHeight, rect.y, lineHeight, lineHeight);
            Rect convertIconRect = new Rect(rect.x + rect.width - 2 * lineHeight, rect.y + lineHeight, lineHeight, lineHeight);
            if (convertibleThingDef != null)
            {
                Text.Font = GameFont.Tiny;
                Widgets.Label(convertLabelRect, (1f / fluidSetting.SexFluid.consumableFluidRatio).ToStringByStyle(ToStringStyle.FloatMaxOne) + "mL");
                Text.Font = GameFont.Small;
                if (Widgets.ButtonImage(convertIconRect, CacheUtility.Tex.IconAutoLoad, true, fluidSetting.AutoConvertTipString()))
                {
                    changed = true;
                    fluidSetting.AutoConvert = !fluidSetting.AutoConvert;
                }
                Texture2D textureConvert = fluidSetting.AutoConvert ? CacheUtility.Tex.CheckOn : CacheUtility.Tex.CheckOff;
                GUI.color = fluidSetting.CanAutoConvert() ? Color.white : Color.grey;
                Widgets.DrawTextureFitted(new Rect(convertIconRect.x, convertIconRect.y, convertIconRect.width / 2, convertIconRect.height / 2), textureConvert, 1);
                GUI.color = Color.white;
            }

            //Destroy Button
            Rect destroyIconRect = new Rect(rect.x + rect.width - lineHeight, rect.y + lineHeight, lineHeight, lineHeight);
            if (Widgets.ButtonImage(destroyIconRect, TexButton.Delete, true, Tr.Destroy))
            {
                changed = true;
                fluidSetting.Destroy = !fluidSetting.Destroy;
            }
            Texture2D textureDestroy = fluidSetting.Destroy ? CacheUtility.Tex.CheckOn : CacheUtility.Tex.CheckOff;
            Widgets.DrawTextureFitted(new Rect(destroyIconRect.x, destroyIconRect.y, destroyIconRect.width / 2, destroyIconRect.height / 2), textureDestroy, 1);
            if (changed)
            {
                compCumCollector.Notify_SettingsChanged();
                foreach (CompCumCollector comp in cumCollectors)
                {
                    if (comp == compCumCollector) { continue; }
                    comp.GetFluidSetting(sexFluidDef).CopyFrom(fluidSetting);
                    comp.Notify_SettingsChanged();
                }
            }
            Text.Anchor = TextAnchor.UpperLeft;
        }
        public static void StoredThingInfo(Rect rect, Thing thing)
        {
            float lineHeight = rect.height - 2f;
            Widgets.ThingIcon(new Rect(rect.x, rect.y, lineHeight, lineHeight), thing);
            Widgets.InfoCardButton(rect.x + rect.width - lineHeight, rect.y, thing);
            rect.width -= lineHeight;
            if (Widgets.ButtonImage(new Rect(rect.x + rect.width - lineHeight, rect.y, lineHeight, lineHeight), TexButton.Drop)) { thing.holdingOwner.TryDrop(thing, ThingPlaceMode.Near, out _); }
            rect.width -= lineHeight;
            Rect labelRect = new Rect(rect.x + 36f, rect.y, rect.width - 36f, rect.height);
            Widgets.Label(labelRect, thing.LabelCap);
        }
    }
}


code:
using RimWorld;
using System;
using Verse;
using UnityEngine;
using RJW_Onahole.Utilities;
using System.Collections.Generic;
using System.Linq;
using RJW_Onahole.Comps;
using RJW_Onahole.Data;
using rjw;
namespace RJW_Onahole.UI
{
    public class Window_ConfigureStorage : Window_DynamicMultiselect<Building_OnaholeBed>
    {
        public override Vector2 InitialSize => new Vector2(800, 400);
        private CompCumCollector cumCollector;
        private HashSet<CompCumCollector> cumCollectors;
        private const float LineHeight = 30f;
        private Vector2 scrollPosition = Vector2.zero;
        private Vector2 scrollPosition2 = Vector2.zero;
        private List<Thing> AllThings
        {
            get
            {
                List<Thing> list = new List<Thing>();
                foreach (ThingOwner thingOwner in this.StoredThings)
                {
                    foreach (Thing thing in thingOwner)
                    {
                        list.Add(thing);
                    }
                }
                return list;
            }
        }
        private List<ThingOwner> StoredThings
        {
            get
            {
                List<ThingOwner> list = new List<ThingOwner>();
                foreach (Building_OnaholeBed bed in this.selectedThings)
                {
                    if (bed is IThingHolder holder)
                    {
                        list.Add(holder.GetDirectlyHeldThings());
                    }
                }
                return list;
            }
        }
        public Window_ConfigureStorage() : base()
        {
            Notify_SelectionChanged();
        }
        protected override void Notify_SelectionChanged()
        {
            this.cumCollector = this.selectedThings.FirstOrDefault()?.TryGetComp<CompCumCollector>();
            if (this.selectedThings.Count > 0)
            {
                this.cumCollectors = new HashSet<CompCumCollector>();
                foreach (Thing thing in this.selectedThings)
                {
                    CompCumCollector comp = thing.TryGetComp<CompCumCollector>();
                    if (comp != null)
                    {
                        this.cumCollectors.Add(comp);
                    }
                }
            }

        }
        public override void DoWindowContents(Rect inRect)
        {
            DrawItems(inRect.LeftHalf().ContractedBy(10f));
            DrawFluids(inRect.RightHalf().ContractedBy(10f));
        }
        private void DrawItems(Rect inRect)
        {
            bool isAltColor = false;
            Text.Font = GameFont.Medium;
            string thingLabel = OTr.Stockpiles;
            float height = Text.CalcHeight(thingLabel, inRect.width);
            Widgets.Label(new Rect(inRect.x, inRect.y, inRect.width, height), thingLabel);
            Text.Font = GameFont.Small;
            inRect.y += height;
            inRect.height -= height;

            Rect outRect = inRect.ContractedBy(10f);
            Rect viewRect = new Rect(0f, 0f, outRect.width, this.AllThings.Count * LineHeight);
            Widgets.AdjustRectsForScrollView(inRect, ref outRect, ref viewRect);
            Widgets.BeginScrollView(outRect, ref scrollPosition, viewRect);
            float y = 0f;
            foreach (Thing thing in this.AllThings)
            {
                if (isAltColor) { Widgets.DrawBoxSolid(new Rect(0, y, outRect.width, LineHeight), Color.grey); }
                isAltColor = !isAltColor;
                UIWidgets.StoredThingInfo(new Rect(0, y, outRect.width, LineHeight), thing);
                y += LineHeight;
            }
            Widgets.EndScrollView();
        }
        private void DrawFluids(Rect inRect)
        {
            if (cumCollector == null) { return; }
            bool isAltColor = false;
            Text.Font = GameFont.Medium;
            string fluidLabel = Tr.FluidStorage;
            float height = Text.CalcHeight(fluidLabel, inRect.width);
            Widgets.Label(new Rect(inRect.x, inRect.y, inRect.width, height), fluidLabel);
            Text.Font = GameFont.Small;
            inRect.y += height;
            inRect.height -= height;

            Rect outRect = inRect.ContractedBy(10f);
            Rect viewRect = new Rect(0f, 0f, outRect.width, CacheUtility.usefulSexFluidDefs.Count * 2f * LineHeight);
            Widgets.AdjustRectsForScrollView(inRect, ref outRect, ref viewRect);
            Widgets.BeginScrollView(outRect, ref scrollPosition2, viewRect);
            float y = 0f;
            // Draw fluid conversion
            foreach (SexFluidDef sexFluidDef in CacheUtility.usefulSexFluidDefs.OrderBy(x => !x.CanAutoConvert()).ThenBy(x => x.defName))
            {
                if (isAltColor) { Widgets.DrawBoxSolid(new Rect(0, y, outRect.width, LineHeight * 2f), Color.black); }
                isAltColor = !isAltColor;
                UIWidgets.FilthConversionWidget(new Rect(0, y, outRect.width, LineHeight * 2f), sexFluidDef, cumCollector, cumCollectors);
                y += LineHeight * 2f;
            }
            Widgets.EndScrollView();
        }
    }
}


code:
using RimWorld;
using System;
using Verse;
using UnityEngine;
using RJW_Onahole.Utilities;
using System.Collections.Generic;
using System.Linq;
using RJW_Onahole.Data;
using rjw;
using RJW_Onahole.Comps;
namespace RJW_Onahole.UI
{
    public abstract class Window_DynamicMultiselect<T> : Window where T : Thing
    {
        protected List<T> selectedThings = new List<T>();
        public Window_DynamicMultiselect()
        {
            doCloseX = true;
            draggable = true;
            closeOnClickedOutside = false;
            EventUtility.OnPostMapClicks += ReselectIfNeeded;
            selectedThings = new List<T>(Find.Selector.SelectedObjects.OfType<T>());

        }
        protected void ReselectIfNeeded()
        {
            if (Find.Selector.FirstSelectedObject is T )
            {
                this.OnSelectionChanged();
                Notify_SelectionChanged();
            }
        }
        protected void OnSelectionChanged()
        {
            selectedThings.Clear();
            selectedThings.AddRange(Find.Selector.SelectedObjects.OfType<T>());
        }
        protected abstract void Notify_SelectionChanged();
        public override void PostClose()
        {
            base.PostClose();
            EventUtility.OnPostMapClicks -= ReselectIfNeeded;
        }
    }
}


code:
using RimWorld;
using System;
using Verse;
using UnityEngine;
using RJW_Onahole.Utilities;
using System.Collections.Generic;
using System.Linq;
using RJW_Onahole.Data;
using rjw;
using RJW_Onahole.Comps;
namespace RJW_Onahole.UI
{
    public class Window_SetRestriction : Window_DynamicMultiselect<Building_OnaholeBed>
    {
        public override Vector2 InitialSize => new Vector2(800f, 700f);
        private Widget_SetRestriction setRestrictionWidget;
        private Widget_MultiAssignable assignableWidget;
        private Widget_SetPrice setPriceWidget;

        public Window_SetRestriction() : base()
        {
            this.Notify_SelectionChanged();
        }
        public override void DoWindowContents(Rect inRect)
        {
            if (selectedThings.Count == 0) { this.Close(); }

            setRestrictionWidget.Draw(inRect.TopPart(0.4f));
            if (this.selectedThings.First().useRestriction.AllowsAnyFromFaction(FactionType.Colonist))
            {
                assignableWidget.Draw(inRect.BottomPart(0.6f).LeftHalf());
            }
            if (this.selectedThings.First().useRestriction.AllowsAnyFromFaction(FactionType.Guest))
            {
                setPriceWidget.Draw(inRect.BottomPart(0.6f).RightHalf());
            }
        }
        protected override void Notify_SelectionChanged()
        {
            if (selectedThings.Count == 0) { return; }
            ApplySettings();
            setRestrictionWidget = new Widget_SetRestriction(this.selectedThings.First().useRestriction, this.selectedThings.Select(x => x.useRestriction).ToList());
            assignableWidget = new Widget_MultiAssignable(this.selectedThings.First().GetComp<CompMultiAssignable>(), this.selectedThings.Select(x => x.GetComp<CompMultiAssignable>()).ToList());
            setPriceWidget = new Widget_SetPrice(this.selectedThings.First(), this.selectedThings);
        }
        public override void PostClose()
        {
            base.PostClose();
            ApplySettings();
        }
        protected void ApplySettings()
        {
            setRestrictionWidget?.OnClose();
            assignableWidget?.OnClose();
            setPriceWidget?.OnClose();
        }

    }
}