using MeshVR; using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using System.Linq; using UnityEngine; using UnityEngine.UI; using UnityEngine.Events; using UnityEngine.EventSystems; using SimpleJSON; using AssetBundles; namespace RunRudolf { public class NippleClippingEliminator : MVRScript { private string date = "Version 2026-02-24"; private bool debug1 = false; //LogError with debug steps private JSONStorableBool debug2; //LogMessage with info on which section is active, available in UI private bool debug3 = false; //LogMessage with step measurement private bool debug4 = false; //LogError with debug steps private float t0; private float t1; private int timer4 = 0; //Modes private JSONStorableBool nippleMode; private JSONStorableBool pubicMode; private JSONStorableBool hairSimProp; private bool isSceneLoaded = false; private bool isActive = false; private string clothingNippleCovering; private string clothingPubicCovering; private string nameLower = ""; private bool hasTopTag = false; private bool hasExposeOpen = false; //Special cases private float morphSum = 0f; private float morphSumOld = 0f; private bool nippleMorphSumChanged = false; private bool nippleSliderValueChanged = false; private float lengthSum = 0f; private float lengthSumOld = 0f; private bool hairLengthSumChanged = false; private bool hairSliderValueChanged = false; class PersonData { public Atom atom; public DAZCharacterSelector geometry; public DAZMorph[] nippleMorphs; public float[] restoreNippleValues; public bool nippleCovered; public bool nippleCoveredOld; public bool isFemale; public Dictionary pubicHairRestoreWidth = new Dictionary(); public bool pubicCovered; public bool pubicCoveredOld; public List clothingCache = new List(32); public bool clothingCacheDirty = true; public int lastClothingChildCount = -1; public int cachedTagsHash; public const int AdditionalNippleSlots = 3; public string[] additionalNippleMorphUids = new string[AdditionalNippleSlots]; public int nextAdditionalNippleSlot = 0; public int lastClothingInstanceCount; } List persons = new List(); class ClothingCacheEntry { public int cachedTagsHash; public DAZClothingItem item; public bool isPotentialTop; public bool isPotentialBottom; public bool isExpose; public bool wasActive; public JSONStorable simStorable; public bool simEnabled; public bool allowDetach; } //Button hook and mouse over detection before preset saving private Atom lastSelectedAtom; private Button hookedStorePresetButton; private bool presetHoverArmed = false; private bool presetSaveInProgress = false; private Coroutine hoverCleanupRoutine; private StorePresetHoverHook hookedHoverHook; //UI private UIDynamicTextField _textField1; //Dynamic addon morph status field private string stateVal = ""; private UIDynamicTextField _textField2; //Help text field private UIDynamicTextField _textField3; //Dynamic active clothing info field private string stateVal3 = ""; private UIDynamicPopup myNippleMorphsPopup1; private JSONStorableStringChooser myNippleMorphsList1; private List myNippleMorphs1; private Dictionary myUidLookup1 = new Dictionary(); private string selectedAdditionalNippleMorphUid = ""; private JSONStorableString selectedAdditionalNippleMorphJSON; private UIDynamicSlider nippleFlattenedSlider; private JSONStorableFloat nippleFlattenedMorphValueJSON; private UIDynamicSlider hairInvisibleSlider; private JSONStorableFloat hairInvisibleLengthValueJSON; private bool refreshNippleMorphsList = false; private List boolParamNamesList; // B69 Addition - List declaration which will be used to check available bool parameters for each clothing item public override void Init() { try { base.Init(); boolParamNamesList = new List(); // B69 Addition - initialising the list of bool parameters as new empty list debug2 = new JSONStorableBool("Enable Debug", false); //Debug mode debug2.storeType = JSONStorableParam.StoreType.Full; RegisterBool(debug2); // B69 Start Change Section // see orig row below now commented out. 2 different bools were attempting to be registered with the same name "Enable" //nippleMode = new JSONStorableBool("Enable", true, tickModeCallback1); nippleMode = new JSONStorableBool("Enable Nipple", true, tickModeCallback1); // B69 End Change Section nippleMode.storeType = JSONStorableParam.StoreType.Full; RegisterBool(nippleMode); // B69 Start Change Section // i've given each JSON Storable bool a unique name for registering so they should both now be triggerable // note these changes don't affect the UI or other functionality in the plugin but do affect any external triggers using these // although that being said, pubicMode trigger was failing to register so wasn't available previously for trigger calls //pubicMode = new JSONStorableBool("Enable", true, tickModeCallback2); pubicMode = new JSONStorableBool("Enable Pubic", true, tickModeCallback2); // B69 End Change Section pubicMode.storeType = JSONStorableParam.StoreType.Full; RegisterBool(pubicMode); hairSimProp = new JSONStorableBool("Auto On/Off Pubic Hair Sim", false); //Auto hair sim deactivation hairSimProp.storeType = JSONStorableParam.StoreType.Full; RegisterBool(hairSimProp); //Start an async coroutine to load stuff which requires the scene to be fully loaded, and prevent Update from starting before everything is initialised StartCoroutine(WaitForLoadingComplete()); //Start create UI createStaticDescriptionTextMain("Nipple Clipping Eliminiator", "Nipple Clipping Eliminiator", false, 34); createStaticDescriptionTextMain("Version", "" + " " + date + "", true, 34); UIDynamic spacer = CreateSpacer(false); spacer.height = 100; UIDynamic spacer2 = CreateSpacer(true); spacer2.height = 48; createStaticDescriptionTextMain("Hide Nipples","Hide Nipples",false,34); CreateToggle(nippleMode, false); nippleFlattenedMorphValueJSON = new JSONStorableFloat("Flattened Nipple %", 0.0001f, 0.0001f, 1.0f, false); //Nipple flattened morph value slider nippleFlattenedMorphValueJSON.storeType = JSONStorableParam.StoreType.Full; RegisterFloat(nippleFlattenedMorphValueJSON); nippleFlattenedMorphValueJSON.setCallbackFunction += (float v) => { floatModeCallback1(); }; nippleFlattenedSlider = CreateSlider(nippleFlattenedMorphValueJSON, false); createStaticDescriptionTextMain("Additional Nipple Morphs","Additional Nipple Morphs",false,34); //Create nipple morphs chooser popup 1 myNippleMorphs1 = new List(); myNippleMorphs1.Add(""); myUidLookup1 = new Dictionary(); myUidLookup1.Add("", ""); myNippleMorphsList1 = new JSONStorableStringChooser("myNippleMorphsPopup1", new List { "" }, "", "Select", switchModeCallback1); myNippleMorphsPopup1 = CreateFilterablePopup(myNippleMorphsList1, false); RegisterStringChooser(myNippleMorphsList1); myNippleMorphsList1.valNoCallback = ""; myNippleMorphsPopup1.popup.onOpenPopupHandlers += () => { myNippleMorphsList1.choices = GetAvailableNippleMorphNames(); if (!myNippleMorphs1.Contains(myNippleMorphsList1.val)) myNippleMorphsList1.val = ""; }; selectedAdditionalNippleMorphJSON = new JSONStorableString("selectedAdditionalNippleMorphJSON","", OnAdditionalNippleMorphLoaded); selectedAdditionalNippleMorphJSON.storeType = JSONStorableParam.StoreType.Full; RegisterString(selectedAdditionalNippleMorphJSON); _textField1 = CreateStaticDescriptionText(this, "", "", false, 34); //Help text field with addon morphs SetupTextField(_textField1, 96f, false, false); //createStaticDescriptionTextMain("","",false,35); createStaticDescriptionTextMain("Hide Pubic","Hide Pubic",false,34); CreateToggle(pubicMode, false); hairInvisibleLengthValueJSON = new JSONStorableFloat("Hidden Hair Length %", 0.0001f, 0.0001f, 1.0f, false); //Invisible pubic hair length value slider hairInvisibleLengthValueJSON.storeType = JSONStorableParam.StoreType.Full; RegisterFloat(hairInvisibleLengthValueJSON); hairInvisibleLengthValueJSON.setCallbackFunction += (float v) => { floatModeCallback2(); }; hairInvisibleSlider = CreateSlider(hairInvisibleLengthValueJSON, false); CreateToggle(hairSimProp, false); //createStaticDescriptionTextMain("","",false,35); createStaticDescriptionTextMain("Status & Debug","Status & Debug",false,34); //Dynamic active hiding clothing info field _textField3 = CreateStaticDescriptionText(this, "", "", false, 34); SetupTextField(_textField3, 106f, false, false); CreateToggle(debug2, false); createStaticDescriptionTextMain("How to Use","How to Use",true,34); JSONStorableString helpText = new JSONStorableString("Help", "Purpose: Eliminates nipple clipping & hides pubic hair under clothing. Clothing names & tags are analyzed, sim detached state is considered. Person/scene/session plugin.\n\n" + "Tags: The clothing names and clothing tags are checked for certain terms, both in- & excluding certain clothing items from hiding nipples and pubic hair. If case of false negatives or positives, use overriding.\n\n" + "Overriding: Add tags to the clothing's 'User Prefs' to override hiding/unhiding: 'hidenipples' or 'hidepubes' to hide it, 'exposed' to unhide it. Tags are saved with the clothing item.\n\n" + "Values Affected: Nipple morphs: Nipple Diameter (f), Nipples Diameter (m/f), Nipple Length (f), Nipples Large (m/f), Nipple Size (f), Nipples Size (m/f), Nipples (m/f), Nipples Apply (f), Nipples Depth (m/f). Add up to three additional nipple morphs. The plugin controls pubic hair width.\n\n" + "Note: Do not change nipple morph values / pubic hair length while hidden (use 'Flattened Nipple %' slider)." ); UIDynamic spacer3 = CreateSpacer(true); spacer.height = 48; GridLayoutGroup glg = spacer3.gameObject.AddComponent(); glg.constraint = GridLayoutGroup.Constraint.FixedColumnCount; glg.constraintCount = 1; glg.cellSize = new Vector2(520, 1030); _textField2 = UIWidgets.CreateVAMTextField(this, spacer3.transform); _textField2.transform.localPosition = Vector3.zero; _textField2.text = helpText.val; //End create UI //Add listeners SuperController.singleton.onBeforeSceneSaveHandlers += BeforeSceneSave; SuperController.singleton.onSceneSavedHandlers += AfterSceneSave; SuperController.singleton.onSceneLoadedHandlers += OnSceneLoaded; //First time build clothing cache foreach (var p in persons) { p.clothingCacheDirty = true; } if (debug2.val) { SuperController.LogMessage("Init() completed"); } } catch (Exception e) { SuperController.LogError($"{nameof(NippleClippingEliminator)}.{nameof(Init)}: {e}"); } } private IEnumerator WaitForLoadingComplete() { while (SuperController.singleton.isLoading) { isSceneLoaded = false; yield return null; } while (SuperController.singleton.freezeAnimation) { isSceneLoaded = false; yield return null; } yield return null; isSceneLoaded = true; isActive = true; //Initially fill the nipple morphs list refreshNippleMorphsList = true; //First initialisation of person atoms in the scene (empties existing list). Don't do too early, as additional nipple morphs will not be ready. InitPersons(clearList: true); //getAllStorables(); //For debug only //getUIHierarchy(); //For debug only if (debug2.val) { SuperController.LogMessage("WaitForLoadingComplete() completed"); } } //System to hook the "Create New Preset" / "Overwrite Preset" (hover & click) button in VaM UI based on currently selected person atom private void AddStorePresetHook() { Atom selectedAtom = SuperController.singleton.GetSelectedAtom(); if (selectedAtom == null || selectedAtom.type != "Person") return; if (selectedAtom == lastSelectedAtom) return; lastSelectedAtom = selectedAtom; Button[] allButtons = selectedAtom.gameObject.GetComponentsInChildren