mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-06 17:58:05 +02:00
ViewVariables Exorcism Part 1: Of paths and commands (#3214)
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Reflection;
|
||||
|
||||
namespace Robust.Shared.ViewVariables;
|
||||
|
||||
internal abstract partial class ViewVariablesManager
|
||||
{
|
||||
public IEnumerable<string> ListPath(string path, VVListPathOptions options)
|
||||
{
|
||||
// Helper to return all domains' full paths.
|
||||
IEnumerable<string> Domains()
|
||||
=> _registeredDomains.Keys.Select(d => $"/{d}");
|
||||
|
||||
// Helper to return full paths when given a full path and a number of paths relative to it.
|
||||
IEnumerable<string> Full(string fullPath, IEnumerable<string> relativePaths)
|
||||
{
|
||||
if (!fullPath.StartsWith('/'))
|
||||
fullPath = $"/{fullPath}";
|
||||
if (fullPath.EndsWith('/'))
|
||||
fullPath = fullPath[..^1];
|
||||
|
||||
return relativePaths
|
||||
.Select(p
|
||||
=> p.StartsWith("[")
|
||||
? $"{fullPath}{p}" // Indexers for the path
|
||||
: string.Join('/', fullPath, p)); // Actual relative paths
|
||||
}
|
||||
|
||||
if (path.StartsWith('/'))
|
||||
path = path[1..];
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return Domains();
|
||||
|
||||
var segments = path.Split('/');
|
||||
|
||||
if (segments.Length == 0)
|
||||
return Domains();
|
||||
|
||||
var domain = segments[0];
|
||||
|
||||
// If the specified domain of the path does not exist, return a list of paths instead.
|
||||
if (!_registeredDomains.TryGetValue(domain, out var data))
|
||||
return Domains();
|
||||
|
||||
// Let the domain handle listing its paths...
|
||||
var domainList = data.List(segments[1..]);
|
||||
|
||||
// If the domain returned null, that means we're dealing with a path we need to resolve.
|
||||
if (domainList != null)
|
||||
return Full($"/{domain}", domainList);
|
||||
|
||||
// Expensive :'(
|
||||
var resolved = ResolvePath(path);
|
||||
|
||||
// Attempt to get an object from the path...
|
||||
if (resolved?.Get() is not {} obj)
|
||||
{
|
||||
// Okay maybe the last segment of the path is not full? Attempt to resolve the prior path
|
||||
segments = segments[..^1];
|
||||
path = string.Join('/', segments);
|
||||
resolved = ResolvePath(path);
|
||||
|
||||
// If not even that worked, we probably just have an invalid path here... Return nothing.
|
||||
if(resolved?.Get() is not {} priorObj)
|
||||
return Enumerable.Empty<string>();
|
||||
|
||||
obj = priorObj;
|
||||
}
|
||||
|
||||
// We need a place to store all the relative paths we find! TODO: Perhaps just yield instead?
|
||||
var paths = new List<string>();
|
||||
|
||||
var type = obj.GetType();
|
||||
|
||||
if (_typeHandlers.TryGetValue(type, out var typeData))
|
||||
{
|
||||
paths.AddRange(typeData.ListPath(resolved));
|
||||
}
|
||||
|
||||
// We also use a set here for unique names as we need to handle member hiding properly...
|
||||
// Starts with all custom paths, as those can hide the "native" members of the object.
|
||||
var uniqueMemberNames = new HashSet<string>(paths);
|
||||
|
||||
// For member hiding handling purposes, we handle the members declared by the object's type itself first.
|
||||
foreach (var memberInfo in type.GetMembers(MembersBindings).OrderBy(m => m.DeclaringType == type))
|
||||
{
|
||||
// Ignore the member if it's not a VV member.
|
||||
if (!ViewVariablesUtility.TryGetViewVariablesAccess(memberInfo, out var memberAccess))
|
||||
continue;
|
||||
|
||||
// Also take access level into account.
|
||||
if (memberAccess < options.MinimumAccess)
|
||||
continue;
|
||||
|
||||
var name = memberInfo.Name;
|
||||
|
||||
// If the member name is not unique, we adds the type specifier to it.
|
||||
if (!uniqueMemberNames.Add(name))
|
||||
name = @$"{name}{{{memberInfo.DeclaringType?.FullName ?? typeof(void).FullName}}}";
|
||||
|
||||
paths.Add(name);
|
||||
|
||||
var memberObj = memberInfo.GetValue(obj);
|
||||
|
||||
if(options.ListIndexers)
|
||||
ListIndexers(memberObj, name, paths);
|
||||
}
|
||||
|
||||
if(options.ListIndexers)
|
||||
ListIndexers(obj, string.Empty, paths);
|
||||
|
||||
return Full(path, paths);
|
||||
}
|
||||
|
||||
private void ListIndexers(object? obj, string name, List<string> paths)
|
||||
{
|
||||
switch (obj)
|
||||
{
|
||||
// Handle dictionaries and lists specially, for indexing purposes...
|
||||
case IDictionary dict:
|
||||
{
|
||||
var keyType = typeof(void);
|
||||
|
||||
if (dict.GetType().GenericTypeArguments is {Length: 2} generics)
|
||||
{
|
||||
// Assume the key type is the first entry...
|
||||
keyType = generics[0];
|
||||
}
|
||||
|
||||
foreach (var key in dict.Keys)
|
||||
{
|
||||
try
|
||||
{
|
||||
var type = key.GetType();
|
||||
string? tag = null;
|
||||
|
||||
// Handle cases such as "Dictionary<object, whatever>"
|
||||
if (type != keyType)
|
||||
tag = $"!type:{type.Name}";
|
||||
|
||||
// Forgive me, Paul... We use serv3 to serialize the value into its "text value".
|
||||
if (SerializeValue(type, key, tag) is not {} value)
|
||||
continue;
|
||||
|
||||
// Enclose in parentheses, in case there's a space in the value.
|
||||
if (value.Contains(' '))
|
||||
value = $"({value})";
|
||||
|
||||
paths.Add($"{name}[{value}]");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case Array array:
|
||||
{
|
||||
var lowerBounds = Enumerable.Range(0, array.Rank)
|
||||
.Select(i => array.GetLowerBound(i))
|
||||
.ToArray();
|
||||
var upperBounds = Enumerable.Range(0, array.Rank)
|
||||
.Select(i => array.GetUpperBound(i))
|
||||
.ToArray();
|
||||
|
||||
var indices = new int[array.Rank];
|
||||
|
||||
lowerBounds.CopyTo(indices, 0);
|
||||
|
||||
while (true)
|
||||
{
|
||||
paths.Add($"{name}[{string.Join(',', indices)}]");
|
||||
|
||||
var finished = false;
|
||||
|
||||
for (var i = indices.Length - 1; i >= -1; i--)
|
||||
{
|
||||
// When at -1, this means that we've successfully iterated all dimensions of the array.
|
||||
if (i == -1)
|
||||
{
|
||||
finished = true;
|
||||
break;
|
||||
}
|
||||
|
||||
ref var index = ref indices[i];
|
||||
index += 1;
|
||||
|
||||
if (index > upperBounds[i])
|
||||
{
|
||||
// We've gone over the upper bound, reset index and increase the next dimension's index.
|
||||
index = lowerBounds[i];
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (finished)
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// We handle Array specially instead of using IList here because of multi-dimensional arrays and variable-bounds arrays.
|
||||
case IList list:
|
||||
{
|
||||
for (var i = 0; i < list.Count; i++)
|
||||
{
|
||||
paths.Add($"{name}[{i}]");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user