feat: Add VV editor for tuples (#6065)

* feat: Add VV editor for tuples

* refactor: make tuple editor work in more cases

* feat: support other arity tuples

* fix: correct release notes entry

* refactor: use a new index selector for tuples

Also yank out silly unused code.

* fix: make all non-ValueTuples readonly

* refactor: spell out ValueTuple arities

,,,,,,,,,,,,,,,,,,,,,
This commit is contained in:
Perry Fraser
2025-07-22 16:31:19 -04:00
committed by GitHub
parent 9ea51432d1
commit 0bf4123b8d
7 changed files with 159 additions and 0 deletions

View File

@@ -43,6 +43,7 @@ END TEMPLATE-->
* `RobustClientPackaging.WriteClientResources()` and `RobustServerPackaging.WriteServerResources()` now have an overload taking in a set of things to ignore in the content resources directory.
* Added `IPrototypeManager.Resolve()`, which logs an error if the resolved prototype does not exist. This is effectively the previous (but not original) default behavior of `IPrototypeManager.TryIndex`.
* There's now a ViewVariables property editor for tuples.
### Bugfixes

View File

@@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.IoC;
using Robust.Shared.ViewVariables;
using static Robust.Client.UserInterface.Controls.BoxContainer;
using CS = System.Runtime.CompilerServices;
namespace Robust.Client.ViewVariables.Editors;
internal sealed class VVPropEditorTuple : VVPropEditor
{
[Dependency] private readonly IClientViewVariablesManagerInternal _viewVariables = default!;
private bool _readOnly;
private readonly List<object?> _tuple = [];
private readonly List<VVPropEditor> _editors = [];
private Type? _actualType;
public VVPropEditorTuple()
{
IoCManager.InjectDependencies(this);
}
protected override Control MakeUI(object? value)
{
var vBoxContainer = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
MinSize = new Vector2(240, 0),
};
if (value is not CS.ITuple tuple)
return vBoxContainer;
// Zero-tuples exist?? I'm just not going to bother with that.
if (tuple.Length == 0)
return vBoxContainer;
_actualType = value.GetType();
// We disallow editing tuples with arity more than 7 since they would
// a pain to construct via reflection. And no one should have tuples
// that large. (8 is bad because last element becomes a ValueTuple<>)
_readOnly = ReadOnly
|| tuple.Length >= 8
|| !IsValueTuple(_actualType); // ToTuple only supports ValueTuples
for (var i = 0; i < tuple.Length; i++)
{
var editor = CreateBox(tuple[i], vBoxContainer);
var index = i; // thanks C#
editor.OnValueChanged += (o, reinterpret) => ValueChanged(ToTuple(o, index), reinterpret);
_tuple.Add(tuple[i]);
_editors.Add(editor);
}
return vBoxContainer;
}
private bool IsValueTuple(Type actualType)
{
if (!actualType.IsGenericType)
return false;
Type[] valueTupleTypes =
[
typeof(ValueTuple<>), typeof(ValueTuple<,>), typeof(ValueTuple<,,>), typeof(ValueTuple<,,,>),
typeof(ValueTuple<,,,,>), typeof(ValueTuple<,,,,,>), typeof(ValueTuple<,,,,,,>), typeof(ValueTuple<,,,,,,,>)
];
return valueTupleTypes.Contains(actualType.GetGenericTypeDefinition());
}
private CS.ITuple ToTuple(object? changed, int index)
{
_tuple[index] = changed;
// I can't seem to make this work using .GetMethod.
// If you know of a better way of doing this... please do.
return (CS.ITuple)typeof(ValueTuple).GetMethods()
.First(x => x is { Name: nameof(ValueTuple.Create), IsGenericMethod: true }
&& x.GetParameters().Length == _tuple.Count)
.MakeGenericMethod(_actualType!.GenericTypeArguments)
.Invoke(null, _tuple.ToArray())!;
}
private VVPropEditor CreateBox<T>(T? entry, BoxContainer parent)
{
var editor = _viewVariables.PropertyFor(entry?.GetType());
// We disallow editing of serverside-only tuples because, uh, I don't
// know how to make it work. Presumably it'd have to be something
// similarly cursed to what I did in ToTuple above.
parent.AddChild(editor.Initialize(entry, _readOnly));
return editor;
}
// Allow selecting, for example, dictionaries within the tuple.
// Wait, why do you have a field with a tuple that holds a dictionary??
public override void WireNetworkSelector(uint sessionId, object[] selectorChain)
{
for (var i = 0; i < _editors.Count; i++)
{
object[] chain = [..selectorChain, new ViewVariablesTupleIndexSelector(i)];
_editors[i].WireNetworkSelector(sessionId, chain);
}
}
}

View File

@@ -11,6 +11,7 @@ using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
using CS = System.Runtime.CompilerServices;
namespace Robust.Client.ViewVariables;
@@ -63,6 +64,8 @@ internal sealed class ViewVariableControlFactory : IViewVariableControlFactory
|| type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>),
_ => new VVPropEditorKeyValuePair()
);
RegisterForAssignableFrom<CS.ITuple>(_ => new VVPropEditorTuple());
RegisterForAssignableFrom<SoundSpecifier>(_ => new VVPropEditorSoundSpecifier(_protoManager, _resManager));
RegisterForAssignableFrom<ISelfSerialize>(type => CreateGenericEditor(type, typeof(VVPropEditorISelfSerializable<>)));
RegisterForAssignableFrom<ViewVariablesBlobMembers.PrototypeReferenceToken>(type => CreateGenericEditor(type, typeof(VVPropEditorIPrototype<>)));

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Robust.Server.ViewVariables.Traits;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
@@ -135,6 +136,10 @@ namespace Robust.Server.ViewVariables
dynamic kv = value;
value = kvPair.Key ? kv.Key : kv.Value;
break;
case ViewVariablesTupleIndexSelector indexSelector
when value is ITuple tuple:
value = indexSelector.Index <= tuple.Length - 1 ? tuple[indexSelector.Index] : null;
break;
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Robust.Server.ViewVariables.Traits;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
@@ -141,6 +142,19 @@ namespace Robust.Server.ViewVariables
};
}
// Handle ValueTuples
if (typeof(ITuple).IsAssignableFrom(valType))
{
var tuple = (ITuple)value;
var items = new object?[tuple.Length];
for (var i = 0; i < tuple.Length; i++)
{
items[i] = MakeValueNetSafe(tuple[i]);
}
return new ViewVariablesBlobMembers.ServerTupleToken { Items = items };
}
// Can't send this value type over the wire.
return new ViewVariablesBlobMembers.ServerValueTypeToken
{

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
@@ -132,6 +133,19 @@ namespace Robust.Shared.ViewVariables
public object Value { get; set; }
}
/// <summary>
/// Wrapper for a non-serializable value-type tuple.
/// </summary>
[Serializable, NetSerializable]
public sealed class ServerTupleToken : ITuple
{
public object[] Items { get; set; }
public object this[int index] => Items[index];
public int Length => Items.Length;
}
/// <summary>
/// Data for a specific property.
/// </summary>

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections;
using System.Runtime.CompilerServices;
using Robust.Shared.Serialization;
namespace Robust.Shared.ViewVariables
@@ -38,6 +39,17 @@ namespace Robust.Shared.ViewVariables
public int Index { get; set; }
}
/// <summary>
/// Used within <see cref="ViewVariablesSessionRelativeSelector.PropertyIndex"/>
/// to refer to an index of a tuple. This should match what would be used
/// with <see cref="ITuple.get_Item"/>.
/// </summary>
[Serializable, NetSerializable]
public sealed class ViewVariablesTupleIndexSelector(int index)
{
public int Index { get; set; } = index;
}
[Serializable, NetSerializable]
public sealed class ViewVariablesSelectorKeyValuePair
{