• Hi Guest!

    We are extremely excited to announce the release of our first Beta1.1 and the first release of our Public AddonKit!
    To participate in the Beta, a subscription to the Entertainer or Creator Tier is required. For access to the Public AddonKit you must be a Creator tier member. Once subscribed, download instructions can be found here.

    Click here for information and guides regarding the VaM2 beta. Join our Discord server for more announcements and community discussion about VaM2.
  • Hi Guest!

    VaM2 Resource Categories have now been added to the Hub! For information on posting VaM2 resources and details about VaM2 related changes to our Community Forums, please see our official announcement here.
BodyLanguage

Plugins + Scripts BodyLanguage

Download [<1 MB]
Damn! It just dawned on me why you use the Yellow Dots near the vagina and anus. You couldn't find the anal trigger and the vaginas, so you created your own! I wasted so much time trying to interact with them, and the solution was right under my nose! You are a genius!!! Haha!
 
Last edited:
Damn! It just dawned on me why you use the Yellow Dots near the vagina and anus. You couldn't find the anal trigger and the vaginas, so you created your own! I wasted so much time trying to interact with them, and the solution was right under my nose! You are a genius!!! Haha!
Sure, I could find the vagina trigger, but it's not usable for my purposes as it is too big and poorly placed. I need to differentiate between the vagina and the anus, which doesn't even have a trigger in vanilla VAM.
 
Sure, I could find the vagina trigger, but it's not usable for my purposes as it is too big and poorly placed. I need to differentiate between the vagina and the anus, which doesn't even have a trigger in vanilla VAM.
I'll repeat it again, damn, you're a genius Cheesy!!!
 
Cheesy, I've noticed an effect on male atoms when they are holding the shaft. The BL plugin is installed on the female, natch. The female atoms don't seem to have this problem when they grab his thing, but the male atom seems to loose his grip. It's pretty random. Sometimes it's fine, sometimes not. Going to another pose and back again can either fix it, or cause it to appear. The jacking motion is being done with the dub.force plugin, but that's just the motion. The grip is from BL. It seems like it's not reliable. Is this a known issue?

1757433114.jpg
 
Cheesy, I've noticed an effect on male atoms when they are holding the shaft. The BL plugin is installed on the female, natch. The female atoms don't seem to have this problem when they grab his thing, but the male atom seems to loose his grip. It's pretty random. Sometimes it's fine, sometimes not. Going to another pose and back again can either fix it, or cause it to appear. The jacking motion is being done with the dub.force plugin, but that's just the motion. The grip is from BL. It seems like it's not reliable. Is this a known issue?

View attachment 523777
v71 doesn't have a male grabbing functionality at all. Is your male an AltFuta? If not, the grabbing is done by some other plugin like DiviningHands.
 
Sorry, my explanation was bad. The male is a male atom. The female atom has BL loaded, with poses saved. The grasp is part of the pose that gets saved by BL. The jacking motion is another plugin, but that's only the back and forth (dub.force). There is no other plugin controlling his hands. The only plugins he has are dub.force, Embody, and VamMoan. What happens is that sometimes the pose gets loaded with the morph Right Fingers Grasp at 0, sometimes at 0.3, which is what it's supposed to be. When it loads the pose and it's 0, reloading the pose with the middle mouse button fixes it.

Fingers Grasp.jpg
 
Cheesy, did you ever go over how to get rid of the inference error as seen below? I just made this account just to ask you this because it fucked over my scene:

1758645424983.png
 
Hi Cheesy. What do yoy say? :)

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;
using SimpleJSON;

namespace Disa
{
// Manages trigger zones for orifices (e.g., Mouth, Vagina, Anus) on a selected Atom (character) in a Unity-based scene.
// Tracks penetration and proximity events, manages penis colliders for male atoms, and handles UI for configuration.
public class OrificeTriggerManager : MVRScript
{
public static OrificeTriggerManager singleton;
private Atom selectedAtom;
private List<object> uiElements;
private List<JSONStorableFloat> sliderStorables;
private List<JSONStorableBool> toggleStorables;
private List<JSONStorableString> textFieldStorables;
private List<UIDynamicButton> buttonElements;
private string selectedOrifice;
private Dictionary<string, Orifice> orifices;
private Dictionary<string, ProximityHandler> proximityHandlers;
private Dictionary<string, OrificeTriggerHandler> triggerHandlers;
private bool debugVisible = true;
private bool penisCollidersVisible = false;
private Dictionary<Atom, List<GameObject>> penisViz = new Dictionary<Atom, List<GameObject>>();
private JSONStorableString mouthStatus;
private JSONStorableString vaginaStatus;
private JSONStorableString anusStatus;
private JSONStorableString mouthProxStatus;
private JSONStorableString vaginaProxStatus;
private JSONStorableString anusProxStatus;
private List<Atom> maleAtoms;
public Dictionary<Atom, Penis> penises;

public Dictionary<string, Orifice> Orifices
{
get { return orifices; }
}

// Initializes the manager, sets up status tracking, finds male atoms, and creates the UI.
public override void Init()
{
singleton = this;
SuperController.LogMessage("OrificeTriggerManager: Initialization started");
uiElements = new List<object>();
sliderStorables = new List<JSONStorableFloat>();
toggleStorables = new List<JSONStorableBool>();
textFieldStorables = new List<JSONStorableString>();
buttonElements = new List<UIDynamicButton>();
orifices = new Dictionary<string, Orifice>();
proximityHandlers = new Dictionary<string, ProximityHandler>();
triggerHandlers = new Dictionary<string, OrificeTriggerHandler>();
selectedOrifice = "Mouth";
maleAtoms = new List<Atom>();
penises = new Dictionary<Atom, Penis>();

// Initialize status strings for tracking penetration and proximity
mouthStatus = new JSONStorableString("Mouth Status", "Penis in mouth: No");
vaginaStatus = new JSONStorableString("Vagina Status", "Penis in vagina: No");
anusStatus = new JSONStorableString("Anus Status", "Penis in anus: No");
mouthProxStatus = new JSONStorableString("Mouth Proximity", "Penis near mouth: No");
vaginaProxStatus = new JSONStorableString("Vagina Proximity", "Penis near vagina: No");
anusProxStatus = new JSONStorableString("Anus Proximity", "Penis near anus: No");

selectedAtom = containingAtom;
if (selectedAtom == null || selectedAtom.type != "Person")
{
SuperController.LogError("OrificeTriggerManager: This script must be added to a Person-type atom.");
return;
}
SuperController.LogMessage("OrificeTriggerManager: Atom automatically selected: " + selectedAtom.uid);
SelectAtom(selectedAtom.uid);

FindMaleAtoms();
CreateUI();
SuperController.LogMessage("OrificeTriggerManager: Initialization completed");
}

// Finds male atoms in the scene and initializes their penis colliders.
private void FindMaleAtoms()
{
maleAtoms.Clear();
penises.Clear();
penisViz.Clear();

List<Atom> allAtoms = SuperController.singleton.GetAtoms();
for (int i = 0; i < allAtoms.Count; i++)
{
Atom atom = allAtoms;
if (atom.type == "Person" && atom != selectedAtom)
{
if (HasPenis(atom))
{
maleAtoms.Add(atom);
penises[atom] = new Penis(atom);
SuperController.LogMessage("OrificeTriggerManager: Found male atom: " + atom.uid);
}
}
}
}

// Checks if an atom has penis-related components or colliders.
private bool HasPenis(Atom atom)
{
if (atom.GetStorableByID("penisTipControl") != null) return true;
if (atom.GetStorableByID("penisMidControl") != null) return true;
if (atom.GetStorableByID("penisBaseControl") != null) return true;

Collider[] colliders = atom.GetComponentsInChildren<Collider>();
foreach (var col in colliders)
{
string colNameLower = col.name.ToLower();
if ((colNameLower.Contains("autocollidergen") || colNameLower.Contains("autogen")) &&
(colNameLower.Contains("hard") || colNameLower.Contains("soft")))
{
Rigidbody[] rbs = col.GetComponentsInParent<Rigidbody>();
foreach (var rb in rbs)
{
if (rb.name.ToLower().Contains("penis"))
return true;
}
}
}
return false;
}

// Selects an atom and sets up its orifices and triggers.
public void SelectAtom(string atomUid)
{
selectedAtom = SuperController.singleton.GetAtomByUid(atomUid);
if (selectedAtom == null)
{
SuperController.LogError("OrificeTriggerManager: Atom " + atomUid + " not found.");
return;
}
SuperController.LogMessage("OrificeTriggerManager: Atom selected: " + atomUid);

orifices.Clear();
proximityHandlers.Clear();
triggerHandlers.Clear();

try
{
string[] orificeNames = new string[] { "Mouth", "Vagina", "Anus" };
for (int i = 0; i < orificeNames.Length; i++)
{
string orificeName = orificeNames;
Orifice orifice = selectedAtom.gameObject.AddComponent<Orifice>();
if (orifice != null)
{
orifice.Init(orificeName, selectedAtom);
if (orifice.rb != null)
{
orifices[orificeName] = orifice;
SetupTriggers(orifice, orificeName);
SuperController.LogMessage("OrificeTriggerManager: Created " + orificeName);
}
else
{
SuperController.LogError("OrificeTriggerManager: Failed to initialize " + orificeName + " due to missing Rigidbody");
UnityEngine.Object.Destroy(orifice);
}
}
else
{
SuperController.LogError("OrificeTriggerManager: Failed to create Orifice component for " + orificeName);
}
}
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error creating orifices: " + e.Message);
}

UpdateAllStatuses();
CreateUI();
}

// Sets up trigger and proximity colliders for an orifice, including their position, scale, and visual debugging.
private void SetupTriggers(Orifice orifice, string name)
{
if (orifice == null || orifice.rb == null)
{
SuperController.LogError("OrificeTriggerManager: Orifice or Rigidbody null for " + name);
return;
}

try
{
// Create entry trigger
GameObject triggerGO = GameObject.CreatePrimitive(PrimitiveType.Capsule);
triggerGO.name = "BL_" + name + "_EnterTrigger";
CapsuleCollider enterTriggerCollider = triggerGO.GetComponent<CapsuleCollider>();
enterTriggerCollider.isTrigger = true;
triggerGO.transform.SetParent(orifice.rb.transform, false);
if (name == "Mouth")
{
triggerGO.transform.localRotation = Quaternion.Euler(90, 0, 0);
}
else if (name == "Anus")
{
triggerGO.transform.localRotation = Quaternion.Euler(25, 0, 0);
}
else
{
triggerGO.transform.localRotation = Quaternion.identity;
}
triggerGO.transform.localScale = new Vector3(orifice.triggerScale.val, orifice.triggerScale.val, orifice.triggerScale.val);
triggerGO.transform.localPosition = new Vector3(0f, orifice.triggerOffsetUp.val + orifice.defaultUp, orifice.triggerOffsetForward.val + orifice.defaultForward);
Renderer renderer = triggerGO.GetComponent<Renderer>();
if (renderer != null)
{
renderer.material = new Material(Shader.Find("Standard"));
renderer.material.color = new Color(0f, 0f, 1f, 0.5f);
renderer.enabled = debugVisible;
}

// Create proximity trigger
GameObject proximityGO = GameObject.CreatePrimitive(PrimitiveType.Capsule);
proximityGO.name = "BL_" + name + "_ProximityTrigger";
CapsuleCollider proximityTrigger = proximityGO.GetComponent<CapsuleCollider>();
proximityTrigger.isTrigger = true;
proximityGO.transform.SetParent(orifice.rb.transform, false);
if (name == "Mouth")
{
proximityGO.transform.localRotation = Quaternion.Euler(90, 0, 0);
}
else if (name == "Anus")
{
proximityGO.transform.localRotation = Quaternion.Euler(25, 0, 0);
}
else
{
proximityGO.transform.localRotation = Quaternion.identity;
}
proximityGO.transform.localScale = new Vector3(orifice.proximityScale.val, orifice.proximityScale.val, orifice.proximityScale.val);
proximityGO.transform.localPosition = new Vector3(0f, orifice.triggerOffsetUp.val + orifice.defaultUp, orifice.triggerOffsetForward.val + orifice.defaultForward);
Renderer proxRenderer = proximityGO.GetComponent<Renderer>();
if (proxRenderer != null)
{
proxRenderer.material = new Material(Shader.Find("Standard"));
proxRenderer.material.SetFloat("_Mode", 2);
proxRenderer.material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
proxRenderer.material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
proxRenderer.material.SetInt("_ZWrite", 0);
proxRenderer.material.DisableKeyword("_ALPHATEST_ON");
proxRenderer.material.EnableKeyword("_ALPHABLEND_ON");
proxRenderer.material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
proxRenderer.material.color = new Color(1f, 1f, 0f, 0.3f);
proxRenderer.enabled = debugVisible && orifice.showProximity.val;
}

OrificeTriggerHandler triggerHandler = triggerGO.AddComponent<OrificeTriggerHandler>();
if (triggerHandler != null)
{
triggerHandler.orifice = orifice;
triggerHandler.isActive = true;
triggerHandlers[name] = triggerHandler;
}

ProximityHandler proximityHandler = proximityGO.AddComponent<ProximityHandler>();
if (proximityHandler != null)
{
proximityHandler.orifice = orifice;
proximityHandler.on = true;
proximityHandlers[name] = proximityHandler;
}

orifice.enterTriggerCollider = enterTriggerCollider;
orifice.proximityTrigger = proximityTrigger;

orifice.showProximity.setCallbackFunction = (bool val) =>
{
if (proxRenderer != null) proxRenderer.enabled = debugVisible && val;
};

orifice.triggerOffsetUp.setCallbackFunction = (float val) =>
{
if (triggerGO != null) triggerGO.transform.localPosition = new Vector3(0f, val + orifice.defaultUp, orifice.triggerOffsetForward.val + orifice.defaultForward);
if (proximityGO != null) proximityGO.transform.localPosition = new Vector3(0f, val + orifice.defaultUp, orifice.triggerOffsetForward.val + orifice.defaultForward);
};
orifice.triggerOffsetForward.setCallbackFunction = (float val) =>
{
if (triggerGO != null) triggerGO.transform.localPosition = new Vector3(0f, orifice.triggerOffsetUp.val + orifice.defaultUp, val + orifice.defaultForward);
if (proximityGO != null) proximityGO.transform.localPosition = new Vector3(0f, orifice.triggerOffsetUp.val + orifice.defaultUp, val + orifice.defaultForward);
};
orifice.triggerScale.setCallbackFunction = (float val) =>
{
if (triggerGO != null) triggerGO.transform.localScale = new Vector3(val, val, val);
};
orifice.proximityScale.setCallbackFunction = (float val) =>
{
if (proximityGO != null) proximityGO.transform.localScale = new Vector3(val, val, val);
};
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error in SetupTriggers for " + name + ": " + e.Message);
}
}

// Creates the UI for configuring orifices and toggling debug/penis collider visibility.
private void CreateUI()
{
ClearUI();
try
{
SetupButton("Mouth Settings", false, delegate { SelectOrifice("Mouth"); });
SetupButton("Vagina Settings", true, delegate { SelectOrifice("Vagina"); });
SetupButton("Anus Settings", false, delegate { SelectOrifice("Anus"); });
SetupButton("Debug", true, ToggleDebugVisibility);
SetupButton("Penis Colliders", true, TogglePenisColliders);

AddStatusUIElements();

SelectOrifice(selectedOrifice);
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error creating UI: " + e.Message);
}
}

// Toggles visibility of trigger and proximity colliders for debugging.
private void ToggleDebugVisibility()
{
debugVisible = !debugVisible;
foreach (var pair in orifices)
{
Orifice orifice = pair.Value;
if (orifice.enterTriggerCollider != null)
{
Renderer renderer = orifice.enterTriggerCollider.gameObject.GetComponent<Renderer>();
if (renderer != null) renderer.enabled = debugVisible;
}
if (orifice.proximityTrigger != null)
{
Renderer proxRenderer = orifice.proximityTrigger.gameObject.GetComponent<Renderer>();
if (proxRenderer != null) proxRenderer.enabled = debugVisible && orifice.showProximity.val;
}
}
SuperController.LogMessage("OrificeTriggerManager: Trigger visibility " + (debugVisible ? "enabled" : "disabled"));
}

// Toggles visibility of penis colliders for debugging.
private void TogglePenisColliders()
{
penisCollidersVisible = !penisCollidersVisible;

if (penisCollidersVisible)
{
FindMaleAtoms();
for (int i = 0; i < maleAtoms.Count; i++)
{
CreatePenisColliderViz(maleAtoms);
}
}
else
{
HideAllPenisViz();
}

SuperController.LogMessage("OrificeTriggerManager: Penis collider visibility " + (penisCollidersVisible ? "enabled" : "disabled"));
}

// Creates visual representations for penis colliders on a male atom.
private void CreatePenisColliderViz(Atom male)
{
if (penisViz.ContainsKey(male))
{
List<GameObject> list = penisViz[male];
for (int i = 0; i < list.Count; i++)
{
GameObject v = list;
if (v != null)
{
v.SetActive(true);
Renderer r = v.GetComponent<Renderer>();
if (r != null) r.enabled = true;
}
}
return;
}

List<GameObject> vizList = new List<GameObject>();
Penis penis = penises.ContainsKey(male) ? penises[male] : null;

if (penis == null)
{
SuperController.LogMessage($"CreatePenisColliderViz: Penis object not found for {male.uid}");
return;
}

// Create transparent material once
Material transparentMat = new Material(Shader.Find("Standard"));
transparentMat.SetFloat("_Mode", 2);
transparentMat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
transparentMat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
transparentMat.SetInt("_ZWrite", 0);
transparentMat.DisableKeyword("_ALPHATEST_ON");
transparentMat.EnableKeyword("_ALPHABLEND_ON");
transparentMat.DisableKeyword("_ALPHAPREMULTIPLY_ON");
transparentMat.color = new Color(1f, 0f, 0f, 0.3f); // Transparent red

// Find penis colliders
Collider[] allColliders = male.GetComponentsInChildren<Collider>();
foreach (var col in allColliders)
{
if (penis.IsPenisCollider(col))
{
// Skip standard penisBase, penisMid, penisTip colliders
string colNameLower = col.name.ToLower();
if (colNameLower.Contains("penisbase") || colNameLower.Contains("penismid") || colNameLower.Contains("penistip"))
{
SuperController.LogMessage($"CreatePenisColliderViz: Skipped standard collider {col.name} for {male.uid}");
continue;
}

GameObject viz = null;

// C# 6.0-compatible type checking
if (col is CapsuleCollider)
{
CapsuleCollider cc = (CapsuleCollider)col;
viz = GameObject.CreatePrimitive(PrimitiveType.Capsule);
viz.name = "PenisColliderViz_Capsule_" + col.name;
viz.transform.SetParent(col.transform, false);
viz.transform.localPosition = cc.center;

// Account for capsule direction
switch (cc.direction)
{
case 0: // X-axis
viz.transform.localRotation = Quaternion.Euler(0f, 0f, 90f);
break;
case 2: // Z-axis
viz.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
break;
default: // Y-axis
viz.transform.localRotation = Quaternion.identity;
break;
}

// Scale capsule
viz.transform.localScale = new Vector3(cc.radius * 2f, cc.height / 2f, cc.radius * 2f);
}
else if (col is SphereCollider)
{
SphereCollider sc = (SphereCollider)col;
viz = GameObject.CreatePrimitive(PrimitiveType.Sphere);
viz.name = "PenisColliderViz_Sphere_" + col.name;
viz.transform.SetParent(col.transform, false);
viz.transform.localPosition = sc.center;
viz.transform.localRotation = Quaternion.identity;
viz.transform.localScale = Vector3.one * (sc.radius * 2f);
}
else if (col is MeshCollider)
{
MeshCollider mc = (MeshCollider)col;
viz = new GameObject("PenisColliderViz_Mesh_" + col.name);
viz.transform.SetParent(col.transform, false);
viz.transform.localPosition = Vector3.zero;
viz.transform.localRotation = Quaternion.identity;
viz.transform.localScale = Vector3.one;

MeshFilter mf = viz.AddComponent<MeshFilter>();
mf.sharedMesh = mc.sharedMesh;

MeshRenderer mr = viz.AddComponent<MeshRenderer>();
mr.material = transparentMat;
mr.enabled = true;
}

if (viz != null)
{
// Remove collider from visualization to avoid interfering with physics
Collider vizCol = viz.GetComponent<Collider>();
if (vizCol != null) UnityEngine.Object.Destroy(vizCol);

// Apply transparent material for primitives
Renderer r = viz.GetComponent<Renderer>();
if (r != null)
{
r.material = transparentMat;
r.enabled = true;
}

vizList.Add(viz);
}
}
}

if (vizList.Count > 0)
{
penisViz[male] = vizList;
SuperController.LogMessage($"CreatePenisColliderViz: Created {vizList.Count} visualizations for {male.uid}");
}
else
{
SuperController.LogMessage($"CreatePenisColliderViz: No CUA penis colliders found for {male.uid}");
}
}

// Hides all penis collider visualizations.
private void HideAllPenisViz()
{
foreach (var pair in penisViz)
{
List<GameObject> list = pair.Value;
if (list != null)
{
for (int i = 0; i < list.Count; i++)
{
GameObject v = list;
if (v != null)
{
v.SetActive(false);
Renderer r = v.GetComponent<Renderer>();
if (r != null) r.enabled = false;
}
}
}
}
}

// Updates the UI to show settings for the selected orifice.
private void SelectOrifice(string orificeName)
{
selectedOrifice = orificeName;
ClearUI();
try
{
SetupButton("Mouth Settings", false, delegate { SelectOrifice("Mouth"); });
SetupButton("Vagina Settings", true, delegate { SelectOrifice("Vagina"); });
SetupButton("Anus Settings", false, delegate { SelectOrifice("Anus"); });
SetupButton("Debug", true, ToggleDebugVisibility);
SetupButton("Penis Colliders", true, TogglePenisColliders);

AddStatusUIElements();

if (orifices.ContainsKey(orificeName))
{
Orifice orifice = orifices[orificeName];
var triggerScaleSlider = CreateSlider(orifice.triggerScale);
uiElements.Add(triggerScaleSlider);
sliderStorables.Add(orifice.triggerScale);
var triggerOffsetUpSlider = CreateSlider(orifice.triggerOffsetUp);
uiElements.Add(triggerOffsetUpSlider);
sliderStorables.Add(orifice.triggerOffsetUp);
var triggerOffsetForwardSlider = CreateSlider(orifice.triggerOffsetForward);
uiElements.Add(triggerOffsetForwardSlider);
sliderStorables.Add(orifice.triggerOffsetForward);
var proximityScaleSlider = CreateSlider(orifice.proximityScale);
uiElements.Add(proximityScaleSlider);
sliderStorables.Add(orifice.proximityScale);
var showProximityToggle = CreateToggle(orifice.showProximity);
uiElements.Add(showProximityToggle);
toggleStorables.Add(orifice.showProximity);
var infoText = CreateTextField(orifice.info);
uiElements.Add(infoText);
textFieldStorables.Add(orifice.info);
}
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error selecting orifice: " + e.Message);
}
}

// Clears all UI elements from the scene.
private void ClearUI()
{
for (int i = 0; i < uiElements.Count; i++)
{
var element = uiElements;
if (element is UIDynamic) RemoveSpacer(element as UIDynamic);
if (element is UIDynamicSlider) RemoveSlider(element as UIDynamicSlider);
if (element is UIDynamicToggle) RemoveToggle(element as UIDynamicToggle);
if (element is UIDynamicTextField) RemoveTextField(element as UIDynamicTextField);
if (element is UIDynamicButton) RemoveButton(element as UIDynamicButton);
}
uiElements.Clear();
sliderStorables.Clear();
toggleStorables.Clear();
textFieldStorables.Clear();
buttonElements.Clear();
}

// Creates a UI button with the specified label and callback.
private void SetupButton(string label, bool rightSide, UnityAction action)
{
UIDynamicButton button = CreateButton(label, rightSide);
button.button.onClick.AddListener(action);
uiElements.Add(button);
buttonElements.Add(button);
}

// Adds status UI elements for displaying penetration and proximity states.
private void AddStatusUIElements()
{
var mouthStatusText = CreateTextField(mouthStatus);
uiElements.Add(mouthStatusText);
textFieldStorables.Add(mouthStatus);
var vaginaStatusText = CreateTextField(vaginaStatus);
uiElements.Add(vaginaStatusText);
textFieldStorables.Add(vaginaStatus);
var anusStatusText = CreateTextField(anusStatus);
uiElements.Add(anusStatusText);
textFieldStorables.Add(anusStatus);
var mouthProxText = CreateTextField(mouthProxStatus);
uiElements.Add(mouthProxText);
textFieldStorables.Add(mouthProxStatus);
var vaginaProxText = CreateTextField(vaginaProxStatus);
uiElements.Add(vaginaProxText);
textFieldStorables.Add(vaginaProxStatus);
var anusProxText = CreateTextField(anusProxStatus);
uiElements.Add(anusProxText);
textFieldStorables.Add(anusProxStatus);
}

// Updates the penetration status display for a specific orifice.
public void UpdateStatus(string orificeName)
{
JSONStorableString status = null;
string message = "";
switch (orificeName)
{
case "Mouth":
status = mouthStatus;
message = "Penis in mouth: ";
break;
case "Vagina":
status = vaginaStatus;
message = "Penis in vagina: ";
break;
case "Anus":
status = anusStatus;
message = "Penis in anus: ";
break;
}
if (status != null && orifices.ContainsKey(orificeName))
{
status.val = message + (orifices[orificeName].isPenetrated ? "Yes" : "No");
}
}

// Updates the proximity status display for a specific orifice.
public void UpdateProxStatus(string orificeName)
{
JSONStorableString status = null;
string message = "";
switch (orificeName)
{
case "Mouth":
status = mouthProxStatus;
message = "Penis near mouth: ";
break;
case "Vagina":
status = vaginaProxStatus;
message = "Penis near vagina: ";
break;
case "Anus":
status = anusProxStatus;
message = "Penis near anus: ";
break;
}
if (status != null && proximityHandlers.ContainsKey(orificeName))
{
ProximityHandler handler = proximityHandlers[orificeName];
bool inProx = handler != null && handler.isProx;
status.val = message + (inProx ? "Yes" : "No");
}
}

// Updates all penetration and proximity statuses.
public void UpdateAllStatuses()
{
foreach (var pair in orifices)
{
UpdateStatus(pair.Key);
UpdateProxStatus(pair.Key);
}
}

// Updates statuses every frame.
private void Update()
{
UpdateAllStatuses();
}

// Cleans up resources when the script is destroyed, including triggers, orifices, and penis visualizations.
public void OnDestroy()
{
try
{
foreach (var pair in orifices)
{
Orifice orifice = pair.Value;
if (orifice != null)
{
if (orifice.enterTriggerCollider != null)
{
UnityEngine.Object.Destroy(orifice.enterTriggerCollider.gameObject);
}
if (orifice.proximityTrigger != null)
{
UnityEngine.Object.Destroy(orifice.proximityTrigger.gameObject);
}
UnityEngine.Object.Destroy(orifice);
}
}
orifices.Clear();
proximityHandlers.Clear();
triggerHandlers.Clear();

foreach (var pair in penisViz)
{
List<GameObject> list = pair.Value;
if (list != null)
{
for (int i = 0; i < list.Count; i++)
{
GameObject v = list;
if (v != null) UnityEngine.Object.Destroy(v);
}
}
}
penisViz.Clear();
SuperController.LogMessage("OrificeTriggerManager: Components destroyed");
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error in OnDestroy: " + e.Message);
}
ClearUI();
}

// Serializes the script's state to JSON for saving.
public override JSONClass GetJSON(bool includePhysical = true, bool includeAppearance = true, bool forceStore = true)
{
JSONClass jc = base.GetJSON(includePhysical, includeAppearance, forceStore);
try
{
foreach (var pair in orifices)
{
if (pair.Value != null)
{
jc[pair.Key] = pair.Value.Store("", includePhysical);
}
}
SuperController.LogMessage("OrificeTriggerManager: JSON saved");
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Serialization error: " + e.Message);
}
return jc;
}

// Restores the script's state from JSON for loading.
public override void RestoreFromJSON(JSONClass jc, bool restorePhysical = true, bool restoreAppearance = true, JSONArray presetAtoms = null, bool setMissingToDefault = true)
{
base.RestoreFromJSON(jc, restorePhysical, restoreAppearance, presetAtoms, setMissingToDefault);
try
{
foreach (var pair in orifices)
{
if (jc.HasKey(pair.Key) && pair.Value != null)
{
pair.Value.Load(jc[pair.Key].AsObject, "");
}
}
UpdateAllStatuses();
SuperController.LogMessage("OrificeTriggerManager: JSON restored");
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Deserialization error: " + e.Message);
}
}
}

// Represents a penis on a male atom, managing its colliders for detection.
public class Penis
{
public Atom atom;
private HashSet<Collider> penisColliders;
private HashSet<string> penisColliderNames;

public Penis(Atom atom)
{
this.atom = atom;
penisColliders = new HashSet<Collider>();
penisColliderNames = new HashSet<string>();

FindPenisControllers();
FindCuaPenisColliders();

SuperController.LogMessage($"Penis: Initialized for {atom.uid}. Found colliders: {penisColliders.Count}");
}

// Finds penis-related controllers and their colliders.
private void FindPenisControllers()
{
Rigidbody[] rigidbodies = atom.GetComponentsInChildren<Rigidbody>();

foreach (Rigidbody rb in rigidbodies)
{
string rbNameLower = rb.name.ToLower();
bool isPenisPart = false;

if (rbNameLower.Contains("penisbase") || rbNameLower.Contains("penis_base")) isPenisPart = true;
else if (rbNameLower.Contains("penismid") || rbNameLower.Contains("penis_mid")) isPenisPart = true;
else if (rbNameLower.Contains("penistip") || rbNameLower.Contains("penis_tip") || rbNameLower.Contains("penistipcontrol")) isPenisPart = true;

if (isPenisPart)
{
Collider[] colliders = rb.GetComponents<Collider>();
foreach (var col in colliders)
{
penisColliders.Add(col);
penisColliderNames.Add(col.name);
SuperController.LogMessage($"Penis: Found controller collider: {col.name} from {rb.name}");
}

Collider[] childColliders = rb.GetComponentsInChildren<Collider>();
foreach (var col in childColliders)
{
if (!penisColliders.Contains(col))
{
penisColliders.Add(col);
penisColliderNames.Add(col.name);
SuperController.LogMessage($"Penis: Found child collider: {col.name} from {rb.name}");
}
}
}
}
}

// Finds custom (CUA) penis colliders.
private void FindCuaPenisColliders()
{
Collider[] allColliders = atom.GetComponentsInChildren<Collider>();

foreach (var col in allColliders)
{
string colNameLower = col.name.ToLower();

if ((colNameLower.Contains("autocollidergen") || colNameLower.Contains("autogen")) &&
(colNameLower.Contains("hard") || colNameLower.Contains("soft")) &&
!penisColliderNames.Contains(col.name))
{
Transform parent = col.transform.parent;
while (parent != null)
{
if (parent.name.ToLower().Contains("penis"))
{
penisColliders.Add(col);
penisColliderNames.Add(col.name);
SuperController.LogMessage($"Penis: Found CUA collider: {col.name}");
break;
}
parent = parent.parent;
}
}
}
}

// Checks if a collider belongs to the penis.
public bool IsPenisCollider(Collider other)
{
if (other == null) return false;

bool isPenisPart = penisColliders.Contains(other);

if (!isPenisPart)
{
string otherNameLower = other.name.ToLower();
isPenisPart = penisColliderNames.Contains(other.name) ||
(otherNameLower.Contains("autocollidergen") && otherNameLower.Contains("hard"));
}

return isPenisPart;
}
}

// Represents an orifice (e.g., Mouth, Vagina, Anus) with trigger and proximity colliders.
public class Orifice : MonoBehaviour
{
public string name;
public Rigidbody rb;
public JSONStorableFloat triggerScale;
public JSONStorableFloat triggerOffsetUp;
public JSONStorableFloat triggerOffsetForward;
public JSONStorableFloat proximityScale;
public JSONStorableBool showProximity;
public JSONStorableString info;
public CapsuleCollider enterTriggerCollider;
public CapsuleCollider proximityTrigger;
public int penetrationCount;
public bool isPenetrated { get { return penetrationCount > 0; } }
public float defaultUp;
public float defaultForward;

// Initializes the orifice with specific settings based on its name.
public virtual void Init(string orificeName, Atom atom)
{
name = orificeName;
try
{
string rbName;
float defaultScale;

switch (name)
{
case "Mouth":
rbName = "ThroatTrigger";
defaultScale = 0.03f;
defaultUp = 0.01f;
defaultForward = 0.07f;
break;
case "Vagina":
rbName = "VaginaTrigger";
defaultScale = 0.01f;
defaultUp = -0.08f;
defaultForward = 0.01f;
break;
case "Anus":
rbName = "pelvis";
defaultScale = 0.01f;
defaultUp = -0.16f;
defaultForward = -0.04f;
break;
default:
rbName = "pelvis";
defaultScale = 0.03f;
defaultUp = 0f;
defaultForward = 0f;
break;
}

var storable = atom.GetStorableByID(rbName);
if (storable != null)
{
rb = storable.GetComponent<Rigidbody>();
}
if (rb == null)
{
rb = atom.gameObject.AddComponent<Rigidbody>();
rb.isKinematic = true;
rb.useGravity = false;
SuperController.LogMessage("Orifice: Rigidbody created for " + name);
}

triggerScale = new JSONStorableFloat("Trigger Scale", defaultScale, 0.01f, 0.5f);
triggerOffsetUp = new JSONStorableFloat("Trigger Offset Up", 0f, -0.2f, 0.2f);
triggerOffsetForward = new JSONStorableFloat("Trigger Offset Forward", 0f, -0.2f, 0.2f);
proximityScale = new JSONStorableFloat("Proximity Scale", defaultScale * 1.5f, 0.01f, 1f);
showProximity = new JSONStorableBool("Show Proximity Zone", true);
info = new JSONStorableString("Information", "Orifice: " + name);
penetrationCount = 0;
SuperController.LogMessage("Orifice: Initialized " + name);
}
catch (Exception e)
{
SuperController.LogError("Orifice: Initialization error for " + name + ": " + e.Message);
}
}

// Serializes the orifice's state to JSON.
public virtual JSONClass Store(string key, bool includePhysical)
{
JSONClass jc = new JSONClass();
try
{
jc["triggerScale"] = triggerScale.val.ToString();
jc["triggerOffsetUp"] = triggerOffsetUp.val.ToString();
jc["triggerOffsetForward"] = triggerOffsetForward.val.ToString();
jc["proximityScale"] = proximityScale.val.ToString();
jc["showProximity"] = showProximity.val.ToString();
jc["info"] = info.val;
jc["penetrationCount"] = penetrationCount.ToString();
}
catch (Exception e)
{
SuperController.LogError("Orifice: Serialization error for " + key + ": " + e.Message);
}
return jc;
}

// Restores the orifice's state from JSON.
public virtual void Load(JSONClass jc, string key)
{
try
{
if (jc.HasKey("triggerScale")) triggerScale.val = float.Parse(jc["triggerScale"]);
if (jc.HasKey("triggerOffsetUp")) triggerOffsetUp.val = float.Parse(jc["triggerOffsetUp"]);
if (jc.HasKey("triggerOffsetForward")) triggerOffsetForward.val = float.Parse(jc["triggerOffsetForward"]);
if (jc.HasKey("proximityScale")) proximityScale.val = float.Parse(jc["proximityScale"]);
if (jc.HasKey("showProximity")) showProximity.val = bool.Parse(jc["showProximity"]);
if (jc.HasKey("info")) info.val = jc["info"];
if (jc.HasKey("penetrationCount")) penetrationCount = int.Parse(jc["penetrationCount"]);
}
catch (Exception e)
{
SuperController.LogError("Orifice: Deserialization error for " + key + ": " + e.Message);
}
}
}

// Handles proximity trigger events, tracking when penis colliders enter or exit the proximity zone.
public class ProximityHandler : MonoBehaviour
{
public Orifice orifice;
public bool on = true;
public bool isProx = false;
private float lastLogTime = 0f;
private const float LogInterval = 2f;
private HashSet<Collider> penisCollidersInProx = new HashSet<Collider>();

void OnTriggerEnter(Collider other)
{
if (!on || orifice == null) return;

if (IsPenisCollider(other))
{
if (penisCollidersInProx.Add(other))
{
SuperController.LogMessage($"[Prox] Penis entered {gameObject.name} for {orifice.name}");
isProx = true;
OrificeTriggerManager.singleton.UpdateProxStatus(orifice.name);
}
}
}

void OnTriggerExit(Collider other)
{
if (!on || orifice == null) return;

if (IsPenisCollider(other))
{
if (penisCollidersInProx.Remove(other))
{
SuperController.LogMessage($"[Prox] Penis exited {gameObject.name} for {orifice.name}");
isProx = penisCollidersInProx.Count > 0;
OrificeTriggerManager.singleton.UpdateProxStatus(orifice.name);
}
}
}

private bool IsPenisCollider(Collider col)
{
if (col == null) return false;
Atom atom = col.gameObject.GetComponentInParent<Atom>();
if (atom == null || !OrificeTriggerManager.singleton.penises.ContainsKey(atom)) return false;
return OrificeTriggerManager.singleton.penises[atom].IsPenisCollider(col);
}
}

// Handles trigger events for an orifice, tracking penetration count.
public class OrificeTriggerHandler : MonoBehaviour
{
public Orifice orifice;
public bool isActive = true;
private float lastLogTime = 0f;
private const float LogInterval = 2f;
private HashSet<Collider> penisCollidersInTrigger = new HashSet<Collider>();

void OnTriggerEnter(Collider other)
{
if (!isActive || orifice == null) return;

if (IsPenisCollider(other))
{
if (penisCollidersInTrigger.Add(other))
{
orifice.penetrationCount++;
SuperController.LogMessage($"[Trig] PENETRATION in {orifice.name}! Count: {orifice.penetrationCount} (collider: {other.name})");
OrificeTriggerManager.singleton.UpdateStatus(orifice.name);
}
}
}

void OnTriggerExit(Collider other)
{
if (!isActive || orifice == null) return;

if (IsPenisCollider(other))
{
if (penisCollidersInTrigger.Remove(other))
{
orifice.penetrationCount--;
if (orifice.penetrationCount < 0) orifice.penetrationCount = 0;
SuperController.LogMessage($"[Trig] Exit from {orifice.name}. Count: {orifice.penetrationCount} (collider: {other.name})");
OrificeTriggerManager.singleton.UpdateStatus(orifice.name);
}
}
}

private bool IsPenisCollider(Collider col)
{
if (col == null) return false;
Atom atom = col.gameObject.GetComponentInParent<Atom>();
if (atom == null || !OrificeTriggerManager.singleton.penises.ContainsKey(atom)) return false;
return OrificeTriggerManager.singleton.penises[atom].IsPenisCollider(col);
}
}
}
 
Cheesy, did you ever go over how to get rid of the inference error as seen below? I just made this account just to ask you this because it fucked over my scene:

View attachment 527795
All persons have to be "On" on and "Advanced Colliders" have to be enabled on plugin load. If this doesn't help I need the first lines of the error stack. Remove the plugin immediately to avoid the stack from being truncated.
 
Hi Cheesy. What do yoy say? :)

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;
using SimpleJSON;

namespace Disa
{
// Manages trigger zones for orifices (e.g., Mouth, Vagina, Anus) on a selected Atom (character) in a Unity-based scene.
// Tracks penetration and proximity events, manages penis colliders for male atoms, and handles UI for configuration.
public class OrificeTriggerManager : MVRScript
{
public static OrificeTriggerManager singleton;
private Atom selectedAtom;
private List<object> uiElements;
private List<JSONStorableFloat> sliderStorables;
private List<JSONStorableBool> toggleStorables;
private List<JSONStorableString> textFieldStorables;
private List<UIDynamicButton> buttonElements;
private string selectedOrifice;
private Dictionary<string, Orifice> orifices;
private Dictionary<string, ProximityHandler> proximityHandlers;
private Dictionary<string, OrificeTriggerHandler> triggerHandlers;
private bool debugVisible = true;
private bool penisCollidersVisible = false;
private Dictionary<Atom, List<GameObject>> penisViz = new Dictionary<Atom, List<GameObject>>();
private JSONStorableString mouthStatus;
private JSONStorableString vaginaStatus;
private JSONStorableString anusStatus;
private JSONStorableString mouthProxStatus;
private JSONStorableString vaginaProxStatus;
private JSONStorableString anusProxStatus;
private List<Atom> maleAtoms;
public Dictionary<Atom, Penis> penises;

public Dictionary<string, Orifice> Orifices
{
get { return orifices; }
}

// Initializes the manager, sets up status tracking, finds male atoms, and creates the UI.
public override void Init()
{
singleton = this;
SuperController.LogMessage("OrificeTriggerManager: Initialization started");
uiElements = new List<object>();
sliderStorables = new List<JSONStorableFloat>();
toggleStorables = new List<JSONStorableBool>();
textFieldStorables = new List<JSONStorableString>();
buttonElements = new List<UIDynamicButton>();
orifices = new Dictionary<string, Orifice>();
proximityHandlers = new Dictionary<string, ProximityHandler>();
triggerHandlers = new Dictionary<string, OrificeTriggerHandler>();
selectedOrifice = "Mouth";
maleAtoms = new List<Atom>();
penises = new Dictionary<Atom, Penis>();

// Initialize status strings for tracking penetration and proximity
mouthStatus = new JSONStorableString("Mouth Status", "Penis in mouth: No");
vaginaStatus = new JSONStorableString("Vagina Status", "Penis in vagina: No");
anusStatus = new JSONStorableString("Anus Status", "Penis in anus: No");
mouthProxStatus = new JSONStorableString("Mouth Proximity", "Penis near mouth: No");
vaginaProxStatus = new JSONStorableString("Vagina Proximity", "Penis near vagina: No");
anusProxStatus = new JSONStorableString("Anus Proximity", "Penis near anus: No");

selectedAtom = containingAtom;
if (selectedAtom == null || selectedAtom.type != "Person")
{
SuperController.LogError("OrificeTriggerManager: This script must be added to a Person-type atom.");
return;
}
SuperController.LogMessage("OrificeTriggerManager: Atom automatically selected: " + selectedAtom.uid);
SelectAtom(selectedAtom.uid);

FindMaleAtoms();
CreateUI();
SuperController.LogMessage("OrificeTriggerManager: Initialization completed");
}

// Finds male atoms in the scene and initializes their penis colliders.
private void FindMaleAtoms()
{
maleAtoms.Clear();
penises.Clear();
penisViz.Clear();

List<Atom> allAtoms = SuperController.singleton.GetAtoms();
for (int i = 0; i < allAtoms.Count; i++)
{
Atom atom = allAtoms;
if (atom.type == "Person" && atom != selectedAtom)
{
if (HasPenis(atom))
{
maleAtoms.Add(atom);
penises[atom] = new Penis(atom);
SuperController.LogMessage("OrificeTriggerManager: Found male atom: " + atom.uid);
}
}
}
}

// Checks if an atom has penis-related components or colliders.
private bool HasPenis(Atom atom)
{
if (atom.GetStorableByID("penisTipControl") != null) return true;
if (atom.GetStorableByID("penisMidControl") != null) return true;
if (atom.GetStorableByID("penisBaseControl") != null) return true;

Collider[] colliders = atom.GetComponentsInChildren<Collider>();
foreach (var col in colliders)
{
string colNameLower = col.name.ToLower();
if ((colNameLower.Contains("autocollidergen") || colNameLower.Contains("autogen")) &&
(colNameLower.Contains("hard") || colNameLower.Contains("soft")))
{
Rigidbody[] rbs = col.GetComponentsInParent<Rigidbody>();
foreach (var rb in rbs)
{
if (rb.name.ToLower().Contains("penis"))
return true;
}
}
}
return false;
}

// Selects an atom and sets up its orifices and triggers.
public void SelectAtom(string atomUid)
{
selectedAtom = SuperController.singleton.GetAtomByUid(atomUid);
if (selectedAtom == null)
{
SuperController.LogError("OrificeTriggerManager: Atom " + atomUid + " not found.");
return;
}
SuperController.LogMessage("OrificeTriggerManager: Atom selected: " + atomUid);

orifices.Clear();
proximityHandlers.Clear();
triggerHandlers.Clear();

try
{
string[] orificeNames = new string[] { "Mouth", "Vagina", "Anus" };
for (int i = 0; i < orificeNames.Length; i++)
{
string orificeName = orificeNames;
Orifice orifice = selectedAtom.gameObject.AddComponent<Orifice>();
if (orifice != null)
{
orifice.Init(orificeName, selectedAtom);
if (orifice.rb != null)
{
orifices[orificeName] = orifice;
SetupTriggers(orifice, orificeName);
SuperController.LogMessage("OrificeTriggerManager: Created " + orificeName);
}
else
{
SuperController.LogError("OrificeTriggerManager: Failed to initialize " + orificeName + " due to missing Rigidbody");
UnityEngine.Object.Destroy(orifice);
}
}
else
{
SuperController.LogError("OrificeTriggerManager: Failed to create Orifice component for " + orificeName);
}
}
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error creating orifices: " + e.Message);
}

UpdateAllStatuses();
CreateUI();
}

// Sets up trigger and proximity colliders for an orifice, including their position, scale, and visual debugging.
private void SetupTriggers(Orifice orifice, string name)
{
if (orifice == null || orifice.rb == null)
{
SuperController.LogError("OrificeTriggerManager: Orifice or Rigidbody null for " + name);
return;
}

try
{
// Create entry trigger
GameObject triggerGO = GameObject.CreatePrimitive(PrimitiveType.Capsule);
triggerGO.name = "BL_" + name + "_EnterTrigger";
CapsuleCollider enterTriggerCollider = triggerGO.GetComponent<CapsuleCollider>();
enterTriggerCollider.isTrigger = true;
triggerGO.transform.SetParent(orifice.rb.transform, false);
if (name == "Mouth")
{
triggerGO.transform.localRotation = Quaternion.Euler(90, 0, 0);
}
else if (name == "Anus")
{
triggerGO.transform.localRotation = Quaternion.Euler(25, 0, 0);
}
else
{
triggerGO.transform.localRotation = Quaternion.identity;
}
triggerGO.transform.localScale = new Vector3(orifice.triggerScale.val, orifice.triggerScale.val, orifice.triggerScale.val);
triggerGO.transform.localPosition = new Vector3(0f, orifice.triggerOffsetUp.val + orifice.defaultUp, orifice.triggerOffsetForward.val + orifice.defaultForward);
Renderer renderer = triggerGO.GetComponent<Renderer>();
if (renderer != null)
{
renderer.material = new Material(Shader.Find("Standard"));
renderer.material.color = new Color(0f, 0f, 1f, 0.5f);
renderer.enabled = debugVisible;
}

// Create proximity trigger
GameObject proximityGO = GameObject.CreatePrimitive(PrimitiveType.Capsule);
proximityGO.name = "BL_" + name + "_ProximityTrigger";
CapsuleCollider proximityTrigger = proximityGO.GetComponent<CapsuleCollider>();
proximityTrigger.isTrigger = true;
proximityGO.transform.SetParent(orifice.rb.transform, false);
if (name == "Mouth")
{
proximityGO.transform.localRotation = Quaternion.Euler(90, 0, 0);
}
else if (name == "Anus")
{
proximityGO.transform.localRotation = Quaternion.Euler(25, 0, 0);
}
else
{
proximityGO.transform.localRotation = Quaternion.identity;
}
proximityGO.transform.localScale = new Vector3(orifice.proximityScale.val, orifice.proximityScale.val, orifice.proximityScale.val);
proximityGO.transform.localPosition = new Vector3(0f, orifice.triggerOffsetUp.val + orifice.defaultUp, orifice.triggerOffsetForward.val + orifice.defaultForward);
Renderer proxRenderer = proximityGO.GetComponent<Renderer>();
if (proxRenderer != null)
{
proxRenderer.material = new Material(Shader.Find("Standard"));
proxRenderer.material.SetFloat("_Mode", 2);
proxRenderer.material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
proxRenderer.material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
proxRenderer.material.SetInt("_ZWrite", 0);
proxRenderer.material.DisableKeyword("_ALPHATEST_ON");
proxRenderer.material.EnableKeyword("_ALPHABLEND_ON");
proxRenderer.material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
proxRenderer.material.color = new Color(1f, 1f, 0f, 0.3f);
proxRenderer.enabled = debugVisible && orifice.showProximity.val;
}

OrificeTriggerHandler triggerHandler = triggerGO.AddComponent<OrificeTriggerHandler>();
if (triggerHandler != null)
{
triggerHandler.orifice = orifice;
triggerHandler.isActive = true;
triggerHandlers[name] = triggerHandler;
}

ProximityHandler proximityHandler = proximityGO.AddComponent<ProximityHandler>();
if (proximityHandler != null)
{
proximityHandler.orifice = orifice;
proximityHandler.on = true;
proximityHandlers[name] = proximityHandler;
}

orifice.enterTriggerCollider = enterTriggerCollider;
orifice.proximityTrigger = proximityTrigger;

orifice.showProximity.setCallbackFunction = (bool val) =>
{
if (proxRenderer != null) proxRenderer.enabled = debugVisible && val;
};

orifice.triggerOffsetUp.setCallbackFunction = (float val) =>
{
if (triggerGO != null) triggerGO.transform.localPosition = new Vector3(0f, val + orifice.defaultUp, orifice.triggerOffsetForward.val + orifice.defaultForward);
if (proximityGO != null) proximityGO.transform.localPosition = new Vector3(0f, val + orifice.defaultUp, orifice.triggerOffsetForward.val + orifice.defaultForward);
};
orifice.triggerOffsetForward.setCallbackFunction = (float val) =>
{
if (triggerGO != null) triggerGO.transform.localPosition = new Vector3(0f, orifice.triggerOffsetUp.val + orifice.defaultUp, val + orifice.defaultForward);
if (proximityGO != null) proximityGO.transform.localPosition = new Vector3(0f, orifice.triggerOffsetUp.val + orifice.defaultUp, val + orifice.defaultForward);
};
orifice.triggerScale.setCallbackFunction = (float val) =>
{
if (triggerGO != null) triggerGO.transform.localScale = new Vector3(val, val, val);
};
orifice.proximityScale.setCallbackFunction = (float val) =>
{
if (proximityGO != null) proximityGO.transform.localScale = new Vector3(val, val, val);
};
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error in SetupTriggers for " + name + ": " + e.Message);
}
}

// Creates the UI for configuring orifices and toggling debug/penis collider visibility.
private void CreateUI()
{
ClearUI();
try
{
SetupButton("Mouth Settings", false, delegate { SelectOrifice("Mouth"); });
SetupButton("Vagina Settings", true, delegate { SelectOrifice("Vagina"); });
SetupButton("Anus Settings", false, delegate { SelectOrifice("Anus"); });
SetupButton("Debug", true, ToggleDebugVisibility);
SetupButton("Penis Colliders", true, TogglePenisColliders);

AddStatusUIElements();

SelectOrifice(selectedOrifice);
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error creating UI: " + e.Message);
}
}

// Toggles visibility of trigger and proximity colliders for debugging.
private void ToggleDebugVisibility()
{
debugVisible = !debugVisible;
foreach (var pair in orifices)
{
Orifice orifice = pair.Value;
if (orifice.enterTriggerCollider != null)
{
Renderer renderer = orifice.enterTriggerCollider.gameObject.GetComponent<Renderer>();
if (renderer != null) renderer.enabled = debugVisible;
}
if (orifice.proximityTrigger != null)
{
Renderer proxRenderer = orifice.proximityTrigger.gameObject.GetComponent<Renderer>();
if (proxRenderer != null) proxRenderer.enabled = debugVisible && orifice.showProximity.val;
}
}
SuperController.LogMessage("OrificeTriggerManager: Trigger visibility " + (debugVisible ? "enabled" : "disabled"));
}

// Toggles visibility of penis colliders for debugging.
private void TogglePenisColliders()
{
penisCollidersVisible = !penisCollidersVisible;

if (penisCollidersVisible)
{
FindMaleAtoms();
for (int i = 0; i < maleAtoms.Count; i++)
{
CreatePenisColliderViz(maleAtoms);
}
}
else
{
HideAllPenisViz();
}

SuperController.LogMessage("OrificeTriggerManager: Penis collider visibility " + (penisCollidersVisible ? "enabled" : "disabled"));
}

// Creates visual representations for penis colliders on a male atom.
private void CreatePenisColliderViz(Atom male)
{
if (penisViz.ContainsKey(male))
{
List<GameObject> list = penisViz[male];
for (int i = 0; i < list.Count; i++)
{
GameObject v = list;
if (v != null)
{
v.SetActive(true);
Renderer r = v.GetComponent<Renderer>();
if (r != null) r.enabled = true;
}
}
return;
}

List<GameObject> vizList = new List<GameObject>();
Penis penis = penises.ContainsKey(male) ? penises[male] : null;

if (penis == null)
{
SuperController.LogMessage($"CreatePenisColliderViz: Penis object not found for {male.uid}");
return;
}

// Create transparent material once
Material transparentMat = new Material(Shader.Find("Standard"));
transparentMat.SetFloat("_Mode", 2);
transparentMat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
transparentMat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
transparentMat.SetInt("_ZWrite", 0);
transparentMat.DisableKeyword("_ALPHATEST_ON");
transparentMat.EnableKeyword("_ALPHABLEND_ON");
transparentMat.DisableKeyword("_ALPHAPREMULTIPLY_ON");
transparentMat.color = new Color(1f, 0f, 0f, 0.3f); // Transparent red

// Find penis colliders
Collider[] allColliders = male.GetComponentsInChildren<Collider>();
foreach (var col in allColliders)
{
if (penis.IsPenisCollider(col))
{
// Skip standard penisBase, penisMid, penisTip colliders
string colNameLower = col.name.ToLower();
if (colNameLower.Contains("penisbase") || colNameLower.Contains("penismid") || colNameLower.Contains("penistip"))
{
SuperController.LogMessage($"CreatePenisColliderViz: Skipped standard collider {col.name} for {male.uid}");
continue;
}

GameObject viz = null;

// C# 6.0-compatible type checking
if (col is CapsuleCollider)
{
CapsuleCollider cc = (CapsuleCollider)col;
viz = GameObject.CreatePrimitive(PrimitiveType.Capsule);
viz.name = "PenisColliderViz_Capsule_" + col.name;
viz.transform.SetParent(col.transform, false);
viz.transform.localPosition = cc.center;

// Account for capsule direction
switch (cc.direction)
{
case 0: // X-axis
viz.transform.localRotation = Quaternion.Euler(0f, 0f, 90f);
break;
case 2: // Z-axis
viz.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
break;
default: // Y-axis
viz.transform.localRotation = Quaternion.identity;
break;
}

// Scale capsule
viz.transform.localScale = new Vector3(cc.radius * 2f, cc.height / 2f, cc.radius * 2f);
}
else if (col is SphereCollider)
{
SphereCollider sc = (SphereCollider)col;
viz = GameObject.CreatePrimitive(PrimitiveType.Sphere);
viz.name = "PenisColliderViz_Sphere_" + col.name;
viz.transform.SetParent(col.transform, false);
viz.transform.localPosition = sc.center;
viz.transform.localRotation = Quaternion.identity;
viz.transform.localScale = Vector3.one * (sc.radius * 2f);
}
else if (col is MeshCollider)
{
MeshCollider mc = (MeshCollider)col;
viz = new GameObject("PenisColliderViz_Mesh_" + col.name);
viz.transform.SetParent(col.transform, false);
viz.transform.localPosition = Vector3.zero;
viz.transform.localRotation = Quaternion.identity;
viz.transform.localScale = Vector3.one;

MeshFilter mf = viz.AddComponent<MeshFilter>();
mf.sharedMesh = mc.sharedMesh;

MeshRenderer mr = viz.AddComponent<MeshRenderer>();
mr.material = transparentMat;
mr.enabled = true;
}

if (viz != null)
{
// Remove collider from visualization to avoid interfering with physics
Collider vizCol = viz.GetComponent<Collider>();
if (vizCol != null) UnityEngine.Object.Destroy(vizCol);

// Apply transparent material for primitives
Renderer r = viz.GetComponent<Renderer>();
if (r != null)
{
r.material = transparentMat;
r.enabled = true;
}

vizList.Add(viz);
}
}
}

if (vizList.Count > 0)
{
penisViz[male] = vizList;
SuperController.LogMessage($"CreatePenisColliderViz: Created {vizList.Count} visualizations for {male.uid}");
}
else
{
SuperController.LogMessage($"CreatePenisColliderViz: No CUA penis colliders found for {male.uid}");
}
}

// Hides all penis collider visualizations.
private void HideAllPenisViz()
{
foreach (var pair in penisViz)
{
List<GameObject> list = pair.Value;
if (list != null)
{
for (int i = 0; i < list.Count; i++)
{
GameObject v = list;
if (v != null)
{
v.SetActive(false);
Renderer r = v.GetComponent<Renderer>();
if (r != null) r.enabled = false;
}
}
}
}
}

// Updates the UI to show settings for the selected orifice.
private void SelectOrifice(string orificeName)
{
selectedOrifice = orificeName;
ClearUI();
try
{
SetupButton("Mouth Settings", false, delegate { SelectOrifice("Mouth"); });
SetupButton("Vagina Settings", true, delegate { SelectOrifice("Vagina"); });
SetupButton("Anus Settings", false, delegate { SelectOrifice("Anus"); });
SetupButton("Debug", true, ToggleDebugVisibility);
SetupButton("Penis Colliders", true, TogglePenisColliders);

AddStatusUIElements();

if (orifices.ContainsKey(orificeName))
{
Orifice orifice = orifices[orificeName];
var triggerScaleSlider = CreateSlider(orifice.triggerScale);
uiElements.Add(triggerScaleSlider);
sliderStorables.Add(orifice.triggerScale);
var triggerOffsetUpSlider = CreateSlider(orifice.triggerOffsetUp);
uiElements.Add(triggerOffsetUpSlider);
sliderStorables.Add(orifice.triggerOffsetUp);
var triggerOffsetForwardSlider = CreateSlider(orifice.triggerOffsetForward);
uiElements.Add(triggerOffsetForwardSlider);
sliderStorables.Add(orifice.triggerOffsetForward);
var proximityScaleSlider = CreateSlider(orifice.proximityScale);
uiElements.Add(proximityScaleSlider);
sliderStorables.Add(orifice.proximityScale);
var showProximityToggle = CreateToggle(orifice.showProximity);
uiElements.Add(showProximityToggle);
toggleStorables.Add(orifice.showProximity);
var infoText = CreateTextField(orifice.info);
uiElements.Add(infoText);
textFieldStorables.Add(orifice.info);
}
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error selecting orifice: " + e.Message);
}
}

// Clears all UI elements from the scene.
private void ClearUI()
{
for (int i = 0; i < uiElements.Count; i++)
{
var element = uiElements;
if (element is UIDynamic) RemoveSpacer(element as UIDynamic);
if (element is UIDynamicSlider) RemoveSlider(element as UIDynamicSlider);
if (element is UIDynamicToggle) RemoveToggle(element as UIDynamicToggle);
if (element is UIDynamicTextField) RemoveTextField(element as UIDynamicTextField);
if (element is UIDynamicButton) RemoveButton(element as UIDynamicButton);
}
uiElements.Clear();
sliderStorables.Clear();
toggleStorables.Clear();
textFieldStorables.Clear();
buttonElements.Clear();
}

// Creates a UI button with the specified label and callback.
private void SetupButton(string label, bool rightSide, UnityAction action)
{
UIDynamicButton button = CreateButton(label, rightSide);
button.button.onClick.AddListener(action);
uiElements.Add(button);
buttonElements.Add(button);
}

// Adds status UI elements for displaying penetration and proximity states.
private void AddStatusUIElements()
{
var mouthStatusText = CreateTextField(mouthStatus);
uiElements.Add(mouthStatusText);
textFieldStorables.Add(mouthStatus);
var vaginaStatusText = CreateTextField(vaginaStatus);
uiElements.Add(vaginaStatusText);
textFieldStorables.Add(vaginaStatus);
var anusStatusText = CreateTextField(anusStatus);
uiElements.Add(anusStatusText);
textFieldStorables.Add(anusStatus);
var mouthProxText = CreateTextField(mouthProxStatus);
uiElements.Add(mouthProxText);
textFieldStorables.Add(mouthProxStatus);
var vaginaProxText = CreateTextField(vaginaProxStatus);
uiElements.Add(vaginaProxText);
textFieldStorables.Add(vaginaProxStatus);
var anusProxText = CreateTextField(anusProxStatus);
uiElements.Add(anusProxText);
textFieldStorables.Add(anusProxStatus);
}

// Updates the penetration status display for a specific orifice.
public void UpdateStatus(string orificeName)
{
JSONStorableString status = null;
string message = "";
switch (orificeName)
{
case "Mouth":
status = mouthStatus;
message = "Penis in mouth: ";
break;
case "Vagina":
status = vaginaStatus;
message = "Penis in vagina: ";
break;
case "Anus":
status = anusStatus;
message = "Penis in anus: ";
break;
}
if (status != null && orifices.ContainsKey(orificeName))
{
status.val = message + (orifices[orificeName].isPenetrated ? "Yes" : "No");
}
}

// Updates the proximity status display for a specific orifice.
public void UpdateProxStatus(string orificeName)
{
JSONStorableString status = null;
string message = "";
switch (orificeName)
{
case "Mouth":
status = mouthProxStatus;
message = "Penis near mouth: ";
break;
case "Vagina":
status = vaginaProxStatus;
message = "Penis near vagina: ";
break;
case "Anus":
status = anusProxStatus;
message = "Penis near anus: ";
break;
}
if (status != null && proximityHandlers.ContainsKey(orificeName))
{
ProximityHandler handler = proximityHandlers[orificeName];
bool inProx = handler != null && handler.isProx;
status.val = message + (inProx ? "Yes" : "No");
}
}

// Updates all penetration and proximity statuses.
public void UpdateAllStatuses()
{
foreach (var pair in orifices)
{
UpdateStatus(pair.Key);
UpdateProxStatus(pair.Key);
}
}

// Updates statuses every frame.
private void Update()
{
UpdateAllStatuses();
}

// Cleans up resources when the script is destroyed, including triggers, orifices, and penis visualizations.
public void OnDestroy()
{
try
{
foreach (var pair in orifices)
{
Orifice orifice = pair.Value;
if (orifice != null)
{
if (orifice.enterTriggerCollider != null)
{
UnityEngine.Object.Destroy(orifice.enterTriggerCollider.gameObject);
}
if (orifice.proximityTrigger != null)
{
UnityEngine.Object.Destroy(orifice.proximityTrigger.gameObject);
}
UnityEngine.Object.Destroy(orifice);
}
}
orifices.Clear();
proximityHandlers.Clear();
triggerHandlers.Clear();

foreach (var pair in penisViz)
{
List<GameObject> list = pair.Value;
if (list != null)
{
for (int i = 0; i < list.Count; i++)
{
GameObject v = list;
if (v != null) UnityEngine.Object.Destroy(v);
}
}
}
penisViz.Clear();
SuperController.LogMessage("OrificeTriggerManager: Components destroyed");
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error in OnDestroy: " + e.Message);
}
ClearUI();
}

// Serializes the script's state to JSON for saving.
public override JSONClass GetJSON(bool includePhysical = true, bool includeAppearance = true, bool forceStore = true)
{
JSONClass jc = base.GetJSON(includePhysical, includeAppearance, forceStore);
try
{
foreach (var pair in orifices)
{
if (pair.Value != null)
{
jc[pair.Key] = pair.Value.Store("", includePhysical);
}
}
SuperController.LogMessage("OrificeTriggerManager: JSON saved");
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Serialization error: " + e.Message);
}
return jc;
}

// Restores the script's state from JSON for loading.
public override void RestoreFromJSON(JSONClass jc, bool restorePhysical = true, bool restoreAppearance = true, JSONArray presetAtoms = null, bool setMissingToDefault = true)
{
base.RestoreFromJSON(jc, restorePhysical, restoreAppearance, presetAtoms, setMissingToDefault);
try
{
foreach (var pair in orifices)
{
if (jc.HasKey(pair.Key) && pair.Value != null)
{
pair.Value.Load(jc[pair.Key].AsObject, "");
}
}
UpdateAllStatuses();
SuperController.LogMessage("OrificeTriggerManager: JSON restored");
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Deserialization error: " + e.Message);
}
}
}

// Represents a penis on a male atom, managing its colliders for detection.
public class Penis
{
public Atom atom;
private HashSet<Collider> penisColliders;
private HashSet<string> penisColliderNames;

public Penis(Atom atom)
{
this.atom = atom;
penisColliders = new HashSet<Collider>();
penisColliderNames = new HashSet<string>();

FindPenisControllers();
FindCuaPenisColliders();

SuperController.LogMessage($"Penis: Initialized for {atom.uid}. Found colliders: {penisColliders.Count}");
}

// Finds penis-related controllers and their colliders.
private void FindPenisControllers()
{
Rigidbody[] rigidbodies = atom.GetComponentsInChildren<Rigidbody>();

foreach (Rigidbody rb in rigidbodies)
{
string rbNameLower = rb.name.ToLower();
bool isPenisPart = false;

if (rbNameLower.Contains("penisbase") || rbNameLower.Contains("penis_base")) isPenisPart = true;
else if (rbNameLower.Contains("penismid") || rbNameLower.Contains("penis_mid")) isPenisPart = true;
else if (rbNameLower.Contains("penistip") || rbNameLower.Contains("penis_tip") || rbNameLower.Contains("penistipcontrol")) isPenisPart = true;

if (isPenisPart)
{
Collider[] colliders = rb.GetComponents<Collider>();
foreach (var col in colliders)
{
penisColliders.Add(col);
penisColliderNames.Add(col.name);
SuperController.LogMessage($"Penis: Found controller collider: {col.name} from {rb.name}");
}

Collider[] childColliders = rb.GetComponentsInChildren<Collider>();
foreach (var col in childColliders)
{
if (!penisColliders.Contains(col))
{
penisColliders.Add(col);
penisColliderNames.Add(col.name);
SuperController.LogMessage($"Penis: Found child collider: {col.name} from {rb.name}");
}
}
}
}
}

// Finds custom (CUA) penis colliders.
private void FindCuaPenisColliders()
{
Collider[] allColliders = atom.GetComponentsInChildren<Collider>();

foreach (var col in allColliders)
{
string colNameLower = col.name.ToLower();

if ((colNameLower.Contains("autocollidergen") || colNameLower.Contains("autogen")) &&
(colNameLower.Contains("hard") || colNameLower.Contains("soft")) &&
!penisColliderNames.Contains(col.name))
{
Transform parent = col.transform.parent;
while (parent != null)
{
if (parent.name.ToLower().Contains("penis"))
{
penisColliders.Add(col);
penisColliderNames.Add(col.name);
SuperController.LogMessage($"Penis: Found CUA collider: {col.name}");
break;
}
parent = parent.parent;
}
}
}
}

// Checks if a collider belongs to the penis.
public bool IsPenisCollider(Collider other)
{
if (other == null) return false;

bool isPenisPart = penisColliders.Contains(other);

if (!isPenisPart)
{
string otherNameLower = other.name.ToLower();
isPenisPart = penisColliderNames.Contains(other.name) ||
(otherNameLower.Contains("autocollidergen") && otherNameLower.Contains("hard"));
}

return isPenisPart;
}
}

// Represents an orifice (e.g., Mouth, Vagina, Anus) with trigger and proximity colliders.
public class Orifice : MonoBehaviour
{
public string name;
public Rigidbody rb;
public JSONStorableFloat triggerScale;
public JSONStorableFloat triggerOffsetUp;
public JSONStorableFloat triggerOffsetForward;
public JSONStorableFloat proximityScale;
public JSONStorableBool showProximity;
public JSONStorableString info;
public CapsuleCollider enterTriggerCollider;
public CapsuleCollider proximityTrigger;
public int penetrationCount;
public bool isPenetrated { get { return penetrationCount > 0; } }
public float defaultUp;
public float defaultForward;

// Initializes the orifice with specific settings based on its name.
public virtual void Init(string orificeName, Atom atom)
{
name = orificeName;
try
{
string rbName;
float defaultScale;

switch (name)
{
case "Mouth":
rbName = "ThroatTrigger";
defaultScale = 0.03f;
defaultUp = 0.01f;
defaultForward = 0.07f;
break;
case "Vagina":
rbName = "VaginaTrigger";
defaultScale = 0.01f;
defaultUp = -0.08f;
defaultForward = 0.01f;
break;
case "Anus":
rbName = "pelvis";
defaultScale = 0.01f;
defaultUp = -0.16f;
defaultForward = -0.04f;
break;
default:
rbName = "pelvis";
defaultScale = 0.03f;
defaultUp = 0f;
defaultForward = 0f;
break;
}

var storable = atom.GetStorableByID(rbName);
if (storable != null)
{
rb = storable.GetComponent<Rigidbody>();
}
if (rb == null)
{
rb = atom.gameObject.AddComponent<Rigidbody>();
rb.isKinematic = true;
rb.useGravity = false;
SuperController.LogMessage("Orifice: Rigidbody created for " + name);
}

triggerScale = new JSONStorableFloat("Trigger Scale", defaultScale, 0.01f, 0.5f);
triggerOffsetUp = new JSONStorableFloat("Trigger Offset Up", 0f, -0.2f, 0.2f);
triggerOffsetForward = new JSONStorableFloat("Trigger Offset Forward", 0f, -0.2f, 0.2f);
proximityScale = new JSONStorableFloat("Proximity Scale", defaultScale * 1.5f, 0.01f, 1f);
showProximity = new JSONStorableBool("Show Proximity Zone", true);
info = new JSONStorableString("Information", "Orifice: " + name);
penetrationCount = 0;
SuperController.LogMessage("Orifice: Initialized " + name);
}
catch (Exception e)
{
SuperController.LogError("Orifice: Initialization error for " + name + ": " + e.Message);
}
}

// Serializes the orifice's state to JSON.
public virtual JSONClass Store(string key, bool includePhysical)
{
JSONClass jc = new JSONClass();
try
{
jc["triggerScale"] = triggerScale.val.ToString();
jc["triggerOffsetUp"] = triggerOffsetUp.val.ToString();
jc["triggerOffsetForward"] = triggerOffsetForward.val.ToString();
jc["proximityScale"] = proximityScale.val.ToString();
jc["showProximity"] = showProximity.val.ToString();
jc["info"] = info.val;
jc["penetrationCount"] = penetrationCount.ToString();
}
catch (Exception e)
{
SuperController.LogError("Orifice: Serialization error for " + key + ": " + e.Message);
}
return jc;
}

// Restores the orifice's state from JSON.
public virtual void Load(JSONClass jc, string key)
{
try
{
if (jc.HasKey("triggerScale")) triggerScale.val = float.Parse(jc["triggerScale"]);
if (jc.HasKey("triggerOffsetUp")) triggerOffsetUp.val = float.Parse(jc["triggerOffsetUp"]);
if (jc.HasKey("triggerOffsetForward")) triggerOffsetForward.val = float.Parse(jc["triggerOffsetForward"]);
if (jc.HasKey("proximityScale")) proximityScale.val = float.Parse(jc["proximityScale"]);
if (jc.HasKey("showProximity")) showProximity.val = bool.Parse(jc["showProximity"]);
if (jc.HasKey("info")) info.val = jc["info"];
if (jc.HasKey("penetrationCount")) penetrationCount = int.Parse(jc["penetrationCount"]);
}
catch (Exception e)
{
SuperController.LogError("Orifice: Deserialization error for " + key + ": " + e.Message);
}
}
}

// Handles proximity trigger events, tracking when penis colliders enter or exit the proximity zone.
public class ProximityHandler : MonoBehaviour
{
public Orifice orifice;
public bool on = true;
public bool isProx = false;
private float lastLogTime = 0f;
private const float LogInterval = 2f;
private HashSet<Collider> penisCollidersInProx = new HashSet<Collider>();

void OnTriggerEnter(Collider other)
{
if (!on || orifice == null) return;

if (IsPenisCollider(other))
{
if (penisCollidersInProx.Add(other))
{
SuperController.LogMessage($"[Prox] Penis entered {gameObject.name} for {orifice.name}");
isProx = true;
OrificeTriggerManager.singleton.UpdateProxStatus(orifice.name);
}
}
}

void OnTriggerExit(Collider other)
{
if (!on || orifice == null) return;

if (IsPenisCollider(other))
{
if (penisCollidersInProx.Remove(other))
{
SuperController.LogMessage($"[Prox] Penis exited {gameObject.name} for {orifice.name}");
isProx = penisCollidersInProx.Count > 0;
OrificeTriggerManager.singleton.UpdateProxStatus(orifice.name);
}
}
}

private bool IsPenisCollider(Collider col)
{
if (col == null) return false;
Atom atom = col.gameObject.GetComponentInParent<Atom>();
if (atom == null || !OrificeTriggerManager.singleton.penises.ContainsKey(atom)) return false;
return OrificeTriggerManager.singleton.penises[atom].IsPenisCollider(col);
}
}

// Handles trigger events for an orifice, tracking penetration count.
public class OrificeTriggerHandler : MonoBehaviour
{
public Orifice orifice;
public bool isActive = true;
private float lastLogTime = 0f;
private const float LogInterval = 2f;
private HashSet<Collider> penisCollidersInTrigger = new HashSet<Collider>();

void OnTriggerEnter(Collider other)
{
if (!isActive || orifice == null) return;

if (IsPenisCollider(other))
{
if (penisCollidersInTrigger.Add(other))
{
orifice.penetrationCount++;
SuperController.LogMessage($"[Trig] PENETRATION in {orifice.name}! Count: {orifice.penetrationCount} (collider: {other.name})");
OrificeTriggerManager.singleton.UpdateStatus(orifice.name);
}
}
}

void OnTriggerExit(Collider other)
{
if (!isActive || orifice == null) return;

if (IsPenisCollider(other))
{
if (penisCollidersInTrigger.Remove(other))
{
orifice.penetrationCount--;
if (orifice.penetrationCount < 0) orifice.penetrationCount = 0;
SuperController.LogMessage($"[Trig] Exit from {orifice.name}. Count: {orifice.penetrationCount} (collider: {other.name})");
OrificeTriggerManager.singleton.UpdateStatus(orifice.name);
}
}
}

private bool IsPenisCollider(Collider col)
{
if (col == null) return false;
Atom atom = col.gameObject.GetComponentInParent<Atom>();
if (atom == null || !OrificeTriggerManager.singleton.penises.ContainsKey(atom)) return false;
return OrificeTriggerManager.singleton.penises[atom].IsPenisCollider(col);
}
}
}
What is this? I heaven't read through, but it seems very similar to my code.
 
What is this? I heaven't read through, but it seems very similar to my code.
Yes, this is the refactored code from your scripts, which I studied:

Orifice.cs: Base class for orifices (Throat, Vagina, Anus), provides trigger parameters (triggerScale, triggerOffsetUp, triggerOffsetForward, proximityScale, showProximity, preventSwitching, enabledJ) and isPenetrated logic.
ProximityHandler.cs: Logic for handling proximity triggers, including OnTriggerEnter/Exit and numCollisions.
OrificeTriggerHandler.cs: Logic for enter triggers, including penetration registration (RegisterCollision) and numCollisions management.
Throat.cs: Throat-specific implementation, including trigger initialization and UI.
Vagina.cs: Vagina-specific implementation, including initialization and UI for bulge/stretch.
Anus.cs: Anus-specific implementation, including twinking parameters and UI.
FillMeUp.cs: The plugin's main script, provides UI methods (CreateButton, CreateTabBar, ClearUI, leftUI/rightUI), automatic atom detection (SuperController.singleton.GetAtoms()), and global references (anus, vagina, throat).
Fuckable.cs: The base class for all "penetrable" objects, provides info, depth, sensitivity parameters, and UI logic.

I thought you'd see how it works. :)

From mine, this is character initialization and penis colliders.
I deliberately didn't attach a video or photo; I wanted you to see how it works for yourself. I think this might be interesting for you, since you're only using yellow proximity triggers, and they're spheres, but I also use penetration triggers.
This plugin is for developers. Other plugins can build on it.
And of course, I'd love to hear your opinion on this script.
 
Last edited:
Sorry if this is mentioned in the discussion already but how do I set up a speed transition trigger?
 
Sorry if this is mentioned in the discussion already but how do I set up a speed transition trigger?
This is explained in the overview of the plugin. It's the same as setting up a depth trigger, like in the example.
 
When you do plan on releasing the v72 update, what will happen to old scenes using the current version? Does it effect poses?

Thanks again for this amazing plugin btw you're awesome ;)(y)
 
Please tell me Why a woman has an orgasm at the same time that a man also has an orgasm. Is there any way to make it unrelated to each other?
 
Would be nice to have a global trigger feature so Embody with Trackers would keep working (ReinitializeIfActive) when we switch poses.
 
By any chance, does the FillMeUp module exist on it's own? or is it possible to carve it off? I do like the whole suite, but I often use other plugins that often conflict.
 
Hello, dear friend and role model. After about 600 attempts to create my own sex automation like yours, I'm a little tired. I'm almost close to your hip movement results using my own methods, but I'm still tired. 38 projects just for hip movement are currently hanging in the air.
I thought I should try something different, because I'm tired again. So I wrote my own AI, which I'm currently expanding; you can see it at the very beginning of the training. It runs on the forces from your files and your experience. I finally looked at your Magnet.cs and Torque.cs files, although I wanted to come to this result myself. I examined them carefully under a magnifying glass, although I had already noticed that they work with three points because they move synchronously.
In short, I applied the same real physical forces as yours. My AI works with them, as well as with all available controllers on the body.
The video shows the very beginning of the training. I'm seeing very good results. As you can see, she's moving her hips and trying to direct them herself without BodyLanguage. The results were impressive at the initial level. I think I'll be doing this exclusively from now on. If you're interested, I can share the code.
How does the learning process work? I play, and the AI remembers poses and movements. The code also contains reinforcement rules for penis search, hip movements, poses, and so on.

 

Attachments

  • 2025-10-24-18-04-3_UbV2Vxe3.mp4
    14.9 MB
Last edited:
I'm getting errors when loading saved scenes that have BodyLanguage running with custom poses. For some reason they'll run fine at first but after loading them a couple times, something goes wrong with the PoseMe plugin and it's just blank and I can't load the poses anymore.

at CheesyFX.Gaze+Ass..ctor (CheesyFX.Person person, System.String side) [0x00000] in <filename unknown>:0
at CheesyFX.Gaze.RegisterPerson (CheesyFX.Person person, CheesyFX.Gaze gaze) [0x00000] in <filename unknown>:0
at CheesyFX.Person.Init (CheesyFX.CapsulePenetrator penetrator) [0x00000] in <filename unknown>:0
at CheesyFX.FillMeUp.AddPenetrator (.Atom atom) [0x00000] in <filename unknown>:0
!> System.InvalidOperationException: Operation is not valid due to the current state of the object
at System.Linq.Enumerable.First[Person] (IEnumerable`1 source, System.Func`2 predicate, Fallback fallback) [0x00000] in <filename unknown>:0
at System.Linq.Enumerable.First[Person] (IEnumerable`1 source, System.Func`2 predicate) [0x00000] in <filename unknown>:0
at CheesyFX.PoseMe.GetSceneContent (Boolean init) [0x00000] in <filename unknown>:0
at CheesyFX.PoseMe.Init () [0x00000] in <filename unknown>:0
!> System.ArgumentOutOfRangeException: Argument is out of range.
Parameter name: index
at System.Collections.Generic.List`1[UnityEngine.Transform].get_Item (Int32 index) [0x00000] in <filename unknown>:0
at CheesyFX.Gaze+Ass..ctor (CheesyFX.Person person, System.String side) [0x00000] in <filename unknown>:0
at CheesyFX.Gaze.RegisterPerson (CheesyFX.Person person, CheesyFX.Gaze gaze) [0x00000] in <filename unknown>:0
at CheesyFX.Person.Init (CheesyFX.CapsulePenetrator penetrator) [0x00000] in <filename unknown>:0
at CheesyFX.FillMeUp.AddPenetrator (.Atom atom) [0x00000] in <filename unknown>:0
!> System.InvalidOperationException: Operation is not valid due to the current state of the object
at System.Linq.Enumerable.First[Person] (IEnumerable`1 source, System.Func`2 predicate, Fallback fallback) [0x00000] in <filename unknown>:0
at System.Linq.Enumerable.First[Person] (IEnumerable`1 source, System.Func`2 predicate) [0x00000] in <filename unknown>:0
at CheesyFX.PoseMe.GetSceneContent (Boolean init) [0x00000] in <filename unknown>:0
at CheesyFX.PoseMe.Init () [0x00000] in <filename unknown>:0
!> System.NullReferenceException: Object reference not set to an instance of an object
at CheesyFX.PoseIdle.Init (CheesyFX.Pose pose) [0x00000] in <filename unknown>:0
at CheesyFX.Pose..ctor (Int32 id, SimpleJSON.JSONClass pose) [0x00000] in <filename unknown>:0
at CheesyFX.PoseMe.AddPose (SimpleJSON.JSONClass jc) [0x00000] in <filename unknown>:0
at CheesyFX.PoseMe.LoadPoseFromJSON (SimpleJSON.JSONClass jc, Boolean apply) [0x00000] in <filename unknown>:0
at CheesyFX.PoseMe.LateRestoreFromJSON (SimpleJSON.JSONClass jc, Boolean restorePhysical, Boolean restoreAppearance, Boolean setMissingToDefault) [0x00000] in <filename unknown>:0
!> System.NullReferenceException: Object reference not set to an instance of an object
at CheesyFX.PoseIdle.Init (CheesyFX.Pose pose) [0x00000] in <filename unknown>:0
at CheesyFX.Pose..ctor (Int32 id, SimpleJSON.JSONClass pose) [0x00000] in <filename unknown>:0
at CheesyFX.PoseMe.AddPose (SimpleJSON.JSONClass jc) [0x00000] in <filename unknown>:0
at CheesyFX.PoseMe.LoadPoseFromJSON (SimpleJSON.JSONClass jc, Boolean apply) [0x00000] in <filename unknown>:0
at CheesyFX.PoseMe.LateRestoreFromJSON (SimpleJSON.JSONClass jc, Boolean restorePhysical, Boolean restoreAppearance, Boolean setMissingToDefault) [0x00000] in <filename unknown>:0
!> FocusOnMe!: FocusOnMe.cslist not found. Install it as a session plugin.
 
Back
Top Bottom