OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / Shared / ParamPath.cs

111 lines · csharp ref:
using System.Reflection;

namespace IFSRenderer.Shared;

/// <summary>
/// Reads a value out of an <see cref="IFSEngine.Model.IFS"/> by animation path.
/// </summary>
/// <remarks>
/// The engine ships the write half (<c>NestedReflectionHelper.SetMemberValueByPath</c>, used by
/// <c>IFS.EvaluateAt</c>) but it is <c>internal</c> and there is no read half - and keying a
/// parameter needs to capture its <em>current</em> value. This mirrors the engine's segment
/// grammar exactly:
/// <list type="bullet">
/// <item><c>Brightness</c> - a property</item>
/// <item><c>Camera.Position.X</c> - nested property, then a struct field</item>
/// <item><c>Node[123]</c> - a method taking an index (<c>IFS.Node(int)</c>)</item>
/// <item><c>RealParams.[weight]</c> - an empty member name means the default indexer,
/// which is how the app addresses dictionary entries</item>
/// </list>
/// </remarks>
public static class ParamPath
{
    public static bool TryGet(object root, string path, out double value)
    {
        value = 0;
        if (root is null || string.IsNullOrWhiteSpace(path))
            return false;

        object current = root;
        foreach (string segment in path.Split('.'))
        {
            if (current is null)
                return false;
            if (!TryStep(current, segment, out current))
                return false;
        }

        try
        {
            value = Convert.ToDouble(current);
            return true;
        }
        catch
        {
            return false;
        }
    }

    private static bool TryStep(object obj, string segment, out object result)
    {
        result = null;
        ParseSegment(segment, out string memberName, out object[] index);
        Type type = obj.GetType();

        PropertyInfo property = type.GetProperty(memberName);
        if (property is not null)
        {
            try
            {
                result = property.GetValue(obj, index);
                return result is not null;
            }
            catch
            {
                return false;
            }
        }

        MethodInfo method = type.GetMethod(memberName,
            index is not null ? [index[0].GetType()] : Type.EmptyTypes);
        if (method is not null)
        {
            try
            {
                result = method.Invoke(obj, index);
                return result is not null;
            }
            catch
            {
                return false;
            }
        }

        FieldInfo field = type.GetField(memberName);
        if (field is not null)
        {
            result = field.GetValue(obj);
            return result is not null;
        }

        return false;
    }

    private static void ParseSegment(string segment, out string memberName, out object[] index)
    {
        if (segment.Contains('['))
        {
            memberName = segment.Split('[')[0];
            if (string.IsNullOrEmpty(memberName))
                memberName = "Item"; // default indexer, e.g. RealParams.[weight]
            string indexString = segment[(segment.IndexOf('[') + 1)..^1];
            index = [int.TryParse(indexString, out int numeric) ? numeric : indexString];
        }
        else
        {
            memberName = segment;
            index = null;
        }
    }
}