I can't publish my first script, but I want to try to share it at least here.
This script works in tandem with DiviningRog or DiviningForeskin or BodyLanguage. It directs the female hips to the male atom's penis. The controls are simple. It can be used on both woman and male atoms. In the plugin, select the woman and men, and enjoy. The license is free, CC BY. I would like the VAM community to develop further, so you can use this plugin in any form, both in paid and free content.
If you want, you can use the code for your own purposes, modify it or do whatever you want.
Enjoy.
Download link:
https://mega.nz/folder/hrAx2JoD#e4AtWJN7rokJsvJhEjhh7A
This script works in tandem with DiviningRog or DiviningForeskin or BodyLanguage. It directs the female hips to the male atom's penis. The controls are simple. It can be used on both woman and male atoms. In the plugin, select the woman and men, and enjoy. The license is free, CC BY. I would like the VAM community to develop further, so you can use this plugin in any form, both in paid and free content.
If you want, you can use the code for your own purposes, modify it or do whatever you want.
Enjoy.
Download link:
https://mega.nz/folder/hrAx2JoD#e4AtWJN7rokJsvJhEjhh7A
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using SimpleJSON;
// This script aligns female hip and thigh controllers to male penis controllers in Virt-A-Mate (VAM).
// It dynamically adjusts the position and rotation of female hips and thighs based on the penis position,
// with a UI to select atoms, adjust settings, and enable alignment.
public class DeviningReach : MVRScript
{
// JSON storables for UI elements
private JSONStorableBool enableAlignment; // Toggle to enable/disable alignment
private JSONStorableString statusText; // Text field to display status messages
private JSONStorableFloat attractionStrength; // Slider for thigh attraction strength
private JSONStorableFloat rotationSpeed; // Slider for hip rotation speed
private JSONStorableFloat pelvisOffset; // Slider for pelvis position offset
private JSONStorableStringChooser maleAtomChooser; // Popup to select male atom
private JSONStorableStringChooser femaleAtomChooser; // Popup to select female atom
private JSONStorableString penisBaseID; // ID for penis base controller
private JSONStorableString penisMidID; // ID for penis mid controller
private JSONStorableString penisTipID; // ID for penis tip controller
private JSONStorableString hipID; // ID for female hip controller
private JSONStorableString pelvisID; // ID for female pelvis controller
private JSONStorableString leftThighID; // ID for left thigh controller
private JSONStorableString rightThighID; // ID for right thigh controller
// Runtime variables
private Atom maleAtom; // Selected male atom
private Atom femaleAtom; // Selected female atom
private bool hasValidTargets = false; // True if all required controllers are found
private bool isInitialized = false; // True after initialization is complete
private float initializationDelay = 1f; // Delay before initializing controllers
private Dictionary<string, FreeControllerV3> controllers = new Dictionary<string, FreeControllerV3>(); // Stores all controllers
private Dictionary<string, Atom> availableAtoms = new Dictionary<string, Atom>(); // Stores all Person atoms in the scene
private Vector3 vaginaPosition; // Position of the female hip (used as vagina reference)
private Vector3 vaginaDirection; // Forward direction of the female hip
private float updateLogTimer = 2f; // Timer for periodic logging
private Vector3 lastLeftThighPos; // Last position of left thigh for smooth movement
private Vector3 lastRightThighPos; // Last position of right thigh for smooth movement
private Quaternion lastLeftThighRot; // Last rotation of left thigh for smooth movement
private Quaternion lastRightThighRot; // Last rotation of right thigh for smooth movement
// Constants for controller IDs
private readonly string[] maleControllerIds = { "penisBaseControl", "penisMidControl", "penisTipControl" }; // Male controller IDs
private readonly string[] femaleControllerIds = { "hipControl", "pelvisControl", "lThighControl", "rThighControl" }; // Female controller IDs
private const string PluginName = "DeviningReach"; // Plugin name for logging
private const float MinPositionDelta = 0.001f; // Minimum position change to prevent jittering
private const float MinRotationDelta = 0.1f; // Minimum rotation change (in degrees) to prevent jittering
// Initializes the plugin, called when the plugin is loaded or reloaded
public override void Init()
{
try
{
SuperController.LogMessage($"{PluginName}: Starting Init");
// Reset atom choosers to prevent auto-initialization on reload
if (maleAtomChooser != null) maleAtomChooser.val = "None";
if (femaleAtomChooser != null) femaleAtomChooser.val = "None";
maleAtom = null;
femaleAtom = null;
isInitialized = false;
hasValidTargets = false;
controllers.Clear();
FindAllPersonAtoms(); // Find all Person atoms in the scene
CreateUI(); // Create the UI elements
SuperController.LogMessage($"{PluginName}: Init completed");
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: Init error: {e}");
}
}
// Creates the UI elements for the plugin (toggles, sliders, popups, buttons)
private void CreateUI()
{
try
{
// Create toggle to enable/disable alignment
enableAlignment = new JSONStorableBool("Enable Alignment", true);
RegisterBool(enableAlignment);
CreateToggle(enableAlignment);
// Create status text field
statusText = new JSONStorableString("Status", "Select one male and one female atom");
RegisterString(statusText);
CreateTextField(statusText).height = 100f;
// Populate atom names for choosers
List<string> atomNames = new List<string> { "None" };
atomNames.AddRange(availableAtoms.Keys);
// Create male atom chooser
maleAtomChooser = new JSONStorableStringChooser("Male Atom", atomNames, "None", "Male Atom",
new JSONStorableStringChooser.SetStringCallback(UpdateAtomSelection));
RegisterStringChooser(maleAtomChooser);
CreatePopup(maleAtomChooser);
// Create female atom chooser
femaleAtomChooser = new JSONStorableStringChooser("Female Atom", atomNames, "None", "Female Atom",
new JSONStorableStringChooser.SetStringCallback(UpdateAtomSelection));
RegisterStringChooser(femaleAtomChooser);
CreatePopup(femaleAtomChooser);
// Create attraction strength slider (controls thigh movement intensity)
attractionStrength = new JSONStorableFloat("Attraction Strength", 0.5f, 0.1f, 1.0f);
RegisterFloat(attractionStrength);
CreateSlider(attractionStrength);
// Create rotation speed slider (controls hip rotation speed)
rotationSpeed = new JSONStorableFloat("Rotation Speed", 2.0f, 0.1f, 10f);
RegisterFloat(rotationSpeed);
CreateSlider(rotationSpeed);
// Create pelvis offset slider (controls pelvis position offset from hip)
pelvisOffset = new JSONStorableFloat("Pelvis Offset", 0.1f, 0.05f, 0.2f);
RegisterFloat(pelvisOffset);
CreateSlider(pelvisOffset);
// Initialize controller ID storables
penisBaseID = new JSONStorableString("Penis Base ID", "penisBaseControl");
RegisterString(penisBaseID);
penisMidID = new JSONStorableString("Penis Mid ID", "penisMidControl");
RegisterString(penisMidID);
penisTipID = new JSONStorableString("Penis Tip ID", "penisTipControl");
RegisterString(penisTipID);
hipID = new JSONStorableString("Hip ID", "hipControl");
RegisterString(hipID);
pelvisID = new JSONStorableString("Pelvis ID", "pelvisControl");
RegisterString(pelvisID);
leftThighID = new JSONStorableString("Left Thigh ID", "lThighControl");
RegisterString(leftThighID);
rightThighID = new JSONStorableString("Right Thigh ID", "rThighControl");
RegisterString(rightThighID);
// Create button to enable all female control points
JSONStorableAction enableAllPoints = new JSONStorableAction("Enable All Points", EnableAllPoints);
RegisterAction(enableAllPoints);
var enableButton = CreateButton("Enable All Points");
enableButton.button.onClick.AddListener(() => { EnableAllPoints(); });
SuperController.LogMessage($"{PluginName}: CreateUI - Enable All Points button registered");
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: CreateUI error: {e}");
}
}
// Updates atom selection when male or female atom is chosen in UI
private void UpdateAtomSelection(string val)
{
try
{
SuperController.LogMessage($"{PluginName}: UpdateAtomSelection called with value: {val}");
// Set male and female atoms based on chooser values
maleAtom = maleAtomChooser.val != "None" && availableAtoms.ContainsKey(maleAtomChooser.val) ? availableAtoms[maleAtomChooser.val] : null;
femaleAtom = femaleAtomChooser.val != "None" && availableAtoms.ContainsKey(femaleAtomChooser.val) ? availableAtoms[femaleAtomChooser.val] : null;
// Prevent selecting the same atom for both male and female
if (maleAtom != null && femaleAtom != null && maleAtom == femaleAtom)
{
SuperController.LogError($"{PluginName}: Cannot select the same atom for both male and female!");
statusText.val = "Error: Cannot select the same atom for both male and female!";
femaleAtom = null;
femaleAtomChooser.val = "None";
}
// Reset initialization to trigger controller setup
isInitialized = false;
initializationDelay = 1f;
SuperController.LogMessage($"{PluginName}: Atom selection updated: Male = {maleAtom?.uid ?? "None"}, Female = {femaleAtom?.uid ?? "None"}");
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: UpdateAtomSelection error: {e}");
}
}
// Finds all Person atoms in the scene
private void FindAllPersonAtoms()
{
try
{
SuperController.LogMessage($"{PluginName}: Finding all Person atoms");
availableAtoms.Clear();
foreach (Atom atom in SuperController.singleton.GetAtoms())
{
if (atom.type == "Person")
{
availableAtoms[atom.uid] = atom;
SuperController.LogMessage($"{PluginName}: Found Person atom: {atom.uid}");
}
}
if (availableAtoms.Count == 0)
{
SuperController.LogError($"{PluginName}: No Person atoms found in scene!");
statusText.val = "Error: Person atoms not found!";
}
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: FindAllPersonAtoms error: {e}");
}
}
// Initializes controllers for male and female atoms
private void InitializeControllers()
{
try
{
SuperController.LogMessage($"{PluginName}: Initializing controllers");
controllers.Clear();
hasValidTargets = false;
List<string> missingControllers = new List<string>();
// Load male controllers without changing their states
if (maleAtom != null)
{
foreach (string controlId in maleControllerIds)
{
FreeControllerV3 controller = maleAtom.GetStorableByID(controlId) as FreeControllerV3;
if (controller != null)
{
controllers[controlId] = controller;
// Log current states without modifying them
SuperController.LogMessage($"{PluginName}: Found {controlId} for {maleAtom.uid}, Current PositionState: {controller.currentPositionState}, Current RotationState: {controller.currentRotationState}, no state changes applied");
}
else
{
SuperController.LogMessage($"{PluginName}: Control {controlId} not found for {maleAtom.uid}");
missingControllers.Add(controlId);
}
}
}
// Load and set states for female controllers
if (femaleAtom != null)
{
foreach (string controlId in femaleControllerIds)
{
FreeControllerV3 controller = femaleAtom.GetStorableByID(controlId) as FreeControllerV3;
if (controller != null)
{
controllers[controlId] = controller;
if (controlId.ToLower().Contains("hip"))
{
controller.currentPositionState = FreeControllerV3.PositionState.On;
controller.currentRotationState = FreeControllerV3.RotationState.On;
SuperController.LogMessage($"{PluginName}: Initialized {controlId} for {femaleAtom.uid}, Position: On, Rotation: On");
}
else if (controlId.ToLower().Contains("pelvis"))
{
controller.currentPositionState = FreeControllerV3.PositionState.ParentLink;
controller.currentRotationState = FreeControllerV3.RotationState.ParentLink;
SuperController.LogMessage($"{PluginName}: Initialized {controlId} for {femaleAtom.uid}, Position: ParentLink, Rotation: ParentLink");
}
else
{
controller.currentPositionState = FreeControllerV3.PositionState.Comply;
controller.currentRotationState = FreeControllerV3.RotationState.Comply;
SuperController.LogMessage($"{PluginName}: Initialized {controlId} for {femaleAtom.uid}, Position: Comply, Rotation: Comply");
}
}
else
{
SuperController.LogMessage($"{PluginName}: Control {controlId} not found for {femaleAtom.uid}");
missingControllers.Add(controlId);
}
}
}
// Check if all required controllers are present
hasValidTargets = controllers.ContainsKey(penisBaseID.val) &&
controllers.ContainsKey(penisTipID.val) &&
controllers.ContainsKey(hipID.val) &&
controllers.ContainsKey(leftThighID.val) &&
controllers.ContainsKey(rightThighID.val);
if (hasValidTargets)
{
SuperController.LogMessage($"{PluginName}: InitializeControllers succeeded");
if (missingControllers.Count > 0)
{
statusText.val = $"Warning: Controllers not found: {string.Join(", ", missingControllers.ToArray())}";
}
else
{
statusText.val = "All female points successfully initialized";
}
// Initialize thigh positions and rotations
if (controllers.ContainsKey(leftThighID.val) && controllers[leftThighID.val].control != null)
{
lastLeftThighPos = controllers[leftThighID.val].control.position;
lastLeftThighRot = controllers[leftThighID.val].control.rotation;
}
if (controllers.ContainsKey(rightThighID.val) && controllers[rightThighID.val].control != null)
{
lastRightThighPos = controllers[rightThighID.val].control.position;
lastRightThighRot = controllers[rightThighID.val].control.rotation;
}
UpdatePelvisAlignment(); // Align pelvis initially
}
else
{
statusText.val = $"Error: Required controllers not found: {string.Join(", ", missingControllers.ToArray())}";
SuperController.LogError($"{PluginName}: InitializeControllers failed - minimum required controllers not found");
}
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: InitializeControllers error: {e}");
statusText.val = "Error initializing controllers";
}
}
// Main update loop, runs every frame
private void Update()
{
try
{
updateLogTimer -= Time.deltaTime;
// Initialize controllers after delay, only if atoms are selected
if (!isInitialized && initializationDelay > 0f)
{
initializationDelay -= Time.deltaTime;
if (initializationDelay <= 0f && maleAtom != null && femaleAtom != null)
{
InitializeControllers();
isInitialized = true;
SuperController.LogMessage($"{PluginName}: Delayed initialization completed");
}
return;
}
// Skip update if alignment is disabled or targets are invalid
if (!enableAlignment.val || !hasValidTargets)
{
if (updateLogTimer <= 0f)
{
SuperController.LogMessage($"{PluginName}: Update skipped - alignment disabled or invalid targets");
updateLogTimer = 2f;
}
return;
}
// Perform alignment calculations
CalculateVaginaPoints();
AdaptiveHipsAlignment();
UpdatePelvisAlignment();
UpdateThighMovement();
UpdateStatusText();
// Reset log timer
if (updateLogTimer <= 0f)
{
updateLogTimer = 2f;
}
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: Update error: {e}");
}
}
// Calculates the position and direction of the vagina based on hip controller
private void CalculateVaginaPoints()
{
if (controllers.ContainsKey(hipID.val))
{
FreeControllerV3 hip = controllers[hipID.val];
vaginaPosition = hip.control.position;
vaginaDirection = hip.control.forward;
}
}
// Aligns female hips to penis direction
private void AdaptiveHipsAlignment()
{
try
{
FreeControllerV3 penisBaseControl = controllers[penisBaseID.val];
FreeControllerV3 penisMidControl = controllers.ContainsKey(penisMidID.val) ? controllers[penisMidID.val] : null;
FreeControllerV3 penisTipControl = controllers[penisTipID.val];
FreeControllerV3 femaleHipControl = controllers[hipID.val];
if (penisBaseControl == null || penisTipControl == null || femaleHipControl == null) return;
if (penisBaseControl.control == null || penisTipControl.control == null || femaleHipControl.control == null) return;
// Calculate penis direction from base to tip
Vector3 penisDirection = (penisBaseControl.control.position - penisTipControl.control.position).normalized;
if (float.IsNaN(penisDirection.x) || float.IsNaN(penisDirection.y) || float.IsNaN(penisDirection.z))
{
SuperController.LogMessage($"{PluginName}: Invalid penis direction, skipping alignment");
return;
}
// Adjust direction using mid controller if available and aligned
if (penisMidControl != null && penisMidControl.control != null)
{
Vector3 tipToMid = (penisMidControl.control.position - penisTipControl.control.position).normalized;
Vector3 midToBase = (penisBaseControl.control.position - penisMidControl.control.position).normalized;
float alignmentAngle = Vector3.Angle(tipToMid, midToBase);
if (alignmentAngle > 10f)
{
SuperController.LogMessage($"{PluginName}: penisMidControl not aligned with base and tip, using tip-to-base direction");
}
else
{
penisDirection = (tipToMid + midToBase).normalized;
}
}
// Rotate hips to align with penis direction
Quaternion currentRotation = femaleHipControl.control.rotation;
Quaternion targetRotation = Quaternion.FromToRotation(currentRotation * Vector3.down, penisDirection) * currentRotation;
Quaternion newRotation = Quaternion.Slerp(currentRotation, targetRotation, Time.deltaTime * rotationSpeed.val);
femaleHipControl.control.rotation = newRotation;
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: AdaptiveHipsAlignment error: {e}");
}
}
// Aligns pelvis position and rotation to hip controller
private void UpdatePelvisAlignment()
{
try
{
if (!controllers.ContainsKey(hipID.val) || !controllers.ContainsKey(pelvisID.val)) return;
FreeControllerV3 hipControl = controllers[hipID.val];
FreeControllerV3 pelvisControl = controllers[pelvisID.val];
if (hipControl == null || pelvisControl == null || hipControl.control == null || pelvisControl.control == null) return;
// Position pelvis with offset from hip
Vector3 offset = hipControl.control.rotation * Vector3.up * pelvisOffset.val;
pelvisControl.control.position = hipControl.control.position + offset;
pelvisControl.control.rotation = hipControl.control.rotation;
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: UpdatePelvisAlignment error: {e}");
}
}
// Moves thighs towards penis tip with smooth interpolation
private void UpdateThighMovement()
{
try
{
// Check if all required controllers are present
if (!controllers.ContainsKey(leftThighID.val) || !controllers.ContainsKey(rightThighID.val) ||
!controllers.ContainsKey(penisTipID.val) || !controllers.ContainsKey(hipID.val))
{
return;
}
FreeControllerV3 leftThigh = controllers[leftThighID.val];
FreeControllerV3 rightThigh = controllers[rightThighID.val];
FreeControllerV3 penisTip = controllers[penisTipID.val];
FreeControllerV3 hip = controllers[hipID.val];
if (leftThigh.control == null || rightThigh.control == null || penisTip.control == null || hip.control == null)
{
return;
}
// Calculate dynamic max distance and correction speed based on attraction strength
float effectiveMaxDistance = Mathf.Lerp(0.2f, 1.0f, attractionStrength.val);
float effectiveThighCorrectionSpeed = Mathf.Lerp(1.0f, 10.0f, attractionStrength.val);
// Skip if penis is too far from vagina
float distanceToVagina = Vector3.Distance(penisTip.control.position, vaginaPosition);
if (distanceToVagina > effectiveMaxDistance)
{
return;
}
// Calculate directions to penis tip
Vector3 toPenisLeft = (penisTip.control.position - lastLeftThighPos).normalized;
Vector3 toPenisRight = (penisTip.control.position - lastRightThighPos).normalized;
if (float.IsNaN(toPenisLeft.x) || float.IsNaN(toPenisRight.x))
{
return;
}
// Calculate target positions for thighs
float thighDistance = 0.3f;
Vector3 targetLeftPos = penisTip.control.position + toPenisLeft * thighDistance;
Vector3 targetRightPos = penisTip.control.position + toPenisRight * thighDistance;
// Calculate position deltas to prevent jittering
float leftPosDelta = Vector3.Distance(leftThigh.control.position, targetLeftPos);
float rightPosDelta = Vector3.Distance(rightThigh.control.position, targetRightPos);
float smoothFactor = Time.deltaTime * effectiveThighCorrectionSpeed * 0.3f;
// Move left thigh if delta is significant
if (leftPosDelta > MinPositionDelta)
{
leftThigh.control.position = Vector3.Lerp(
leftThigh.control.position,
targetLeftPos,
smoothFactor
);
lastLeftThighPos = leftThigh.control.position;
}
// Move right thigh if delta is significant
if (rightPosDelta > MinPositionDelta)
{
rightThigh.control.position = Vector3.Lerp(
rightThigh.control.position,
targetRightPos,
smoothFactor
);
lastRightThighPos = rightThigh.control.position;
}
// Align thigh rotations with hip
Quaternion targetLeftRot = Quaternion.LookRotation(hip.control.forward, hip.control.up);
Quaternion targetRightRot = Quaternion.LookRotation(hip.control.forward, hip.control.up);
float leftRotDelta = Quaternion.Angle(leftThigh.control.rotation, targetLeftRot);
float rightRotDelta = Quaternion.Angle(rightThigh.control.rotation, targetRightRot);
// Rotate left thigh if delta is significant
if (leftRotDelta > MinRotationDelta)
{
leftThigh.control.rotation = Quaternion.Slerp(
leftThigh.control.rotation,
targetLeftRot,
smoothFactor
);
lastLeftThighRot = leftThigh.control.rotation;
}
// Rotate right thigh if delta is significant
if (rightRotDelta > MinRotationDelta)
{
rightThigh.control.rotation = Quaternion.Slerp(
rightThigh.control.rotation,
targetRightRot,
smoothFactor
);
lastRightThighRot = rightThigh.control.rotation;
}
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: UpdateThighMovement error: {e}");
}
}
// Updates the status text with alignment information
private void UpdateStatusText()
{
try
{
if (!hasValidTargets)
{
statusText.val = "Error: Required controllers not found";
return;
}
FreeControllerV3 penisTip = controllers[penisTipID.val];
FreeControllerV3 penisBase = controllers[penisBaseID.val];
FreeControllerV3 hip = controllers[hipID.val];
if (penisTip == null || penisBase == null || hip == null || penisTip.control == null || penisBase.control == null || hip.control == null)
{
statusText.val = "Error: Controllers not initialized";
return;
}
// Calculate and display alignment metrics
float distance = Vector3.Distance(penisTip.control.position, hip.control.position);
Vector3 angles = hip.control.rotation.eulerAngles;
Vector3 downDirection = hip.control.rotation * Vector3.down;
Vector3 penisDirection = (penisBase.control.position - penisTip.control.position).normalized;
float alignmentAngle = Vector3.Angle(downDirection, penisDirection);
statusText.val = $"Distance: {distance:F2} m\n" +
$"Hip Angles: X={angles.x:F1}° Y={angles.y:F1}° Z={angles.z:F1}°\n" +
$"Alignment: {alignmentAngle:F1}°\n" +
$"Attraction Strength: {attractionStrength.val:F2}\n" +
$"Male: {maleAtom?.uid ?? "None"}\n" +
$"Female: {femaleAtom?.uid ?? "None"}";
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: UpdateStatusText error: {e}");
}
}
// Enables all female control points via button click
private void EnableAllPoints()
{
try
{
SuperController.LogMessage($"{PluginName}: Enabling all female control points");
controllers.Clear();
hasValidTargets = false;
enableAlignment.val = true; // Enable alignment toggle
InitializeControllers(); // Reinitialize controllers
if (hasValidTargets)
{
SuperController.LogMessage($"{PluginName}: EnableAllPoints succeeded");
}
else
{
SuperController.LogError($"{PluginName}: EnableAllPoints failed");
}
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: EnableAllPoints error: {e}");
statusText.val = "Error enabling points";
}
}
// Called when the plugin is unloaded or reloaded, clears controllers without modifying states
public void OnDestroy()
{
try
{
SuperController.LogMessage($"{PluginName}: OnDestroy called, clearing controllers without changing states");
controllers.Clear(); // Clear controller dictionary without touching controller states
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: OnDestroy error: {e}");
}
}
}
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using SimpleJSON;
// This script aligns female hip and thigh controllers to male penis controllers in Virt-A-Mate (VAM).
// It dynamically adjusts the position and rotation of female hips and thighs based on the penis position,
// with a UI to select atoms, adjust settings, and enable alignment.
public class DeviningReach : MVRScript
{
// JSON storables for UI elements
private JSONStorableBool enableAlignment; // Toggle to enable/disable alignment
private JSONStorableString statusText; // Text field to display status messages
private JSONStorableFloat attractionStrength; // Slider for thigh attraction strength
private JSONStorableFloat rotationSpeed; // Slider for hip rotation speed
private JSONStorableFloat pelvisOffset; // Slider for pelvis position offset
private JSONStorableStringChooser maleAtomChooser; // Popup to select male atom
private JSONStorableStringChooser femaleAtomChooser; // Popup to select female atom
private JSONStorableString penisBaseID; // ID for penis base controller
private JSONStorableString penisMidID; // ID for penis mid controller
private JSONStorableString penisTipID; // ID for penis tip controller
private JSONStorableString hipID; // ID for female hip controller
private JSONStorableString pelvisID; // ID for female pelvis controller
private JSONStorableString leftThighID; // ID for left thigh controller
private JSONStorableString rightThighID; // ID for right thigh controller
// Runtime variables
private Atom maleAtom; // Selected male atom
private Atom femaleAtom; // Selected female atom
private bool hasValidTargets = false; // True if all required controllers are found
private bool isInitialized = false; // True after initialization is complete
private float initializationDelay = 1f; // Delay before initializing controllers
private Dictionary<string, FreeControllerV3> controllers = new Dictionary<string, FreeControllerV3>(); // Stores all controllers
private Dictionary<string, Atom> availableAtoms = new Dictionary<string, Atom>(); // Stores all Person atoms in the scene
private Vector3 vaginaPosition; // Position of the female hip (used as vagina reference)
private Vector3 vaginaDirection; // Forward direction of the female hip
private float updateLogTimer = 2f; // Timer for periodic logging
private Vector3 lastLeftThighPos; // Last position of left thigh for smooth movement
private Vector3 lastRightThighPos; // Last position of right thigh for smooth movement
private Quaternion lastLeftThighRot; // Last rotation of left thigh for smooth movement
private Quaternion lastRightThighRot; // Last rotation of right thigh for smooth movement
// Constants for controller IDs
private readonly string[] maleControllerIds = { "penisBaseControl", "penisMidControl", "penisTipControl" }; // Male controller IDs
private readonly string[] femaleControllerIds = { "hipControl", "pelvisControl", "lThighControl", "rThighControl" }; // Female controller IDs
private const string PluginName = "DeviningReach"; // Plugin name for logging
private const float MinPositionDelta = 0.001f; // Minimum position change to prevent jittering
private const float MinRotationDelta = 0.1f; // Minimum rotation change (in degrees) to prevent jittering
// Initializes the plugin, called when the plugin is loaded or reloaded
public override void Init()
{
try
{
SuperController.LogMessage($"{PluginName}: Starting Init");
// Reset atom choosers to prevent auto-initialization on reload
if (maleAtomChooser != null) maleAtomChooser.val = "None";
if (femaleAtomChooser != null) femaleAtomChooser.val = "None";
maleAtom = null;
femaleAtom = null;
isInitialized = false;
hasValidTargets = false;
controllers.Clear();
FindAllPersonAtoms(); // Find all Person atoms in the scene
CreateUI(); // Create the UI elements
SuperController.LogMessage($"{PluginName}: Init completed");
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: Init error: {e}");
}
}
// Creates the UI elements for the plugin (toggles, sliders, popups, buttons)
private void CreateUI()
{
try
{
// Create toggle to enable/disable alignment
enableAlignment = new JSONStorableBool("Enable Alignment", true);
RegisterBool(enableAlignment);
CreateToggle(enableAlignment);
// Create status text field
statusText = new JSONStorableString("Status", "Select one male and one female atom");
RegisterString(statusText);
CreateTextField(statusText).height = 100f;
// Populate atom names for choosers
List<string> atomNames = new List<string> { "None" };
atomNames.AddRange(availableAtoms.Keys);
// Create male atom chooser
maleAtomChooser = new JSONStorableStringChooser("Male Atom", atomNames, "None", "Male Atom",
new JSONStorableStringChooser.SetStringCallback(UpdateAtomSelection));
RegisterStringChooser(maleAtomChooser);
CreatePopup(maleAtomChooser);
// Create female atom chooser
femaleAtomChooser = new JSONStorableStringChooser("Female Atom", atomNames, "None", "Female Atom",
new JSONStorableStringChooser.SetStringCallback(UpdateAtomSelection));
RegisterStringChooser(femaleAtomChooser);
CreatePopup(femaleAtomChooser);
// Create attraction strength slider (controls thigh movement intensity)
attractionStrength = new JSONStorableFloat("Attraction Strength", 0.5f, 0.1f, 1.0f);
RegisterFloat(attractionStrength);
CreateSlider(attractionStrength);
// Create rotation speed slider (controls hip rotation speed)
rotationSpeed = new JSONStorableFloat("Rotation Speed", 2.0f, 0.1f, 10f);
RegisterFloat(rotationSpeed);
CreateSlider(rotationSpeed);
// Create pelvis offset slider (controls pelvis position offset from hip)
pelvisOffset = new JSONStorableFloat("Pelvis Offset", 0.1f, 0.05f, 0.2f);
RegisterFloat(pelvisOffset);
CreateSlider(pelvisOffset);
// Initialize controller ID storables
penisBaseID = new JSONStorableString("Penis Base ID", "penisBaseControl");
RegisterString(penisBaseID);
penisMidID = new JSONStorableString("Penis Mid ID", "penisMidControl");
RegisterString(penisMidID);
penisTipID = new JSONStorableString("Penis Tip ID", "penisTipControl");
RegisterString(penisTipID);
hipID = new JSONStorableString("Hip ID", "hipControl");
RegisterString(hipID);
pelvisID = new JSONStorableString("Pelvis ID", "pelvisControl");
RegisterString(pelvisID);
leftThighID = new JSONStorableString("Left Thigh ID", "lThighControl");
RegisterString(leftThighID);
rightThighID = new JSONStorableString("Right Thigh ID", "rThighControl");
RegisterString(rightThighID);
// Create button to enable all female control points
JSONStorableAction enableAllPoints = new JSONStorableAction("Enable All Points", EnableAllPoints);
RegisterAction(enableAllPoints);
var enableButton = CreateButton("Enable All Points");
enableButton.button.onClick.AddListener(() => { EnableAllPoints(); });
SuperController.LogMessage($"{PluginName}: CreateUI - Enable All Points button registered");
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: CreateUI error: {e}");
}
}
// Updates atom selection when male or female atom is chosen in UI
private void UpdateAtomSelection(string val)
{
try
{
SuperController.LogMessage($"{PluginName}: UpdateAtomSelection called with value: {val}");
// Set male and female atoms based on chooser values
maleAtom = maleAtomChooser.val != "None" && availableAtoms.ContainsKey(maleAtomChooser.val) ? availableAtoms[maleAtomChooser.val] : null;
femaleAtom = femaleAtomChooser.val != "None" && availableAtoms.ContainsKey(femaleAtomChooser.val) ? availableAtoms[femaleAtomChooser.val] : null;
// Prevent selecting the same atom for both male and female
if (maleAtom != null && femaleAtom != null && maleAtom == femaleAtom)
{
SuperController.LogError($"{PluginName}: Cannot select the same atom for both male and female!");
statusText.val = "Error: Cannot select the same atom for both male and female!";
femaleAtom = null;
femaleAtomChooser.val = "None";
}
// Reset initialization to trigger controller setup
isInitialized = false;
initializationDelay = 1f;
SuperController.LogMessage($"{PluginName}: Atom selection updated: Male = {maleAtom?.uid ?? "None"}, Female = {femaleAtom?.uid ?? "None"}");
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: UpdateAtomSelection error: {e}");
}
}
// Finds all Person atoms in the scene
private void FindAllPersonAtoms()
{
try
{
SuperController.LogMessage($"{PluginName}: Finding all Person atoms");
availableAtoms.Clear();
foreach (Atom atom in SuperController.singleton.GetAtoms())
{
if (atom.type == "Person")
{
availableAtoms[atom.uid] = atom;
SuperController.LogMessage($"{PluginName}: Found Person atom: {atom.uid}");
}
}
if (availableAtoms.Count == 0)
{
SuperController.LogError($"{PluginName}: No Person atoms found in scene!");
statusText.val = "Error: Person atoms not found!";
}
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: FindAllPersonAtoms error: {e}");
}
}
// Initializes controllers for male and female atoms
private void InitializeControllers()
{
try
{
SuperController.LogMessage($"{PluginName}: Initializing controllers");
controllers.Clear();
hasValidTargets = false;
List<string> missingControllers = new List<string>();
// Load male controllers without changing their states
if (maleAtom != null)
{
foreach (string controlId in maleControllerIds)
{
FreeControllerV3 controller = maleAtom.GetStorableByID(controlId) as FreeControllerV3;
if (controller != null)
{
controllers[controlId] = controller;
// Log current states without modifying them
SuperController.LogMessage($"{PluginName}: Found {controlId} for {maleAtom.uid}, Current PositionState: {controller.currentPositionState}, Current RotationState: {controller.currentRotationState}, no state changes applied");
}
else
{
SuperController.LogMessage($"{PluginName}: Control {controlId} not found for {maleAtom.uid}");
missingControllers.Add(controlId);
}
}
}
// Load and set states for female controllers
if (femaleAtom != null)
{
foreach (string controlId in femaleControllerIds)
{
FreeControllerV3 controller = femaleAtom.GetStorableByID(controlId) as FreeControllerV3;
if (controller != null)
{
controllers[controlId] = controller;
if (controlId.ToLower().Contains("hip"))
{
controller.currentPositionState = FreeControllerV3.PositionState.On;
controller.currentRotationState = FreeControllerV3.RotationState.On;
SuperController.LogMessage($"{PluginName}: Initialized {controlId} for {femaleAtom.uid}, Position: On, Rotation: On");
}
else if (controlId.ToLower().Contains("pelvis"))
{
controller.currentPositionState = FreeControllerV3.PositionState.ParentLink;
controller.currentRotationState = FreeControllerV3.RotationState.ParentLink;
SuperController.LogMessage($"{PluginName}: Initialized {controlId} for {femaleAtom.uid}, Position: ParentLink, Rotation: ParentLink");
}
else
{
controller.currentPositionState = FreeControllerV3.PositionState.Comply;
controller.currentRotationState = FreeControllerV3.RotationState.Comply;
SuperController.LogMessage($"{PluginName}: Initialized {controlId} for {femaleAtom.uid}, Position: Comply, Rotation: Comply");
}
}
else
{
SuperController.LogMessage($"{PluginName}: Control {controlId} not found for {femaleAtom.uid}");
missingControllers.Add(controlId);
}
}
}
// Check if all required controllers are present
hasValidTargets = controllers.ContainsKey(penisBaseID.val) &&
controllers.ContainsKey(penisTipID.val) &&
controllers.ContainsKey(hipID.val) &&
controllers.ContainsKey(leftThighID.val) &&
controllers.ContainsKey(rightThighID.val);
if (hasValidTargets)
{
SuperController.LogMessage($"{PluginName}: InitializeControllers succeeded");
if (missingControllers.Count > 0)
{
statusText.val = $"Warning: Controllers not found: {string.Join(", ", missingControllers.ToArray())}";
}
else
{
statusText.val = "All female points successfully initialized";
}
// Initialize thigh positions and rotations
if (controllers.ContainsKey(leftThighID.val) && controllers[leftThighID.val].control != null)
{
lastLeftThighPos = controllers[leftThighID.val].control.position;
lastLeftThighRot = controllers[leftThighID.val].control.rotation;
}
if (controllers.ContainsKey(rightThighID.val) && controllers[rightThighID.val].control != null)
{
lastRightThighPos = controllers[rightThighID.val].control.position;
lastRightThighRot = controllers[rightThighID.val].control.rotation;
}
UpdatePelvisAlignment(); // Align pelvis initially
}
else
{
statusText.val = $"Error: Required controllers not found: {string.Join(", ", missingControllers.ToArray())}";
SuperController.LogError($"{PluginName}: InitializeControllers failed - minimum required controllers not found");
}
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: InitializeControllers error: {e}");
statusText.val = "Error initializing controllers";
}
}
// Main update loop, runs every frame
private void Update()
{
try
{
updateLogTimer -= Time.deltaTime;
// Initialize controllers after delay, only if atoms are selected
if (!isInitialized && initializationDelay > 0f)
{
initializationDelay -= Time.deltaTime;
if (initializationDelay <= 0f && maleAtom != null && femaleAtom != null)
{
InitializeControllers();
isInitialized = true;
SuperController.LogMessage($"{PluginName}: Delayed initialization completed");
}
return;
}
// Skip update if alignment is disabled or targets are invalid
if (!enableAlignment.val || !hasValidTargets)
{
if (updateLogTimer <= 0f)
{
SuperController.LogMessage($"{PluginName}: Update skipped - alignment disabled or invalid targets");
updateLogTimer = 2f;
}
return;
}
// Perform alignment calculations
CalculateVaginaPoints();
AdaptiveHipsAlignment();
UpdatePelvisAlignment();
UpdateThighMovement();
UpdateStatusText();
// Reset log timer
if (updateLogTimer <= 0f)
{
updateLogTimer = 2f;
}
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: Update error: {e}");
}
}
// Calculates the position and direction of the vagina based on hip controller
private void CalculateVaginaPoints()
{
if (controllers.ContainsKey(hipID.val))
{
FreeControllerV3 hip = controllers[hipID.val];
vaginaPosition = hip.control.position;
vaginaDirection = hip.control.forward;
}
}
// Aligns female hips to penis direction
private void AdaptiveHipsAlignment()
{
try
{
FreeControllerV3 penisBaseControl = controllers[penisBaseID.val];
FreeControllerV3 penisMidControl = controllers.ContainsKey(penisMidID.val) ? controllers[penisMidID.val] : null;
FreeControllerV3 penisTipControl = controllers[penisTipID.val];
FreeControllerV3 femaleHipControl = controllers[hipID.val];
if (penisBaseControl == null || penisTipControl == null || femaleHipControl == null) return;
if (penisBaseControl.control == null || penisTipControl.control == null || femaleHipControl.control == null) return;
// Calculate penis direction from base to tip
Vector3 penisDirection = (penisBaseControl.control.position - penisTipControl.control.position).normalized;
if (float.IsNaN(penisDirection.x) || float.IsNaN(penisDirection.y) || float.IsNaN(penisDirection.z))
{
SuperController.LogMessage($"{PluginName}: Invalid penis direction, skipping alignment");
return;
}
// Adjust direction using mid controller if available and aligned
if (penisMidControl != null && penisMidControl.control != null)
{
Vector3 tipToMid = (penisMidControl.control.position - penisTipControl.control.position).normalized;
Vector3 midToBase = (penisBaseControl.control.position - penisMidControl.control.position).normalized;
float alignmentAngle = Vector3.Angle(tipToMid, midToBase);
if (alignmentAngle > 10f)
{
SuperController.LogMessage($"{PluginName}: penisMidControl not aligned with base and tip, using tip-to-base direction");
}
else
{
penisDirection = (tipToMid + midToBase).normalized;
}
}
// Rotate hips to align with penis direction
Quaternion currentRotation = femaleHipControl.control.rotation;
Quaternion targetRotation = Quaternion.FromToRotation(currentRotation * Vector3.down, penisDirection) * currentRotation;
Quaternion newRotation = Quaternion.Slerp(currentRotation, targetRotation, Time.deltaTime * rotationSpeed.val);
femaleHipControl.control.rotation = newRotation;
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: AdaptiveHipsAlignment error: {e}");
}
}
// Aligns pelvis position and rotation to hip controller
private void UpdatePelvisAlignment()
{
try
{
if (!controllers.ContainsKey(hipID.val) || !controllers.ContainsKey(pelvisID.val)) return;
FreeControllerV3 hipControl = controllers[hipID.val];
FreeControllerV3 pelvisControl = controllers[pelvisID.val];
if (hipControl == null || pelvisControl == null || hipControl.control == null || pelvisControl.control == null) return;
// Position pelvis with offset from hip
Vector3 offset = hipControl.control.rotation * Vector3.up * pelvisOffset.val;
pelvisControl.control.position = hipControl.control.position + offset;
pelvisControl.control.rotation = hipControl.control.rotation;
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: UpdatePelvisAlignment error: {e}");
}
}
// Moves thighs towards penis tip with smooth interpolation
private void UpdateThighMovement()
{
try
{
// Check if all required controllers are present
if (!controllers.ContainsKey(leftThighID.val) || !controllers.ContainsKey(rightThighID.val) ||
!controllers.ContainsKey(penisTipID.val) || !controllers.ContainsKey(hipID.val))
{
return;
}
FreeControllerV3 leftThigh = controllers[leftThighID.val];
FreeControllerV3 rightThigh = controllers[rightThighID.val];
FreeControllerV3 penisTip = controllers[penisTipID.val];
FreeControllerV3 hip = controllers[hipID.val];
if (leftThigh.control == null || rightThigh.control == null || penisTip.control == null || hip.control == null)
{
return;
}
// Calculate dynamic max distance and correction speed based on attraction strength
float effectiveMaxDistance = Mathf.Lerp(0.2f, 1.0f, attractionStrength.val);
float effectiveThighCorrectionSpeed = Mathf.Lerp(1.0f, 10.0f, attractionStrength.val);
// Skip if penis is too far from vagina
float distanceToVagina = Vector3.Distance(penisTip.control.position, vaginaPosition);
if (distanceToVagina > effectiveMaxDistance)
{
return;
}
// Calculate directions to penis tip
Vector3 toPenisLeft = (penisTip.control.position - lastLeftThighPos).normalized;
Vector3 toPenisRight = (penisTip.control.position - lastRightThighPos).normalized;
if (float.IsNaN(toPenisLeft.x) || float.IsNaN(toPenisRight.x))
{
return;
}
// Calculate target positions for thighs
float thighDistance = 0.3f;
Vector3 targetLeftPos = penisTip.control.position + toPenisLeft * thighDistance;
Vector3 targetRightPos = penisTip.control.position + toPenisRight * thighDistance;
// Calculate position deltas to prevent jittering
float leftPosDelta = Vector3.Distance(leftThigh.control.position, targetLeftPos);
float rightPosDelta = Vector3.Distance(rightThigh.control.position, targetRightPos);
float smoothFactor = Time.deltaTime * effectiveThighCorrectionSpeed * 0.3f;
// Move left thigh if delta is significant
if (leftPosDelta > MinPositionDelta)
{
leftThigh.control.position = Vector3.Lerp(
leftThigh.control.position,
targetLeftPos,
smoothFactor
);
lastLeftThighPos = leftThigh.control.position;
}
// Move right thigh if delta is significant
if (rightPosDelta > MinPositionDelta)
{
rightThigh.control.position = Vector3.Lerp(
rightThigh.control.position,
targetRightPos,
smoothFactor
);
lastRightThighPos = rightThigh.control.position;
}
// Align thigh rotations with hip
Quaternion targetLeftRot = Quaternion.LookRotation(hip.control.forward, hip.control.up);
Quaternion targetRightRot = Quaternion.LookRotation(hip.control.forward, hip.control.up);
float leftRotDelta = Quaternion.Angle(leftThigh.control.rotation, targetLeftRot);
float rightRotDelta = Quaternion.Angle(rightThigh.control.rotation, targetRightRot);
// Rotate left thigh if delta is significant
if (leftRotDelta > MinRotationDelta)
{
leftThigh.control.rotation = Quaternion.Slerp(
leftThigh.control.rotation,
targetLeftRot,
smoothFactor
);
lastLeftThighRot = leftThigh.control.rotation;
}
// Rotate right thigh if delta is significant
if (rightRotDelta > MinRotationDelta)
{
rightThigh.control.rotation = Quaternion.Slerp(
rightThigh.control.rotation,
targetRightRot,
smoothFactor
);
lastRightThighRot = rightThigh.control.rotation;
}
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: UpdateThighMovement error: {e}");
}
}
// Updates the status text with alignment information
private void UpdateStatusText()
{
try
{
if (!hasValidTargets)
{
statusText.val = "Error: Required controllers not found";
return;
}
FreeControllerV3 penisTip = controllers[penisTipID.val];
FreeControllerV3 penisBase = controllers[penisBaseID.val];
FreeControllerV3 hip = controllers[hipID.val];
if (penisTip == null || penisBase == null || hip == null || penisTip.control == null || penisBase.control == null || hip.control == null)
{
statusText.val = "Error: Controllers not initialized";
return;
}
// Calculate and display alignment metrics
float distance = Vector3.Distance(penisTip.control.position, hip.control.position);
Vector3 angles = hip.control.rotation.eulerAngles;
Vector3 downDirection = hip.control.rotation * Vector3.down;
Vector3 penisDirection = (penisBase.control.position - penisTip.control.position).normalized;
float alignmentAngle = Vector3.Angle(downDirection, penisDirection);
statusText.val = $"Distance: {distance:F2} m\n" +
$"Hip Angles: X={angles.x:F1}° Y={angles.y:F1}° Z={angles.z:F1}°\n" +
$"Alignment: {alignmentAngle:F1}°\n" +
$"Attraction Strength: {attractionStrength.val:F2}\n" +
$"Male: {maleAtom?.uid ?? "None"}\n" +
$"Female: {femaleAtom?.uid ?? "None"}";
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: UpdateStatusText error: {e}");
}
}
// Enables all female control points via button click
private void EnableAllPoints()
{
try
{
SuperController.LogMessage($"{PluginName}: Enabling all female control points");
controllers.Clear();
hasValidTargets = false;
enableAlignment.val = true; // Enable alignment toggle
InitializeControllers(); // Reinitialize controllers
if (hasValidTargets)
{
SuperController.LogMessage($"{PluginName}: EnableAllPoints succeeded");
}
else
{
SuperController.LogError($"{PluginName}: EnableAllPoints failed");
}
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: EnableAllPoints error: {e}");
statusText.val = "Error enabling points";
}
}
// Called when the plugin is unloaded or reloaded, clears controllers without modifying states
public void OnDestroy()
{
try
{
SuperController.LogMessage($"{PluginName}: OnDestroy called, clearing controllers without changing states");
controllers.Clear(); // Clear controller dictionary without touching controller states
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: OnDestroy error: {e}");
}
}
}
Attachments
Last edited: