Skip to content

Commit 3c6a024

Browse files
authored
Auto-generate prototype variants at runtime (space-wizards#6692)
1 parent 2e4aa09 commit 3c6a024

7 files changed

Lines changed: 578 additions & 32 deletions

File tree

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
using JetBrains.Annotations;
2+
using NUnit.Framework;
3+
using Robust.Shared.GameObjects;
4+
using Robust.Shared.IoC;
5+
using Robust.Shared.Prototypes;
6+
using Robust.Shared.Serialization.Manager;
7+
using Robust.Shared.Serialization.Manager.Attributes;
8+
9+
namespace Robust.UnitTesting.Shared.Prototypes;
10+
11+
[UsedImplicitly]
12+
[TestFixture]
13+
internal sealed partial class PrototypeVariantizationTest : OurRobustUnitTest
14+
{
15+
private const string TestProtoId = "TestPrototype";
16+
private const string TestProtoVariantAId = "TestPrototypeVariantA";
17+
private const string TestProtoVariantBId = "TestPrototypeVariantB";
18+
19+
private IPrototypeManager protoManager = default!;
20+
21+
protected override Type[] ExtraComponents => new[] { typeof(PrototypeVariantizationTestComponent) };
22+
23+
[OneTimeSetUp]
24+
public void Setup()
25+
{
26+
IoCManager.Resolve<ISerializationManager>().Initialize();
27+
protoManager = IoCManager.Resolve<IPrototypeManager>();
28+
protoManager.Initialize();
29+
protoManager.LoadString(DOCUMENT, changed: new());
30+
protoManager.ResolveResults();
31+
}
32+
33+
/// <summary>
34+
/// Tests that the prototypes defined in the test YAML document, as well as their expected variants,
35+
/// are properly generated and can be resolved from the prototype manager.
36+
/// </summary>
37+
[Test]
38+
public void TestPrototypesExist()
39+
{
40+
Assert.Multiple(() =>
41+
{
42+
Assert.That(protoManager.Resolve<EntityPrototype>(TestProtoId, out var _));
43+
Assert.That(protoManager.Resolve<EntityPrototype>(TestProtoVariantAId, out _));
44+
Assert.That(protoManager.Resolve<EntityPrototype>(TestProtoVariantBId, out _));
45+
});
46+
}
47+
48+
/// <summary>
49+
/// Tests that the value modifications defined in the test YAML document are properly applied to all variants.
50+
/// </summary>
51+
[Test]
52+
public void TestValueModification()
53+
{
54+
// Original
55+
Assert.Multiple(() =>
56+
{
57+
var testProto = protoManager.Index<EntityPrototype>(TestProtoId);
58+
Assert.That(testProto.Components, Contains.Key("PrototypeVariantizationTest"));
59+
60+
var comp = testProto.Components["PrototypeVariantizationTest"].Component as PrototypeVariantizationTestComponent;
61+
Assert.That(comp, Is.Not.Null);
62+
Assert.That(comp!.NumValue, Is.EqualTo(1));
63+
Assert.That(comp!.EnumValue, Is.EqualTo(PrototypeVariantizationTestEnum.First));
64+
Assert.That(comp!.StringValue, Is.EqualTo("string-1"));
65+
Assert.That(comp!.StringArray.SequenceEqual(["el-1"]));
66+
67+
Assert.That(comp!.StringDict.TryGetValue("key1", out var value1));
68+
Assert.That(value1, Is.EqualTo("string-1a"));
69+
Assert.That(comp!.StringDict.TryGetValue("key2", out var value2));
70+
Assert.That(value2, Is.EqualTo("string-1b"));
71+
72+
Assert.That(comp!.RecursiveRecord?.Child?.Child?.Value, Is.EqualTo("child-1"));
73+
});
74+
75+
// First variant
76+
Assert.Multiple(() =>
77+
{
78+
var testProtoVariantA = protoManager.Index<EntityPrototype>(TestProtoVariantAId);
79+
Assert.That(testProtoVariantA.Components, Contains.Key("PrototypeVariantizationTest"));
80+
81+
var compVariantA = testProtoVariantA.Components["PrototypeVariantizationTest"].Component as PrototypeVariantizationTestComponent;
82+
Assert.That(compVariantA, Is.Not.Null);
83+
Assert.That(compVariantA!.NumValue, Is.EqualTo(1.5f));
84+
Assert.That(compVariantA!.EnumValue, Is.EqualTo(PrototypeVariantizationTestEnum.Second));
85+
Assert.That(compVariantA!.StringValue, Is.EqualTo("string-2"));
86+
Assert.That(compVariantA!.StringArray.SequenceEqual(["el-1", "el-2"]));
87+
88+
Assert.That(compVariantA!.StringDict.TryGetValue("key1", out var value1));
89+
Assert.That(value1, Is.EqualTo("string-2a"));
90+
Assert.That(compVariantA!.StringDict.TryGetValue("key2", out var value2));
91+
Assert.That(value2, Is.EqualTo("string-2b"));
92+
93+
Assert.That(compVariantA!.RecursiveRecord?.Child?.Child?.Value, Is.EqualTo("child-2"));
94+
});
95+
96+
// Second variant
97+
Assert.Multiple(() =>
98+
{
99+
var testProtoVariantB = protoManager.Index<EntityPrototype>(TestProtoVariantBId);
100+
Assert.That(testProtoVariantB.Components, Contains.Key("PrototypeVariantizationTest"));
101+
102+
var compVariantB = testProtoVariantB.Components["PrototypeVariantizationTest"].Component as PrototypeVariantizationTestComponent;
103+
Assert.That(compVariantB, Is.Not.Null);
104+
Assert.That(compVariantB!.NumValue, Is.EqualTo(2));
105+
Assert.That(compVariantB!.EnumValue, Is.EqualTo(PrototypeVariantizationTestEnum.Third));
106+
Assert.That(compVariantB!.StringValue, Is.EqualTo("string-3"));
107+
Assert.That(compVariantB!.StringArray.SequenceEqual(["el-1", "el-2", "el-3"]));
108+
109+
Assert.That(compVariantB!.StringDict.TryGetValue("key1", out var value1));
110+
Assert.That(value1, Is.EqualTo("string-3a"));
111+
Assert.That(compVariantB!.StringDict.TryGetValue("key2", out var value2));
112+
Assert.That(value2, Is.EqualTo("string-3b"));
113+
114+
Assert.That(compVariantB!.RecursiveRecord?.Child?.Child?.Value, Is.EqualTo("child-3"));
115+
});
116+
}
117+
118+
const string DOCUMENT = $@"
119+
- type: entity
120+
id: !type:CreateVariants
121+
values: [ TestPrototype, TestPrototypeVariantA, TestPrototypeVariantB ]
122+
components:
123+
- type: PrototypeVariantizationTest
124+
numValue: !type:CreateVariants
125+
values: [ 1, 1.5, 2 ]
126+
enumValue: !type:CreateVariants
127+
values: [ 0, 1, 2 ]
128+
stringValue: !type:CreateVariants
129+
values: [ string-1, string-2, string-3 ]
130+
stringArray: !type:CreateVariants
131+
sequences: [ [ el-1 ], [ el-1, el-2 ], [ el-1, el-2, el-3 ] ]
132+
stringDict:
133+
key1: !type:CreateVariants
134+
values: [ string-1a, string-2a, string-3a ]
135+
key2: !type:CreateVariants
136+
values: [ string-1b, string-2b, string-3b ]
137+
recursiveRecord: !type:RecursionTestRecord
138+
child:
139+
child:
140+
value: !type:CreateVariants
141+
values: [ child-1, child-2, child-3 ]
142+
";
143+
}
144+
145+
internal sealed partial class PrototypeVariantizationTestComponent : Component
146+
{
147+
[DataField] public float NumValue = -1;
148+
[DataField] public PrototypeVariantizationTestEnum EnumValue = PrototypeVariantizationTestEnum.Invalid;
149+
[DataField] public string StringValue = string.Empty;
150+
[DataField] public string[] StringArray = Array.Empty<string>();
151+
[DataField] public Dictionary<string, string> StringDict = new();
152+
[DataField] public RecursionTestRecord RecursiveRecord = new();
153+
}
154+
155+
[DataDefinition]
156+
internal sealed partial record RecursionTestRecord
157+
{
158+
[DataField] public RecursionTestRecord? Child = null;
159+
[DataField] public string Value = string.Empty;
160+
}
161+
162+
public enum PrototypeVariantizationTestEnum : int
163+
{
164+
Invalid = -1,
165+
First = 0,
166+
Second = 1,
167+
Third = 2,
168+
}
169+
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
using Robust.Shared.Serialization.Manager.Attributes;
2+
3+
namespace Robust.Shared.Prototypes;
4+
5+
/// <summary>
6+
/// This class is part of a mechanism that is used to automatically generate variants of prototypes at run time.
7+
/// It is substituted in the place of a value or linear collection (e.g., lists and arrays) in a YAML prototype definition.
8+
/// When the prototype manager reads this class, it will use the arrays defined here to dynamically populate the associated value/collection.
9+
/// To generate prototype variants, the ID of the prototype must also be replaced with a reference to this class.
10+
/// </summary>
11+
/// <example>
12+
/// Here is example YAML code that will auto-generate three different prototypes, each with its own distinct set of values, at runtime:
13+
///
14+
/// - type: entity
15+
/// id: !type:CreateVariants
16+
/// variants: [ TestEntityA, TestEntityB, TestEntityC ]
17+
/// components:
18+
/// - type: Test
19+
/// floatValue: !type:CreateVariants
20+
/// values: [ 0.5, 1, 1.5 ]
21+
/// enumValue: !type:CreateVariants
22+
/// values: [ 1, 2, 3 ]
23+
/// stringArray: !type:CreateVariants
24+
/// sequences: [ [ string-1 ], [ string-1, string-2 ], [ string-1, string-2, string-3 ] ]
25+
/// - type: Sprite
26+
/// sprite: path/to/overlay.rsi
27+
/// layers:
28+
/// - sprite: !type:CreateVariants
29+
/// values: [ path/to/base.rsi, path/to/base_alt1.rsi, path/to/base_alt2.rsi ]
30+
/// state: baseState
31+
/// map: ["firstLayer"]
32+
/// - state: overlayState
33+
/// map: ["secondLayer"]
34+
///
35+
/// The second prototype to be generated will have the following values:
36+
///
37+
/// - type: entity
38+
/// id: TestEntityB
39+
/// components:
40+
/// - type: Test
41+
/// floatValue: 1
42+
/// enumValue: 2
43+
/// stringArray: [ string-1, string-2 ]
44+
/// - type: Sprite
45+
/// sprite: path/to/overlay.rsi
46+
/// layers:
47+
/// - sprite: path/to/base_alt1.rsi
48+
/// state: baseState
49+
/// map: ["firstLayer"]
50+
/// - state: overlayState
51+
/// map: ["secondLayer"]
52+
/// </example>
53+
[DataDefinition]
54+
public sealed partial class CreateVariants
55+
{
56+
/// <summary>
57+
/// Use this field when replacing a single value (e.g., a number or string) in a prototype at runtime.
58+
/// </summary>
59+
[VariantValuesField]
60+
public string[]? Values;
61+
62+
/// <summary>
63+
/// Use this field when replacing a collection of values (e.g., an array) in a prototype at runtime.
64+
/// </summary>
65+
[VariantSequencesField]
66+
public string[][]? Sequences;
67+
}
68+
69+
/// <summary>
70+
/// Denotes an array that will be used to populate variants of a prototype at runtime.
71+
/// </summary>
72+
public sealed class VariantValuesFieldAttribute : DataFieldAttribute
73+
{
74+
public const string Name = "values";
75+
76+
/// <summary>
77+
/// Denotes an array that will be used to populate variants of a prototype at runtime.
78+
/// </summary>
79+
/// <param name="priority">See <see cref="DataFieldBaseAttribute.Priority"/>.</param>
80+
public VariantValuesFieldAttribute(int priority = 1) :
81+
base(Name, false, priority, false, false)
82+
{
83+
}
84+
}
85+
86+
/// <summary>
87+
/// Denotes an array that will be used to populate variants of a prototype at runtime.
88+
/// </summary>
89+
public sealed class VariantSequencesFieldAttribute : DataFieldAttribute
90+
{
91+
public const string Name = "sequences";
92+
93+
/// <summary>
94+
/// Denotes an array that will be used to populate variants of a prototype at runtime.
95+
/// </summary>
96+
/// <param name="priority">See <see cref="DataFieldBaseAttribute.Priority"/>.</param>
97+
public VariantSequencesFieldAttribute(int priority = 1) :
98+
base(Name, false, priority, false, false)
99+
{
100+
}
101+
}

Robust.Shared/Prototypes/IPrototypeManager.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,14 @@ void ReloadPrototypes(
573573
/// <param name="prototypes">List of entity prototypes that form part category or null.</param>
574574
/// <returns>True if the provided <see cref="EntityCategoryPrototype"/> id has a matching list of <see cref="EntityPrototype"/> False otherwise.</returns>
575575
bool TryGetEntityPrototypesByCategory(ProtoId<EntityCategoryPrototype> category, [NotNullWhen(true)] out IReadOnlyList<EntityPrototype>? prototypes);
576+
577+
/// <summary>
578+
/// Tries to get the list of all associated variants for a given prototype.
579+
/// </summary>
580+
/// <param name="collectionMember">The prototype being indexed.</param>
581+
/// <param name="collectionVariants">The collection of variants this prototype belongs to.</param>
582+
/// <returns>Returns true if the prototype is part of a variant collection, false otherwise.</returns>
583+
bool TryGetVariantCollection<T>(ProtoId<T> collectionMember, [NotNullWhen(true)] out List<ProtoId<T>>? collectionVariants) where T : class, IPrototype;
576584
}
577585

578586
internal interface IPrototypeManagerInternal : IPrototypeManager

0 commit comments

Comments
 (0)