using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using HarmonyLib;
using RimWorld;
using Verse;
using UnityEngine;

namespace RDFGenes
{
    public static class RDFGeneUtility
    {
        // Internal implementation genes are real GeneDefs because Big & Small consumes
        // their PawnExtension graphics data. They must stay in DefDatabase so the head
        // helper system can resolve them by defName. Cache and filter them from xenotype-editor
        // layout lists instead of deleting the defs.
        private static readonly Dictionary<string, GeneDef> HiddenInternalGeneDefs = new Dictionary<string, GeneDef>();

        // v1.0.4p PERFORMANCE PASS:
        // Keep hot render/tick paths free of repeated DefDatabase scans, LINQ allocations and
        // reflection. Gene changes are very rare compared with rendering/ticking, so a weak
        // per-pawn cache is invalidated whenever Pawn_GeneTracker adds/removes a gene.
        private sealed class RDFPawnCache
        {
            public readonly Dictionary<string, bool> activeGeneByDefName = new Dictionary<string, bool>();

            public bool bodyScanned;
            public bool hasRDFBody;
            public bool hasRubberBitchBody;
            public bool hasDollmareBody;
            public bool hasSentientDisplayDollBody;
            public bool hasBodyShape;
            public string shapeKey;
            public string neutralPath;
            public bool useRDFSkinMaterials;
            public Vector2 bodyDrawSize = Vector2.one;
            public bool tintNeutralWithSkin = true;

            public bool skinScanned;
            public bool hasRDFSkin;
            public bool hasActiveRDFSkin;
            public string skinColorKey;
            public string skinFinishKey;
            public Color activeRDFSkinColor = Color.white;
        }

        private sealed class PawnAccessor
        {
            public FieldInfo field;
            public PropertyInfo property;
            public FieldInfo[] fallbackPawnFields;
        }

        public sealed class SAZFieldCache
        {
            public FieldInfo currentHediffName;
            public FieldInfo bodyPartName;
            public FieldInfo colorType;
            public FieldInfo isBreasts;
            public FieldInfo drawColor1;
            public FieldInfo drawColor2;
        }

        private static readonly ConditionalWeakTable<Pawn, RDFPawnCache> PawnCaches = new ConditionalWeakTable<Pawn, RDFPawnCache>();
        private static readonly Dictionary<string, GeneDef> GeneDefCache = new Dictionary<string, GeneDef>();
        private static readonly Dictionary<string, HediffDef> HediffDefCache = new Dictionary<string, HediffDef>();
        private static readonly Dictionary<string, BodyTypeDef> BodyTypeDefCache = new Dictionary<string, BodyTypeDef>();
        private static readonly Dictionary<Type, PawnAccessor> PawnAccessorCache = new Dictionary<Type, PawnAccessor>();
        private static readonly Dictionary<Type, SAZFieldCache> SAZFieldsByType = new Dictionary<Type, SAZFieldCache>();

        private static readonly Dictionary<string, Color> RDFSkinColorByDefName = new Dictionary<string, Color>
        {
            { "RDF_Skin_Pink_Glossy", new Color(0.805f, 0.386f, 0.560f) },
            { "RDF_Skin_Pink_Metallic", new Color(0.809f, 0.494f, 0.626f) },
            { "RDF_Skin_Pink_Wet", new Color(0.551f, 0.278f, 0.391f) },
            { "RDF_Skin_Black_Glossy", new Color(0.198f, 0.193f, 0.207f) },
            { "RDF_Skin_Black_Metallic", new Color(0.353f, 0.348f, 0.358f) },
            { "RDF_Skin_Black_Wet", new Color(0.151f, 0.151f, 0.160f) },
            { "RDF_Skin_Purple_Glossy", new Color(0.428f, 0.264f, 0.584f) },
            { "RDF_Skin_Purple_Metallic", new Color(0.527f, 0.405f, 0.645f) },
            { "RDF_Skin_Purple_Wet", new Color(0.306f, 0.198f, 0.405f) },
            { "RDF_Skin_Silver_Glossy", new Color(0.612f, 0.631f, 0.668f) },
            { "RDF_Skin_Silver_Metallic", new Color(0.664f, 0.678f, 0.706f) },
            { "RDF_Skin_Silver_Wet", new Color(0.424f, 0.438f, 0.461f) },
            { "RDF_Skin_Gold_Glossy", new Color(0.739f, 0.569f, 0.278f) },
            { "RDF_Skin_Gold_Metallic", new Color(0.758f, 0.635f, 0.414f) },
            { "RDF_Skin_Gold_Wet", new Color(0.508f, 0.400f, 0.202f) },
            { "RDF_Skin_HotPink_Glossy", new Color(0.805f, 0.212f, 0.513f) },
            { "RDF_Skin_HotPink_Metallic", new Color(0.809f, 0.362f, 0.593f) },
            { "RDF_Skin_HotPink_Wet", new Color(0.551f, 0.165f, 0.362f) },
            { "RDF_Skin_Bubblegum_Glossy", new Color(0.805f, 0.518f, 0.631f) },
            { "RDF_Skin_Bubblegum_Metallic", new Color(0.809f, 0.598f, 0.678f) },
            { "RDF_Skin_Bubblegum_Wet", new Color(0.551f, 0.367f, 0.438f) },
            { "RDF_Skin_RDFPink_Glossy", new Color(0.776f, 0.278f, 0.471f) },
            { "RDF_Skin_RDFPink_Metallic", new Color(0.786f, 0.414f, 0.560f) },
            { "RDF_Skin_RDFPink_Wet", new Color(0.532f, 0.202f, 0.329f) }
        };

        private static RDFPawnCache CreatePawnCache(Pawn pawn)
        {
            return new RDFPawnCache();
        }

        private static RDFPawnCache GetPawnCache(Pawn pawn)
        {
            return PawnCaches.GetValue(pawn, CreatePawnCache);
        }

        public static void InvalidatePawnCache(Pawn pawn)
        {
            if (pawn != null) PawnCaches.Remove(pawn);
        }

        public static GeneDef GetGeneDefCached(string defName)
        {
            if (string.IsNullOrEmpty(defName)) return null;
            GeneDef def;
            if (GeneDefCache.TryGetValue(defName, out def)) return def;
            def = DefDatabase<GeneDef>.GetNamedSilentFail(defName);
            GeneDefCache[defName] = def;
            return def;
        }

        public static HediffDef GetHediffDefCached(string defName)
        {
            if (string.IsNullOrEmpty(defName)) return null;
            HediffDef def;
            if (HediffDefCache.TryGetValue(defName, out def)) return def;
            def = DefDatabase<HediffDef>.GetNamedSilentFail(defName);
            HediffDefCache[defName] = def;
            return def;
        }

        public static BodyTypeDef GetBodyTypeDefCached(string defName)
        {
            if (string.IsNullOrEmpty(defName)) return null;
            BodyTypeDef def;
            if (BodyTypeDefCache.TryGetValue(defName, out def)) return def;
            def = DefDatabase<BodyTypeDef>.GetNamedSilentFail(defName);
            BodyTypeDefCache[defName] = def;
            return def;
        }

        public static Pawn ExtractPawnFast(object instance)
        {
            if (instance == null) return null;
            Pawn direct = instance as Pawn;
            if (direct != null) return direct;

            Type t = instance.GetType();
            PawnAccessor accessor;
            if (!PawnAccessorCache.TryGetValue(t, out accessor))
            {
                accessor = new PawnAccessor();
                string[] names = new string[] { "pawn", "___pawn", "_pawn", "Pawn", "<pawn>k__BackingField" };
                for (int i = 0; i < names.Length && accessor.field == null; i++)
                    accessor.field = AccessTools.Field(t, names[i]);

                if (accessor.field == null)
                {
                    accessor.property = AccessTools.Property(t, "pawn");
                    if (accessor.property == null) accessor.property = AccessTools.Property(t, "Pawn");
                }

                if (accessor.field == null && accessor.property == null)
                {
                    List<FieldInfo> pawnFields = new List<FieldInfo>();
                    FieldInfo[] fields = t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
                    for (int i = 0; i < fields.Length; i++)
                    {
                        FieldInfo candidate = fields[i];
                        if (candidate != null && typeof(Pawn).IsAssignableFrom(candidate.FieldType))
                            pawnFields.Add(candidate);
                    }
                    accessor.fallbackPawnFields = pawnFields.ToArray();
                }

                PawnAccessorCache[t] = accessor;
            }

            if (accessor.field != null)
                return accessor.field.GetValue(instance) as Pawn;

            if (accessor.property != null && accessor.property.CanRead)
            {
                try { return accessor.property.GetValue(instance, null) as Pawn; }
                catch { return null; }
            }

            if (accessor.fallbackPawnFields != null)
            {
                for (int i = 0; i < accessor.fallbackPawnFields.Length; i++)
                {
                    Pawn p = accessor.fallbackPawnFields[i].GetValue(instance) as Pawn;
                    if (p != null) return p;
                }
            }
            return null;
        }

        public static SAZFieldCache GetSAZFields(Type t)
        {
            if (t == null) return null;
            SAZFieldCache fields;
            if (SAZFieldsByType.TryGetValue(t, out fields)) return fields;

            fields = new SAZFieldCache();
            fields.currentHediffName = AccessTools.Field(t, "currentHediffName");
            fields.bodyPartName = AccessTools.Field(t, "bodyPartName");
            fields.colorType = AccessTools.Field(t, "colorType");
            fields.isBreasts = AccessTools.Field(t, "isBreasts");
            fields.drawColor1 = AccessTools.Field(t, "drawColor1");
            fields.drawColor2 = AccessTools.Field(t, "drawColor2");
            SAZFieldsByType[t] = fields;
            return fields;
        }

        public static bool IsInternalImplementationGene(GeneDef def)
        {
            if (def == null || string.IsNullOrEmpty(def.defName)) return false;
            return def.defName.StartsWith("RDF_Gene_DisplayDollHeadGraphic_", StringComparison.Ordinal) ||
                   def.defName == "RDF_Gene_Internal_AnimalCentauress" ||
                   def.defName == "RDF_Gene_Internal_SentientRubberdollVariantController";
        }

        public static void HideInternalGenesFromPickers()
        {
            try
            {
                List<GeneDef> internalDefs = DefDatabase<GeneDef>.AllDefsListForReading
                    .Where(IsInternalImplementationGene).ToList();

                foreach (GeneDef def in internalDefs)
                    HiddenInternalGeneDefs[def.defName] = def;

                if (internalDefs.Count > 0)
                    Log.Message("[RDF Genes] Cached " + internalDefs.Count + " internal implementation genes; xenotype UI filtering is active.");
            }
            catch (Exception ex)
            {
                Log.Warning("[RDF Genes] Could not cache internal implementation genes: " + ex.Message);
            }
        }

        public static GeneDef GetInternalGeneDef(string defName)
        {
            if (string.IsNullOrEmpty(defName)) return null;
            GeneDef def;
            if (HiddenInternalGeneDefs.TryGetValue(defName, out def) && def != null) return def;
            return GetGeneDefCached(defName);
        }

        public static bool HasGene(Pawn pawn, string defName)
        {
            if (pawn == null || pawn.genes == null || string.IsNullOrEmpty(defName)) return false;

            RDFPawnCache cache = GetPawnCache(pawn);
            bool cached;
            if (cache.activeGeneByDefName.TryGetValue(defName, out cached)) return cached;

            GeneDef def = GetGeneDefCached(defName);
            if (def == null)
            {
                cache.activeGeneByDefName[defName] = false;
                return false;
            }

            bool result = false;
            List<Gene> genes = pawn.genes.GenesListForReading;
            for (int i = 0; i < genes.Count; i++)
            {
                Gene gene = genes[i];
                if (gene != null && gene.def == def && gene.Active)
                {
                    result = true;
                    break;
                }
            }

            cache.activeGeneByDefName[defName] = result;
            return result;
        }

        public static Hediff EnsureHediff(Pawn pawn, HediffDef def)
        {
            if (pawn == null || pawn.health == null || def == null) return null;
            Hediff h = pawn.health.hediffSet.GetFirstHediffOfDef(def);
            if (h == null) h = pawn.health.AddHediff(def);
            return h;
        }

        public static void RemoveHediff(Pawn pawn, HediffDef def)
        {
            if (pawn == null || pawn.health == null || def == null) return;
            Hediff h = pawn.health.hediffSet.GetFirstHediffOfDef(def);
            if (h != null) pawn.health.RemoveHediff(h);
        }

        public static Hediff FindLatexCoating(Pawn pawn)
        {
            if (pawn == null || pawn.health == null) return null;
            foreach (Hediff h in pawn.health.hediffSet.hediffs)
            {
                if (h == null || h.def == null) continue;
                string n = h.def.defName;
                if (n.IndexOf("latex", StringComparison.OrdinalIgnoreCase) >= 0 &&
                    (n.IndexOf("coat", StringComparison.OrdinalIgnoreCase) >= 0 ||
                     n.IndexOf("coated", StringComparison.OrdinalIgnoreCase) >= 0)) return h;
            }
            return null;
        }

        private static void EnsureSkinCache(Pawn pawn, RDFPawnCache cache)
        {
            if (cache.skinScanned) return;
            cache.skinScanned = true;
            cache.hasRDFSkin = false;
            cache.hasActiveRDFSkin = false;
            cache.skinColorKey = null;
            cache.skinFinishKey = null;
            cache.activeRDFSkinColor = Color.white;

            if (pawn == null || pawn.genes == null) return;

            List<Gene> genes = pawn.genes.GenesListForReading;
            for (int i = 0; i < genes.Count; i++)
            {
                Gene gene = genes[i];
                if (gene == null || gene.def == null || string.IsNullOrEmpty(gene.def.defName)) continue;
                string name = gene.def.defName;
                if (!name.StartsWith("RDF_Skin_", StringComparison.Ordinal)) continue;

                cache.hasRDFSkin = true;
                if (!gene.Active) continue;

                Color color;
                if (!RDFSkinColorByDefName.TryGetValue(name, out color)) continue;

                string[] parts = name.Split('_');
                if (parts.Length != 4) continue;

                cache.hasActiveRDFSkin = true;
                cache.skinColorKey = parts[2];
                cache.skinFinishKey = parts[3];
                cache.activeRDFSkinColor = color;
                break;
            }
        }

        private static void EnsureBodyCache(Pawn pawn, RDFPawnCache cache)
        {
            if (cache.bodyScanned) return;
            cache.bodyScanned = true;
            cache.hasRDFBody = false;
            cache.hasRubberBitchBody = false;
            cache.hasDollmareBody = false;
            cache.hasSentientDisplayDollBody = false;
            cache.hasBodyShape = false;
            cache.shapeKey = null;
            cache.neutralPath = null;
            cache.useRDFSkinMaterials = false;
            cache.bodyDrawSize = Vector2.one;
            cache.tintNeutralWithSkin = true;

            if (pawn == null || pawn.genes == null) return;

            List<Gene> genes = pawn.genes.GenesListForReading;
            for (int i = 0; i < genes.Count; i++)
            {
                Gene gene = genes[i];
                if (gene == null || gene.def == null || string.IsNullOrEmpty(gene.def.defName)) continue;
                string name = gene.def.defName;
                if (!name.StartsWith("RDF_Gene_Body_", StringComparison.Ordinal)) continue;

                cache.hasRDFBody = true;
                if (name == "RDF_Gene_Body_RubberBitch") cache.hasRubberBitchBody = true;
                if (name == "RDF_Gene_Body_CentauressSentient" || name == "RDF_Gene_Body_Dollmare") cache.hasDollmareBody = true;
                if (name == "RDF_Gene_Body_SentientDisplayDoll") cache.hasSentientDisplayDollBody = true;

                if (cache.hasBodyShape) continue;

                if (name == "RDF_Gene_Body_DollFigureStandard")
                {
                    cache.shapeKey = "DollFigureStandard";
                    cache.neutralPath = "Things/Pawn/Humanlike/Bodies/RDF/RDF_01_DollFigure_Standard";
                    cache.useRDFSkinMaterials = true;
                    cache.hasBodyShape = true;
                }
                else if (name == "RDF_Gene_Body_CurvyDoll")
                {
                    cache.shapeKey = "CurvyDoll";
                    cache.neutralPath = "Things/Pawn/Humanlike/Bodies/RDF/RDF_02_CurvyDoll";
                    cache.useRDFSkinMaterials = true;
                    cache.hasBodyShape = true;
                }
                else if (name == "RDF_Gene_Body_PlushDoll")
                {
                    cache.shapeKey = "PlushDoll";
                    cache.neutralPath = "Things/Pawn/Humanlike/Bodies/RDF/RDF_03_PlushDoll";
                    cache.useRDFSkinMaterials = true;
                    cache.hasBodyShape = true;
                }
                else if (name == "RDF_Gene_Body_VoluptuousDoll")
                {
                    cache.shapeKey = "VoluptuousDoll";
                    cache.neutralPath = "Things/Pawn/Humanlike/Bodies/RDF/RDF_04_VoluptuousDoll";
                    cache.useRDFSkinMaterials = true;
                    cache.hasBodyShape = true;
                }
                else if (name == "RDF_Gene_Body_DisplayDoll")
                {
                    cache.shapeKey = "DisplayDoll";
                    cache.neutralPath = "Things/Pawn/Humanlike/Bodies/RDF/RDF_05_DisplayDoll";
                    cache.useRDFSkinMaterials = true;
                    cache.hasBodyShape = true;
                }
                else
                {
                    RDFBodyGraphicExtension ext = gene.def.GetModExtension<RDFBodyGraphicExtension>();
                    if (ext != null && !string.IsNullOrEmpty(ext.graphicPath))
                    {
                        cache.neutralPath = ext.graphicPath;
                        cache.shapeKey = !string.IsNullOrEmpty(ext.materialShapeKey)
                            ? ext.materialShapeKey
                            : name.Substring("RDF_Gene_Body_".Length);
                        cache.useRDFSkinMaterials = ext.useRDFSkinMaterials;
                        cache.bodyDrawSize = new Vector2(
                            ext.drawSizeX > 0.01f ? ext.drawSizeX : 1f,
                            ext.drawSizeY > 0.01f ? ext.drawSizeY : 1f);
                        cache.tintNeutralWithSkin = ext.tintNeutralWithSkin;
                        cache.hasBodyShape = true;
                    }
                }
            }
        }

        public static bool TryGetActiveRDFSkinColor(Pawn pawn, out Color color)
        {
            color = Color.white;
            if (pawn == null || pawn.genes == null) return false;
            RDFPawnCache cache = GetPawnCache(pawn);
            EnsureSkinCache(pawn, cache);
            if (!cache.hasActiveRDFSkin) return false;
            color = cache.activeRDFSkinColor;
            return true;
        }

        public static bool HasRDFSkinGenePresent(Pawn pawn)
        {
            if (pawn == null || pawn.genes == null) return false;
            RDFPawnCache cache = GetPawnCache(pawn);
            EnsureSkinCache(pawn, cache);
            return cache.hasRDFSkin;
        }

        public static bool HasRDFBodyGenePresent(Pawn pawn)
        {
            if (pawn == null || pawn.genes == null) return false;
            RDFPawnCache cache = GetPawnCache(pawn);
            EnsureBodyCache(pawn, cache);
            return cache.hasRDFBody;
        }

        public static bool HasRubberBitchBody(Pawn pawn)
        {
            if (pawn == null || pawn.genes == null) return false;
            RDFPawnCache cache = GetPawnCache(pawn);
            EnsureBodyCache(pawn, cache);
            return cache.hasRubberBitchBody;
        }

        public static bool HasDollmareBody(Pawn pawn)
        {
            if (pawn == null || pawn.genes == null) return false;
            RDFPawnCache cache = GetPawnCache(pawn);
            EnsureBodyCache(pawn, cache);
            return cache.hasDollmareBody;
        }

        public static bool HasSentientDisplayDollBody(Pawn pawn)
        {
            if (pawn == null || pawn.genes == null) return false;
            RDFPawnCache cache = GetPawnCache(pawn);
            EnsureBodyCache(pawn, cache);
            return cache.hasSentientDisplayDollBody;
        }

        // 0 = none, 1 = Rubber Bitch, 2 = Sentient Display Doll, 3 = Dollmare.
        // One weak-cache lookup is cheaper than three separate body-family queries in the
        // global BaseHeadOffsetAt postfix.
        public static int GetSpecialHeadBodyKind(Pawn pawn)
        {
            if (pawn == null || pawn.genes == null) return 0;
            RDFPawnCache cache = GetPawnCache(pawn);
            EnsureBodyCache(pawn, cache);
            if (cache.hasRubberBitchBody) return 1;
            if (cache.hasSentientDisplayDollBody) return 2;
            if (cache.hasDollmareBody) return 3;
            return 0;
        }

        public static void EnforceFullBodyEquipmentRules(Pawn pawn)
        {
            if (pawn == null || !HasRDFBodyGenePresent(pawn)) return;

            // Full RDF body replacements are the pawn's outer shell. Ordinary apparel/armor
            // is incompatible with that geometry, so move worn items to inventory (or drop them).
            if (pawn.apparel != null && pawn.apparel.WornApparelCount > 0)
            {
                List<Apparel> worn = pawn.apparel.WornApparel;
                for (int i = worn.Count - 1; i >= 0; i--)
                {
                    Apparel apparel = worn[i];
                    if (apparel == null) continue;
                    try
                    {
                        pawn.apparel.Remove(apparel);
                        bool stored = pawn.inventory != null && pawn.inventory.innerContainer.TryAdd(apparel);
                        if (!stored && pawn.Spawned && pawn.Map != null)
                            GenPlace.TryPlaceThing(apparel, pawn.Position, pawn.Map, ThingPlaceMode.Near);
                    }
                    catch (Exception ex)
                    {
                        Log.Warning("[RDF Genes] Could not remove incompatible apparel from " + pawn.LabelShort + ": " + ex.Message);
                    }
                }
            }

            // Sentient Display Dolls only accept ranged primary weapons.
            if (HasSentientDisplayDollBody(pawn) && pawn.equipment != null)
            {
                ThingWithComps primary = pawn.equipment.Primary;
                if (primary != null && primary.def != null && primary.def.IsWeapon && !primary.def.IsRangedWeapon)
                {
                    try
                    {
                        pawn.equipment.Remove(primary);
                        bool stored = pawn.inventory != null && pawn.inventory.innerContainer.TryAdd(primary);
                        if (!stored && pawn.Spawned && pawn.Map != null)
                            GenPlace.TryPlaceThing(primary, pawn.Position, pawn.Map, ThingPlaceMode.Near);
                    }
                    catch (Exception ex)
                    {
                        Log.Warning("[RDF Genes] Could not reject melee weapon from " + pawn.LabelShort + ": " + ex.Message);
                    }
                }
            }
        }

        public static void EnforceDisplayDollHeadEnvironmentalImmunity(Pawn pawn)
        {
            if (pawn == null || pawn.health == null || !HasGene(pawn, "RDF_Gene_Head_DisplayDoll")) return;
            List<Hediff> hediffs = pawn.health.hediffSet.hediffs;
            for (int i = hediffs.Count - 1; i >= 0; i--)
            {
                Hediff h = hediffs[i];
                if (h == null || h.def == null || string.IsNullOrEmpty(h.def.defName)) continue;
                string n = h.def.defName;
                if (string.Equals(n, "ToxicBuildup", StringComparison.OrdinalIgnoreCase) ||
                    n.IndexOf("rotstink", StringComparison.OrdinalIgnoreCase) >= 0 ||
                    n.IndexOf("rotgas", StringComparison.OrdinalIgnoreCase) >= 0)
                    pawn.health.RemoveHediff(h);
            }
        }

        public static bool IsRDFSkinGene(GeneDef def)
        {
            return def != null && def.defName != null && def.defName.StartsWith("RDF_Skin_", StringComparison.Ordinal);
        }

        public static bool IsRDFBodyGene(GeneDef def)
        {
            return def != null && def.defName != null && def.defName.StartsWith("RDF_Gene_Body_", StringComparison.Ordinal);
        }

        // Detect visual genes that compete with the RDF surface/body.  We intentionally
        // keep ears, tails, paws and other appendage genes untouched.
        public static bool IsCompetingSkinGene(GeneDef def)
        {
            if (def == null || IsRDFSkinGene(def)) return false;
            string n = (def.defName ?? string.Empty).ToLowerInvariant();
            string l = (def.label ?? string.Empty).ToLowerInvariant();

            if (n.StartsWith("skin_", StringComparison.Ordinal) || n == "furskin" ||
                n.Contains("furbody") || n.Contains("fur_body") ||
                n.Contains("scalebody") || n.Contains("scale_body") ||
                l == "skin color" || l == "furbody" || l == "fur body" ||
                l.Contains("furry body")) return true;

            // Also recognize GeneDefs carrying a skinColorOverride without taking a
            // compile-time dependency on the exact GeneDef field layout.
            FieldInfo skinField = AccessTools.Field(def.GetType(), "skinColorOverride");
            if (skinField != null)
            {
                object value = skinField.GetValue(def);
                if (value != null) return true;
            }

            FieldInfo displayField = AccessTools.Field(def.GetType(), "displayCategory");
            object category = displayField != null ? displayField.GetValue(def) : null;
            Def categoryDef = category as Def;
            if (categoryDef != null && categoryDef.defName != null &&
                string.Equals(categoryDef.defName, "Melanin", StringComparison.OrdinalIgnoreCase)) return true;

            return false;
        }

        public static bool IsCompetingBodyGene(GeneDef def)
        {
            if (def == null || IsRDFBodyGene(def)) return false;
            string n = (def.defName ?? string.Empty).ToLowerInvariant();
            string l = (def.label ?? string.Empty).ToLowerInvariant();
            if (n.StartsWith("body_", StringComparison.Ordinal) ||
                l == "standard body" || l == "thin body" || l == "hulk body" || l == "fat body") return true;

            FieldInfo displayField = AccessTools.Field(def.GetType(), "displayCategory");
            object category = displayField != null ? displayField.GetValue(def) : null;
            Def categoryDef = category as Def;
            if (categoryDef != null && categoryDef.defName != null &&
                string.Equals(categoryDef.defName, "BodyType", StringComparison.OrdinalIgnoreCase)) return true;

            return false;
        }

        public static bool TryGetRDFBodyShape(Pawn pawn, out string shapeKey, out string neutralPath)
        {
            bool useRDFSkinMaterials;
            return TryGetRDFBodyShape(pawn, out shapeKey, out neutralPath, out useRDFSkinMaterials);
        }

        public static bool TryGetRDFBodyShape(Pawn pawn, out string shapeKey, out string neutralPath, out bool useRDFSkinMaterials)
        {
            shapeKey = null;
            neutralPath = null;
            useRDFSkinMaterials = false;
            if (pawn == null || pawn.genes == null) return false;

            RDFPawnCache cache = GetPawnCache(pawn);
            EnsureBodyCache(pawn, cache);
            if (!cache.hasBodyShape) return false;

            shapeKey = cache.shapeKey;
            neutralPath = cache.neutralPath;
            useRDFSkinMaterials = cache.useRDFSkinMaterials;
            return true;
        }

        public static bool TryGetRDFSkinMaterial(Pawn pawn, out string colorKey, out string finishKey)
        {
            colorKey = null;
            finishKey = null;
            if (pawn == null || pawn.genes == null) return false;

            RDFPawnCache cache = GetPawnCache(pawn);
            EnsureSkinCache(pawn, cache);
            if (!cache.hasActiveRDFSkin) return false;

            colorKey = cache.skinColorKey;
            finishKey = cache.skinFinishKey;
            return true;
        }

        public static bool TryGetRDFReplacementBodyGraphic(Pawn pawn, out Graphic graphic)
        {
            graphic = null;
            if (pawn == null || pawn.genes == null) return false;

            RDFPawnCache cache = GetPawnCache(pawn);
            EnsureBodyCache(pawn, cache);
            if (!cache.hasBodyShape) return false;

            string colorKey;
            string finishKey;
            if (cache.useRDFSkinMaterials && TryGetRDFSkinMaterial(pawn, out colorKey, out finishKey))
            {
                string materialPath = "Things/Pawn/Humanlike/RDFSkin/" + colorKey + "/" + finishKey + "/" + cache.shapeKey;
                graphic = GraphicDatabase.Get<Graphic_Multi>(materialPath, ShaderDatabase.Cutout, cache.bodyDrawSize, Color.white);
                return graphic != null;
            }

            Color tint = Color.white;
            if (cache.tintNeutralWithSkin && pawn.story != null) tint = pawn.story.SkinColor;
            graphic = GraphicDatabase.Get<Graphic_Multi>(cache.neutralPath, ShaderDatabase.Cutout, cache.bodyDrawSize, tint);
            return graphic != null;
        }


        public static HeadTypeDef DesiredDisplayDollHeadType(Pawn pawn)
        {
            if (pawn == null || pawn.genes == null || !HasGene(pawn, "RDF_Gene_Head_DisplayDoll")) return null;

            string colorKey;
            string finishKey;
            if (!TryGetRDFSkinMaterial(pawn, out colorKey, out finishKey))
            {
                colorKey = "Black";
                finishKey = "Glossy";
            }

            return DefDatabase<HeadTypeDef>.GetNamedSilentFail("RDF_DisplayDollHead_" + colorKey + "_" + finishKey);
        }

        public static void UpdateDisplayDollHeadType(Pawn pawn)
        {
            if (pawn == null || pawn.story == null) return;
            HeadTypeDef desired = DesiredDisplayDollHeadType(pawn);
            if (desired != null && pawn.story.headType != desired) pawn.story.headType = desired;
        }

        public static void RefreshVisuals(Pawn pawn)
        {
            if (pawn == null) return;
            InvalidatePawnCache(pawn);
            UpdateDisplayDollHeadType(pawn);
            ResetSizedApparelCaches(pawn);
            if (pawn.Drawer != null && pawn.Drawer.renderer != null) pawn.Drawer.renderer.SetAllGraphicsDirty();
        }

        public static bool IsRDFBodyType(BodyTypeDef bodyType)
        {
            if (bodyType == null || string.IsNullOrEmpty(bodyType.defName)) return false;
            return bodyType.defName.StartsWith("RDFBody_", StringComparison.Ordinal);
        }

        public static void ResetSizedApparelCaches(Pawn pawn)
        {
            if (pawn == null || pawn.AllComps == null) return;
            foreach (ThingComp comp in pawn.AllComps)
            {
                if (comp == null) continue;
                Type t = comp.GetType();
                if (t == null || t.FullName != "SizedApparel.ApparelRecorderComp") continue;
                MethodInfo resetSkeleton = AccessTools.Method(t, "ResetSkeleton");
                MethodInfo resetBody = AccessTools.Method(t, "ResetBodyAddons");
                if (resetSkeleton != null) resetSkeleton.Invoke(comp, null);
                if (resetBody != null) resetBody.Invoke(comp, null);
                break;
            }
        }

        public static float CoatingRegrowthFactor(Pawn pawn)
        {
            if (!HasGene(pawn, "RDF_Gene_LatexCoatingRegeneration")) return 1f;
            Hediff h = FindLatexCoating(pawn);
            if (h == null) return 1f;
            // Match the public gene description and the ordinary InjuryHealingFactor boost:
            // light +25%, moderate +50%, heavy/full +100%.
            if (h.Severity >= 0.75f) return 2.00f;
            if (h.Severity >= 0.35f) return 1.50f;
            return 1.25f;
        }
    }


    public static class RDFSentientRubberdollSupplementalGenes
    {
        private static List<GeneDef> cachedResolved;
        // Supplemental external genes shared by the finished sentient RDF xenotypes.
        // The two Licentia-style chemical impervious genes are implied/generated defs, so
        // resolve them by normalized label/DefName instead of hard-coding a version-specific name.
        private static readonly string[] ExactDefNames =
        {
            "AlcoholImpervious",
            "GoJuiceImpervious",
            "PsychiteImpervious",
            "SmokeleafImpervious",
            "WakeUpImpervious",
            "rjw_genes_hormonal_saliva"
        };

        private static readonly string[] NormalizedTargets =
        {
            "alcoholimpervious",
            "gojuiceimpervious",
            "psychiteimpervious",
            "smokeleafimpervious",
            "wakeupimpervious",
            "hormonalserumimpervious",
            "aphrodisiacextractimpervious",
            "hormonalsaliva",
            "greatsex",
            "greatersex"
        };

        private static string Normalize(string value)
        {
            if (string.IsNullOrEmpty(value)) return string.Empty;
            return new string(value.Where(char.IsLetterOrDigit).Select(char.ToLowerInvariant).ToArray());
        }

        public static List<GeneDef> ResolveAll()
        {
            if (cachedResolved != null) return cachedResolved;

            HashSet<GeneDef> found = new HashSet<GeneDef>();

            foreach (string defName in ExactDefNames)
            {
                GeneDef def = RDFGeneUtility.GetGeneDefCached(defName);
                if (def != null) found.Add(def);
            }

            HashSet<string> wanted = new HashSet<string>(NormalizedTargets);
            foreach (GeneDef def in DefDatabase<GeneDef>.AllDefsListForReading)
            {
                if (def == null) continue;
                string name = Normalize(def.defName);
                string label = Normalize(def.label);
                bool directMatch = wanted.Contains(name) || wanted.Contains(label);
                bool hormonalImpervious = (name.Contains("hormonalserum") || label.Contains("hormonalserum")) &&
                    (name.Contains("impervious") || label.Contains("impervious"));
                bool aphrodisiacImpervious = (name.Contains("aphrodisiacextract") || label.Contains("aphrodisiacextract")) &&
                    (name.Contains("impervious") || label.Contains("impervious"));
                // The requested "greater sex" gene is the Sex work/skill
                // aptitude/passion gene, NOT RJW hypersexuality.  Depending on the
                // installed skill-gene generator it may be an implied GeneDef with a
                // generated DefName, so resolve it primarily by its visible label.
                bool greatSexPassion = label == "greatsex" || label == "greatersex" ||
                    ((name.Contains("sex") || label.Contains("sex")) &&
                     (name.Contains("great") || name.Contains("greater") || label.Contains("great") || label.Contains("greater")) &&
                     !name.Contains("hypersexual") && !label.Contains("hypersexual")) ||
                    ((name.Contains("sex") || label.Contains("sex")) &&
                     (name.Contains("passion") || label.Contains("passion")) &&
                     (name.Contains("major") || label.Contains("major") || name.Contains("great") || label.Contains("great")));
                if (directMatch || hormonalImpervious || aphrodisiacImpervious || greatSexPassion)
                    found.Add(def);
            }

            cachedResolved = found.ToList();
            return cachedResolved;
        }

        public static void AddToXenotypeDef()
        {
            try
            {
                FieldInfo genesField = AccessTools.Field(typeof(XenotypeDef), "genes");
                if (genesField == null) return;

                List<GeneDef> resolved = ResolveAll();
                string[] targetXenotypes = { "RDF_SentientRubberdoll", "RDF_Dollmare", "RDF_SentientRubberBitch" };
                foreach (string xenotypeName in targetXenotypes)
                {
                    XenotypeDef xenotype = DefDatabase<XenotypeDef>.GetNamedSilentFail(xenotypeName);
                    if (xenotype == null) continue;
                    List<GeneDef> genes = genesField.GetValue(xenotype) as List<GeneDef>;
                    if (genes == null) continue;
                    foreach (GeneDef def in resolved)
                    {
                        if (!genes.Contains(def)) genes.Add(def);
                    }
                }
                Log.Message("[RDF Genes] Rubberdoll/Dollmare/Rubber Bitch supplemental genes resolved: " +
                    string.Join(", ", resolved.Select(g => g.defName).ToArray()));
            }
            catch (Exception ex)
            {
                Log.Warning("[RDF Genes] Could not add final Sentient Rubberdoll supplemental genes to XenotypeDef: " + ex.Message);
            }
        }

        public static void EnsureOnPawn(Pawn pawn)
        {
            if (pawn == null || pawn.genes == null) return;

            // This is the legacy supplemental package selected specifically for the original
            // three finished xenotypes. New combat xenotypes also use the shared appearance
            // controller, so do not let that controller accidentally grant them this package.
            string xenotypeName = pawn.genes.Xenotype != null ? pawn.genes.Xenotype.defName : null;
            if (xenotypeName != "RDF_SentientRubberdoll" &&
                xenotypeName != "RDF_Dollmare" &&
                xenotypeName != "RDF_SentientRubberBitch")
                return;

            foreach (GeneDef def in ResolveAll())
            {
                bool has = pawn.genes.GenesListForReading.Any(g => g != null && g.def == def);
                if (!has) pawn.genes.AddGene(def, true);
            }
        }
    }

    // Optional role/cosmetic genes for the v1.0.4 RDF faction-ready xenotypes.
    // These are resolved at runtime instead of hard-referenced in XML so RDF Genes remains
    // load-safe when a cosmetic/Big & Small addon changes or is not installed. The core
    // xenotype, RDF physiology, RJW anatomy and combat identity remain valid without them.
    public static class RDFNewCombatXenotypeSupplementalGenes
    {
        private static readonly Dictionary<string, List<GeneDef>> ResolvedByXenotype = new Dictionary<string, List<GeneDef>>();
        private static readonly string[] TargetXenotypes =
        {
            "RDF_Dollfox",
            "RDF_SecurityDoll",
            "RDF_ProwlerDoll"
        };

        private static GeneDef FirstAvailable(params string[] defNames)
        {
            if (defNames == null) return null;
            foreach (string defName in defNames)
            {
                if (string.IsNullOrEmpty(defName)) continue;
                GeneDef def = RDFGeneUtility.GetGeneDefCached(defName);
                if (def != null) return def;
            }
            return null;
        }

        private static void AddIfFound(List<GeneDef> result, params string[] alternatives)
        {
            GeneDef def = FirstAvailable(alternatives);
            if (def != null && !result.Contains(def)) result.Add(def);
        }

        public static List<GeneDef> ResolveFor(string xenotypeName)
        {
            List<GeneDef> cached;
            if (!string.IsNullOrEmpty(xenotypeName) && ResolvedByXenotype.TryGetValue(xenotypeName, out cached))
                return cached;

            List<GeneDef> result = new List<GeneDef>();

            if (xenotypeName == "RDF_Dollfox")
            {
                // Prefer the dedicated AFU fox cosmetics already present in the user's
                // setup; fall back through Big & Small and finally vanilla-safe visuals.
                AddIfFound(result, "AFU_HA_FoxEar", "BS_FoxEars", "BS_AnimalEars", "Ears_Cat");
                AddIfFound(result, "AFU_BA_FluffyFoxTail", "AFU_BA_FoxTail", "Tail_Furry");
                // Optional pounce ability: several B&S-family packs have used different
                // DefNames over time. Take the first one that actually exists.
                AddIfFound(result, "VU_Pounce", "BS_Pounce", "Pounce");
            }
            else if (xenotypeName == "RDF_SecurityDoll")
            {
                AddIfFound(result, "BS_LargeFrame");
                AddIfFound(result, "BS_NaturalArmor");
            }
            else if (xenotypeName == "RDF_ProwlerDoll")
            {
                AddIfFound(result, "BS_SmallFrame");
                AddIfFound(result, "BS_AnimalEars", "Ears_Cat");
                AddIfFound(result, "AFU_BA_LovelyCatTail", "Tail_Smooth");
                AddIfFound(result, "VU_Pounce", "BS_Pounce", "Pounce");
            }

            if (!string.IsNullOrEmpty(xenotypeName)) ResolvedByXenotype[xenotypeName] = result;
            return result;
        }

        public static void AddToXenotypeDef()
        {
            try
            {
                FieldInfo genesField = AccessTools.Field(typeof(XenotypeDef), "genes");
                if (genesField == null) return;

                foreach (string xenotypeName in TargetXenotypes)
                {
                    XenotypeDef xenotype = DefDatabase<XenotypeDef>.GetNamedSilentFail(xenotypeName);
                    if (xenotype == null) continue;

                    List<GeneDef> genes = genesField.GetValue(xenotype) as List<GeneDef>;
                    if (genes == null) continue;

                    List<GeneDef> resolved = ResolveFor(xenotypeName);
                    foreach (GeneDef def in resolved)
                    {
                        if (!genes.Contains(def)) genes.Add(def);
                    }

                    Log.Message("[RDF Genes] " + xenotypeName + " optional role genes resolved: " +
                        (resolved.Count > 0 ? string.Join(", ", resolved.Select(g => g.defName).ToArray()) : "none"));
                }
            }
            catch (Exception ex)
            {
                Log.Warning("[RDF Genes] Could not add v1.0.4 combat-xenotype supplemental genes: " + ex.Message);
            }
        }

        public static void EnsureOnPawn(Pawn pawn)
        {
            if (pawn == null || pawn.genes == null) return;
            string xenotypeName = pawn.genes.Xenotype != null ? pawn.genes.Xenotype.defName : null;
            if (string.IsNullOrEmpty(xenotypeName) || !TargetXenotypes.Contains(xenotypeName)) return;

            foreach (GeneDef def in ResolveFor(xenotypeName))
            {
                bool has = pawn.genes.GenesListForReading.Any(g => g != null && g.def == def);
                if (!has) pawn.genes.AddGene(def, true);
            }
        }
    }

    public class RDFHediffExtension : DefModExtension { public HediffDef hediff; }
    public class RDFBodyTypeExtension : DefModExtension { public BodyTypeDef bodyType; }

    // Generic external body-graphic hook. Creature bridge mods can add a body gene
    // without creating a custom RimWorld BodyTypeDef. This preserves the normal
    // humanlike renderer/anchors while replacing only the naked body graphic.
    public class RDFBodyGraphicExtension : DefModExtension
    {
        public string graphicPath;
        public string materialShapeKey;
        public bool useRDFSkinMaterials = false;
        public float drawSizeX = 1f;
        public float drawSizeY = 1f;
        public bool tintNeutralWithSkin = true;
    }
    public class RDFAnimalBodyLinkExtension : DefModExtension
    {
        public string creatureBodyKey;
        public string creatureThingDef;
        public string earGeneDefName;
        public string tailGeneDefName;
    }
    public class RDFTemperatureExtension : DefModExtension { public string mode; public HediffDef hediff; }

    // Horse-tail worker keeps the approved normal-pawn placement untouched, but when the
    // enlarged Dollmare body is active it moves the same separate tail gene outward/down
    // by the extra 50% body scale so the root stays on the rump instead of the old human anchor.
    public class RDFHorseTailWorker : PawnRenderNodeWorker_AttachmentBody
    {
        // Intentionally no direct OffsetFor override here. RimWorld 1.6 changed the
        // overridable signature exposed by this worker path in the user's assembly,
        // which caused CS0115 in v0.9b. A Harmony postfix on the base worker method
        // applies the Dollmare-only correction instead and keeps normal-pawn offsets locked.
    }

    // Big & Small-style RDF body renderer. The important part is architectural:
    // the GeneDef forces vanilla Female BodyType, and this node replaces only the visible
    // body graphic. This keeps vanilla/SAZ/RJW head, breast and genital anchors intact.
    public class RDFBodyRenderNodeProperties : PawnRenderNodeProperties
    {
        public string shapeKey;
        public string texturePath;
    }

    public class RDFBodyRenderNode : PawnRenderNode
    {
        private RDFBodyRenderNodeProperties rdfProps;
        private Pawn rdfPawn;

        public RDFBodyRenderNode(Pawn pawn, PawnRenderNodeProperties props, PawnRenderTree tree) : base(pawn, props, tree)
        {
            rdfPawn = pawn;
            rdfProps = props as RDFBodyRenderNodeProperties;
        }

        public override Graphic GraphicFor(Pawn pawn)
        {
            Pawn usePawn = pawn != null ? pawn : rdfPawn;
            if (usePawn == null || rdfProps == null || string.IsNullOrEmpty(rdfProps.texturePath)) return null;

            // When an RDF material-skin gene is present its full-body material graphic is
            // the final visible body. Do not stack the neutral shape underneath it.
            if (RDFGeneUtility.HasRDFSkinGenePresent(usePawn)) return null;

            Color skin = usePawn.story != null ? usePawn.story.SkinColor : Color.white;
            return GraphicDatabase.Get<Graphic_Multi>(rdfProps.texturePath, ShaderDatabase.Cutout, Vector2.one, skin);
        }
    }

    // Gene render-node properties for the RDF synthetic skin finish.
    // The node chooses the correct texture for the pawn's current RDF body type.
    public class RDFSkinRenderNodeProperties : PawnRenderNodeProperties
    {
        public string colorKey;
        public string finishKey;
    }

    public class RDFSkinRenderNode : PawnRenderNode
    {
        private RDFSkinRenderNodeProperties rdfProps;
        private Pawn rdfPawn;

        public RDFSkinRenderNode(Pawn pawn, PawnRenderNodeProperties props, PawnRenderTree tree) : base(pawn, props, tree)
        {
            rdfPawn = pawn;
            rdfProps = props as RDFSkinRenderNodeProperties;
        }

        private static string ShapeKey(Pawn pawn)
        {
            if (pawn == null || pawn.genes == null) return "DollFigureStandard";
            foreach (Gene g in pawn.genes.GenesListForReading)
            {
                if (g == null || g.def == null || !g.Active) continue;
                switch (g.def.defName)
                {
                    case "RDF_Gene_Body_CurvyDoll": return "CurvyDoll";
                    case "RDF_Gene_Body_PlushDoll": return "PlushDoll";
                    case "RDF_Gene_Body_VoluptuousDoll": return "VoluptuousDoll";
                    case "RDF_Gene_Body_DisplayDoll": return "DisplayDoll";
                    case "RDF_Gene_Body_DollFigureStandard": return "DollFigureStandard";
                }
            }
            return "DollFigureStandard";
        }

        public override Graphic GraphicFor(Pawn pawn)
        {
            Pawn usePawn = pawn != null ? pawn : rdfPawn;
            if (rdfProps == null || string.IsNullOrEmpty(rdfProps.colorKey) || string.IsNullOrEmpty(rdfProps.finishKey)) return null;
            string path = "Things/Pawn/Humanlike/RDFSkin/" + rdfProps.colorKey + "/" + rdfProps.finishKey + "/" + ShapeKey(usePawn);
            return GraphicDatabase.Get<Graphic_Multi>(path);
        }
    }
    public class RDFResourceProducerExtension : DefModExtension
    {
        public ThingDef outputThing;
        public int intervalTicks = 60000;
        public int stackCount = 1;
        public float nutritionCost = 0.03f;
    }

    public class Gene_RDFPermanentHediff : Gene
    {
        public override void PostAdd()
        {
            base.PostAdd();
            RDFHediffExtension ext = def.GetModExtension<RDFHediffExtension>();
            if (ext != null) RDFGeneUtility.EnsureHediff(pawn, ext.hediff);
        }
        public override void PostRemove()
        {
            RDFHediffExtension ext = def.GetModExtension<RDFHediffExtension>();
            if (ext != null) RDFGeneUtility.RemoveHediff(pawn, ext.hediff);
            base.PostRemove();
        }
    }

    public class Gene_RDFBodyShape : Gene
    {
        private void EnforceFemaleFormat()
        {
            if (pawn == null || pawn.story == null) return;
            BodyTypeDef female = BodyTypeDefOf.Female;
            if (female != null && pawn.story.bodyType != female) pawn.story.bodyType = female;
        }

        public override void PostAdd()
        {
            base.PostAdd();
            EnforceFemaleFormat();
            RDFGeneUtility.EnforceFullBodyEquipmentRules(pawn);
            RDFGeneUtility.RefreshVisuals(pawn);
        }

        public override void Tick()
        {
            base.Tick();
            if (pawn != null && pawn.IsHashIntervalTick(250))
            {
                EnforceFemaleFormat();
                RDFGeneUtility.EnforceFullBodyEquipmentRules(pawn);
            }
        }

        public override void PostRemove()
        {
            base.PostRemove();
            RDFGeneUtility.RefreshVisuals(pawn);
        }
    }

    public class Gene_RDFSkin : Gene
    {
        public override void PostAdd()
        {
            base.PostAdd();
            RDFGeneUtility.RefreshVisuals(pawn);
        }

        public override void PostRemove()
        {
            base.PostRemove();
            RDFGeneUtility.RefreshVisuals(pawn);
        }
    }


    // The Sentient Rubberdoll permanently rolls one body, one hair color and one RDF
    // skin/finish. The five fixed-body RDF xenotypes share this controller only for their
    // persistent hair/material selection. A single delayed SAZ visual resync runs after pawn
    // comps are available, then stops.
    public class Gene_RDFSentientRubberdollVariantController : Gene
    {
        private bool delayedVisualSyncDone;
        private bool supplementalGenesEnsured;
        private static readonly string[] BodyChoices =
        {
            "RDF_Gene_Body_DollFigureStandard",
            "RDF_Gene_Body_CurvyDoll",
            "RDF_Gene_Body_PlushDoll",
            "RDF_Gene_Body_VoluptuousDoll",
            "RDF_Gene_Body_DisplayDoll",
            "RDF_Gene_Body_SentientDisplayDoll"
        };

        private static readonly string[] HairChoices =
        {
            "RDF_Hair_Black",
            "RDF_Hair_Pink",
            "RDF_Hair_HotPink",
            "RDF_Hair_PalePink",
            "RDF_Hair_MetallicSilver",
            "RDF_Hair_White",
            "RDF_Hair_VioletBlack",
            "RDF_Hair_Blonde"
        };

        private static readonly string[] SkinChoices =
        {
            "RDF_Skin_Pink_Glossy", "RDF_Skin_Pink_Metallic", "RDF_Skin_Pink_Wet",
            "RDF_Skin_Black_Glossy", "RDF_Skin_Black_Metallic", "RDF_Skin_Black_Wet",
            "RDF_Skin_Purple_Glossy", "RDF_Skin_Purple_Metallic", "RDF_Skin_Purple_Wet",
            "RDF_Skin_Silver_Glossy", "RDF_Skin_Silver_Metallic", "RDF_Skin_Silver_Wet",
            "RDF_Skin_Gold_Glossy", "RDF_Skin_Gold_Metallic", "RDF_Skin_Gold_Wet",
            "RDF_Skin_HotPink_Glossy", "RDF_Skin_HotPink_Metallic", "RDF_Skin_HotPink_Wet",
            "RDF_Skin_Bubblegum_Glossy", "RDF_Skin_Bubblegum_Metallic", "RDF_Skin_Bubblegum_Wet",
            "RDF_Skin_RDFPink_Glossy", "RDF_Skin_RDFPink_Metallic", "RDF_Skin_RDFPink_Wet"
        };

        // Dollmare and Sentient Rubber Bitch share the same material pool; Bubblegum is excluded.
        private static readonly string[] DollmareSkinChoices =
        {
            "RDF_Skin_Pink_Glossy", "RDF_Skin_Pink_Metallic", "RDF_Skin_Pink_Wet",
            "RDF_Skin_Black_Glossy", "RDF_Skin_Black_Metallic", "RDF_Skin_Black_Wet",
            "RDF_Skin_Purple_Glossy", "RDF_Skin_Purple_Metallic", "RDF_Skin_Purple_Wet",
            "RDF_Skin_Silver_Glossy", "RDF_Skin_Silver_Metallic", "RDF_Skin_Silver_Wet",
            "RDF_Skin_Gold_Glossy", "RDF_Skin_Gold_Metallic", "RDF_Skin_Gold_Wet",
            "RDF_Skin_HotPink_Glossy", "RDF_Skin_HotPink_Metallic", "RDF_Skin_HotPink_Wet",
            "RDF_Skin_RDFPink_Glossy", "RDF_Skin_RDFPink_Metallic", "RDF_Skin_RDFPink_Wet"
        };

        private bool HasAny(string[] choices)
        {
            if (pawn == null || pawn.genes == null) return false;
            return pawn.genes.GenesListForReading.Any(g => g != null && g.def != null &&
                choices.Contains(g.def.defName));
        }

        private string EnsureOne(string[] choices)
        {
            if (pawn == null || pawn.genes == null || choices == null || choices.Length == 0) return null;

            foreach (Gene g in pawn.genes.GenesListForReading)
            {
                if (g == null || g.def == null) continue;
                if (choices.Contains(g.def.defName)) return g.def.defName;
            }

            // Try all choices starting from a random slot. This avoids a broken Def silently
            // preventing the xenotype from receiving a variant.
            int start = Rand.Range(0, choices.Length);
            for (int offset = 0; offset < choices.Length; offset++)
            {
                string defName = choices[(start + offset) % choices.Length];
                GeneDef chosen = RDFGeneUtility.GetGeneDefCached(defName);
                if (chosen == null) continue;
                pawn.genes.AddGene(chosen, true);
                return defName;
            }
            return null;
        }

        private void EnsureVariants(bool logSelection)
        {
            if (pawn == null || pawn.genes == null) return;

            // The hidden controller is shared by the variable humanoid Rubberdoll and five
            // fixed-body xenotypes. Detect fixed xenotypes directly as well as the legacy
            // quadruped body genes so PostAdd ordering can never roll an unwanted body first.
            string xenotypeName = pawn.genes.Xenotype != null ? pawn.genes.Xenotype.defName : null;
            bool dollmare = string.Equals(xenotypeName, "RDF_Dollmare", StringComparison.Ordinal) ||
                RDFGeneUtility.HasDollmareBody(pawn);
            bool rubberBitch = string.Equals(xenotypeName, "RDF_SentientRubberBitch", StringComparison.Ordinal) ||
                RDFGeneUtility.HasRubberBitchBody(pawn);
            bool dollfox = string.Equals(xenotypeName, "RDF_Dollfox", StringComparison.Ordinal);
            bool securityDoll = string.Equals(xenotypeName, "RDF_SecurityDoll", StringComparison.Ordinal);
            bool prowlerDoll = string.Equals(xenotypeName, "RDF_ProwlerDoll", StringComparison.Ordinal);

            string fixedBody = null;
            if (dollmare) fixedBody = "RDF_Gene_Body_CentauressSentient";
            else if (rubberBitch) fixedBody = "RDF_Gene_Body_RubberBitch";
            else if (dollfox) fixedBody = "RDF_Gene_Body_CurvyDoll";
            else if (securityDoll) fixedBody = "RDF_Gene_Body_VoluptuousDoll";
            else if (prowlerDoll) fixedBody = "RDF_Gene_Body_DollFigureStandard";

            if (!string.IsNullOrEmpty(fixedBody))
            {
                // Remove every other humanoid Rubberdoll body. For fixed humanoid xenotypes
                // retain exactly their assigned member of BodyChoices; for quadrupeds remove all.
                List<Gene> wrongBodies = pawn.genes.GenesListForReading
                    .Where(g => g != null && g.def != null && BodyChoices.Contains(g.def.defName) &&
                        !string.Equals(g.def.defName, fixedBody, StringComparison.Ordinal))
                    .ToList();
                foreach (Gene wrongBody in wrongBodies) pawn.genes.RemoveGene(wrongBody);

                GeneDef fixedBodyDef = RDFGeneUtility.GetGeneDefCached(fixedBody);
                bool hasFixedBody = pawn.genes.GenesListForReading.Any(g => g != null && g.def == fixedBodyDef);
                if (fixedBodyDef != null && !hasFixedBody) pawn.genes.AddGene(fixedBodyDef, true);
            }

            string body = !string.IsNullOrEmpty(fixedBody) ? fixedBody : EnsureOne(BodyChoices);
            string hair = EnsureOne(HairChoices);
            // Dollmare and Rubber Bitch keep their original no-Bubblegum pool. The three
            // v1.0.4 humanoid combat xenotypes deliberately use the complete 24-skin pool.
            string skin = EnsureOne((dollmare || rubberBitch) ? DollmareSkinChoices : SkinChoices);

            if (logSelection)
            {
                string variantLabel = dollmare ? "Dollmare" :
                    (rubberBitch ? "Sentient Rubber Bitch" :
                    (dollfox ? "RDF Dollfox" :
                    (securityDoll ? "RDF Security Doll" :
                    (prowlerDoll ? "RDF Prowler Doll" : "Sentient Rubberdoll"))));
                Log.Message("[RDF Genes] " + variantLabel +
                    " variant roll for " + pawn.LabelShort +
                    ": body=" + (body ?? "MISSING") + ", hair=" + (hair ?? "MISSING") +
                    ", skin=" + (skin ?? "MISSING"));
            }

            RDFGeneUtility.RefreshVisuals(pawn);
        }

        private bool TryEnsureSupplementalGenes()
        {
            if (pawn == null || pawn.genes == null || pawn.genes.Xenotype == null) return false;

            string xenotypeName = pawn.genes.Xenotype.defName;
            if (xenotypeName == "RDF_SentientRubberdoll" ||
                xenotypeName == "RDF_Dollmare" ||
                xenotypeName == "RDF_SentientRubberBitch")
            {
                RDFSentientRubberdollSupplementalGenes.EnsureOnPawn(pawn);
            }

            RDFNewCombatXenotypeSupplementalGenes.EnsureOnPawn(pawn);
            return true;
        }

        public override void PostAdd()
        {
            base.PostAdd();
            // During some PawnGenerator paths the controller's PostAdd can fire before
            // GeneTracker.Xenotype is assigned. Only mark this complete once the xenotype
            // is known; Tick will retry once if necessary.
            supplementalGenesEnsured = TryEnsureSupplementalGenes();
            EnsureVariants(true);
        }

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

            if (!supplementalGenesEnsured && pawn != null && pawn.IsHashIntervalTick(30))
                supplementalGenesEnsured = TryEnsureSupplementalGenes();

            // Gene PostAdd can run before Sized Apparel has attached its recorder comp.
            // Wait until that comp exists, then rebuild its skeleton/body-addon caches once.
            // Do NOT run a repeating 600-tick repair: the selected genes are persistent and
            // repeatedly dirtying SAZ caches caused the visible anatomy/body snap in v0.11n.
            if (!delayedVisualSyncDone && pawn != null && pawn.AllComps != null && pawn.IsHashIntervalTick(30))
            {
                bool sazReady = false;
                for (int i = 0; i < pawn.AllComps.Count; i++)
                {
                    ThingComp comp = pawn.AllComps[i];
                    if (comp != null && comp.GetType() != null &&
                        comp.GetType().FullName == "SizedApparel.ApparelRecorderComp")
                    {
                        sazReady = true;
                        break;
                    }
                }
                if (sazReady)
                {
                    EnsureVariants(false);
                    RDFGeneUtility.RefreshVisuals(pawn);
                    delayedVisualSyncDone = true;
                }
            }
        }
    }

    // Appearance-only anatomy gene. It never changes the underlying RJW breast Hediff,
    // breast size, lactation or other mechanics; it only refreshes the SAZ graphic lookup.
    public class Gene_RDFAnatomyAppearance : Gene
    {
        public override void PostAdd()
        {
            base.PostAdd();
            RDFGeneUtility.RefreshVisuals(pawn);
        }

        public override void PostRemove()
        {
            base.PostRemove();
            RDFGeneUtility.RefreshVisuals(pawn);
        }
    }

    public class Gene_RDFDisplayDollHead : Gene
    {
        private const string HelperPrefix = "RDF_Gene_DisplayDollHeadGraphic_";
        private readonly List<Gene> helperScratch = new List<Gene>();

        private string DesiredHelperDefName()
        {
            string colorKey;
            string finishKey;
            if (!RDFGeneUtility.TryGetRDFSkinMaterial(pawn, out colorKey, out finishKey))
            {
                colorKey = "Black";
                finishKey = "Glossy";
            }
            return HelperPrefix + colorKey + "_" + finishKey;
        }

        // Only dirty graphics when the hidden Display Doll head helper actually changes.
        // Rebuilding SAZ graphics every second caused visible one-frame anatomy flicker.
        private bool SyncHeadGraphicHelper()
        {
            if (pawn == null || pawn.genes == null) return false;

            string desiredName = DesiredHelperDefName();
            Gene desiredGene = null;
            bool changed = false;
            helperScratch.Clear();
            List<Gene> genes = pawn.genes.GenesListForReading;
            for (int i = 0; i < genes.Count; i++)
            {
                Gene candidate = genes[i];
                if (candidate != null && candidate.def != null &&
                    candidate.def.defName.StartsWith(HelperPrefix, StringComparison.Ordinal))
                    helperScratch.Add(candidate);
            }

            foreach (Gene helper in helperScratch)
            {
                if (helper.def.defName == desiredName)
                {
                    desiredGene = helper;
                    continue;
                }
                pawn.genes.RemoveGene(helper);
                changed = true;
            }

            if (desiredGene == null)
            {
                GeneDef desiredDef = RDFGeneUtility.GetInternalGeneDef(desiredName);
                if (desiredDef != null)
                {
                    pawn.genes.AddGene(desiredDef, true);
                    changed = true;
                }
            }

            if (changed) RDFGeneUtility.RefreshVisuals(pawn);
            return changed;
        }

        private void RemoveHeadGraphicHelpers()
        {
            if (pawn == null || pawn.genes == null) return;
            helperScratch.Clear();
            List<Gene> genes = pawn.genes.GenesListForReading;
            for (int i = 0; i < genes.Count; i++)
            {
                Gene candidate = genes[i];
                if (candidate != null && candidate.def != null &&
                    candidate.def.defName.StartsWith(HelperPrefix, StringComparison.Ordinal))
                    helperScratch.Add(candidate);
            }
            foreach (Gene helper in helperScratch) pawn.genes.RemoveGene(helper);
            RDFGeneUtility.RefreshVisuals(pawn);
        }

        public override void PostAdd()
        {
            base.PostAdd();
            SyncHeadGraphicHelper();
            RDFGeneUtility.EnforceDisplayDollHeadEnvironmentalImmunity(pawn);
        }

        public override void Tick()
        {
            base.Tick();
            if (pawn != null && pawn.IsHashIntervalTick(250))
            {
                // Cheap no-op when the material helper is already correct; importantly,
                // this no longer calls SetAllGraphicsDirty/ResetBodyAddons every second.
                SyncHeadGraphicHelper();
                RDFGeneUtility.EnforceDisplayDollHeadEnvironmentalImmunity(pawn);
            }
        }

        public override void PostRemove()
        {
            RemoveHeadGraphicHelpers();
            base.PostRemove();
        }
    }

    public class Gene_RDFBodyType : Gene
    {
        private BodyTypeDef previousBodyType;
        private RDFBodyTypeExtension cachedExtension;

        private RDFBodyTypeExtension Extension
        {
            get
            {
                if (cachedExtension == null) cachedExtension = def.GetModExtension<RDFBodyTypeExtension>();
                return cachedExtension;
            }
        }

        private void ApplyBodyType()
        {
            RDFBodyTypeExtension ext = Extension;
            if (pawn == null || pawn.story == null || ext == null || ext.bodyType == null) return;

            // Pawn generation and some appearance mods can assign body type after genes are added.
            // Re-assert the RDF body type so the approved RDF body texture actually remains active.
            if (pawn.story.bodyType != ext.bodyType)
            {
                if (previousBodyType == null && pawn.story.bodyType != null) previousBodyType = pawn.story.bodyType;
                pawn.story.bodyType = ext.bodyType;
                RDFGeneUtility.RefreshVisuals(pawn);
            }
        }

        public override void PostAdd()
        {
            base.PostAdd();
            ApplyBodyType();
        }

        public override void Tick()
        {
            base.Tick();
            if (pawn != null && pawn.IsHashIntervalTick(250)) ApplyBodyType();
        }

        public override void PostRemove()
        {
            if (pawn != null && pawn.story != null && previousBodyType != null)
            {
                pawn.story.bodyType = previousBodyType;
                RDFGeneUtility.RefreshVisuals(pawn);
            }
            base.PostRemove();
        }

        public override void ExposeData()
        {
            base.ExposeData();
            Scribe_Defs.Look(ref previousBodyType, "rdfPreviousBodyType");
        }
    }

    public class Gene_AgelessDoll : Gene
    {
        private long freezeAgeTicks;
        public override void PostAdd()
        {
            base.PostAdd();
            if (pawn == null || pawn.ageTracker == null) return;
            long age25 = 25L * GenDate.TicksPerYear;
            freezeAgeTicks = Math.Max(age25, pawn.ageTracker.AgeBiologicalTicks);
        }
        public override void Tick()
        {
            base.Tick();
            if (pawn == null || pawn.ageTracker == null || !pawn.IsHashIntervalTick(2500)) return;
            long age25 = 25L * GenDate.TicksPerYear;
            if (freezeAgeTicks <= 0) freezeAgeTicks = Math.Max(age25, pawn.ageTracker.AgeBiologicalTicks);
            if (pawn.ageTracker.AgeBiologicalTicks >= age25 && pawn.ageTracker.AgeBiologicalTicks > freezeAgeTicks)
                pawn.ageTracker.AgeBiologicalTicks = freezeAgeTicks;
        }
        public override void ExposeData()
        {
            base.ExposeData();
            Scribe_Values.Look(ref freezeAgeTicks, "rdfFreezeAgeTicks", 0L);
        }
    }

    public class Gene_RegenerativeDoll : Gene
    {
        public override void Tick()
        {
            base.Tick();
            if (pawn == null || pawn.health == null || !pawn.IsHashIntervalTick(2500)) return;

            List<Hediff> hediffs = pawn.health.hediffSet.hediffs;
            int missingCount = 0;
            for (int i = 0; i < hediffs.Count; i++)
            {
                Hediff_MissingPart missingPart = hediffs[i] as Hediff_MissingPart;
                if (missingPart != null && missingPart.Part != null) missingCount++;
            }

            HediffDef reconstructDef = RDFGeneUtility.GetHediffDefCached("RDF_SyntheticReconstruction");
            if (missingCount == 0)
            {
                RDFGeneUtility.RemoveHediff(pawn, reconstructDef);
                return;
            }
            RDFGeneUtility.EnsureHediff(pawn, reconstructDef);

            if (RDFGeneUtility.HasGene(pawn, "RDF_Gene_RepairHunger") && pawn.needs != null && pawn.needs.food != null && !pawn.needs.food.Starving)
            {
                float burden = Math.Min(3f, 0.75f + missingCount * 0.35f);
                pawn.needs.food.CurLevel -= 0.006f * burden;
            }

            float coatFactor = RDFGeneUtility.CoatingRegrowthFactor(pawn);
            for (int i = hediffs.Count - 1; i >= 0; i--)
            {
                Hediff_MissingPart h = hediffs[i] as Hediff_MissingPart;
                if (h == null || h.Part == null) continue;
                long needed = (long)(DaysForPart(h.Part, pawn) * GenDate.TicksPerDay / coatFactor);
                if (h.ageTicks < needed) continue;
                try
                {
                    pawn.health.RestorePart(h.Part, null, true);
                    if (pawn.health.hediffSet.hediffs.Contains(h)) pawn.health.RemoveHediff(h);
                }
                catch (Exception e) { Log.Warning("[RDF Genes] Regeneration failed for " + h.Part + ": " + e.Message); }
            }
        }

        private static float DaysForPart(BodyPartRecord part, Pawn pawn)
        {
            string n = part.def.defName.ToLowerInvariant();
            float min = 3f, max = 6f;
            if (n.Contains("finger") || n.Contains("toe")) { min = 1f; max = 2f; }
            else if (n.Contains("eye") || n.Contains("ear") || n.Contains("nose")) { min = 2f; max = 3.5f; }
            else if (n.Contains("hand") || n.Contains("foot")) { min = 3.5f; max = 6f; }
            else if (n.Contains("arm") || n.Contains("leg")) { min = 6f; max = 10f; }
            else if (n.Contains("heart") || n.Contains("lung") || n.Contains("liver") || n.Contains("kidney") || n.Contains("stomach")) { min = 5f; max = 10f; }
            int seed = Math.Abs((part.Index * 397) ^ (pawn.thingIDNumber * 17) ^ n.GetHashCode());
            float t = (seed % 1000) / 999f;
            return min + (max - min) * t;
        }
    }

    public class Gene_LatexCoatingRegeneration : Gene
    {
        public override void Tick()
        {
            base.Tick();
            if (pawn == null || pawn.health == null || !pawn.IsHashIntervalTick(250)) return;
            HediffDef boostDef = RDFGeneUtility.GetHediffDefCached("RDF_CoatingRegenBoost");
            Hediff coating = RDFGeneUtility.FindLatexCoating(pawn);
            if (coating == null) { RDFGeneUtility.RemoveHediff(pawn, boostDef); return; }
            Hediff boost = RDFGeneUtility.EnsureHediff(pawn, boostDef);
            if (boost == null) return;
            boost.Severity = coating.Severity >= 0.75f ? 0.9f : (coating.Severity >= 0.35f ? 0.5f : 0.1f);
        }
        public override void PostRemove()
        {
            RDFGeneUtility.RemoveHediff(pawn, RDFGeneUtility.GetHediffDefCached("RDF_CoatingRegenBoost"));
            base.PostRemove();
        }
    }

    public class Gene_SyntheticImmuneSystem : Gene
    {
        private HediffDef Effect { get { return RDFGeneUtility.GetHediffDefCached("RDF_SyntheticImmuneEffect"); } }
        public override void PostAdd() { base.PostAdd(); RDFGeneUtility.EnsureHediff(pawn, Effect); }
        public override void PostRemove() { RDFGeneUtility.RemoveHediff(pawn, Effect); base.PostRemove(); }
        public override void Tick()
        {
            base.Tick();
            if (pawn == null || pawn.health == null || !pawn.IsHashIntervalTick(2500)) return;
            List<Hediff> hediffs = pawn.health.hediffSet.hediffs;
            for (int i = 0; i < hediffs.Count; i++)
            {
                Hediff h = hediffs[i];
                if (h == null || h.def == null) continue;
                string n = h.def.defName;
                if (n.IndexOf("foodpoison", StringComparison.OrdinalIgnoreCase) >= 0) h.Severity = Math.Max(0f, h.Severity - 0.05f);
                else if (n.IndexOf("infection", StringComparison.OrdinalIgnoreCase) >= 0) h.Severity = Math.Max(0f, h.Severity - 0.015f);
            }
        }
    }

    public class Gene_ToxproofDoll : Gene
    {
        private HediffDef Effect { get { return RDFGeneUtility.GetHediffDefCached("RDF_ToxproofEffect"); } }
        public override void PostAdd() { base.PostAdd(); RDFGeneUtility.EnsureHediff(pawn, Effect); }
        public override void PostRemove() { RDFGeneUtility.RemoveHediff(pawn, Effect); base.PostRemove(); }
        public override void Tick()
        {
            base.Tick();
            if (pawn == null || pawn.health == null || !pawn.IsHashIntervalTick(250)) return;
            List<Hediff> hediffs = pawn.health.hediffSet.hediffs;
            for (int i = hediffs.Count - 1; i >= 0; i--)
            {
                Hediff h = hediffs[i];
                if (h == null || h.def == null) continue;
                string n = h.def.defName;
                if (string.Equals(n, "ToxicBuildup", StringComparison.OrdinalIgnoreCase) ||
                    n.IndexOf("rotstink", StringComparison.OrdinalIgnoreCase) >= 0) pawn.health.RemoveHediff(h);
            }
        }
    }

    public class Gene_VacuumProofDoll : Gene
    {
        public override void Tick()
        {
            base.Tick();
            if (pawn == null || pawn.health == null || !pawn.IsHashIntervalTick(120)) return;
            List<Hediff> hediffs = pawn.health.hediffSet.hediffs;
            for (int i = hediffs.Count - 1; i >= 0; i--)
            {
                Hediff h = hediffs[i];
                if (h == null || h.def == null) continue;
                string n = h.def.defName;
                if (n.IndexOf("vacuum", StringComparison.OrdinalIgnoreCase) >= 0 ||
                    n.IndexOf("decompression", StringComparison.OrdinalIgnoreCase) >= 0) pawn.health.RemoveHediff(h);
            }
        }
    }

    public class Gene_RDFTemperatureSensitivity : Gene
    {
        private RDFTemperatureExtension cachedExtension;
        private RDFTemperatureExtension Extension
        {
            get
            {
                if (cachedExtension == null) cachedExtension = def.GetModExtension<RDFTemperatureExtension>();
                return cachedExtension;
            }
        }

        public override void Tick()
        {
            base.Tick();
            if (pawn == null || pawn.health == null || !pawn.IsHashIntervalTick(250)) return;
            RDFTemperatureExtension ext = Extension;
            if (ext == null || ext.hediff == null || !pawn.Spawned) return;
            float temp = pawn.AmbientTemperature;
            float sev = 0f;
            if (string.Equals(ext.mode, "Heat", StringComparison.OrdinalIgnoreCase))
            {
                if (temp > 50f) sev = 0.9f; else if (temp > 40f) sev = 0.5f; else if (temp > 30f) sev = 0.1f;
            }
            else
            {
                if (temp < -25f) sev = 0.9f; else if (temp < -10f) sev = 0.5f; else if (temp < 5f) sev = 0.1f;
            }
            if (sev <= 0f) { RDFGeneUtility.RemoveHediff(pawn, ext.hediff); return; }
            Hediff h = RDFGeneUtility.EnsureHediff(pawn, ext.hediff);
            if (h != null) h.Severity = sev;
        }
        public override void PostRemove()
        {
            RDFTemperatureExtension ext = Extension;
            if (ext != null) RDFGeneUtility.RemoveHediff(pawn, ext.hediff);
            base.PostRemove();
        }
    }

    public class Gene_SyntheticGestation : Gene
    {
        private HediffDef Support { get { return RDFGeneUtility.GetHediffDefCached("RDF_SyntheticGestationSupport"); } }
        public override void Tick()
        {
            base.Tick();
            if (pawn == null || pawn.health == null || !pawn.IsHashIntervalTick(250)) return;
            bool pregnant = false;
            List<Hediff> hediffs = pawn.health.hediffSet.hediffs;
            for (int i = 0; i < hediffs.Count; i++)
            {
                Hediff h = hediffs[i];
                if (h != null && h.def != null && h.def.defName.IndexOf("pregnan", StringComparison.OrdinalIgnoreCase) >= 0)
                {
                    pregnant = true;
                    break;
                }
            }
            if (pregnant) RDFGeneUtility.EnsureHediff(pawn, Support); else RDFGeneUtility.RemoveHediff(pawn, Support);
        }
        public override void PostRemove() { RDFGeneUtility.RemoveHediff(pawn, Support); base.PostRemove(); }
    }

    public class Gene_SleeplessDoll : Gene
    {
        public override void Tick()
        {
            base.Tick();
            if (pawn == null || pawn.needs == null || pawn.needs.rest == null || !pawn.IsHashIntervalTick(150)) return;
            pawn.needs.rest.CurLevel = 1f;
        }
    }

    public class Gene_RDFResourceProducer : Gene
    {
        private int ticksRemaining;
        private RDFResourceProducerExtension cachedExtension;
        private RDFResourceProducerExtension Extension
        {
            get
            {
                if (cachedExtension == null) cachedExtension = def.GetModExtension<RDFResourceProducerExtension>();
                return cachedExtension;
            }
        }

        public override void PostMake()
        {
            base.PostMake();
            RDFResourceProducerExtension ext = Extension;
            ticksRemaining = ext != null ? ext.intervalTicks : 60000;
        }
        public override void Tick()
        {
            base.Tick();
            if (pawn == null || !pawn.IsHashIntervalTick(60)) return;
            RDFResourceProducerExtension ext = Extension;
            if (ext == null || ext.outputThing == null) return;
            ticksRemaining -= 60;
            if (ticksRemaining > 0) return;
            ticksRemaining = ext.intervalTicks;
            if (pawn.needs != null && pawn.needs.food != null)
            {
                if (pawn.needs.food.Starving || pawn.needs.food.CurLevel < Math.Max(0.12f, ext.nutritionCost + 0.03f)) return;
                pawn.needs.food.CurLevel -= ext.nutritionCost;
            }
            Thing thing = ThingMaker.MakeThing(ext.outputThing);
            thing.stackCount = Math.Max(1, ext.stackCount);
            if (pawn.Spawned && pawn.Map != null) GenPlace.TryPlaceThing(thing, pawn.Position, pawn.Map, ThingPlaceMode.Near);
            else if (pawn.inventory != null) pawn.inventory.innerContainer.TryAdd(thing);
        }
        public override void ExposeData()
        {
            base.ExposeData();
            Scribe_Values.Look(ref ticksRemaining, "rdfResourceTicksRemaining", 60000);
        }
    }

    public class Gene_RubberGenitalia : Gene
    {
        private bool markersInitialized;

        // IMPORTANT: Rubber Genitalia is a material modifier, not an RJW genital type.
        // It must coexist with Human, Horse, Dragon, Canine, RDF Doll, Living Latex, etc.
        // Therefore these are ordinary marker Hediffs on the Genitals/Anus body parts and
        // NEVER replace or remove any rjw.HediffDef_SexPart Hediffs.

        public override void PostAdd()
        {
            base.PostAdd();
            markersInitialized = EnsureRubberMarkers(pawn);
        }

        public override void Tick()
        {
            base.Tick();
            if (pawn == null) return;

            // RJW may add body parts after Biotech's initial gene setup. Retry quickly only
            // until both available RDF material markers are established. Afterwards perform a
            // very rare self-heal check instead of scanning body parts every two seconds forever.
            int interval = markersInitialized ? 2500 : 120;
            if (!pawn.IsHashIntervalTick(interval)) return;
            markersInitialized = EnsureRubberMarkers(pawn);
        }

        public override void PostRemove()
        {
            RemoveRubberMarkers(pawn);
            base.PostRemove();
        }

        private static BodyPartRecord FindBodyPart(Pawn p, string defName)
        {
            if (p == null || p.RaceProps == null || p.RaceProps.body == null) return null;
            return p.RaceProps.body.AllParts.FirstOrDefault(x => x.def != null && x.def.defName == defName);
        }

        private static bool HasHediffOnPart(Pawn p, HediffDef def, BodyPartRecord part)
        {
            if (p == null || p.health == null || def == null || part == null) return false;
            return p.health.hediffSet.hediffs.Any(h => h != null && h.def == def && h.Part == part);
        }

        private static bool EnsureRubberMarkers(Pawn p)
        {
            if (p == null || p.health == null) return false;

            HediffDef genitalMarker = RDFGeneUtility.GetHediffDefCached("RDF_RubberGenitaliaEffect");
            HediffDef anusMarker = RDFGeneUtility.GetHediffDefCached("RDF_RubberAnusEffect");
            BodyPartRecord genitals = FindBodyPart(p, "Genitals");
            BodyPartRecord anus = FindBodyPart(p, "Anus");

            bool genitalReady = genitalMarker == null || genitals == null;
            if (genitalMarker != null && genitals != null)
            {
                if (!HasHediffOnPart(p, genitalMarker, genitals)) p.health.AddHediff(genitalMarker, genitals);
                genitalReady = HasHediffOnPart(p, genitalMarker, genitals);
            }

            bool anusReady = anusMarker == null || anus == null;
            if (anusMarker != null && anus != null)
            {
                if (!HasHediffOnPart(p, anusMarker, anus)) p.health.AddHediff(anusMarker, anus);
                anusReady = HasHediffOnPart(p, anusMarker, anus);
            }

            // If RJW body parts are not present yet, keep the short retry interval.
            if (genitalMarker != null && genitals == null) genitalReady = false;
            if (anusMarker != null && anus == null) anusReady = false;
            return genitalReady && anusReady;
        }

        private static void RemoveRubberMarkers(Pawn p)
        {
            if (p == null || p.health == null) return;
            List<Hediff> hediffs = p.health.hediffSet.hediffs;
            for (int i = hediffs.Count - 1; i >= 0; i--)
            {
                Hediff h = hediffs[i];
                if (h == null || h.def == null) continue;
                string name = h.def.defName;
                if (name == "RDF_RubberGenitaliaEffect" || name == "RDF_RubberAnusEffect")
                    p.health.RemoveHediff(h);
            }
        }
    }

    public static class RDFSizedApparelCompatibility
    {
        private static readonly string[] RDFBodyTypes = new string[]
        {
            "RDFBody_DollFigureStandard",
            "RDFBody_CurvyDoll",
            "RDFBody_PlushDoll",
            "RDFBody_VoluptuousDoll",
            "RDFBody_DisplayDoll"
        };

        private static object CloneEntry(object source)
        {
            if (source == null) return null;
            Type t = source.GetType();
            object clone = Activator.CreateInstance(t);
            foreach (FieldInfo f in t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
            {
                if (f.IsInitOnly) continue;
                f.SetValue(clone, f.GetValue(source));
            }
            return clone;
        }

        private static object FindByBodyType(IList list, string bodyType)
        {
            if (list == null) return null;
            foreach (object entry in list)
            {
                if (entry == null) continue;
                FieldInfo bt = AccessTools.Field(entry.GetType(), "bodyType");
                string value = bt != null ? bt.GetValue(entry) as string : null;
                if (string.Equals(value, bodyType, StringComparison.Ordinal)) return entry;
            }
            return null;
        }

        private static void AddFemaleAliases(Type defType, string listFieldName)
        {
            if (defType == null) return;
            Def humanDef = GenDefDatabase.GetDef(defType, "Human");
            if (humanDef == null) return;

            FieldInfo listField = AccessTools.Field(defType, listFieldName);
            IList list = listField != null ? listField.GetValue(humanDef) as IList : null;
            if (list == null) return;

            object female = FindByBodyType(list, "Female");
            if (female == null) return;

            FieldInfo btField = AccessTools.Field(female.GetType(), "bodyType");
            if (btField == null) return;

            foreach (string rdfType in RDFBodyTypes)
            {
                if (FindByBodyType(list, rdfType) != null) continue;
                object clone = CloneEntry(female);
                if (clone == null) continue;
                btField.SetValue(clone, rdfType);
                list.Add(clone);
            }
        }

        public static void InstallFemaleAliases()
        {
            try
            {
                Type bodyDefType = AccessTools.TypeByName("SizedApparel.BodyDef");
                Type skeletonDefType = AccessTools.TypeByName("SizedApparel.SkeletonDef");
                if (bodyDefType == null || skeletonDefType == null) return;

                // All five RDF body types are female-format bodies. Register them directly
                // inside SAZ's Human BodyDef/SkeletonDef as aliases of its Female entries.
                // This makes SAZ natively create breasts/nipples/genital addons and use the
                // exact Female bone positions without changing the actual RDF BodyTypeDef.
                AddFemaleAliases(bodyDefType, "bodies");
                AddFemaleAliases(skeletonDefType, "skeletons");
                Log.Message("[RDF Genes] Registered RDF body types as SAZ Female-format aliases.");
            }
            catch (Exception ex)
            {
                Log.Warning("[RDF Genes] Could not register SAZ female body aliases: " + ex.Message);
            }
        }
    }

    [StaticConstructorOnStartup]
    public static class RDFGenesHarmonyInit
    {
        static RDFGenesHarmonyInit()
        {
            // v0.6f: v0.6c body architecture remains locked. Face/head and SAZ breasts now use
            // the RDF skin gene's actual palette color directly (Pink/Black/Purple/etc.), independent
            // of Glossy/Metallic/Wet. The body PNG keeps its baked material lighting.
            Harmony harmony = new Harmony("RDF.RubberDollFactory.Genes");
            RDFGeneUtility.HideInternalGenesFromPickers();
            RDFSentientRubberdollSupplementalGenes.AddToXenotypeDef();
            RDFNewCombatXenotypeSupplementalGenes.AddToXenotypeDef();

            // Cache invalidation only when a pawn's genes actually change. This replaces the
            // former global Gene.Active postfix in hot render/tick paths.
            MethodInfo geneTrackerChangedPost = AccessTools.Method(typeof(RDFGenesHarmonyInit), "GeneTrackerChangedPostfix");
            MethodInfo[] geneTrackerMethods = typeof(Pawn_GeneTracker).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
            for (int i = 0; i < geneTrackerMethods.Length; i++)
            {
                MethodInfo method = geneTrackerMethods[i];
                if (method == null || (method.Name != "AddGene" && method.Name != "RemoveGene")) continue;
                if (geneTrackerChangedPost != null) harmony.Patch(method, postfix: new HarmonyMethod(geneTrackerChangedPost));
            }

            // v0.11e EDITOR CLEANUP:
            // Big & Small can suppress drawing of helper genes, but the vanilla xenotype
            // layout can still reserve card slots for them. Filter the helper GeneDefs out
            // of DrawGenes/DrawSection list arguments before layout, with DrawGene as a
            // final fail-safe. The helper defs themselves remain fully available to B&S.
            Type xenotypeDialogType = AccessTools.TypeByName("RimWorld.Dialog_CreateXenotype");
            if (xenotypeDialogType == null) xenotypeDialogType = AccessTools.TypeByName("Dialog_CreateXenotype");
            MethodInfo filterGeneListsPrefix = AccessTools.Method(typeof(RDFGenesHarmonyInit), "FilterInternalGeneListsPrefix");
            MethodInfo skipInternalGenePrefix = AccessTools.Method(typeof(RDFGenesHarmonyInit), "SkipInternalGeneDrawPrefix");
            if (xenotypeDialogType != null)
            {
                foreach (string methodName in new[] { "DrawGenes", "DrawSection" })
                {
                    MethodInfo target = AccessTools.Method(xenotypeDialogType, methodName);
                    if (target != null && filterGeneListsPrefix != null)
                        harmony.Patch(target, prefix: new HarmonyMethod(filterGeneListsPrefix));
                }
                MethodInfo drawGene = AccessTools.Method(xenotypeDialogType, "DrawGene");
                if (drawGene != null && skipInternalGenePrefix != null)
                    harmony.Patch(drawGene, prefix: new HarmonyMethod(skipInternalGenePrefix));
            }

            // BODY REPLACEMENT v0.6c:
            // Do not add an attachment on top of / behind the vanilla body.  Replace the
            // Graphic returned by the existing vanilla Body render node itself.  This keeps
            // the exact Female node transform, head position and all SAZ/RJW child anchors.
            Type bodyNodeType = AccessTools.TypeByName("Verse.PawnRenderNode_Body");
            if (bodyNodeType == null) bodyNodeType = AccessTools.TypeByName("PawnRenderNode_Body");
            MethodInfo bodyGraphicFor = bodyNodeType != null ? AccessTools.Method(bodyNodeType, "GraphicFor", new Type[] { typeof(Pawn) }) : null;
            MethodInfo bodyGraphicPost = AccessTools.Method(typeof(RDFGenesHarmonyInit), "BodyGraphicForPostfix");
            if (bodyGraphicFor != null && bodyGraphicPost != null)
            {
                HarmonyMethod hm = new HarmonyMethod(bodyGraphicPost);
                hm.priority = Priority.Last;
                harmony.Patch(bodyGraphicFor, postfix: hm);
                Log.Message("[RDF Genes] Installed Female base-body graphic replacement patch.");
            }
            else
            {
                Log.Warning("[RDF Genes] Could not find PawnRenderNode_Body.GraphicFor; RDF body shapes will not render.");
            }

            // Display Doll head replacement is provided by hidden Big and Small PawnExtension.headPaths
            // helper genes. Do not force HeadTypeDefs or replace the head through a Harmony graphic hook.

            MethodInfo bleedGetter = AccessTools.PropertyGetter(typeof(Hediff), "BleedRate");
            MethodInfo bleedPost = AccessTools.Method(typeof(RDFGenesHarmonyInit), "BleedRatePostfix");
            if (bleedGetter != null && bleedPost != null) harmony.Patch(bleedGetter, postfix: new HarmonyMethod(bleedPost));

            MethodInfo inherited = AccessTools.Method(typeof(PregnancyUtility), "GetInheritedGeneSet", new Type[] { typeof(Pawn), typeof(Pawn) });
            MethodInfo inheritedPost = AccessTools.Method(typeof(RDFGenesHarmonyInit), "InheritedGeneSetPostfix");
            if (inherited != null && inheritedPost != null) harmony.Patch(inherited, postfix: new HarmonyMethod(inheritedPost));

            // v1.0.4p PERFORMANCE: no global Gene.Active patch. RDF body/skin genes now use
            // vanilla exclusion tags (BodyType / SkinColor / SkinColorOverride), so visual
            // priority is resolved by Biotech when genes change instead of on every Active read.

            // COLOR PRIORITY v0.6g: the working RDF body renderer keeps the real Female
            // BodyType, so force the pawn's effective skin color at the source. This makes
            // the normal RimWorld head/face renderer use a finish-aware RDF material tint matched to the
            // selected glossy/metallic/wet material instead of the endogenous skin gene.
            MethodInfo storySkinGetter = AccessTools.PropertyGetter(typeof(Pawn_StoryTracker), "SkinColor");
            MethodInfo storySkinPost = AccessTools.Method(typeof(RDFGenesHarmonyInit), "PawnStorySkinColorPostfix");
            if (storySkinGetter != null && storySkinPost != null)
            {
                HarmonyMethod skinHm = new HarmonyMethod(storySkinPost);
                skinHm.priority = Priority.Last;
                harmony.Patch(storySkinGetter, postfix: skinHm);
                Log.Message("[RDF Genes] Installed RDF face/skin color override patch.");
            }
            else
            {
                Log.Warning("[RDF Genes] Could not find Pawn_StoryTracker.SkinColor; RDF face color may remain vanilla.");
            }

            // Sized Apparel Zero renders breasts/genitals as separate body-part graphics.
            // Its cached body color can remain vanilla even when a Biotech skinColorOverride is active.
            // Patch only SAZ's Skin-colored body parts so RDF skin genes propagate to breasts and anatomy
            // without replacing or altering the underlying RJW Hediffs/types/sizes.
            Type sizedPartType = AccessTools.TypeByName("SizedApparel.SizedApparelBodyPart");
            MethodInfo sizedColorTarget = sizedPartType != null ? AccessTools.Method(sizedPartType, "ResolveDrawColors") : null;
            MethodInfo sizedColorPost = AccessTools.Method(typeof(RDFGenesHarmonyInit), "SizedApparelResolveDrawColorsPostfix");
            if (sizedColorTarget != null && sizedColorPost != null)
                harmony.Patch(sizedColorTarget, postfix: new HarmonyMethod(sizedColorPost));

            // RDF body types use the approved SAZ Female body as their placement reference.
            // During SAZ skeleton/body-addon lookup only, temporarily present RDF body types as Female.
            // This restores the same breast/anatomy placement as the standard SAZ female body without
            // changing the RDF body texture itself or moving the pawn's head.
            Type sazCompType = AccessTools.TypeByName("SizedApparel.ApparelRecorderComp");
            MethodInfo sazSkeletonLookup = sazCompType != null ? AccessTools.Method(sazCompType, "GetSkeletonDefSkeleton") : null;
            MethodInfo sazBodyLookup = sazCompType != null ? AccessTools.Method(sazCompType, "GetBodyDefAddons") : null;
            MethodInfo sazSkeletonPre = AccessTools.Method(typeof(RDFGenesHarmonyInit), "SizedApparelSkeletonLookupPrefix");
            MethodInfo sazLookupPre = AccessTools.Method(typeof(RDFGenesHarmonyInit), "SizedApparelBodyTypeLookupPrefix");
            MethodInfo sazLookupPost = AccessTools.Method(typeof(RDFGenesHarmonyInit), "SizedApparelBodyTypeLookupPostfix");
            if (sazSkeletonLookup != null && sazSkeletonPre != null && sazLookupPost != null)
            {
                harmony.Patch(sazSkeletonLookup, prefix: new HarmonyMethod(sazSkeletonPre), postfix: new HarmonyMethod(sazLookupPost));
                Log.Message("[RDF Genes] Installed Rubber Bitch custom SAZ skeleton-anchor lookup.");
            }
            if (sazBodyLookup != null && sazLookupPre != null && sazLookupPost != null)
                harmony.Patch(sazBodyLookup, prefix: new HarmonyMethod(sazLookupPre), postfix: new HarmonyMethod(sazLookupPost));

            // v1.0.4p PERFORMANCE: one BaseHeadOffsetAt postfix for all three RDF body
            // families that need custom head seating. This preserves the exact locked offsets
            // while avoiding three global postfix invocations per head-anchor calculation.
            MethodInfo baseHeadOffsetTarget = AccessTools.Method(typeof(PawnRenderer), "BaseHeadOffsetAt");
            MethodInfo baseHeadOffsetPost = AccessTools.Method(typeof(RDFGenesHarmonyInit), "RDFBaseHeadOffsetPostfix");
            if (baseHeadOffsetTarget != null && baseHeadOffsetPost != null)
            {
                HarmonyMethod headHm = new HarmonyMethod(baseHeadOffsetPost);
                headHm.priority = Priority.Last;
                harmony.Patch(baseHeadOffsetTarget, postfix: headHm);
                Log.Message("[RDF Genes] Installed consolidated RDF BaseHeadOffsetAt positioning patch (v1.0.4p).");
            }

            // SAZ body-part texture lookup also reads pawn.story.bodyType directly later.
            // Our RDF body types deliberately reuse the Female SAZ breast/genital art and placement,
            // so present them as Female only for this lookup. This does NOT change the actual RDF
            // body type or its approved body texture.
            Type sazBodyPartType = AccessTools.TypeByName("SizedApparel.SizedApparelBodyPart");
            MethodInfo sazBodyPartGraphic = sazBodyPartType != null ? AccessTools.Method(sazBodyPartType, "GetBodyPartGraphics") : null;
            MethodInfo sazPartPre = AccessTools.Method(typeof(RDFGenesHarmonyInit), "SizedApparelBodyPartGraphicPrefix");
            MethodInfo sazPartPost = AccessTools.Method(typeof(RDFGenesHarmonyInit), "SizedApparelBodyPartGraphicPostfix");
            if (sazBodyPartGraphic != null && sazPartPre != null && sazPartPost != null)
                harmony.Patch(sazBodyPartGraphic, prefix: new HarmonyMethod(sazPartPre), postfix: new HarmonyMethod(sazPartPost));

            // v0.11f anatomy appearance is handled inside the same SAZ body-part
            // prefix/postfix above. Using one combined state avoids Harmony __state
            // collisions while temporarily changing both body type and texture hediff name.

            // v0.11d functional audit: finish the two damage-sensitivity genes that previously
            // only had partial/placeholder behavior. Patch Thing.PreApplyDamage so this remains
            // independent of specific weapon/projectile implementations and works with modded
            // DamageDefs identified by their defName.
            MethodInfo preDamageTarget = AccessTools.Method(
                typeof(Thing),
                "PreApplyDamage",
                new Type[] { typeof(DamageInfo).MakeByRefType(), typeof(bool).MakeByRefType() });
            MethodInfo preDamagePrefix = AccessTools.Method(typeof(RDFGenesHarmonyInit), "RDFDamageSensitivityPrefix");
            if (preDamageTarget != null && preDamagePrefix != null)
            {
                harmony.Patch(preDamageTarget, prefix: new HarmonyMethod(preDamagePrefix));
                Log.Message("[RDF Genes] Installed flammable/solvent damage sensitivity patch (v0.11d).");
            }
            else
            {
                Log.Warning("[RDF Genes] Could not find Thing.PreApplyDamage; flammable/solvent damage multipliers will be unavailable.");
            }

            if (!ModsConfig.IsActive("rim.job.world")) return;

            Type stdType = AccessTools.TypeByName("rjw.std");
            MethodInfo stdTarget = stdType != null ? AccessTools.Method(stdType, "IsImmune") : null;
            MethodInfo stdPost = AccessTools.Method(typeof(RDFGenesHarmonyInit), "STDIsImmunePostfix");
            if (stdTarget != null && stdPost != null) harmony.Patch(stdTarget, postfix: new HarmonyMethod(stdPost));

            Type sexNeedType = AccessTools.TypeByName("rjw.Need_Sex");
            MethodInfo needTarget = sexNeedType != null ? AccessTools.Method(sexNeedType, "NeedInterval") : null;
            MethodInfo needPre = AccessTools.Method(typeof(RDFGenesHarmonyInit), "SexNeedPrefix");
            MethodInfo needPost = AccessTools.Method(typeof(RDFGenesHarmonyInit), "SexNeedPostfix");
            if (needTarget != null && needPre != null && needPost != null)
                harmony.Patch(needTarget, prefix: new HarmonyMethod(needPre), postfix: new HarmonyMethod(needPost));
        }

        public static void GeneTrackerChangedPostfix(Pawn_GeneTracker __instance)
        {
            RDFGeneUtility.InvalidatePawnCache(RDFGeneUtility.ExtractPawnFast(__instance));
        }

        public static void SizedApparelSkeletonLookupPrefix(object __instance, out BodyTypeDef __state)
        {
            __state = null;
            Pawn pawn = ExtractPawn(__instance);
            if (pawn == null || pawn.story == null) return;

            // v0.7c: only during SAZ skeleton selection, make the quadruped body use
            // its own anchor skeleton. The actual pawn BodyType remains Female before
            // and after this call, so the working vanilla head/body renderer is preserved.
            if (RDFGeneUtility.HasRubberBitchBody(pawn))
            {
                BodyTypeDef anchor = RDFGeneUtility.GetBodyTypeDefCached("RDFBody_RubberBitchAnchor");
                if (anchor != null)
                {
                    __state = pawn.story.bodyType;
                    pawn.story.bodyType = anchor;
                    return;
                }
            }

            if (RDFGeneUtility.HasSentientDisplayDollBody(pawn))
            {
                BodyTypeDef anchor = RDFGeneUtility.GetBodyTypeDefCached("RDFBody_SentientDisplayDollAnchor");
                if (anchor != null)
                {
                    __state = pawn.story.bodyType;
                    pawn.story.bodyType = anchor;
                    return;
                }
            }

            if (RDFGeneUtility.HasDollmareBody(pawn))
            {
                BodyTypeDef anchor = RDFGeneUtility.GetBodyTypeDefCached("RDFBody_DollmareAnchor");
                if (anchor != null)
                {
                    __state = pawn.story.bodyType;
                    pawn.story.bodyType = anchor;
                    return;
                }
            }

            if (!RDFGeneUtility.IsRDFBodyType(pawn.story.bodyType)) return;
            BodyTypeDef female = BodyTypeDefOf.Female;
            if (female == null) return;
            __state = pawn.story.bodyType;
            pawn.story.bodyType = female;
        }

        public static void SizedApparelBodyTypeLookupPrefix(object __instance, out BodyTypeDef __state)
        {
            __state = null;
            if (__instance == null) return;
            Pawn pawn = RDFGeneUtility.ExtractPawnFast(__instance);
            if (pawn == null || pawn.story == null || !RDFGeneUtility.IsRDFBodyType(pawn.story.bodyType)) return;

            __state = pawn.story.bodyType;
            pawn.story.bodyType = BodyTypeDefOf.Female;
        }

        public static void SizedApparelBodyTypeLookupPostfix(object __instance, BodyTypeDef __state)
        {
            if (__state == null || __instance == null) return;
            Pawn pawn = RDFGeneUtility.ExtractPawnFast(__instance);
            if (pawn == null || pawn.story == null) return;
            pawn.story.bodyType = __state;
        }

        public sealed class SAZBodyPartGraphicState
        {
            public BodyTypeDef originalBodyType;
            public string originalHediffName;
            public bool bodyTypeChanged;
            public bool hediffNameChanged;
        }

        public static void SizedApparelBodyPartGraphicPrefix(object __instance, out SAZBodyPartGraphicState __state)
        {
            __state = null;
            if (__instance == null) return;

            Pawn pawn = ExtractPawn(__instance);
            if (pawn == null) return;

            SAZBodyPartGraphicState state = new SAZBodyPartGraphicState();

            // Existing RDF-body normalization: SAZ has reliable Human/Female anatomy
            // texture variants, while the actual pawn keeps its RDF body type.
            if (pawn.story != null && RDFGeneUtility.IsRDFBodyType(pawn.story.bodyType))
            {
                state.originalBodyType = pawn.story.bodyType;
                state.bodyTypeChanged = true;
                pawn.story.bodyType = BodyTypeDefOf.Female;
            }

            if (pawn.genes != null)
            {
                RDFGeneUtility.SAZFieldCache fields = RDFGeneUtility.GetSAZFields(__instance.GetType());
                FieldInfo hediffField = fields != null ? fields.currentHediffName : null;
                FieldInfo partField = fields != null ? fields.bodyPartName : null;

                if (hediffField != null && hediffField.FieldType == typeof(string) && partField != null)
                {
                    string partName = partField.GetValue(__instance) as string;
                    if (!string.IsNullOrEmpty(partName))
                    {
                        bool dollAnatomy = RDFGeneUtility.HasGene(pawn, "RDF_Gene_DollAnatomy");
                        bool livingLatex = RDFGeneUtility.HasGene(pawn, "RDF_Gene_LivingLatexAnatomy");
                        string target = null;

                        if (partName.IndexOf("Breast", StringComparison.OrdinalIgnoreCase) >= 0)
                        {
                            if (dollAnatomy) target = "RDFDollBreasts";
                            else if (livingLatex) target = "RDFLivingLatexBreasts";
                        }
                        else if (partName.IndexOf("Anus", StringComparison.OrdinalIgnoreCase) >= 0)
                        {
                            if (dollAnatomy) target = "RDFDollAnus";
                            else if (livingLatex) target = "RDFLivingLatexAnus";
                        }
                        else if (partName.IndexOf("Penis", StringComparison.OrdinalIgnoreCase) >= 0 ||
                                 partName.IndexOf("Balls", StringComparison.OrdinalIgnoreCase) >= 0)
                        {
                            if (dollAnatomy) target = "RDFDollPenis";
                            else if (livingLatex) target = "RDFLivingLatexPenis";
                        }
                        else if (partName.IndexOf("Vagina", StringComparison.OrdinalIgnoreCase) >= 0)
                        {
                            if (dollAnatomy) target = "RDFDollVagina";
                            else if (livingLatex) target = "RDFLivingLatexVagina";
                        }

                        if (!string.IsNullOrEmpty(target))
                        {
                            string original = hediffField.GetValue(__instance) as string;
                            if (!string.Equals(original, target, StringComparison.Ordinal))
                            {
                                state.originalHediffName = original;
                                state.hediffNameChanged = true;
                                hediffField.SetValue(__instance, target);
                            }
                        }
                    }
                }
            }

            if (state.bodyTypeChanged || state.hediffNameChanged)
                __state = state;
        }

        public static void SizedApparelBodyPartGraphicPostfix(object __instance, SAZBodyPartGraphicState __state)
        {
            if (__state == null || __instance == null) return;

            if (__state.hediffNameChanged)
            {
                RDFGeneUtility.SAZFieldCache fields = RDFGeneUtility.GetSAZFields(__instance.GetType());
                FieldInfo hediffField = fields != null ? fields.currentHediffName : null;
                if (hediffField != null && hediffField.FieldType == typeof(string))
                    hediffField.SetValue(__instance, __state.originalHediffName);
            }

            if (__state.bodyTypeChanged)
            {
                Pawn pawn = ExtractPawn(__instance);
                if (pawn != null && pawn.story != null)
                    pawn.story.bodyType = __state.originalBodyType;
            }
        }

        private static Pawn ExtractPawn(object instance)
        {
            return RDFGeneUtility.ExtractPawnFast(instance);
        }

        public static void RDFBaseHeadOffsetPostfix(object __instance, object[] __args, ref Vector3 __result)
        {
            Pawn pawn = ExtractPawn(__instance);
            if (pawn == null) return;

            int bodyKind = RDFGeneUtility.GetSpecialHeadBodyKind(pawn);
            if (bodyKind == 0) return;
            bool rubberBitch = bodyKind == 1;
            bool displayDoll = bodyKind == 2;

            Rot4 facing = pawn.Rotation;
            if (__args != null)
            {
                for (int i = 0; i < __args.Length; i++)
                {
                    object arg = __args[i];
                    if (arg != null && arg.GetType() == typeof(Rot4))
                    {
                        facing = (Rot4)arg;
                        break;
                    }
                }
            }

            if (rubberBitch)
            {
                if (facing == Rot4.East) { __result.x += 0.22f; __result.z += 0.12f; }
                else if (facing == Rot4.West) { __result.x -= 0.22f; __result.z += 0.12f; }
                else if (facing == Rot4.North) __result.z += 0.22f;
                else if (facing == Rot4.South) __result.z += 0.10f;
                return;
            }

            if (displayDoll)
            {
                if (facing == Rot4.South) __result.z += 0.27f;
                else if (facing == Rot4.North) { __result.x -= 0.05f; __result.z += 0.22f; }
                else if (facing == Rot4.East) { __result.x -= 0.08f; __result.z += 0.16f; }
                else if (facing == Rot4.West) { __result.x += 0.08f; __result.z += 0.16f; }
                return;
            }

            // Dollmare offsets are the exact currently approved v0.9j values.
            if (facing == Rot4.East) { __result.x += 0.13f; __result.z += 0.135f; }
            else if (facing == Rot4.West) { __result.x -= 0.13f; __result.z += 0.135f; }
            else if (facing == Rot4.South) __result.z += 0.125f;
            else if (facing == Rot4.North) __result.z += 0.135f;
        }

        public static void RubberBitchHeadOffsetPostfix(PawnRenderNode __0, PawnDrawParms __1, ref Vector3 __result)
{
    if (__0 == null || __0.tree == null) return;
    Pawn pawn = __0.tree.pawn;
    if (!RDFGeneUtility.HasRubberBitchBody(pawn)) return;

    // v0.7g screenshot-guided tuning:
    // - side views: much higher and farther toward the facing/front side
    // - back view: higher
    // - front view: unchanged
    if (__1.facing == Rot4.East)
    {
        __result.x -= 0.54f;
        __result.z += 0.74f;
    }
    else if (__1.facing == Rot4.West)
    {
        __result.x += 0.54f;
        __result.z += 0.74f;
    }
    else if (__1.facing == Rot4.North)
    {
        __result.z += 0.72f;
    }
}


        public static void SizedApparelResolveDrawColorsPostfix(object __instance, ref Tuple<Color, Color> __result)
        {
            if (__instance == null || __result == null) return;

            Type instanceType = __instance.GetType();
            Pawn pawn = ExtractPawn(__instance);
            Color rdfColor;
            if (!RDFGeneUtility.TryGetActiveRDFSkinColor(pawn, out rdfColor)) return;
            RDFGeneUtility.SAZFieldCache fields = RDFGeneUtility.GetSAZFields(instanceType);

            // SAZ 1.6 stores the pawn as ___pawn in the current build. Older builds used
            // pawn, which is why the previous patch never saw the pawn and breasts kept
            // their endogenous color. Restrict the forced color to Skin body parts and
            // breasts so nipples and custom RJW genital-type coloration remain untouched.
            bool shouldTint = false;

            FieldInfo colorTypeField = fields != null ? fields.colorType : null;
            object colorType = colorTypeField != null ? colorTypeField.GetValue(__instance) : null;
            if (colorType != null && string.Equals(colorType.ToString(), "Skin", StringComparison.OrdinalIgnoreCase))
                shouldTint = true;

            FieldInfo breastsField = fields != null ? fields.isBreasts : null;
            if (breastsField != null && breastsField.FieldType == typeof(bool))
            {
                object b = breastsField.GetValue(__instance);
                if (b is bool && (bool)b) shouldTint = true;
            }

            FieldInfo nameField = fields != null ? fields.bodyPartName : null;
            string bodyPartName = nameField != null ? nameField.GetValue(__instance) as string : null;
            if (!string.IsNullOrEmpty(bodyPartName) && bodyPartName.IndexOf("Breast", StringComparison.OrdinalIgnoreCase) >= 0)
                shouldTint = true;

            // RDF anatomy textures are neutral material masks.  Tint them with the
            // exact active RDF skin gene color so anatomy follows all 8 colors and
            // the Glossy / Metallic / Wet material variants without duplicating
            // thousands of SAZ textures.
            FieldInfo hediffNameField = fields != null ? fields.currentHediffName : null;
            string currentHediffName = hediffNameField != null ? hediffNameField.GetValue(__instance) as string : null;
            if (!string.IsNullOrEmpty(currentHediffName) &&
                (currentHediffName.StartsWith("RDFDoll", StringComparison.Ordinal) ||
                 currentHediffName.StartsWith("RDFLivingLatex", StringComparison.Ordinal)))
                shouldTint = true;

            if (!shouldTint) return;

            __result = new Tuple<Color, Color>(rdfColor, rdfColor);

            // Keep SAZ's cached draw colors synchronized with the returned value.
            FieldInfo draw1 = fields != null ? fields.drawColor1 : null;
            FieldInfo draw2 = fields != null ? fields.drawColor2 : null;
            if (draw1 != null && draw1.FieldType == typeof(Color)) draw1.SetValue(__instance, rdfColor);
            if (draw2 != null && draw2.FieldType == typeof(Color)) draw2.SetValue(__instance, rdfColor);
        }

        public static void FilterInternalGeneListsPrefix(object[] __args)
        {
            if (__args == null) return;
            foreach (object arg in __args)
            {
                IList list = arg as IList;
                if (list == null || list.IsReadOnly) continue;

                for (int i = list.Count - 1; i >= 0; i--)
                {
                    GeneDef def = list[i] as GeneDef;
                    if (RDFGeneUtility.IsInternalImplementationGene(def))
                        list.RemoveAt(i);
                }
            }
        }

        public static bool SkipInternalGeneDrawPrefix(object[] __args)
        {
            if (__args == null) return true;
            foreach (object arg in __args)
            {
                GeneDef def = arg as GeneDef;
                if (RDFGeneUtility.IsInternalImplementationGene(def))
                    return false;
            }
            return true;
        }

        public static void BodyGraphicForPostfix(Pawn __0, ref Graphic __result)
        {
            Graphic rdfGraphic;
            if (RDFGeneUtility.TryGetRDFReplacementBodyGraphic(__0, out rdfGraphic) && rdfGraphic != null)
                __result = rdfGraphic;
        }

        public static void RDFDamageSensitivityPrefix(Thing __instance, ref DamageInfo dinfo)
        {
            Pawn pawn = __instance as Pawn;
            if (pawn == null || pawn.genes == null || dinfo.Def == null || string.IsNullOrEmpty(dinfo.Def.defName)) return;

            string n = dinfo.Def.defName.ToLowerInvariant();
            float multiplier = 1f;

            // Highly Flammable Latex: existing Flammability x2 makes ignition easier;
            // this completes the intended effect with +50% direct fire/flame damage.
            bool chemicalBurn = n.Contains("acid") || n.Contains("corros") || n.Contains("solvent") ||
                                n.Contains("caustic") || n.Contains("chemicalburn") || n.Contains("chemical_burn");
            bool fireDamage = !chemicalBurn && (n.Contains("flame") || n.Contains("fire") || n == "burn" || n.StartsWith("burn_"));
            if (fireDamage && RDFGeneUtility.HasGene(pawn, "RDF_Gene_HighlyFlammableLatex"))
                multiplier *= 1.50f;

            // Solvent Sensitivity: broad compatibility by common DamageDef naming conventions.
            bool solventDamage = n.Contains("acid") || n.Contains("corros") || n.Contains("solvent") ||
                                 n.Contains("caustic") || n.Contains("chemicalburn") || n.Contains("chemical_burn");
            if (solventDamage && RDFGeneUtility.HasGene(pawn, "RDF_Gene_SolventSensitivity"))
                multiplier *= 1.50f;

            if (Math.Abs(multiplier - 1f) > 0.001f)
                dinfo.SetAmount(dinfo.Amount * multiplier);
        }

        public static void BleedRatePostfix(Hediff __instance, ref float __result)
        {
            if (__result <= 0f || __instance == null || __instance.pawn == null) return;
            Pawn p = __instance.pawn;
            if (RDFGeneUtility.HasGene(p, "RDF_Gene_SelfSealingLatex")) __result *= 0.15f;
            else if (RDFGeneUtility.HasGene(p, "RDF_Gene_RubberFlesh")) __result *= 0.50f;
        }

        public static void STDIsImmunePostfix(ref bool __result, object[] __args)
        {
            if (__result || __args == null) return;
            Pawn p = __args.OfType<Pawn>().FirstOrDefault();
            if (p != null && RDFGeneUtility.HasGene(p, "RDF_Gene_RubberGenitalia")) __result = true;
        }

        public static void SexNeedPrefix(Need __instance, out float __state) { __state = __instance != null ? __instance.CurLevel : 0f; }
        public static void SexNeedPostfix(Need __instance, float __state)
        {
            if (__instance == null) return;
            Pawn p = RDFGeneUtility.ExtractPawnFast(__instance);
            if (p == null || !RDFGeneUtility.HasGene(p, "RDF_Gene_PleasureDoll")) return;
            float normalDrop = __state - __instance.CurLevel;
            if (normalDrop > 0f) __instance.CurLevel -= normalDrop * 0.75f;
        }

        public static void InheritedGeneSetPostfix(Pawn father, Pawn mother, ref GeneSet __result)
        {
            if (__result == null || father == null || mother == null) return;
            bool stable = RDFGeneUtility.HasGene(father, "RDF_Gene_StableRDFInheritance") || RDFGeneUtility.HasGene(mother, "RDF_Gene_StableRDFInheritance");
            bool variable = RDFGeneUtility.HasGene(father, "RDF_Gene_VariableRDFInheritance") || RDFGeneUtility.HasGene(mother, "RDF_Gene_VariableRDFInheritance");
            if (!stable && !variable) return;

            string[] prefixes = { "RDF_Skin_", "RDF_Hair_", "RDF_Gene_Body_" };
            foreach (string prefix in prefixes)
            {
                List<GeneDef> candidates = new List<GeneDef>();
                foreach (Pawn parent in new[] { father, mother })
                {
                    if (parent.genes == null) continue;
                    foreach (Gene g in parent.genes.GenesListForReading)
                        if (g != null && g.Active && g.def != null && g.def.defName.StartsWith(prefix, StringComparison.Ordinal)) candidates.Add(g.def);
                }
                if (candidates.Count == 0) continue;
                GeneDef chosen = stable ? candidates[0] : candidates[Rand.Range(0, candidates.Count)];
                __result.AddGene(chosen);
            }
        }
    }
}
