VaM 1.x I'm trying to write a simple script for looks. How can I verify that a directory exists in the VAM space?

Threads regarding the original VaM 1.x
Joined
Jan 8, 2024
Messages
20
Reactions
3
I'm trying to write a simple script for looks. How can I verify that a directory exists in the VAM space?


C#:
var looks1 = new List<string>();
var path1 = SuperController.singleton.NormalizePath(path);
if (!SuperController.singleton.DirectoryExists(path1))
    return looks1;
var files1 = SuperController.singleton.GetFilesAtPath(path1, "*.json");
 
Yes, DirectoryExists.
You can't manipulate directories outside of "Saved" and "Custom" due to security measures.
 
Upvote 0
Yes, DirectoryExists.
You can't manipulate directories outside of "Saved" and "Custom" due to security measures.
That's understandable... I have now decided through try...catch. As I understand it, there is no native verification method((
 
Upvote 0
I don't remember SuperController containing DirectoryExists.

Your code might not compile in VAM.

For file system use FileManagerSecure.

This code should help you get started:

C#:
using System.Collections.Generic;
using MVR.FileManagementSecure; // FileManagerSecure (VaM's sandboxed file API)

public class LooksLister : MVRScript
{
    // Returns the .vap (appearance preset) files in a folder, or an empty list.
    private List<string> GetLooks(string path)
    {
        var looks = new List<string>();

        // Tidy up slashes etc. so the path is in VaM's expected format.
        string normalized = SuperController.singleton.NormalizePath(path);

        // Don't use System.IO (Directory.Exists): VaM blocks it for plugins.
        // FileManagerSecure is the allowed equivalent. It returns:
        //   true  -> folder exists and is inside VaM's allowed area
        //   false -> folder doesn't exist
        // It throws only if the folder exists but is OUTSIDE the allowed area, hence the try/catch below.
        try
        {
            if (!FileManagerSecure.DirectoryExists(normalized))
                return looks; // nothing there, give back the empty list

            // Returns full paths, or null if the path isn't allowed.
            string[] files = SuperController.singleton.GetFilesAtPath(normalized, "*.vap");
            if (files != null)
                looks.AddRange(files);
        }
        catch (System.Exception e) // path outside Saved/Custom
        {
            SuperController.LogError("Can't read '" + path + "': " + e.Message);
        }
        return looks;
    }

    // Typical call. Looks live under Custom/Atom/Person/Appearance.
    // Use "Saved/..." or "Custom/..." paths; anything else is blocked.
    private void Start()
    {
        List<string> looks = GetLooks("Custom/Atom/Person/Appearance");
        SuperController.LogMessage("Found " + looks.Count + " looks");
    }
}

For gender checking you can start with this:

C#:
// Returns true if the Person atom is a female character.
// 'atom' is the atom you're asking about; inside an MVRScript you can use
// the built-in 'containingAtom' for the atom the plugin is on.
private bool IsFemale(Atom atom)
{
    // Only Person atoms have a character. Other atom types (lights, cubes...) don't.
    if (atom == null || atom.type != "Person")
        return false;

    // "geometry" is the storable that owns the character's body/skin/morphs.
    // 'as' does a safe cast: gives null instead of throwing if it's the wrong type.
    var selector = atom.GetStorableByID("geometry") as DAZCharacterSelector;
    if (selector == null)
        return false; // character not ready yet

    return selector.gender == DAZCharacterSelector.Gender.Female;
}

// Every Person on stage, with their gender:
private void ListPeople()
{
    // GetAtoms() gives all atoms in the scene; we filter to People ourselves.
    foreach (Atom atom in SuperController.singleton.GetAtoms())
    {
        if (atom.type != "Person") continue;
        var selector = atom.GetStorableByID("geometry") as DAZCharacterSelector;
        if (selector == null) continue;

        // Enums print as their name, so this logs "Female" or "Male".
        SuperController.LogMessage(atom.uid + ": " + selector.gender);
    }
}
 
Upvote 0
And now the questions for the real backfill))

1. Is it possible to create a popup notification for the user directly in his view from a c# script? It is necessary to notify about plugin events.

2. Is there a way for a script to highlight a character's atom for a while to indicate that it is active at the moment?
 
Upvote 0
1. Is it possible to create a popup notification for the user directly in his view from a c# script? It is necessary to notify about plugin events.
If it's just to notify you can just use SuperController.LogMessage or SuperController.LogError.

VaM Exposes a dialog, so you can use:
C#:
using UnityEngine.Events;

// OK-only message
SuperController.AlertUser("Hello from my plugin!", () => SuperController.LogMessage("User clicked OK"));

// OK / Cancel confirmation
SuperController.AlertUser(
    "Reset all settings?",
    () => SuperController.LogMessage("Confirmed"),   // OK
    () => SuperController.LogMessage("Cancelled"));  // Cancel

2. Is there a way for a script to highlight a character's atom for a while to indicate that it is active at the moment?
This becomes very complex in my opinion. There is no simple way that is exposed by VaM.
I guess a simple way would be to draw primitive shapes and add them to the scene, it's flexible.

You could hack it by selecting the controller:

C#:
var atom = SuperController.singleton.GetAtomByUid("Person");
var fc = atom.GetStorableByID("control") as FreeControllerV3; // or atom.freeControllers[0]
SuperController.singleton.SelectController(fc);
 
Upvote 0
For 1 (and previous questions)
  • For files It's FileManagerSecure, always use that for file manipulation. Use VAMStory as a reference I got a bunch of use of this in there. vams_minisaves.cs in the package is a good (simple) start.
  • For basic "alerts" SuperController.singleton.Alert
  • For basic ""confirms" SuperController.singleton.Alert with a delegate on the second parameter
For 2: no, this would require specific shaders to do that. But you could draw a debug box / specific box with a transparent shader, to do a similar selection effect. Doing custom shaders on VAM characters in VAM 1 is a nightmare, so I would consider an "alternate" approach instead.
 
Upvote 0
Anyway, I wrote a little perversion, it solves my problem with notifications. If anyone is interested:

C#:
/// <summary>
/// Helper class for simplified creation, configuration, and management of Virt-A-Mate UI elements and screen message overlays.
/// </summary>
public class UIHelper
    : IDisposable
{

    private GameObject _msgOverlayGO = null;
    private UnityEngine.UI.Text _msgOverlayText = null;
    private float _msgOverlayTimer = 0f;


    /* consts */


    /// <summary>
    /// Default text color for on-screen overlay messages.
    /// </summary>
    public readonly Color DEFAULT_COLOR = Color.green;

    /// <summary>
    /// Default alpha transparency for on-screen overlay messages.
    /// </summary>
    public const float DEFAULT_ALPHA = 0.5f;

    /// <summary>
    /// Default display duration in seconds for on-screen overlay messages.
    /// </summary>
    public const float DEFAULT_DELAY = 2f;


    /* ctor/dtor */


    /// <summary>
    /// Initializes a new instance of the <see cref="UIHelper"/> class and constructs the 3D message overlay canvas.
    /// </summary>
    /// <param name="pluginName">The name of the parent plugin used as a prefix in log messages.</param>
    public UIHelper(
        string pluginName)
    {
        PluginName = pluginName;
        _makeMessageOverlay();
    }


    /// <summary>
    /// Releases all Unity UI GameObjects and resources used by the <see cref="UIHelper"/> instance.
    /// </summary>
    public void Dispose()
    {
        _dispose(true);
        GC.SuppressFinalize(this);
    }
    private bool _disposed = false;


    /// <summary>
    /// Protected implementation of the Dispose pattern to safely destroy managed Unity objects.
    /// </summary>
    /// <param name="disposing">True if called explicitly from <see cref="Dispose"/>; false if called from a finalizer.</param>
    protected virtual void _dispose(
        bool disposing)
    {
        if (_disposed)
            return;
        if (disposing)
            _destroyMessageOverlay();
        _disposed = true;
    }


    /* readonly properties */


    /// <summary>
    /// Gets the name of the associated plugin.
    /// </summary>
    public string PluginName { get; }


    /* methods */


    /// <summary>
    /// Centralized service for outputting informational plugin messages to the system log and optionally to the 3D screen overlay.
    /// </summary>
    /// <param name="text">The message text to display.</param>
    /// <param name="showInUI">Flag indicating whether to force display the message on the user's 3D-View overlay.</param>
    /// <param name="textColor">Optional custom text color for the on-screen overlay.</param>
    /// <param name="delay">Optional custom display duration in seconds before fading out.</param>
    public void OutMessage(
        string text,
        bool showInUI = false,
        Color? textColor = null,
        float? delay = null)
    {
        try
        {
            string formattedMsg1 = $"[{PluginName}] {text}";
            SuperController.LogMessage(formattedMsg1);
            if (showInUI)
                ShowMessage(text, textColor, delay);
        }
        catch (Exception e)
        {
            SuperController.LogError($"[{PluginName}] UIHelper.OutMessage() Critical Error: {e.Message}");
        }
    }


    /// <summary>
    /// Centralized service for outputting plugin error messages to the system error log and optionally to the 3D screen overlay.
    /// </summary>
    /// <param name="text">The error message text to display.</param>
    /// <param name="showInUI">Flag indicating whether to force display the error on the user's 3D-View overlay.</param>
    /// <param name="textColor">Optional custom text color (defaults to red if null).</param>
    /// <param name="delay">Optional custom display duration in seconds before fading out.</param>
    public void OutError(
        string text,
        bool showInUI = false,
        Color? textColor = null,
        float? delay = null)
    {
        try
        {
            string formattedErr1 = $"[{PluginName}] {text}";
            SuperController.LogError(formattedErr1);
            if (showInUI)
                ShowMessage(text, textColor ?? Color.red, delay);
        }
        catch (Exception e)
        {
            SuperController.LogError($"[{PluginName}] UIHelper.OutError() Critical Error: {e.Message}");
        }
    }


    /// <summary>
    /// Checks for a physical keyboard key press based on the selected value in a <see cref="JSONStorableStringChooser"/>
    /// and safely invokes the associated action delegate.
    /// </summary>
    /// <param name="chooser">The dropdown chooser instance containing the configured hotkey name.</param>
    /// <param name="action">The action delegate to execute when the hotkey is pressed.</param>
    public void TestHotkey(
        JSONStorableStringChooser chooser,
        Action action)
    {
        if (chooser == null || action == null)
            return;
        try
        {
            if (chooser.val != "None"
                && Input.GetKeyDown((KeyCode)Enum.Parse(typeof(KeyCode), chooser.val)))
                action();
        }
        catch (Exception e)
        {
            OutError($"UIHelper.TestHotkey() [Param: {chooser.name}] Error: {e.Message}");
        }
    }


    /// <summary>
    /// Activates the 3D overlay canvas, resets text opacity, and displays the specified message string.
    /// </summary>
    /// <param name="message">The text string to render on the overlay.</param>
    /// <param name="textColor">Optional text color (uses <see cref="DEFAULT_COLOR"/> if null).</param>
    /// <param name="delay">Optional lifetime in seconds (uses <see cref="DEFAULT_DELAY"/> if null).</param>
    public void ShowMessage(
        string message,
        Color? textColor = null,
        float? delay = null)
    {
        if (_msgOverlayText != null && _msgOverlayGO != null)
        {
            _msgOverlayText.text = message;
            var color1 = textColor ?? DEFAULT_COLOR;
            color1.a = DEFAULT_ALPHA;
            _msgOverlayText.color = color1;
            _msgOverlayGO.SetActive(true);
            _msgOverlayTimer = delay ?? DEFAULT_DELAY;
        }
    }


    /// <summary>
    /// Ticks every frame in the plugin's Update loop to decrement the overlay timer and apply a smooth alpha fade-out effect.
    /// </summary>
    public void MuteMessage()
    {
        if (_msgOverlayGO != null && _msgOverlayTimer > 0f)
        {
            _msgOverlayTimer -= Time.unscaledDeltaTime;
            if (_msgOverlayTimer <= 0f)
                _msgOverlayGO.SetActive(false);
            else if (_msgOverlayText != null && _msgOverlayTimer < 1f)
            {
                var color1 = _msgOverlayText.color;
                color1.a = _msgOverlayTimer * DEFAULT_ALPHA;
                _msgOverlayText.color = color1;
            }
        }
    }


    /* privates */


    /// <summary>
    /// Constructs the WorldSpace UI Canvas and Text components parented to the center monitor camera.
    /// </summary>
    private void _makeMessageOverlay()
    {
        _msgOverlayGO = new GameObject("UIHelper_MessageOverlay");
        var targetCam1 = SuperController.singleton.lookCamera ?? SuperController.singleton.MonitorCenterCamera;
        if (targetCam1 != null)
            _msgOverlayGO.transform.SetParent(targetCam1.transform, false);
        _msgOverlayGO.transform.localPosition = new Vector3(0f, 0f, 1f);
        var canvas1 = _msgOverlayGO.AddComponent<Canvas>();
        canvas1.renderMode = RenderMode.WorldSpace;
        _msgOverlayGO.AddComponent<CanvasScaler>().dynamicPixelsPerUnit = 100;
        var canvasRect1 = _msgOverlayGO.GetComponent<RectTransform>();
        canvasRect1.sizeDelta = new Vector2(800f, 200f);
        canvasRect1.localScale = new Vector3(0.003f, 0.003f, 0.003f);
        var text1 = new GameObject("UIHelper_MessageOverlay_Text");
        text1.transform.SetParent(_msgOverlayGO.transform, false);
        _msgOverlayText = text1.AddComponent<UnityEngine.UI.Text>();
        _msgOverlayText.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
        _msgOverlayText.fontSize = 32;
        _msgOverlayText.alignment = TextAnchor.MiddleCenter;
        var color1 = DEFAULT_COLOR;
        color1.a = DEFAULT_ALPHA;
        _msgOverlayText.color = color1;
        var textRect1 = text1.GetComponent<RectTransform>();
        textRect1.anchoredPosition = Vector2.zero;
        textRect1.sizeDelta = new Vector2(800f, 200f);
        _msgOverlayGO.SetActive(false);
    }


    /// <summary>
    /// Destroys the overlay GameObject if it exists.
    /// </summary>
    private void _destroyMessageOverlay()
    {
        if (_msgOverlayGO != null)
            UnityEngine.Object.Destroy(_msgOverlayGO);
    }


    // SuperController.AlertUser("Hello from my plugin!", () => SuperController.LogMessage("User clicked OK"));
    // SuperController.AlertUser("Reset all settings?",
    //      () => SuperController.LogMessage("Confirmed"),   // OK
    //      () => SuperController.LogMessage("Cancelled"));  // Cancel

}
 
Last edited:
Upvote 0
Back
Top Bottom