ViewVariables Exorcism Part 1: Of paths and commands (#3214)

This commit is contained in:
Vera Aguilera Puerto
2022-10-14 19:03:44 +02:00
committed by GitHub
parent 80e390a74b
commit d404494018
53 changed files with 2765 additions and 207 deletions

View File

@@ -31,7 +31,9 @@ Template for new versions:
### Breaking changes
*None yet*
* Removed `SI`, `SIoC`, `I`, `IoC`, `SE` and `CE` VV command prefixes.
* `SI`, `SIoC`, `I` and `IoC` are replaced by VV paths under `/ioc/` and `/c/ioc/`.
* `SE` and `CE` are replaced by VV paths under `/system/` and `/c/system`.
### New features
@@ -40,6 +42,14 @@ Template for new versions:
* `net.mtu_expand`
* `net.mtu_expand_frequency`
* `net.mtu_expand_fail_attempts`
* Added a whole load of features to ViewVariables.
* Added VV Paths, which allow you to refer to an object by a path, e.g. `/entity/1234/Transform/WorldPosition`
* Added VV Domains, which allow you to add "handlers" for the top-most VV Path segment, e.g. `/entity` is a domain and so is `/player`...
* Added VV Type Handlers, which allow you to add "custom paths" under specific types, even dynamically!
* Added VV Path networking, which allows you to read/write/invoke paths remotely, both from server to client and from client to server.
* Added `vvread`, `vvwrite` and `vvinvoke` commands, which allow you to read, write and invoke VV paths.
* Added autocompletion to all VV commands.
* Please note that the VV GUI still remains the same. It will be updated to use these new features in the future.
### Bugfixes

View File

@@ -33,6 +33,7 @@ using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Robust.Client
{
@@ -113,8 +114,9 @@ namespace Robust.Client
IoCManager.Register<IPlacementManager, PlacementManager>();
IoCManager.Register<IOverlayManager, OverlayManager>();
IoCManager.Register<IOverlayManagerInternal, OverlayManager>();
IoCManager.Register<IViewVariablesManager, ViewVariablesManager>();
IoCManager.Register<IViewVariablesManagerInternal, ViewVariablesManager>();
IoCManager.Register<IViewVariablesManager, ClientViewVariablesManager>();
IoCManager.Register<IClientViewVariablesManager, ClientViewVariablesManager>();
IoCManager.Register<IClientViewVariablesManagerInternal, ClientViewVariablesManager>();
IoCManager.Register<IClientConGroupController, ClientConGroupController>();
IoCManager.Register<IScriptClient, ScriptClient>();
}

View File

@@ -196,7 +196,7 @@ namespace Robust.Client.Console
{
private readonly ScriptConsoleClient _owner;
[field: Dependency] public override IViewVariablesManager vvm { get; } = default!;
[field: Dependency] public override IClientViewVariablesManager vvm { get; } = default!;
public ScriptGlobalsImpl(ScriptConsoleClient owner)
{
@@ -243,7 +243,7 @@ namespace Robust.Client.Console
[PublicAPI]
public abstract class ScriptGlobals : ScriptGlobalsShared
{
public abstract IViewVariablesManager vvm { get; }
public abstract IClientViewVariablesManager vvm { get; }
public abstract void vv(object a);
}

View File

@@ -58,7 +58,7 @@ namespace Robust.Client
[Dependency] private readonly IOverlayManagerInternal _overlayManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
[Dependency] private readonly IViewVariablesManagerInternal _viewVariablesManager = default!;
[Dependency] private readonly IClientViewVariablesManagerInternal _viewVariablesManager = default!;
[Dependency] private readonly IDiscordRichPresence _discord = default!;
[Dependency] private readonly IClydeInternal _clyde = default!;
[Dependency] private readonly IClydeAudioInternal _clydeAudio = default!;

View File

@@ -0,0 +1,26 @@
using System.Collections.Generic;
using Robust.Shared.ViewVariables;
namespace Robust.Client.ViewVariables;
internal sealed partial class ClientViewVariablesManager
{
private void InitializeDomains()
{
RegisterDomain("guihover", ResolveGuiHoverObject, ListGuiHoverPaths);
}
private (ViewVariablesPath? path, string[] segments) ResolveGuiHoverObject(string path)
{
var segments = path.Split('/');
return (_userInterfaceManager.CurrentlyHovered != null
? new ViewVariablesInstancePath(_userInterfaceManager.CurrentlyHovered)
: null, segments);
}
private IEnumerable<string>? ListGuiHoverPaths(string[] segments)
{
return null;
}
}

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.ViewVariables.Editors;
@@ -13,6 +14,7 @@ using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
@@ -20,8 +22,9 @@ using static Robust.Client.ViewVariables.Editors.VVPropEditorNumeric;
namespace Robust.Client.ViewVariables
{
internal sealed class ViewVariablesManager : ViewVariablesManagerShared, IViewVariablesManagerInternal
internal sealed partial class ClientViewVariablesManager : ViewVariablesManager, IClientViewVariablesManagerInternal
{
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IRobustSerializer _robustSerializer = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
@@ -41,8 +44,10 @@ namespace Robust.Client.ViewVariables
private readonly Dictionary<uint, TaskCompletionSource<ViewVariablesBlob>> _requestedData
= new();
public void Initialize()
public override void Initialize()
{
base.Initialize();
InitializeDomains();
_netManager.RegisterNetMessage<MsgViewVariablesOpenSession>(_netMessageOpenSession);
_netManager.RegisterNetMessage<MsgViewVariablesRemoteData>(_netMessageRemoteData);
_netManager.RegisterNetMessage<MsgViewVariablesCloseSession>(_netMessageCloseSession);
@@ -230,6 +235,12 @@ namespace Robust.Client.ViewVariables
window.Open();
}
public void OpenVV(string path)
{
if (ReadPath(path) is {} obj)
OpenVV(obj);
}
public async void OpenVV(ViewVariablesObjectSelector selector)
{
var window = new DefaultWindow
@@ -406,16 +417,28 @@ namespace Robust.Client.ViewVariables
_requestedSessions.Remove(message.RequestId);
tcs.SetException(new SessionDenyException(message.Reason));
}
protected override bool CheckPermissions(INetChannel channel)
{
// Acquiesce, client!! Do what the server tells you.
return true;
}
protected override bool TryGetSession(Guid guid, [NotNullWhen(true)] out ICommonSession? session)
{
session = null;
return false;
}
}
[Virtual]
public class SessionDenyException : Exception
{
public SessionDenyException(MsgViewVariablesDenySession.DenyReason reason)
public SessionDenyException(ViewVariablesResponseCode reason)
{
Reason = reason;
}
public MsgViewVariablesDenySession.DenyReason Reason { get; }
public ViewVariablesResponseCode Reason { get; }
}
}

View File

@@ -96,7 +96,7 @@ namespace Robust.Client.ViewVariables.Editors
if (_selector is not ViewVariablesSessionRelativeSelector selector
|| _localValue is not ViewVariablesBlobMembers.PrototypeReferenceToken protoToken) return;
var vvm = IoCManager.Resolve<IViewVariablesManagerInternal>();
var vvm = IoCManager.Resolve<IClientViewVariablesManagerInternal>();
if (!vvm.TryGetSession(selector.SessionId, out var session)) return;
@@ -116,7 +116,7 @@ namespace Robust.Client.ViewVariables.Editors
private void OnInspectButtonPressed(BaseButton.ButtonEventArgs obj)
{
var vvm = IoCManager.Resolve<IViewVariablesManager>();
var vvm = IoCManager.Resolve<IClientViewVariablesManager>();
if(_selector != null)
vvm.OpenVV(_selector);

View File

@@ -9,7 +9,7 @@ namespace Robust.Client.ViewVariables.Editors
{
public sealed class VVPropEditorKeyValuePair : VVPropEditor
{
[Dependency] private readonly IViewVariablesManagerInternal _viewVariables = default!;
[Dependency] private readonly IClientViewVariablesManagerInternal _viewVariables = default!;
private VVPropEditor? _propertyEditorK;
private VVPropEditor? _propertyEditorV;

View File

@@ -36,7 +36,7 @@ namespace Robust.Client.ViewVariables.Editors
private void ButtonOnOnPressed(BaseButton.ButtonEventArgs obj)
{
var vvm = IoCManager.Resolve<IViewVariablesManager>();
var vvm = IoCManager.Resolve<IClientViewVariablesManager>();
if (_selector != null)
{
vvm.OpenVV(_selector);

View File

@@ -2,7 +2,7 @@ using Robust.Shared.ViewVariables;
namespace Robust.Client.ViewVariables
{
public interface IViewVariablesManager
public interface IClientViewVariablesManager : IViewVariablesManager
{
/// <summary>
/// Open a VV window for a locally existing object.
@@ -10,6 +10,12 @@ namespace Robust.Client.ViewVariables
/// <param name="obj">The object to VV.</param>
void OpenVV(object obj);
/// <summary>
/// Open a VV window for a locally existing object.
/// </summary>
/// <param name="path">The VV path to the object to VV.</param>
void OpenVV(string path);
/// <summary>
/// Open a VV window for a remotely existing object.
/// </summary>

View File

@@ -8,7 +8,7 @@ using Robust.Shared.ViewVariables;
namespace Robust.Client.ViewVariables
{
internal interface IViewVariablesManagerInternal : IViewVariablesManager
internal interface IClientViewVariablesManagerInternal : IClientViewVariablesManager
{
void Initialize();
@@ -66,7 +66,7 @@ namespace Robust.Client.ViewVariables
/// Gets a collection of trait IDs that are agreed upon so <see cref="ViewVariablesInstanceObject"/> knows which traits to instantiate.
/// </summary>
/// <seealso cref="ViewVariablesBlobMetadata.Traits" />
/// <seealso cref="ViewVariablesManagerShared.TraitIdsFor"/>
/// <seealso cref="Shared.ViewVariables.ViewVariablesManager.TraitIdsFor"/>
ICollection<object> TraitIdsFor(Type type);
}
}

View File

@@ -62,7 +62,7 @@ namespace Robust.Client.ViewVariables.Instances
private bool _serverLoaded;
public ViewVariablesInstanceEntity(IViewVariablesManagerInternal vvm, IEntityManager entityManager, IRobustSerializer robustSerializer) : base(vvm, robustSerializer)
public ViewVariablesInstanceEntity(IClientViewVariablesManagerInternal vvm, IEntityManager entityManager, IRobustSerializer robustSerializer) : base(vvm, robustSerializer)
{
_entityManager = entityManager;
}

View File

@@ -26,7 +26,7 @@ namespace Robust.Client.ViewVariables.Instances
public ViewVariablesRemoteSession? Session { get; private set; }
public object? Object { get; private set; }
public ViewVariablesInstanceObject(IViewVariablesManagerInternal vvm, IRobustSerializer robustSerializer)
public ViewVariablesInstanceObject(IClientViewVariablesManagerInternal vvm, IRobustSerializer robustSerializer)
: base(vvm, robustSerializer) { }
public override void Initialize(DefaultWindow window, object obj)

View File

@@ -11,7 +11,7 @@ namespace Robust.Client.ViewVariables.Traits
{
internal sealed class ViewVariablesTraitMembers : ViewVariablesTrait
{
private readonly IViewVariablesManagerInternal _vvm;
private readonly IClientViewVariablesManagerInternal _vvm;
private readonly IRobustSerializer _robustSerializer;
private BoxContainer _memberList = default!;
@@ -27,7 +27,7 @@ namespace Robust.Client.ViewVariables.Traits
instance.AddTab("Members", _memberList);
}
public ViewVariablesTraitMembers(IViewVariablesManagerInternal vvm, IRobustSerializer robustSerializer)
public ViewVariablesTraitMembers(IClientViewVariablesManagerInternal vvm, IRobustSerializer robustSerializer)
{
_robustSerializer = robustSerializer;
_vvm = vvm;

View File

@@ -1,94 +1,51 @@
using System.Collections;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.IoC.Exceptions;
using Robust.Shared.Maths;
using Robust.Shared.Reflection;
using Robust.Shared.ViewVariables;
using Robust.Shared.ViewVariables.Commands;
namespace Robust.Client.ViewVariables
{
[UsedImplicitly]
public sealed class ViewVariablesCommand : IConsoleCommand
public sealed class ViewVariablesCommand : ViewVariablesBaseCommand, IConsoleCommand
{
public string Command => "vv";
public string Description => "Opens View Variables.";
public string Help => "Usage: vv <entity ID|IoC interface name|SIoC interface name>";
[Dependency] private readonly IClientViewVariablesManager _cvvm = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "vv";
public override string Description => "Opens View Variables.";
public override string Help => "Usage: vv <path|entity ID|guihover>";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var vvm = IoCManager.Resolve<IViewVariablesManager>();
// If you don't provide an entity ID, it opens the test class.
// Spooky huh.
if (args.Length == 0)
{
vvm.OpenVV(new VVTest());
_cvvm.OpenVV(new ViewVariablesPathSelector("/vvtest"));
return;
}
if (args.Length > 1)
{
shell.WriteError($"Incorrect number of arguments. Did you forget to quote a path?");
return;
}
var valArg = args[0];
if (valArg.StartsWith("SI"))
{
if (valArg.StartsWith("SIoC"))
valArg = valArg.Substring(4);
// Server-side IoC selector.
var selector = new ViewVariablesIoCSelector(valArg.Substring(1));
vvm.OpenVV(selector);
if (valArg.StartsWith("/c"))
{
// Remove "/c" before calling method.
_cvvm.OpenVV(valArg[2..]);
return;
}
if (valArg.StartsWith("I"))
if (valArg.StartsWith("/"))
{
if (valArg.StartsWith("IoC"))
valArg = valArg.Substring(3);
// Client-side IoC selector.
var reflection = IoCManager.Resolve<IReflectionManager>();
if (!reflection.TryLooseGetType(valArg, out var type))
{
shell.WriteLine("Unable to find that type.");
return;
}
object obj;
try
{
obj = IoCManager.ResolveType(type);
}
catch (UnregisteredTypeException)
{
shell.WriteLine("Unable to find that type.");
return;
}
vvm.OpenVV(obj);
return;
}
// Client side entity system.
if (valArg.StartsWith("CE"))
{
valArg = valArg.Substring(2);
var reflection = IoCManager.Resolve<IReflectionManager>();
if (!reflection.TryLooseGetType(valArg, out var type))
{
shell.WriteLine("Unable to find that type.");
return;
}
vvm.OpenVV(IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem(type));
}
if (valArg.StartsWith("SE"))
{
// Server-side Entity system selector.
var selector = new ViewVariablesEntitySystemSelector(valArg.Substring(2));
vvm.OpenVV(selector);
var selector = new ViewVariablesPathSelector(valArg);
_cvvm.OpenVV(selector);
return;
}
@@ -101,7 +58,7 @@ namespace Robust.Client.ViewVariables
shell.WriteLine("Not currently hovering any control.");
return;
}
vvm.OpenVV(obj);
_cvvm.OpenVV(obj);
return;
}
@@ -116,37 +73,11 @@ namespace Robust.Client.ViewVariables
if (!entityManager.EntityExists(entity))
{
shell.WriteLine("That entity does not exist locally. Attempting to open remote view...");
vvm.OpenVV(new ViewVariablesEntitySelector(entity));
_cvvm.OpenVV(new ViewVariablesEntitySelector(entity));
return;
}
vvm.OpenVV(entity);
}
/// <summary>
/// Test class to test local VV easily without connecting to the server.
/// </summary>
private sealed class VVTest : IEnumerable<object>
{
[ViewVariables(VVAccess.ReadWrite)] private int x = 10;
[ViewVariables]
public Dictionary<object, object> Dict => new() {{"a", "b"}, {"c", "d"}};
[ViewVariables]
public List<object> List => new() {1, 2, 3, 4, 5, 6, 7, 8, 9, x, 11, 12, 13, 14, 15, this};
[ViewVariables] private Vector2 Vector = (50, 50);
public IEnumerator<object> GetEnumerator()
{
return List.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
_cvvm.OpenVV(entity);
}
}
}

View File

@@ -18,10 +18,10 @@ namespace Robust.Client.ViewVariables
/// </summary>
internal abstract class ViewVariablesInstance
{
public readonly IViewVariablesManagerInternal ViewVariablesManager;
public readonly IClientViewVariablesManagerInternal ViewVariablesManager;
protected readonly IRobustSerializer _robustSerializer;
protected ViewVariablesInstance(IViewVariablesManagerInternal vvm, IRobustSerializer robustSerializer)
protected ViewVariablesInstance(IClientViewVariablesManagerInternal vvm, IRobustSerializer robustSerializer)
{
ViewVariablesManager = vvm;
_robustSerializer = robustSerializer;
@@ -53,7 +53,7 @@ namespace Robust.Client.ViewVariables
{
}
protected internal static IEnumerable<IGrouping<Type, Control>> LocalPropertyList(object obj, IViewVariablesManagerInternal vvm,
protected internal static IEnumerable<IGrouping<Type, Control>> LocalPropertyList(object obj, IClientViewVariablesManagerInternal vvm,
IRobustSerializer robustSerializer)
{
var styleOther = false;

View File

@@ -21,10 +21,10 @@ namespace Robust.Client.ViewVariables
private readonly Label _bottomLabel;
private readonly IViewVariablesManagerInternal _viewVariablesManager;
private readonly IClientViewVariablesManagerInternal _viewVariablesManager;
private readonly IRobustSerializer _robustSerializer;
public ViewVariablesPropertyControl(IViewVariablesManagerInternal viewVars, IRobustSerializer robustSerializer)
public ViewVariablesPropertyControl(IClientViewVariablesManagerInternal viewVars, IRobustSerializer robustSerializer)
{
MouseFilter = MouseFilterMode.Pass;

View File

@@ -328,7 +328,7 @@ namespace Robust.Server
_playerManager.Initialize(MaxPlayers);
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
IoCManager.Resolve<IPlacementManager>().Initialize();
IoCManager.Resolve<IViewVariablesHost>().Initialize();
IoCManager.Resolve<IServerViewVariablesInternal>().Initialize();
// Call Init in game assemblies.
_modLoader.BroadcastRunLevel(ModRunLevel.Init);

View File

@@ -25,6 +25,7 @@ using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Robust.Server
{
@@ -67,7 +68,8 @@ namespace Robust.Server
IoCManager.Register<IStatusHost, StatusHost>();
IoCManager.Register<ISystemConsoleManager, SystemConsoleManager>();
IoCManager.Register<ITileDefinitionManager, TileDefinitionManager>();
IoCManager.Register<IViewVariablesHost, ViewVariablesHost>();
IoCManager.Register<IViewVariablesManager, ServerViewVariablesManager>();
IoCManager.Register<IServerViewVariablesInternal, ServerViewVariablesManager>();
IoCManager.Register<IWatchdogApi, WatchdogApi>();
IoCManager.Register<IScriptHost, ScriptHost>();
IoCManager.Register<IMetricsManager, MetricsManager>();

View File

@@ -1,9 +1,10 @@
using System;
using System.Collections.Generic;
using Robust.Shared.ViewVariables;
namespace Robust.Server.ViewVariables
{
internal interface IViewVariablesHost
internal interface IServerViewVariablesInternal : IViewVariablesManager
{
void Initialize();

View File

@@ -6,7 +6,7 @@ namespace Robust.Server.ViewVariables
{
internal interface IViewVariablesSession
{
IViewVariablesHost Host { get; }
IServerViewVariablesInternal Host { get; }
IRobustSerializer RobustSerializer { get; }
NetUserId PlayerUser { get; }
object Object { get; }

View File

@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Network;
using Robust.Shared.ViewVariables;
namespace Robust.Server.ViewVariables;
internal sealed partial class ServerViewVariablesManager
{
private void InitializeDomains()
{
RegisterDomain("player", ResolvePlayerObject, ListPlayerPaths);
}
private (ViewVariablesPath? Path, string[] Segments) ResolvePlayerObject(string path)
{
var empty = (new ViewVariablesInstancePath(_playerManager), Array.Empty<string>());
if (string.IsNullOrEmpty(path))
return empty;
var segments = path.Split('/');
if (segments.Length == 0)
return empty;
var identifier = segments[0];
if (_playerManager.TryGetSessionByUsername(identifier, out var session))
return (new ViewVariablesInstancePath(session), segments[1..]);
if (_playerManager.TryGetPlayerDataByUsername(identifier, out var data))
return (new ViewVariablesInstancePath(data), segments[1..]);
if (!Guid.TryParse(identifier, out var guid))
return EmptyResolve;
var netId = new NetUserId(guid);
if (_playerManager.TryGetSessionById(netId, out session))
return (new ViewVariablesInstancePath(session), segments[1..]);
if (_playerManager.TryGetPlayerData(netId, out data))
return (new ViewVariablesInstancePath(data), segments[1..]);
return EmptyResolve;
}
private IEnumerable<string>? ListPlayerPaths(string[] segments)
{
if (segments.Length > 1)
return null;
if (segments.Length == 1
&& (_playerManager.TryGetSessionByUsername(segments[0], out _)
|| Guid.TryParse(segments[0], out var guid)
&& _playerManager.TryGetSessionById(new NetUserId(guid), out _)))
{
return null;
}
return _playerManager.Sessions
.Select(s => s.Name)
.Concat(_playerManager.Sessions
.Select(s => s.UserId.UserId.ToString()));
}
}

View File

@@ -9,15 +9,15 @@ using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
using static Robust.Shared.Network.Messages.MsgViewVariablesDenySession;
namespace Robust.Server.ViewVariables
{
internal sealed class ViewVariablesHost : ViewVariablesManagerShared, IViewVariablesHost
internal sealed partial class ServerViewVariablesManager : ViewVariablesManager, IServerViewVariablesInternal
{
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
@@ -31,8 +31,10 @@ namespace Robust.Server.ViewVariables
private uint _nextSessionId = 1;
public void Initialize()
public override void Initialize()
{
base.Initialize();
InitializeDomains();
_netManager.RegisterNetMessage<MsgViewVariablesReqSession>(_msgReqSession);
_netManager.RegisterNetMessage<MsgViewVariablesReqData>(_msgReqData);
_netManager.RegisterNetMessage<MsgViewVariablesModifyRemote>(_msgModifyRemote);
@@ -99,7 +101,7 @@ namespace Robust.Server.ViewVariables
private void _msgReqSession(MsgViewVariablesReqSession message)
{
void Deny(DenyReason reason)
void Deny(ViewVariablesResponseCode reason)
{
var denyMsg = new MsgViewVariablesDenySession();
denyMsg.RequestId = message.RequestId;
@@ -110,7 +112,7 @@ namespace Robust.Server.ViewVariables
var player = _playerManager.GetSessionByChannel(message.MsgChannel);
if (!_groupController.CanViewVar(player))
{
Deny(DenyReason.NoAccess);
Deny(ViewVariablesResponseCode.NoAccess);
return;
}
@@ -124,7 +126,7 @@ namespace Robust.Server.ViewVariables
if (compType == null ||
!_entityManager.TryGetComponent(componentSelector.Entity, compType, out var component))
{
Deny(DenyReason.NoObject);
Deny(ViewVariablesResponseCode.NoObject);
return;
}
@@ -135,7 +137,7 @@ namespace Robust.Server.ViewVariables
{
if (!_entityManager.EntityExists(entitySelector.Entity))
{
Deny(DenyReason.NoObject);
Deny(ViewVariablesResponseCode.NoObject);
return;
}
@@ -148,7 +150,7 @@ namespace Robust.Server.ViewVariables
|| relSession.PlayerUser != message.MsgChannel.UserId)
{
// TODO: logging?
Deny(DenyReason.NoObject);
Deny(ViewVariablesResponseCode.NoObject);
return;
}
@@ -157,25 +159,25 @@ namespace Robust.Server.ViewVariables
{
if (!relSession.TryGetRelativeObject(sessionRelativeSelector.PropertyIndex, out value))
{
Deny(DenyReason.InvalidRequest);
Deny(ViewVariablesResponseCode.InvalidRequest);
return;
}
}
catch (ArgumentOutOfRangeException)
{
Deny(DenyReason.NoObject);
Deny(ViewVariablesResponseCode.NoObject);
return;
}
catch (Exception e)
{
Logger.ErrorS("vv", "Exception while retrieving value for session. {0}", e);
Deny(DenyReason.NoObject);
Deny(ViewVariablesResponseCode.NoObject);
return;
}
if (value == null || value.GetType().IsValueType)
{
Deny(DenyReason.NoObject);
Deny(ViewVariablesResponseCode.NoObject);
return;
}
@@ -187,7 +189,7 @@ namespace Robust.Server.ViewVariables
var reflectionManager = IoCManager.Resolve<IReflectionManager>();
if (!reflectionManager.TryLooseGetType(ioCSelector.TypeName, out var type))
{
Deny(DenyReason.InvalidRequest);
Deny(ViewVariablesResponseCode.InvalidRequest);
return;
}
@@ -199,15 +201,26 @@ namespace Robust.Server.ViewVariables
var reflectionManager = IoCManager.Resolve<IReflectionManager>();
if (!reflectionManager.TryLooseGetType(esSelector.TypeName, out var type))
{
Deny(DenyReason.InvalidRequest);
Deny(ViewVariablesResponseCode.InvalidRequest);
return;
}
theObject = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem(type);
break;
}
case ViewVariablesPathSelector paSelector:
{
if (ResolvePath(paSelector.Path)?.Get() is not {} obj)
{
Deny(ViewVariablesResponseCode.NoObject);
return;
}
theObject = obj;
break;
}
default:
Deny(DenyReason.InvalidRequest);
Deny(ViewVariablesResponseCode.InvalidRequest);
return;
}
@@ -272,5 +285,24 @@ namespace Robust.Server.ViewVariables
return false;
}
}
protected override bool CheckPermissions(INetChannel channel)
{
return _playerManager.TryGetSessionByChannel(channel, out var session) && _groupController.CanViewVar(session);
}
protected override bool TryGetSession(Guid guid, [NotNullWhen(true)] out ICommonSession? session)
{
if (guid != Guid.Empty
&& _playerManager.TryGetSessionById(new NetUserId(guid), out var player)
&& !_groupController.CanViewVar(player)) // Can't VV other admins.
{
session = player;
return true;
}
session = null;
return false;
}
}
}

View File

@@ -11,7 +11,7 @@ namespace Robust.Server.ViewVariables
internal sealed class ViewVariablesSession : IViewVariablesSession
{
private readonly List<ViewVariablesTrait> _traits = new();
public IViewVariablesHost Host { get; }
public IServerViewVariablesInternal Host { get; }
public IRobustSerializer RobustSerializer { get; }
public NetUserId PlayerUser { get; }
public object Object { get; }
@@ -24,7 +24,7 @@ namespace Robust.Server.ViewVariables
/// The session ID for this session. This is what the server and client use to talk about this session.
/// </param>
/// <param name="host">The view variables host owning this session.</param>
public ViewVariablesSession(NetUserId playerUser, object o, uint sessionId, IViewVariablesHost host,
public ViewVariablesSession(NetUserId playerUser, object o, uint sessionId, IServerViewVariablesInternal host,
IRobustSerializer robustSerializer)
{
PlayerUser = playerUser;

View File

@@ -31,7 +31,7 @@ namespace Robust.Shared.GameObjects
#endif
private DependencyCollection _systemDependencyCollection = default!;
private List<Type> _systemTypes = new();
private readonly List<Type> _systemTypes = new();
private static readonly Histogram _tickUsageHistogram = Metrics.CreateHistogram("robust_entity_systems_update_usage",
"Amount of time spent processing each entity system", new HistogramConfiguration
@@ -349,6 +349,16 @@ namespace Robust.Shared.GameObjects
_extraLoadedTypes.Add(typeof(T));
}
public IEnumerable<Type> GetEntitySystemTypes()
{
return _systemTypes;
}
public bool TryGetEntitySystem(Type sysType, [NotNullWhen(true)] out object? system)
{
return _systemDependencyCollection.TryResolveType(sysType, out system);
}
public object GetEntitySystem(Type sysType)
{
return _systemDependencyCollection.ResolveType(sysType);

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.IoC.Exceptions;
@@ -127,6 +128,8 @@ namespace Robust.Shared.GameObjects
/// </exception>
void LoadExtraSystemType<T>() where T : IEntitySystem, new();
IEnumerable<Type> GetEntitySystemTypes();
bool TryGetEntitySystem(Type sysType, [NotNullWhen(true)] out object? system);
object GetEntitySystem(Type sysType);
}
}

View File

@@ -51,6 +51,14 @@ namespace Robust.Shared.IoC
_parentCollection = parentCollection;
}
/// <inheritdoc />
public IEnumerable<Type> GetRegisteredTypes()
{
return _parentCollection != null
? _services.Keys.Concat(_parentCollection.GetRegisteredTypes())
: _services.Keys;
}
/// <inheritdoc />
public bool TryResolveType<T>([NotNullWhen(true)] out T? instance)
{

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Robust.Shared.IoC.Exceptions;
@@ -29,6 +30,11 @@ namespace Robust.Shared.IoC
/// <seealso cref="IReflectionManager"/>
public interface IDependencyCollection
{
/// <summary>
/// Enumerates over all registered types.
/// </summary>
IEnumerable<Type> GetRegisteredTypes();
/// <summary>
/// Registers an interface to an implementation, to make it accessible to <see cref="DependencyCollection.Resolve{T}"/>
/// <see cref="IDependencyCollection.BuildGraph"/> MUST be called after this method to make the new interface available.

View File

@@ -1,4 +1,5 @@
using Lidgren.Network;
using Robust.Shared.ViewVariables;
using Robust.Shared.Serialization;
#nullable disable
@@ -21,12 +22,12 @@ namespace Robust.Shared.Network.Messages
/// <summary>
/// Reason for why the request was denied.
/// </summary>
public DenyReason Reason { get; set; }
public ViewVariablesResponseCode Reason { get; set; }
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
RequestId = buffer.ReadUInt32();
Reason = (DenyReason)buffer.ReadUInt16();
Reason = (ViewVariablesResponseCode)buffer.ReadUInt16();
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
@@ -34,23 +35,5 @@ namespace Robust.Shared.Network.Messages
buffer.Write(RequestId);
buffer.Write((ushort)Reason);
}
public enum DenyReason : ushort
{
/// <summary>
/// Come back with admin access.
/// </summary>
NoAccess = 401,
/// <summary>
/// Object pointing to by the selector does not exist.
/// </summary>
NoObject = 404,
/// <summary>
/// Request was invalid or something.
/// </summary>
InvalidRequest = 400,
}
}
}

View File

@@ -32,6 +32,11 @@ namespace Robust.Shared.Prototypes
{
void Initialize();
/// <summary>
/// Returns an IEnumerable to iterate all registered prototype kind by their ID.
/// </summary>
IEnumerable<string> GetPrototypeKinds();
/// <summary>
/// Return an IEnumerable to iterate all prototypes of a certain type.
/// </summary>
@@ -261,6 +266,11 @@ namespace Robust.Shared.Prototypes
ReloadPrototypeTypes();
}
public IEnumerable<string> GetPrototypeKinds()
{
return _prototypeTypes.Keys;
}
public IEnumerable<T> EnumeratePrototypes<T>() where T : class, IPrototype
{
if (!_hasEverBeenReloaded)

View File

@@ -1,11 +1,13 @@
using System;
using System.Linq;
using System.Reflection;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Reflection
{
internal static class ReflectionExtensions
{
public static Type GetUnderlyingType(this MemberInfo member)
internal static Type GetUnderlyingType(this MemberInfo member)
{
return member.MemberType switch
{
@@ -16,5 +18,64 @@ namespace Robust.Shared.Reflection
_ => throw new ArgumentException("MemberInfo must be one of: EventInfo, FieldInfo, MethodInfo, PropertyInfo")
};
}
internal static object? GetValue(this MemberInfo member, object instance)
{
return member switch
{
FieldInfo field => field.GetValue(instance),
PropertyInfo property => property.GetValue(instance),
_ => throw new ArgumentOutOfRangeException(nameof(member))
};
}
internal static void SetValue(this MemberInfo member, object instance, object? value)
{
switch (member)
{
case FieldInfo field:
{
field.SetValue(instance, value);
return;
}
case PropertyInfo property:
{
property.SetValue(instance, value);
return;
}
}
}
internal static MemberInfo? GetSingleMember(this Type type, string member, Type? declaringType = null)
{
var members = type
.GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(m => m.Name == member)
.ToArray();
if (members.Length == 0)
return null;
if (declaringType != null)
return members.SingleOrDefault(m => m.DeclaringType == declaringType);
return members.Length > 1
// In case there's member hiding going on, grab the one declared by the type of the object by default.
? members.SingleOrDefault(m => m.DeclaringType == type)
: members[0];
}
internal static PropertyInfo? GetIndexer(this Type type)
{
foreach (var pInfo in type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
if (pInfo.GetIndexParameters().Length == 0)
continue;
return pInfo;
}
return null;
}
}
}

View File

@@ -0,0 +1,24 @@
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
namespace Robust.Shared.Serialization;
/// <summary>
/// So by default, YamlDotNet appends "..." to the end of a serialized document.
/// In the YAML spec, three dots signify the end of a document.
/// If you're serializing a single document, this is pretty much useless. This emitter removes these dots entirely.
/// </summary>
public sealed class YamlNoDocEndDotsFix : IEmitter
{
private readonly IEmitter _next;
public YamlNoDocEndDotsFix(IEmitter next)
{
this._next = next;
}
public void Emit(ParsingEvent @event)
{
_next.Emit(@event is DocumentEnd ? new DocumentEnd(true) : @event);
}
}

View File

@@ -0,0 +1,43 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Network;
namespace Robust.Shared.ViewVariables.Commands;
public abstract class ViewVariablesBaseCommand : IConsoleCommand
{
[Dependency] protected readonly INetManager _netMan = default!;
[Dependency] protected readonly IViewVariablesManager _vvm = default!;
public abstract string Command { get; }
public abstract string Description { get; }
public abstract string Help { get; }
public abstract void Execute(IConsoleShell shell, string argStr, string[] args);
public virtual async ValueTask<CompletionResult> GetCompletionAsync(IConsoleShell shell, string[] args, CancellationToken cancel)
{
if (args.Length is 0 or > 1)
return CompletionResult.Empty;
var path = args[0];
if(_netMan.IsClient)
{
if(path.StartsWith("/c"))
return CompletionResult.FromOptions(
_vvm.ListPath(path[2..], new())
.Select(p => new CompletionOption($"/c{p}", null, CompletionOptionFlags.PartialCompletion)));
return CompletionResult.FromOptions((await _vvm.ListRemotePath(path, new()))
.Select(p => new CompletionOption(p, null, CompletionOptionFlags.PartialCompletion))
.Append(new CompletionOption("/c", "Client-side paths", CompletionOptionFlags.PartialCompletion)));
}
return CompletionResult.FromOptions(
_vvm.ListPath(path, new())
.Select(p => new CompletionOption(p, null, CompletionOptionFlags.PartialCompletion)));
}
}

View File

@@ -0,0 +1,38 @@
using Robust.Shared.Console;
using Robust.Shared.IoC;
namespace Robust.Shared.ViewVariables.Commands;
public sealed class ViewVariablesInvokeCommand : ViewVariablesBaseCommand, IConsoleCommand
{
public override string Command => "vvinvoke";
public override string Description => "Invoke/Call a path with arguments using VV.";
public override string Help => $"{Command} <path> [arguments...]";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length == 0)
{
shell.WriteError("Not enough arguments!");
return;
}
var path = args[0];
var arguments = string.Join(string.Empty, args[1..]);
if (_netMan.IsClient)
{
if (!path.StartsWith("/c"))
{
_vvm.InvokeRemotePath(path, arguments);
return;
}
// Remove "/c"
path = path[2..];
}
var obj = _vvm.InvokePath(path, arguments);
shell.WriteLine(obj?.ToString() ?? "null");
}
}

View File

@@ -0,0 +1,37 @@
using Robust.Shared.Console;
namespace Robust.Shared.ViewVariables.Commands;
public sealed class ViewVariablesReadCommand : ViewVariablesBaseCommand, IConsoleCommand
{
public override string Command => "vvread";
public override string Description => "Retrieve a path's value using VV.";
public override string Help => $"{Command} <path>";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length == 0)
{
shell.WriteError("Not enough arguments!");
return;
}
var path = args[0];
if (_netMan.IsClient)
{
if (!path.StartsWith("/c"))
{
shell.WriteLine(await _vvm.ReadRemotePath(path) ?? "null");
return;
}
// Remove "/c"
path = path[2..];
}
// TODO: Maybe serialize this with serv3 before printing?
var obj = _vvm.ReadPath(path);
shell.WriteLine(obj?.ToString() ?? "null");
}
}

View File

@@ -0,0 +1,36 @@
using Robust.Shared.Console;
namespace Robust.Shared.ViewVariables.Commands;
public sealed class ViewVariablesWriteCommand : ViewVariablesBaseCommand, IConsoleCommand
{
public override string Command => "vvwrite";
public override string Description => "Modify a path's value using VV.";
public override string Help => $"{Command} <path> <value>";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
shell.WriteError("Incorrect number of arguments!");
return;
}
var path = args[0];
var value = args[1];
if (_netMan.IsClient)
{
if (!path.StartsWith("/c"))
{
_vvm.WriteRemotePath(path, value);
return;
}
// Remove "/c"
path = path[2..];
}
_vvm.WritePath(path, value);
}
}

View File

@@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Robust.Shared.GameObjects;
using Robust.Shared.Players;
namespace Robust.Shared.ViewVariables;
public interface IViewVariablesManager
{
/// <summary>
/// Allows you to register the handlers for a domain.
/// Domains are the top-level segments of a VV path.
/// They provide ViewVariables with access to any number of objects.
/// A proper domain should only handle the next segment of the path.
///
/// <code>/entity/12345</code>
///
/// In the example above, "entity" would be a registered domain
/// and "12345" would be an object (entity UID) that is resolved by it.
///
/// </summary>
/// <param name="domain">The name of the domain to register.</param>
/// <param name="resolveObject">The handler for resolving paths.</param>
/// <param name="list">The handler for listing objects under the domain.</param>
/// <seealso cref="UnregisterDomain"/>
void RegisterDomain(string domain, DomainResolveObject resolveObject, DomainListPaths list);
/// <summary>
/// Unregisters the handlers for a given domain.
/// </summary>
/// <param name="domain">The name of the domain to unregister.</param>
/// <returns>Whether the domain existed and was able to be unregistered or not.</returns>
/// <seealso cref="RegisterDomain"/>
bool UnregisterDomain(string domain);
/// <summary>
/// Retrieves the type handler for a given type.
/// Creates it if it didn't exist already. Allows you to register custom handlers for a type.
/// Type handlers expand the paths available under a certain type on VV.
///
/// <code>/entity/12345/Custom</code>
///
/// In the example above, "Custom" could be a path that the type handler for <see cref="EntityUid"/> provided.
/// It does not exist on the <see cref="EntityUid"/> declaration, but that does not matter:
/// VV treats it the same as a "real" member under that type.
///
/// </summary>
/// <returns>The type handler object, which allows you register handlers and paths for the type.</returns>
/// <seealso cref="RegisterDomain"/>
ViewVariablesTypeHandler<T> GetTypeHandler<T>();
/// <param name="path">The path to be resolved.</param>
/// <returns>An object representing the path, or null if the path couldn't be resolved.</returns>
ViewVariablesPath? ResolvePath(string path);
object? ReadPath(string path);
void WritePath(string path, string value);
object? InvokePath(string path, string arguments);
IEnumerable<string> ListPath(string path, VVListPathOptions options);
Task<string?> ReadRemotePath(string path, ICommonSession? session = null);
Task WriteRemotePath(string path, string value, ICommonSession? session = null);
Task<string?> InvokeRemotePath(string path, string arguments, ICommonSession? session = null);
Task<IEnumerable<string>> ListRemotePath(string path, VVListPathOptions options, ICommonSession? session = null);
}

View File

@@ -0,0 +1,183 @@
using System;
using System.IO;
using Lidgren.Network;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Robust.Shared.ViewVariables;
internal abstract class MsgViewVariablesPath : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Command;
public uint RequestId { get; set; } = 0;
public string Path { get; set; } = string.Empty;
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
RequestId = buffer.ReadUInt32();
Path = buffer.ReadString();
}
public override void WriteToBuffer(NetOutgoingMessage buffer , IRobustSerializer serializer)
{
buffer.Write(RequestId);
buffer.Write(Path);
}
}
internal abstract class MsgViewVariablesPathReq : MsgViewVariablesPath
{
public Guid Session { get; set; } = Guid.Empty;
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
base.ReadFromBuffer(buffer, serializer);
Session = buffer.ReadGuid();
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
base.WriteToBuffer(buffer, serializer);
buffer.Write(Session);
}
}
internal abstract class MsgViewVariablesPathReqVal : MsgViewVariablesPathReq
{
public string? Value { get; set; } = null;
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
base.ReadFromBuffer(buffer, serializer);
Value = buffer.ReadString();
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
base.WriteToBuffer(buffer, serializer);
buffer.Write(Value);
}
}
internal abstract class MsgViewVariablesPathRes : MsgViewVariablesPath
{
public string[] Response { get; set; } = Array.Empty<string>();
public ViewVariablesResponseCode ResponseCode { get; set; } = ViewVariablesResponseCode.Ok;
internal MsgViewVariablesPathRes()
{
}
internal MsgViewVariablesPathRes(MsgViewVariablesPathReq req)
{
Path = req.Path;
RequestId = req.RequestId;
}
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
base.ReadFromBuffer(buffer, serializer);
ResponseCode = (ViewVariablesResponseCode) buffer.ReadUInt16();
var length = buffer.ReadInt32();
Response = new string[length];
for (var i = 0; i < length; i++)
{
Response[i] = buffer.ReadString();
}
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
base.WriteToBuffer(buffer, serializer);
buffer.Write((ushort)ResponseCode);
buffer.Write(Response.Length);
foreach (var value in Response)
{
buffer.Write(value);
}
}
}
internal sealed class MsgViewVariablesReadPathReq : MsgViewVariablesPathReq
{
}
internal sealed class MsgViewVariablesReadPathRes : MsgViewVariablesPathRes
{
public MsgViewVariablesReadPathRes()
{
}
public MsgViewVariablesReadPathRes(MsgViewVariablesReadPathReq req) : base(req)
{
}
}
internal sealed class MsgViewVariablesWritePathReq : MsgViewVariablesPathReqVal
{
}
internal sealed class MsgViewVariablesWritePathRes : MsgViewVariablesPathRes
{
public MsgViewVariablesWritePathRes()
{
}
public MsgViewVariablesWritePathRes(MsgViewVariablesWritePathReq req) : base(req)
{
}
}
internal sealed class MsgViewVariablesInvokePathReq : MsgViewVariablesPathReqVal
{
}
internal sealed class MsgViewVariablesInvokePathRes : MsgViewVariablesPathRes
{
public MsgViewVariablesInvokePathRes()
{
}
public MsgViewVariablesInvokePathRes(MsgViewVariablesInvokePathReq req) : base(req)
{
}
}
internal sealed class MsgViewVariablesListPathReq : MsgViewVariablesPathReq
{
public VVListPathOptions Options { get; set; }
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
base.ReadFromBuffer(buffer, serializer);
var length = buffer.ReadInt32();
using var stream = buffer.ReadAlignedMemory(length);
Options = serializer.Deserialize<VVListPathOptions>(stream);
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
base.WriteToBuffer(buffer, serializer);
var stream = new MemoryStream();
serializer.Serialize(stream, Options);
buffer.Write((int)stream.Length);
buffer.Write(stream.AsSpan());
}
}
internal sealed class MsgViewVariablesListPathRes : MsgViewVariablesPathRes
{
public MsgViewVariablesListPathRes()
{
}
public MsgViewVariablesListPathRes(MsgViewVariablesListPathReq req) : base(req)
{
}
}

View File

@@ -0,0 +1,20 @@
using System;
using Robust.Shared.Serialization;
namespace Robust.Shared.ViewVariables;
// ReSharper disable once InconsistentNaming
[Serializable, NetSerializable]
public readonly struct VVListPathOptions
{
public VVAccess MinimumAccess { get; init; }
public bool ListIndexers { get; init; }
public int RemoteListLength { get; init; }
public VVListPathOptions()
{
MinimumAccess = VVAccess.ReadOnly;
ListIndexers = true;
RemoteListLength = ViewVariablesManager.MaxListPathResponseLength;
}
}

View File

@@ -1,11 +1,12 @@
using System;
using Robust.Shared.Serialization;
namespace Robust.Shared.ViewVariables
{
/// <summary>
/// Attribute to make a property or field accessible to VV.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
public sealed class ViewVariablesAttribute : Attribute
{
public readonly VVAccess Access = VVAccess.ReadOnly;
@@ -21,6 +22,7 @@ namespace Robust.Shared.ViewVariables
}
}
[Serializable, NetSerializable]
public enum VVAccess : byte
{
/// <summary>
@@ -31,6 +33,6 @@ namespace Robust.Shared.ViewVariables
/// <summary>
/// This property is read and writable.
/// </summary>
ReadWrite,
ReadWrite = 1,
}
}

View File

@@ -32,7 +32,7 @@ namespace Robust.Shared.ViewVariables
/// For flexibility, these traits can be any object that the server/client need to understand each other.
/// At the moment though the only thing they're used with is <see cref="ViewVariablesTraits"/>.
/// </summary>
/// <seealso cref="ViewVariablesManagerShared.TraitIdsFor" />
/// <seealso cref="ViewVariablesManager.TraitIdsFor" />
public List<object> Traits { get; set; }
/// <summary>

View File

@@ -0,0 +1,294 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
namespace Robust.Shared.ViewVariables;
public delegate (ViewVariablesPath? path, string[] segments) DomainResolveObject(string path);
public delegate IEnumerable<string>? DomainListPaths(string[] segments);
internal abstract partial class ViewVariablesManager
{
protected static readonly (ViewVariablesPath? Path, string[] Segments) EmptyResolve = (null, Array.Empty<string>());
private readonly Dictionary<string, DomainData> _registeredDomains = new();
protected readonly Dictionary<Guid, WeakReference<object>> _vvObjectStorage = new();
public void RegisterDomain(string domain, DomainResolveObject resolveObject, DomainListPaths list)
{
_registeredDomains.Add(domain, new DomainData(resolveObject, list));
}
public bool UnregisterDomain(string domain)
{
return _registeredDomains.Remove(domain);
}
private void InitializeDomains()
{
RegisterDomain("ioc", ResolveIoCObject, ListIoCPaths);
RegisterDomain("entity", ResolveEntityObject, ListEntityPaths);
RegisterDomain("system", ResolveEntitySystemObject, ListEntitySystemPaths);
RegisterDomain("prototype", ResolvePrototypeObject, ListPrototypePaths);
RegisterDomain("object", ResolveStoredObject, ListStoredObjectPaths);
RegisterDomain("vvtest", ResolveVvTestObject, ListVvTestObjectPaths);
}
private (ViewVariablesPath? Path, string[] Segments) ResolveIoCObject(string path)
{
var empty = (new ViewVariablesInstancePath(IoCManager.Instance), Array.Empty<string>());
if (string.IsNullOrEmpty(path) || IoCManager.Instance == null)
return empty;
var segments = path.Split('/');
if (segments.Length == 0)
return empty;
var service = segments[0];
if (!_reflectionMan.TryLooseGetType(service, out var type))
return EmptyResolve;
return IoCManager.Instance.TryResolveType(type, out var obj)
? (new ViewVariablesInstancePath(obj), segments[1..])
: EmptyResolve;
}
private IEnumerable<string>? ListIoCPaths(string[] segments)
{
if (segments.Length > 1 || IoCManager.Instance is not {} deps)
return null;
if (segments.Length == 1
&& _reflectionMan.TryLooseGetType(segments[0], out var type)
&& deps.TryResolveType(type, out _))
{
return null;
}
return deps.GetRegisteredTypes()
.Select(t => t.Name);
}
private (ViewVariablesPath? Path, string[] Segments) ResolveEntityObject(string path)
{
if (string.IsNullOrEmpty(path))
return EmptyResolve;
var segments = path.Split('/');
if (segments.Length == 0)
return EmptyResolve;
if (!int.TryParse(segments[0], out var num) || num <= 0)
return EmptyResolve;
var uid = new EntityUid(num);
return (new ViewVariablesInstancePath(uid), segments[1..]);
}
private IEnumerable<string>? ListEntityPaths(string[] segments)
{
if (segments.Length > 1)
return null;
if (segments.Length == 1
&& EntityUid.TryParse(segments[0], out var u)
&& _entMan.EntityExists(u))
{
return null;
}
return _entMan.GetEntities()
.Select(uid => uid.ToString());
}
public (ViewVariablesPath? Path, string[] Segments) ResolveEntitySystemObject(string path)
{
var entSysMan = _entMan.EntitySysManager;
var empty = (new ViewVariablesInstancePath(entSysMan), Array.Empty<string>());
if (string.IsNullOrEmpty(path))
return empty;
var segments = path.Split('/');
if (segments.Length == 0)
return empty;
var sys = segments[0];
if (!_reflectionMan.TryLooseGetType(sys, out var type))
return EmptyResolve;
return entSysMan.TryGetEntitySystem(type, out var obj)
? (new ViewVariablesInstancePath(obj), segments[1..])
: EmptyResolve;
}
private IEnumerable<string>? ListEntitySystemPaths(string[] segments)
{
if (segments.Length > 1)
return null;
var entSysMan = _entMan.EntitySysManager;
if (segments.Length == 1
&& _reflectionMan.TryLooseGetType(segments[0], out var type)
&& entSysMan.TryGetEntitySystem(type, out _))
{
return null;
}
return _entMan.EntitySysManager
.GetEntitySystemTypes()
.Select(t => t.Name);
}
private (ViewVariablesPath? Path, string[] Segments) ResolvePrototypeObject(string path)
{
var empty = (new ViewVariablesInstancePath(_protoMan), Array.Empty<string>());
if (string.IsNullOrEmpty(path) || IoCManager.Instance == null)
return empty;
var segments = path.Split('/');
if (segments.Length <= 1)
return empty;
var kind = segments[0];
var id = segments[1];
if (!_protoMan.TryGetVariantType(kind, out var kindType)
|| !_protoMan.TryIndex(kindType, id, out var prototype))
return EmptyResolve;
return (new ViewVariablesInstancePath(prototype), segments[2..]);
}
private IEnumerable<string>? ListPrototypePaths(string[] segments)
{
switch (segments.Length)
{
case 1 or 2:
{
var kind = segments[0];
var prototype = segments.Length == 1 ? string.Empty : segments[1];
if(!_protoMan.HasVariant(kind))
goto case 0;
if (_protoMan.TryIndex(_protoMan.GetVariantType(kind), prototype, out _))
goto case default;
return _protoMan.EnumeratePrototypes(kind)
.Select(p => $"{kind}/{p.ID}");
}
case 0:
{
return _protoMan
.GetPrototypeKinds();
}
default:
{
return null;
}
}
}
private (ViewVariablesPath? Path, string[] Segments) ResolveStoredObject(string path)
{
if (string.IsNullOrEmpty(path))
return EmptyResolve;
var segments = path.Split('/');
if (segments.Length == 0
|| !Guid.TryParse(segments[0], out var guid)
|| !_vvObjectStorage.TryGetValue(guid, out var weakRef)
|| !weakRef.TryGetTarget(out var obj))
return EmptyResolve;
return (new ViewVariablesInstancePath(obj), segments[1..]);
}
private IEnumerable<string>? ListStoredObjectPaths(string[] segments)
{
if (segments.Length > 1)
return null;
if (segments.Length == 1
&& Guid.TryParse(segments[0], out var guid)
&& _vvObjectStorage.ContainsKey(guid))
{
return null;
}
return _vvObjectStorage.Keys
.Select(g => g.ToString());
}
private (ViewVariablesPath? path, string[] segments) ResolveVvTestObject(string path)
{
var segments = path.Split('/');
return (new ViewVariablesInstancePath(new VvTest()), segments);
}
private IEnumerable<string>? ListVvTestObjectPaths(string[] segments)
{
return null;
}
/// <summary>
/// Test class to test local VV easily without connecting to the server.
/// </summary>
[SuppressMessage("ReSharper", "UnusedMember.Local")]
[SuppressMessage("ReSharper", "InconsistentNaming")]
private sealed class VvTest : IEnumerable<object>
{
[ViewVariables(VVAccess.ReadWrite)] private int x = 10;
[ViewVariables] public Dictionary<object, object> Dict => new() {{"a", "b"}, {"c", "d"}};
[ViewVariables] public List<object> List => new() {1, 2, 3, 4, 5, 6, 7, 8, 9, x, 11, 12, 13, 14, 15, this};
[ViewVariables] public int[,] MultiDimensionalArray = new int[5, 2] {{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 0}};
[ViewVariables] private Vector2 Vector = (50, 50);
public IEnumerator<object> GetEnumerator()
{
return List.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
internal sealed class DomainData
{
public readonly DomainResolveObject ResolveObject;
public readonly DomainListPaths List;
public DomainData(DomainResolveObject resolveObject, DomainListPaths list)
{
ResolveObject = resolveObject;
List = list;
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -0,0 +1,203 @@
using System;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Robust.Shared.Reflection;
namespace Robust.Shared.ViewVariables;
internal abstract partial class ViewVariablesManager
{
private static readonly Regex IndexerRegex = new (@"\[[^\[]+\]", RegexOptions.Compiled);
private static readonly Regex TypeSpecifierRegex = new (@"\{[^\{]+\}", RegexOptions.Compiled);
public ViewVariablesPath? ResolvePath(string path)
{
if (string.IsNullOrEmpty(path))
return null;
if (path.StartsWith('/'))
path = path[1..];
if (path.EndsWith('/'))
path = path[..^1];
var segments = path.Split('/');
// Technically, this should never happen... But hey, better be safe than sorry?
if (segments.Length == 0)
return null;
var domain = segments[0];
if (!_registeredDomains.TryGetValue(domain, out var domainData))
return null;
var (newPath, relativePath) = domainData.ResolveObject(string.Join('/', segments[1..]));
return ResolveRelativePath(newPath, relativePath);
}
private ViewVariablesPath? ResolveRelativePath(ViewVariablesPath? path, string[] segments)
{
// Who needs recursion, am I right?
while (true)
{
// Empty path, return current path. This can happen as we slowly take away segments from the array.
if (segments.Length == 0)
{
return path;
}
if (path?.Get() is not {} obj)
return null;
var nextSegment = segments[0];
if (string.IsNullOrEmpty(nextSegment))
{
// Let's ignore that...
segments = segments[1..];
continue;
}
var specifiers = TypeSpecifierRegex.Matches(nextSegment);
var indexers = IndexerRegex.Matches(nextSegment);
var nextSegmentClean = TypeSpecifierRegex.Replace(
IndexerRegex.Replace(nextSegment, string.Empty), string.Empty);
// Yeah, that's not valid bud.
if (specifiers.Count > 1)
return null;
VVAccess? access = null;
if (specifiers.Count == 1 || ResolveTypeHandlers(path, nextSegmentClean) is not {} customPath)
{
Type? declaringType = null;
if (specifiers.Count == 1 && _reflectionMan.GetType(specifiers[0].Value[1..^1]) is {} t)
{
declaringType = t;
}
var memberInfo = obj.GetType().GetSingleMember(nextSegmentClean, declaringType);
if (memberInfo == null || !ViewVariablesUtility.TryGetViewVariablesAccess(memberInfo, out access))
return null;
path = memberInfo switch
{
FieldInfo or PropertyInfo => new ViewVariablesFieldOrPropertyPath(obj, memberInfo),
MethodInfo methodInfo => new ViewVariablesMethodPath(obj, methodInfo),
_ => throw new InvalidOperationException("Invalid member! Must be a property, field or method.")
};
}
else
{
path = customPath;
access = VVAccess.ReadWrite;
}
// After this, obj is essentially the parent.
foreach (Match match in indexers)
{
path = ResolveIndexing(path, ParseArguments(match.Value[1..^1]), access.Value);
}
segments = segments[1..];
}
}
private ViewVariablesPath? ResolveIndexing(ViewVariablesPath? path, string[] arguments, VVAccess access)
{
if (path?.Get() is not {} obj || arguments.Length == 0)
return null;
var type = obj.GetType();
// Multidimensional arrays... More like, painful arrays.
if (type.IsArray && type.GetArrayRank() > 1)
{
var getter = type.GetSingleMember("Get") as MethodInfo;
var setter = type.GetSingleMember("Set") as MethodInfo;
if (getter == null && setter == null)
return null;
var p = DeserializeArguments(
getter?.GetParameters().Select(p => p.ParameterType).ToArray()
?? setter!.GetParameters()[1..].Select(p => p.ParameterType).ToArray(),
0, arguments);
object? Get()
{
return getter?.Invoke(obj, p);
}
void Set(object? value)
{
if(p != null && access == VVAccess.ReadWrite)
setter?.Invoke(obj, new[] {value}.Concat(p).ToArray());
}
return new ViewVariablesFakePath(Get, Set, null, getter?.ReturnType ?? setter!.GetParameters()[0].ParameterType);
}
// No indexer.
if (type.GetIndexer() is not {} indexer)
return null;
var parametersInfo = indexer.GetIndexParameters();
var parameters = DeserializeArguments(
parametersInfo.Select(p => p.ParameterType).ToArray(),
parametersInfo.Count(p => p.IsOptional),
arguments);
if (parameters == null)
return null;
return new ViewVariablesIndexedPath(obj, indexer, parameters, access);
}
private ViewVariablesPath? ResolveTypeHandlers(ViewVariablesPath path, string relativePath)
{
if (path.Get() is not {} obj
|| string.IsNullOrEmpty(relativePath)
|| relativePath.Contains('/'))
return null;
var origType = obj.GetType();
var type = origType;
// First go through the inheritance chain, from current type to base types...
while (type != null)
{
if (_typeHandlers.TryGetValue(type, out var data))
{
var newPath = data.HandlePath(path, relativePath);
if (newPath != null)
return newPath;
}
type = type.BaseType;
}
// Then go through all the implemented interfaces, if any.
foreach (var interfaceType in origType.GetInterfaces())
{
if (!_typeHandlers.TryGetValue(interfaceType, out var data))
continue;
if (data.HandlePath(path, relativePath) is {} newPath)
return newPath;
}
// Not handled by a custom type handler!
return null;
}
}

View File

@@ -0,0 +1,393 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Security.AccessControl;
using System.Threading.Tasks;
using Robust.Shared.Network;
using Robust.Shared.Players;
namespace Robust.Shared.ViewVariables;
internal abstract partial class ViewVariablesManager
{
internal const int MaxListPathResponseLength = 500;
private uint _nextReadRequestId = 0;
private uint _nextWriteRequestId = 0;
private uint _nextInvokeRequestId = 0;
private uint _nextListRequestId = 0;
private readonly Dictionary<uint, TaskCompletionSource<string?>> _readRequests = new();
private readonly Dictionary<uint, TaskCompletionSource> _writeRequests = new();
private readonly Dictionary<uint, TaskCompletionSource<string?>> _invokeRequests = new();
private readonly Dictionary<uint, TaskCompletionSource<IEnumerable<string>>> _listRequests = new();
private void InitializeRemote()
{
_netMan.RegisterNetMessage<MsgViewVariablesReadPathReq>(ReadRemotePathRequest);
_netMan.RegisterNetMessage<MsgViewVariablesWritePathReq>(WriteRemotePathRequest);
_netMan.RegisterNetMessage<MsgViewVariablesInvokePathReq>(InvokeRemotePathRequest);
_netMan.RegisterNetMessage<MsgViewVariablesListPathReq>(ListRemotePathRequest);
_netMan.RegisterNetMessage<MsgViewVariablesReadPathRes>(ReadRemotePathResponse);
_netMan.RegisterNetMessage<MsgViewVariablesWritePathRes>(WriteRemotePathResponse);
_netMan.RegisterNetMessage<MsgViewVariablesInvokePathRes>(InvokeRemotePathResponse);
_netMan.RegisterNetMessage<MsgViewVariablesListPathRes>(ListRemotePathResponse);
}
public Task<string?> ReadRemotePath(string path, ICommonSession? session = null)
{
if (!_netMan.IsConnected || (_netMan.IsServer && session == null))
return Task.FromResult<string?>(null);
var msg = new MsgViewVariablesReadPathReq()
{
RequestId = unchecked(_nextReadRequestId++),
Path = path,
Session = session?.UserId ?? Guid.Empty,
};
var tsc = new TaskCompletionSource<string?>();
_readRequests.Add(msg.RequestId, tsc);
SendMessage(msg, session?.ConnectedClient);
return tsc.Task;
}
public Task WriteRemotePath(string path, string value, ICommonSession? session = null)
{
if (!_netMan.IsConnected || (_netMan.IsServer && session == null))
return Task.CompletedTask;
var msg = new MsgViewVariablesWritePathReq()
{
RequestId = unchecked(_nextWriteRequestId++),
Path = path,
Value = value,
Session = session?.UserId ?? Guid.Empty,
};
var tsc = new TaskCompletionSource();
_writeRequests.Add(msg.RequestId, tsc);
SendMessage(msg, session?.ConnectedClient);
return tsc.Task;
}
public Task<string?> InvokeRemotePath(string path, string arguments, ICommonSession? session = null)
{
if (!_netMan.IsConnected || (_netMan.IsServer && session == null))
return Task.FromResult<string?>(null);
var msg = new MsgViewVariablesInvokePathReq()
{
RequestId = unchecked(_nextInvokeRequestId++),
Path = path,
Value = arguments,
Session = session?.UserId ?? Guid.Empty,
};
var tsc = new TaskCompletionSource<string?>();
_invokeRequests.Add(msg.RequestId, tsc);
SendMessage(msg, session?.ConnectedClient);
return tsc.Task;
}
public Task<IEnumerable<string>> ListRemotePath(string path, VVListPathOptions options, ICommonSession? session = null)
{
if (!_netMan.IsConnected || (_netMan.IsServer && session == null))
return Task.FromResult(Enumerable.Empty<string>());
var msg = new MsgViewVariablesListPathReq()
{
RequestId = unchecked(_nextListRequestId++),
Path = path,
Options = options,
Session = session?.UserId ?? Guid.Empty,
};
var tsc = new TaskCompletionSource<IEnumerable<string>>();
_listRequests.Add(msg.RequestId, tsc);
SendMessage(msg, session?.ConnectedClient);
return tsc.Task;
}
private async void ReadRemotePathRequest(MsgViewVariablesReadPathReq req)
{
if (!CheckPermissions(req.MsgChannel))
{
SendMessage(new MsgViewVariablesReadPathRes(req)
{
ResponseCode = ViewVariablesResponseCode.NoAccess,
}, req.MsgChannel);
return;
}
if (_netMan.IsServer && TryGetSession(req.Session, out var session))
{
var value = await ReadRemotePath(req.Path, session);
SendMessage(new MsgViewVariablesReadPathRes(req)
{
Response = new []{value ?? "null"}
}, req.MsgChannel);
return;
}
var obj = ReadPath(req.Path);
if (obj == null)
{
SendMessage(new MsgViewVariablesReadPathRes(req)
{
ResponseCode = ViewVariablesResponseCode.NoObject,
}, req.MsgChannel);
return;
}
string val;
try
{
val = SerializeValue(obj.GetType(), obj) ?? obj.ToString() ?? "null";
}
catch (Exception)
{
val = obj.ToString() ?? "null";
}
SendMessage(new MsgViewVariablesReadPathRes(req)
{
Response = new []{ val }
}, req.MsgChannel);
}
private async void WriteRemotePathRequest(MsgViewVariablesWritePathReq req)
{
if (!CheckPermissions(req.MsgChannel))
{
_netMan.ServerSendMessage(new MsgViewVariablesWritePathRes(req)
{
ResponseCode = ViewVariablesResponseCode.NoAccess,
}, req.MsgChannel);
return;
}
if (_netMan.IsServer && TryGetSession(req.Session, out var session))
{
await WriteRemotePath(req.Path, req.Value ?? string.Empty, session);
SendMessage(new MsgViewVariablesWritePathRes(req), req.MsgChannel);
return;
}
var path = ResolvePath(req.Path);
if (path == null)
{
SendMessage(new MsgViewVariablesWritePathRes(req)
{
ResponseCode = ViewVariablesResponseCode.NoObject,
}, req.MsgChannel);
return;
}
var value = req.Value != null ? DeserializeValue(path.Type, req.Value) : null;
try
{
path.Set(value);
}
catch (Exception)
{
SendMessage(new MsgViewVariablesWritePathRes(req)
{
ResponseCode = ViewVariablesResponseCode.InvalidRequest,
}, req.MsgChannel);
return;
}
SendMessage(new MsgViewVariablesWritePathRes(req), req.MsgChannel);
}
private async void InvokeRemotePathRequest(MsgViewVariablesInvokePathReq req)
{
if (!CheckPermissions(req.MsgChannel))
{
_netMan.ServerSendMessage(new MsgViewVariablesInvokePathRes(req)
{
Path = req.Path, ResponseCode = ViewVariablesResponseCode.NoAccess,
}, req.MsgChannel);
return;
}
if (_netMan.IsServer && TryGetSession(req.Session, out var session))
{
var retVal = await InvokeRemotePath(req.Path, req.Value ?? string.Empty, session);
SendMessage(new MsgViewVariablesInvokePathRes(req)
{
Response = new []{retVal ?? "null"}
}, req.MsgChannel);
return;
}
var path = ResolvePath(req.Path);
if (path == null)
{
SendMessage(new MsgViewVariablesInvokePathRes(req)
{
ResponseCode = ViewVariablesResponseCode.NoObject,
}, req.MsgChannel);
return;
}
var args = req.Value != null ? ParseArguments(req.Value) : Array.Empty<string>();
var desArgs = DeserializeArguments(path.InvokeParameterTypes, (int)path.InvokeOptionalParameters, args);
object? value;
try
{
value = path.Invoke(desArgs);
}
catch (Exception)
{
SendMessage(new MsgViewVariablesInvokePathRes(req)
{
ResponseCode = ViewVariablesResponseCode.InvalidRequest,
}, req.MsgChannel);
return;
}
string val;
try
{
val = SerializeValue(path.InvokeReturnType, value) ?? value?.ToString() ?? "null";
}
catch (Exception)
{
val = value?.ToString() ?? "null";
}
SendMessage(new MsgViewVariablesInvokePathRes(req)
{
Response = new []{val},
}, req.MsgChannel);
}
private async void ListRemotePathRequest(MsgViewVariablesListPathReq req)
{
if (!CheckPermissions(req.MsgChannel))
{
_netMan.ServerSendMessage(new MsgViewVariablesListPathRes(req)
{
ResponseCode = ViewVariablesResponseCode.NoAccess,
}, req.MsgChannel);
return;
}
if (_netMan.IsServer && TryGetSession(req.Session, out var session))
{
var response = await ListRemotePath(req.Path, req.Options, session);
SendMessage(new MsgViewVariablesListPathRes(req)
{
Response = response.ToArray(),
}, req.MsgChannel);
return;
}
var enumerable = ListPath(req.Path, req.Options)
.OrderByDescending(p => p.StartsWith(req.Path))
.Take(Math.Min(MaxListPathResponseLength, req.Options.RemoteListLength))
.ToArray();
SendMessage(new MsgViewVariablesListPathRes(req)
{
Response = enumerable,
}, req.MsgChannel);
}
private void ReadRemotePathResponse(MsgViewVariablesReadPathRes res)
{
if (!_readRequests.Remove(res.RequestId, out var tsc))
return;
if (res.ResponseCode != ViewVariablesResponseCode.Ok)
{
tsc.TrySetResult(null); // TODO: Use exceptions
return;
}
if (res.Response.Length == 0)
{
tsc.TrySetResult(null);
return;
}
tsc.TrySetResult(res.Response[0]);
}
private void WriteRemotePathResponse(MsgViewVariablesWritePathRes res)
{
if (!_writeRequests.Remove(res.RequestId, out var tsc))
return;
// TODO: Use exceptions
tsc.SetResult();
}
private void InvokeRemotePathResponse(MsgViewVariablesInvokePathRes res)
{
if (!_invokeRequests.Remove(res.RequestId, out var tsc))
return;
if (res.ResponseCode != ViewVariablesResponseCode.Ok)
{
tsc.TrySetResult(null); // TODO: Use exceptions
return;
}
if (res.Response.Length == 0)
{
tsc.TrySetResult(null);
return;
}
tsc.TrySetResult(res.Response[0]);
}
private void ListRemotePathResponse(MsgViewVariablesListPathRes res)
{
if (!_listRequests.Remove(res.RequestId, out var tsc))
return;
if (res.ResponseCode != ViewVariablesResponseCode.Ok)
{
tsc.TrySetResult(Enumerable.Empty<string>()); // TODO: Use exceptions
return;
}
tsc.TrySetResult(res.Response);
}
private void SendMessage(NetMessage msg, INetChannel? channel = null)
{
// I'm surprised this isn't a method in INetManager already...
if (_netMan.IsServer)
{
if (channel == null)
throw new ArgumentNullException(nameof(channel));
_netMan.ServerSendMessage(msg, channel);
}
else
{
_netMan.ClientSendMessage(msg);
}
}
protected abstract bool CheckPermissions(INetChannel channel);
protected abstract bool TryGetSession(Guid guid, [NotNullWhen(true)] out ICommonSession? session);
}

View File

@@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Markdown;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;
namespace Robust.Shared.ViewVariables;
internal abstract partial class ViewVariablesManager
{
private static string[] ParseArguments(string arguments)
{
var args = new List<string>();
var parentheses = false;
var builder = new StringBuilder();
var i = 0;
while (i < arguments.Length)
{
var current = arguments[i];
switch (current)
{
case '(':
parentheses = true;
break;
case ')' when parentheses:
parentheses = false;
break;
case ',' when !parentheses:
args.Add(builder.ToString());
builder.Clear();
break;
default:
if (!parentheses && char.IsWhiteSpace(current))
break;
builder.Append(current);
break;
}
i++;
}
if(builder.Length != 0)
args.Add(builder.ToString());
return args.ToArray();
}
private object?[]? DeserializeArguments(Type[] argumentTypes, int optionalArguments, string[] arguments)
{
// Incorrect number of arguments!
if (arguments.Length < argumentTypes.Length - optionalArguments || arguments.Length > argumentTypes.Length)
return null;
var parameters = new List<object?>();
for (var i = 0; i < arguments.Length; i++)
{
var argument = arguments[i];
var type = argumentTypes[i];
var value = DeserializeValue(type, argument);
parameters.Add(value);
}
for (var i = 0; i < argumentTypes.Length - arguments.Length; i++)
{
parameters.Add(Type.Missing);
}
return parameters.ToArray();
}
private object? DeserializeValue(Type type, string value)
{
// Check if the argument is a VV path, and if not, deserialize the value with serv3.
if (ResolvePath(value)?.Get() is {} resolved && resolved.GetType().IsAssignableTo(type))
return resolved;
try
{
// Here we go serialization moment
using TextReader stream = new StringReader(value);
var yamlStream = new YamlStream();
yamlStream.Load(stream);
var document = yamlStream.Documents[0];
var rootNode = document.RootNode;
return _serMan.Read(type, rootNode.ToDataNode());
}
catch (Exception)
{
return null;
}
}
private string? SerializeValue(Type type, object? value, string? nodeTag = null)
{
if (value == null || type == typeof(void))
return null;
var objType = type;
var node = _serMan.WriteValue(type, value);
// Don't replace an existing tag if it's null.
if(!string.IsNullOrEmpty(nodeTag))
node.Tag = nodeTag;
var document = new YamlDocument(node.ToYamlNode());
var stream = new YamlStream {document};
using var writer = new StringWriter(new StringBuilder());
// Remove the three funny dots from the end of the string...
stream.Save(new YamlNoDocEndDotsFix(new YamlMappingFix(new Emitter(writer))), false);
return writer.ToString();
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.GameObjects;
namespace Robust.Shared.ViewVariables;
internal abstract partial class ViewVariablesManager
{
private readonly Dictionary<Type, ViewVariablesTypeHandler> _typeHandlers = new();
public ViewVariablesTypeHandler<T> GetTypeHandler<T>()
{
if (_typeHandlers.TryGetValue(typeof(T), out var h))
return (ViewVariablesTypeHandler<T>)h;
var handler = new ViewVariablesTypeHandler<T>();
_typeHandlers.Add(typeof(T), handler);
return handler;
}
private void InitializeTypeHandlers()
{
GetTypeHandler<EntityUid>()
.AddHandler(EntityComponentHandler, EntityComponentList)
.AddPath("Delete", uid => ViewVariablesPath.FromInvoker(_ => _entMan.DeleteEntity(uid)))
.AddPath("QueueDelete", uid => ViewVariablesPath.FromInvoker(_ => _entMan.QueueDeleteEntity(uid)));
}
private ViewVariablesPath? EntityComponentHandler(EntityUid uid, string relativePath)
{
if (!_entMan.EntityExists(uid)
|| !_compFact.TryGetRegistration(relativePath, out var registration, true)
|| !_entMan.TryGetComponent(uid, registration.Idx, out var component))
return null;
return new ViewVariablesComponentPath(component, uid);
}
private IEnumerable<string> EntityComponentList(EntityUid uid)
{
return _entMan.GetComponents(uid)
.Select(component => _compFact.GetComponentName(component.GetType()));
}
}

View File

@@ -0,0 +1,89 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization.Manager;
namespace Robust.Shared.ViewVariables;
internal abstract partial class ViewVariablesManager : IViewVariablesManager
{
[Dependency] private readonly ISerializationManager _serMan = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IComponentFactory _compFact = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly IReflectionManager _reflectionMan = default!;
[Dependency] private readonly INetManager _netMan = default!;
private readonly Dictionary<Type, HashSet<object>> _cachedTraits = new();
private const BindingFlags MembersBindings =
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
public virtual void Initialize()
{
InitializeDomains();
InitializeTypeHandlers();
InitializeRemote();
}
public object? ReadPath(string path)
{
return ResolvePath(path)?.Get();
}
public void WritePath(string path, string value)
{
var resPath = ResolvePath(path);
resPath?.Set(DeserializeValue(resPath.Type, value));
}
public object? InvokePath(string path, string arguments)
{
var resPath = ResolvePath(path);
if (resPath == null)
return null;
var args = ParseArguments(arguments);
var desArgs =
DeserializeArguments(resPath.InvokeParameterTypes, (int)resPath.InvokeOptionalParameters, args);
return resPath.Invoke(desArgs);
}
/// <summary>
/// Figures out which VV traits an object type has. This method is in shared so the client and server agree on this mess.
/// </summary>
/// <seealso cref="ViewVariablesBlobMetadata.Traits"/>
public ICollection<object> TraitIdsFor(Type type)
{
if (!_cachedTraits.TryGetValue(type, out var traits))
{
traits = new HashSet<object>();
_cachedTraits.Add(type, traits);
if (ViewVariablesUtility.TypeHasVisibleMembers(type))
{
traits.Add(ViewVariablesTraits.Members);
}
if (typeof(IEnumerable).IsAssignableFrom(type))
{
traits.Add(ViewVariablesTraits.Enumerable);
}
if (typeof(EntityUid).IsAssignableFrom(type))
{
traits.Add(ViewVariablesTraits.Entity);
}
}
return traits;
}
}

View File

@@ -1,41 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
namespace Robust.Shared.ViewVariables
{
internal abstract class ViewVariablesManagerShared
{
private readonly Dictionary<Type, HashSet<object>> _cachedTraits = new();
/// <summary>
/// Figures out which VV traits an object type has. This method is in shared so the client and server agree on this mess.
/// </summary>
/// <seealso cref="ViewVariablesBlobMetadata.Traits"/>
public ICollection<object> TraitIdsFor(Type type)
{
if (!_cachedTraits.TryGetValue(type, out var traits))
{
traits = new HashSet<object>();
_cachedTraits.Add(type, traits);
if (ViewVariablesUtility.TypeHasVisibleMembers(type))
{
traits.Add(ViewVariablesTraits.Members);
}
if (typeof(IEnumerable).IsAssignableFrom(type))
{
traits.Add(ViewVariablesTraits.Enumerable);
}
if (typeof(EntityUid).IsAssignableFrom(type))
{
traits.Add(ViewVariablesTraits.Entity);
}
}
return traits;
}
}
}

View File

@@ -96,4 +96,15 @@ namespace Robust.Shared.ViewVariables
public string TypeName { get; }
}
[Serializable, NetSerializable]
public sealed class ViewVariablesPathSelector : ViewVariablesObjectSelector
{
public ViewVariablesPathSelector(string path)
{
Path = path;
}
public string Path { get; }
}
}

View File

@@ -0,0 +1,324 @@
using System;
using System.Linq;
using System.Reflection;
using Robust.Shared.GameObjects;
using Robust.Shared.Reflection;
namespace Robust.Shared.ViewVariables;
/// <summary>
/// Represents a ViewVariables path. Allows you to "Get", "Set" or "Invoke" the path.
/// </summary>
[Virtual]
public abstract class ViewVariablesPath
{
/// <summary>
/// The type that is both returned by the <see cref="Get"/> method and used by the <see cref="Set"/> method.
/// </summary>
public abstract Type Type { get; }
/// <summary>
/// Gets the value of the path, if possible.
/// </summary>
/// <returns>The value of the path, or null. Same type as <see cref="Type"/>.</returns>
public abstract object? Get();
/// <summary>
/// Sets the value of the path, if possible.
/// </summary>
/// <param name="value">The new value to set the path to. Must be of the same type as <see cref="Type"/>.</param>
public abstract void Set(object? value);
/// <summary>
/// Invokes the path, if possible.
/// </summary>
/// <param name="parameters">The parameters that the function takes.</param>
/// <returns>The object returned by invoking the function, or null.</returns>
public abstract object? Invoke(object?[]? parameters);
/// <summary>
/// The types of all parameters in the <see cref="Invoke"/> method.
/// </summary>
/// <seealso cref="InvokeOptionalParameters"/>
public virtual Type[] InvokeParameterTypes { get; } = Array.Empty<Type>();
/// <summary>
/// The number of optional parameters in the <see cref="Invoke"/> method, starting from the end of the array.
/// </summary>
/// <seealso cref="InvokeParameterTypes"/>
public virtual uint InvokeOptionalParameters { get; } = 0;
/// <summary>
/// The type of the object returned by the <see cref="Invoke"/> method, or <see cref="Void"/> if none.
/// </summary>
public virtual Type InvokeReturnType { get; } = typeof(void);
#region Static helper methods
/// <summary>
/// Creates a <see cref="ViewVariablesFakePath"/> given an object.
/// </summary>
public static ViewVariablesFakePath FromObject(object obj)
=> new(() => obj, null, null, obj.GetType());
/// <summary>
/// Creates a <see cref="ViewVariablesFakePath"/> given a getter function.
/// </summary>
public static ViewVariablesFakePath FromGetter(Func<object?> getter, Type type)
=> new(getter, null, null, type);
/// <summary>
/// Creates a <see cref="ViewVariablesFakePath"/> given a setter function.
/// </summary>
public static ViewVariablesFakePath FromSetter(Action<object?> setter, Type type)
=> new(null, setter, null, type);
/// <summary>
/// Creates a <see cref="ViewVariablesFakePath"/> given a function to be invoked.
/// </summary>
public static ViewVariablesFakePath FromInvoker(Func<object?, object?> invoker,
Type[]? invokeParameterTypes = null, uint invokeOptionalParameters = 0, Type? invokeReturnType = null)
=> new(null, null, invoker, null, invokeParameterTypes, invokeOptionalParameters, invokeReturnType);
/// <summary>
/// Creates a <see cref="ViewVariablesFakePath"/> given a function to be invoked.
/// </summary>
public static ViewVariablesFakePath FromInvoker(Action<object?> invoker,
Type[]? invokeParameterTypes = null, uint invokeOptionalParameters = 0, Type? invokeReturnType = null)
=> new(null, null, invoker, null, invokeParameterTypes, invokeOptionalParameters, invokeReturnType);
#endregion
}
internal sealed class ViewVariablesFieldOrPropertyPath : ViewVariablesPath
{
internal ViewVariablesFieldOrPropertyPath(object? obj, MemberInfo member)
{
if (member is not (FieldInfo or PropertyInfo))
throw new ArgumentException("Member must be either a field or a property!", nameof(member));
_object = obj;
_member = member;
ViewVariablesUtility.TryGetViewVariablesAccess(member, out _access);
}
private readonly object? _object;
private readonly MemberInfo _member;
private readonly VVAccess? _access;
public override Type Type => _member.GetUnderlyingType();
public override object? Get()
{
if (_access == null)
return null;
try
{
return _object != null
? _member.GetValue(_object)
: null;
}
catch (Exception)
{
return null;
}
}
public override void Set(object? value)
{
if (_access != VVAccess.ReadWrite)
return;
if (_object != null)
_member.SetValue(_object, value);
}
public override object? Invoke(object?[]? parameters) => null;
}
internal sealed class ViewVariablesMethodPath : ViewVariablesPath
{
internal ViewVariablesMethodPath(object? obj, MethodInfo method)
{
_object = obj;
_method = method;
ViewVariablesUtility.TryGetViewVariablesAccess(method, out _access);
}
private readonly object? _object;
private readonly MethodInfo _method;
private readonly VVAccess? _access;
public override Type Type => typeof(void);
public override Type InvokeReturnType => _method.ReturnType;
public override object? Get() => null;
public override void Set(object? value)
{
}
public override object? Invoke(object?[]? parameters)
{
if (_access != VVAccess.ReadWrite)
return null;
return _object != null
? _method.Invoke(_object, parameters)
: null;
}
public override Type[] InvokeParameterTypes
=> _access == VVAccess.ReadWrite
? _method.GetParameters().Select(info => info.ParameterType).ToArray()
: Array.Empty<Type>();
public override uint InvokeOptionalParameters
=> _access == VVAccess.ReadWrite
? (uint) _method.GetParameters().Count(info => info.IsOptional)
: 0;
}
internal sealed class ViewVariablesIndexedPath : ViewVariablesPath
{
internal ViewVariablesIndexedPath(object? obj, PropertyInfo indexer, object?[] index, VVAccess? parentAccess)
{
if (indexer.GetIndexParameters().Length == 0)
throw new ArgumentException("PropertyInfo is not an indexer!", nameof(indexer));
_object = obj;
_indexer = indexer;
_index = index;
_access = parentAccess;
}
private readonly object? _object;
private readonly PropertyInfo _indexer;
private readonly object?[] _index;
private readonly VVAccess? _access;
public override Type Type => _indexer.GetUnderlyingType();
public override object? Get()
{
if (_access == null)
return null;
try
{
return _object != null
? _indexer.GetValue(_object, _index)
: null;
}
catch (Exception)
{
return null;
}
}
public override void Set(object? value)
{
if(_access == VVAccess.ReadWrite && _object != null)
_indexer.SetValue(_object, value, _index);
}
public override object? Invoke(object?[]? parameters) => null;
}
public sealed class ViewVariablesInstancePath : ViewVariablesPath
{
public ViewVariablesInstancePath(object? obj)
{
_object = obj;
}
private readonly object? _object;
public override Type Type => _object?.GetType() ?? typeof(void);
public override object? Get() => _object;
public override void Set(object? value)
{
}
public override object? Invoke(object?[]? parameters) => null;
}
public sealed class ViewVariablesComponentPath : ViewVariablesPath
{
public readonly object? Component;
public readonly EntityUid Owner;
public override Type Type => Component?.GetType() ?? typeof(void);
public ViewVariablesComponentPath(object? component, EntityUid owner)
{
Component = component;
Owner = owner;
}
public override object? Get()
{
return Component;
}
public override void Set(object? value) { }
public override object? Invoke(object?[]? parameters) => null;
}
public sealed class ViewVariablesFakePath : ViewVariablesPath
{
public ViewVariablesFakePath(Func<object?>? getter, Action<object?>? setter, Func<object?, object?>? invoker = null,
Type? type = null, Type[]? invokeParameterTypes = null, uint invokeOptionalParameters = 0, Type? invokeReturnType = null)
{
_getter = getter;
_setter = setter;
_invoker = invoker;
Type = type ?? typeof(void);
InvokeParameterTypes = invokeParameterTypes ?? Array.Empty<Type>();
InvokeOptionalParameters = invokeOptionalParameters;
InvokeReturnType = invokeReturnType ?? typeof(void);
}
public ViewVariablesFakePath(Func<object?>? getter, Action<object?>? setter, Action<object?> invoker,
Type? type = null, Type[]? invokeParameterTypes = null, uint invokeOptionalParameters = 0, Type? invokeReturnType = null)
: this(getter, setter, null, type, invokeParameterTypes, invokeOptionalParameters, invokeReturnType)
{
_invoker = p =>
{
invoker(p);
return null;
};
}
private readonly Func<object?>? _getter;
private readonly Action<object?>? _setter;
private readonly Func<object?, object?>? _invoker;
public override Type Type { get; }
public override Type[] InvokeParameterTypes { get; }
public override uint InvokeOptionalParameters { get; }
public override Type InvokeReturnType { get; }
public override object? Get()
{
return _getter?.Invoke();
}
public override void Set(object? value)
{
_setter?.Invoke(value);
}
public override object? Invoke(object?[]? parameters)
{
return _invoker?.Invoke(parameters);
}
public ViewVariablesFakePath WithGetter(Func<object?> getter, Type? type = null)
=> new(getter, _setter, _invoker, type ?? Type, InvokeParameterTypes, InvokeOptionalParameters, InvokeReturnType);
public ViewVariablesFakePath WithSetter(Action<object?> setter, Type? type = null)
=> new(_getter, setter, _invoker, type ?? Type, InvokeParameterTypes, InvokeOptionalParameters, InvokeReturnType);
public ViewVariablesFakePath WithInvoker(Func<object?, object?> invoker,
Type[]? invokeParameterTypes = null, uint invokeOptionalParameters = 0, Type? invokeReturnType = null)
=> new(_getter, _setter, invoker, Type, invokeParameterTypes, invokeOptionalParameters,
invokeReturnType);
}

View File

@@ -0,0 +1,24 @@
namespace Robust.Shared.ViewVariables;
public enum ViewVariablesResponseCode : ushort
{
/// <summary>
/// Request went through successfully.
/// </summary>
Ok = 200,
/// <summary>
/// Request was invalid or something.
/// </summary>
InvalidRequest = 400,
/// <summary>
/// Come back with admin access.
/// </summary>
NoAccess = 401,
/// <summary>
/// Object pointing to by the selector does not exist.
/// </summary>
NoObject = 404,
}

View File

@@ -0,0 +1,240 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
namespace Robust.Shared.ViewVariables;
public delegate ViewVariablesPath? HandleTypePath(ViewVariablesPath path, string relativePath);
public delegate ViewVariablesPath? HandleTypePath<in T>(T? obj, string relativePath);
public delegate IEnumerable<string> ListTypeCustomPaths(ViewVariablesPath path);
public delegate IEnumerable<string> ListTypeCustomPaths<in T>(T? obj);
public delegate ViewVariablesPath? PathHandler(ViewVariablesPath path);
public delegate ViewVariablesPath? PathHandler<in T>(T obj);
public delegate ViewVariablesPath? PathHandlerNullable<in T>(T? obj);
public delegate ViewVariablesPath? PathHandlerComponent<in T>(EntityUid uid, T component);
public delegate TValue ComponentPropertyGetter<in TComp, out TValue>(EntityUid uid, TComp comp);
public delegate void ComponentPropertySetter<in TComp, in TValue>(EntityUid uid, TValue value, TComp? comp);
public abstract class ViewVariablesTypeHandler
{
internal abstract ViewVariablesPath? HandlePath(ViewVariablesPath path, string relativePath);
internal abstract IEnumerable<string> ListPath(ViewVariablesPath path);
}
public sealed class ViewVariablesTypeHandler<T> : ViewVariablesTypeHandler
{
private readonly List<TypeHandlerData> _handlers = new();
private readonly Dictionary<string, PathHandler> _paths = new();
internal ViewVariablesTypeHandler()
{
}
/// <summary>
/// Adding handler methods allow you to dynamically create and return ViewVariables paths for any sort of path.
/// </summary>
/// <remarks>
/// The handlers are iterated in the order they were registered in.
/// Handlers registered with this method take precedence over handlers registered for specific relative paths.
/// </remarks>
/// <returns>The same object instance, so you can chain method calls.</returns>
public ViewVariablesTypeHandler<T> AddHandler(HandleTypePath<T> handle, ListTypeCustomPaths<T> list)
{
ViewVariablesPath? HandleWrapper(ViewVariablesPath path, string relativePath)
=> handle((T?)path.Get(), relativePath);
IEnumerable<string> ListWrapper(ViewVariablesPath path)
=> list((T?) path.Get());
_handlers.Add(new TypeHandlerData(HandleWrapper, ListWrapper, handle, list));
return this;
}
/// <inheritdoc cref="AddHandler(Robust.Shared.ViewVariables.HandleTypePath{T},Robust.Shared.ViewVariables.ListTypeCustomPaths{T})"/>
public ViewVariablesTypeHandler<T> AddHandler(HandleTypePath handle, ListTypeCustomPaths list)
{
_handlers.Add(new TypeHandlerData(handle, list));
return this;
}
/// <summary>
/// Remove a specific handler method pair from the type handler.
/// </summary>
/// <returns>The same object instance, so you can chain method calls.</returns>
/// <exception cref="ArgumentException">If the methods specified were not registered.</exception>
public ViewVariablesTypeHandler<T> RemoveHandler(HandleTypePath<T> handle, ListTypeCustomPaths<T> list)
{
for (var i = 0; i < _handlers.Count; i++)
{
var data = _handlers[i];
if (data.OriginalHandle != handle || data.OriginalList != list)
continue;
_handlers.RemoveAt(i);
return this;
}
throw new ArgumentException("The specified arguments were not found in the list!");
}
/// <inheritdoc cref="RemoveHandler(Robust.Shared.ViewVariables.HandleTypePath{T},Robust.Shared.ViewVariables.ListTypeCustomPaths{T})"/>
public ViewVariablesTypeHandler<T> RemoveHandler(HandleTypePath handle, ListTypeCustomPaths list)
{
for (var i = 0; i < _handlers.Count; i++)
{
var data = _handlers[i];
if (data.Handle != handle || data.List != list)
continue;
_handlers.RemoveAt(i);
return this;
}
throw new ArgumentException("The specified arguments were not found in the list!");
}
/// <summary>
/// With this method you can register a handler to handle a specific path relative to the type instance.
/// </summary>
/// <returns>The same object instance, so you can chain method calls.</returns>
public ViewVariablesTypeHandler<T> AddPath(string path, PathHandler<T> handler)
{
ViewVariablesPath? Wrapper(T? t)
=> t != null ? handler(t) : null;
return AddPathNullable(path, (PathHandlerNullable<T>) Wrapper);
}
/// <inheritdoc cref="AddPath(string,PathHandler)"/>
/// <remarks>As opposed to <see cref="AddPath(string,PathHandler)"/>, here the passed object is nullable.</remarks>
/// <!-- The reason this isn't called "AddPath" is because it'd cause many ambiguous invocations.-->
public ViewVariablesTypeHandler<T> AddPathNullable(string path, PathHandlerNullable<T> handler)
{
ViewVariablesPath? Wrapper(ViewVariablesPath p)
=> handler((T?) p.Get());
return AddPath(path, Wrapper);
}
/// <inheritdoc cref="AddPath(string,PathHandler)"/>
/// <remarks>As opposed to the rest of "AddPath" methods, this one is specific to entity components.</remarks>
public ViewVariablesTypeHandler<T> AddPath(string path, PathHandlerComponent<T> handler)
{
ViewVariablesPath? Wrapper(ViewVariablesPath p)
{
if (p is not ViewVariablesComponentPath pc || pc.Get() is not {} obj)
return null;
return handler(pc.Owner, (T) obj);
}
return AddPath(path, Wrapper);
}
/// <inheritdoc cref="AddPath(string,PathHandler)"/>
public ViewVariablesTypeHandler<T> AddPath<TValue>(string path, ComponentPropertyGetter<T, TValue> getter,
ComponentPropertySetter<T, TValue>? setter = null)
{
// Gee, these wrappers are getting more and more complicated...
ViewVariablesPath? Wrapper(ViewVariablesPath p)
{
if (p is not ViewVariablesComponentPath pc || pc.Get() is not {} obj)
return null;
var comp = (T) obj;
var newPath = ViewVariablesPath.FromGetter(() => getter(pc.Owner, comp), typeof(TValue));
if (setter != null)
{
newPath = newPath.WithSetter(value =>
{
// In case it explodes with a NRE or something!
try
{
setter(pc.Owner, (TValue) value!, comp);
}
catch (NullReferenceException e)
{
Logger.ErrorS(nameof(ViewVariablesManager), e,
$"NRE caught in setter for path \"{path}\" for type \"{typeof(T).Name}\"...");
}
});
}
return newPath;
}
return AddPath(path, Wrapper);
}
/// <inheritdoc cref="AddPath(string,PathHandler)"/>
public ViewVariablesTypeHandler<T> AddPath(string path, PathHandler handler)
{
_paths.Add(path, handler);
return this;
}
/// <summary>
/// Removes a handler for a specific relative path.
/// </summary>
/// <returns>The same object instance, so you can chain method calls.</returns>
public ViewVariablesTypeHandler<T> RemovePath(string path)
{
_paths.Remove(path);
return this;
}
internal override ViewVariablesPath? HandlePath(ViewVariablesPath path, string relativePath)
{
// Dynamic handlers take precedence. Iterated by order of registration.
foreach (var data in _handlers)
{
if (data.Handle(path, relativePath) is {} dynPath)
return dynPath;
}
// Finally, try to get a static handler.
return _paths.TryGetValue(relativePath, out var handler)
? handler(path)
: null;
}
internal override IEnumerable<string> ListPath(ViewVariablesPath path)
{
foreach (var data in _handlers)
{
foreach (var p in data.List(path))
{
yield return p;
}
}
foreach (var (p, handler) in _paths)
{
if (handler(path) is {})
yield return p;
}
}
private sealed class TypeHandlerData
{
public readonly HandleTypePath Handle;
public readonly ListTypeCustomPaths List;
public readonly HandleTypePath<T>? OriginalHandle;
public readonly ListTypeCustomPaths<T>? OriginalList;
public TypeHandlerData(HandleTypePath handle, ListTypeCustomPaths list,
HandleTypePath<T>? origHandle = null, ListTypeCustomPaths<T>? origList = null)
{
Handle = handle;
List = list;
OriginalHandle = origHandle;
OriginalList = origList;
}
}
}