Skip to content

Commit bdef9e3

Browse files
authored
Fix reloading prototypes with AlwaysPushInheritance (#5612)
1 parent 4364820 commit bdef9e3

File tree

8 files changed

+138
-82
lines changed

8 files changed

+138
-82
lines changed

RELEASE-NOTES.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ END TEMPLATE-->
4343

4444
### Bugfixes
4545

46-
*None yet*
46+
* Fixed prototype reloading/hotloading not properly handling data-fields with the `AlwaysPushInheritanceAttribute`
4747

4848
### Other
4949

Robust.Client/Replays/Loading/ReplayLoadManager.Checkpoints.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,9 @@ private void LoadPrototype(
352352
// prototype changes when jumping around in time. This also requires reworking how the initial
353353
// implicit state data is generated, because we can't simply cache it anymore.
354354
// Also, does reloading prototypes in release mode modify existing entities?
355+
// Yes, yes it does. See PrototypeReloadSystem.UpdateEntity()
356+
// Its just not supported ATM.
357+
// TBH it'd be easier if overriding existing prototypes in release mode was just forbidden.
355358

356359
var msg = $"Overwriting an existing prototype! Kind: {kind.Name}. Ids: {string.Join(", ", ids)}";
357360
if (_confMan.GetCVar(CVars.ReplayIgnoreErrors))
@@ -361,7 +364,6 @@ private void LoadPrototype(
361364
}
362365
}
363366

364-
_protoMan.ResolveResults();
365367
_protoMan.ReloadPrototypes(changed);
366368
_locMan.ReloadLocalizations();
367369
}

Robust.Shared/Prototypes/IPrototypeManager.cs

+10-4
Original file line numberDiff line numberDiff line change
@@ -284,15 +284,21 @@ Dictionary<string, HashSet<ErrorNode>> ValidateDirectory(ResPath path,
284284
void LoadDefaultPrototypes(Dictionary<Type, HashSet<string>>? loaded = null);
285285

286286
/// <summary>
287-
/// Syncs all inter-prototype data. Call this when operations adding new prototypes are done.
287+
/// Call this when operations adding new prototypes are done.
288+
/// This will handle prototype inheritance, instance creation, and update entity categories.
289+
/// When loading extra prototypes, or reloading a subset of existing prototypes, you should probably use
290+
/// <see cref="ReloadPrototypes"/> instead.
288291
/// </summary>
289292
void ResolveResults();
290293

291294
/// <summary>
292-
/// Invokes <see cref="PrototypesReloaded"/> with information about the modified prototypes.
293-
/// When built with development tools, this will also push inheritance for reloaded prototypes/
295+
/// This should be called after new or updated prototypes ahve been loaded.
296+
/// This will handle prototype inheritance, instance creation, and update entity categories.
297+
/// It will also invoke <see cref="PrototypesReloaded"/> and raise a <see cref="PrototypesReloadedEventArgs"/>
298+
/// event with information about the modified prototypes.
294299
/// </summary>
295-
void ReloadPrototypes(Dictionary<Type, HashSet<string>> modified,
300+
void ReloadPrototypes(
301+
Dictionary<Type, HashSet<string>> modified,
296302
Dictionary<Type, HashSet<string>>? removed = null);
297303

298304
/// <summary>

Robust.Shared/Prototypes/PrototypeManager.YamlLoad.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,10 @@ private void MergeMapping(
209209

210210
var kindData = _kinds[kind];
211211

212-
if (!overwrite && kindData.Results.ContainsKey(id))
212+
if (!overwrite && kindData.RawResults.ContainsKey(id))
213213
throw new PrototypeLoadException($"Duplicate ID: '{id}' for kind '{kind}");
214214

215-
kindData.Results[id] = data;
215+
kindData.RawResults[id] = data;
216216

217217
if (kindData.Inheritance is { } inheritance)
218218
{
@@ -295,6 +295,7 @@ public void RemoveString(string prototypes)
295295
kindData.UnfrozenInstances ??= kindData.Instances.ToDictionary();
296296
kindData.UnfrozenInstances.Remove(id);
297297
kindData.Results.Remove(id);
298+
kindData.RawResults.Remove(id);
298299
modified.Add(kindData);
299300
}
300301
}

Robust.Shared/Prototypes/PrototypeManager.cs

+82-57
Original file line numberDiff line numberDiff line change
@@ -335,98 +335,117 @@ protected void ReloadPrototypes(IEnumerable<ResPath> filePaths)
335335
}
336336

337337
/// <inheritdoc />
338-
public void ReloadPrototypes(Dictionary<Type, HashSet<string>> modified,
338+
public void ReloadPrototypes(
339+
Dictionary<Type, HashSet<string>> modified,
339340
Dictionary<Type, HashSet<string>>? removed = null)
340341
{
341-
#if TOOLS
342342
var prototypeTypeOrder = modified.Keys.ToList();
343343
prototypeTypeOrder.Sort(SortPrototypesByPriority);
344344

345-
var pushed = new Dictionary<Type, HashSet<string>>();
345+
var byType = new Dictionary<Type, PrototypesReloadedEventArgs.PrototypeChangeSet>();
346346
var modifiedKinds = new HashSet<KindData>();
347+
var toProcess = new HashSet<string>();
348+
var processQueue = new Queue<string>();
347349

348350
foreach (var kind in prototypeTypeOrder)
349351
{
352+
var modifiedInstances = new Dictionary<string, IPrototype>();
350353
var kindData = _kinds[kind];
351-
if (!kind.IsAssignableTo(typeof(IInheritingPrototype)))
352-
{
353-
foreach (var id in modified[kind])
354-
{
355-
var prototype = (IPrototype)_serializationManager.Read(kind, kindData.Results[id])!;
356-
kindData.UnfrozenInstances ??= kindData.Instances.ToDictionary();
357-
kindData.UnfrozenInstances[id] = prototype;
358-
modifiedKinds.Add(kindData);
359-
}
360354

361-
continue;
362-
}
355+
var tree = kindData.Inheritance;
356+
toProcess.Clear();
357+
processQueue.Clear();
358+
359+
DebugTools.AssertEqual(kind.IsAssignableTo(typeof(IInheritingPrototype)), tree != null);
360+
DebugTools.Assert(tree != null || kindData.RawResults == kindData.Results);
363361

364-
var tree = kindData.Inheritance!;
365-
var processQueue = new Queue<string>();
366362
foreach (var id in modified[kind])
367363
{
364+
AddToQueue(id);
365+
}
366+
367+
void AddToQueue(string id)
368+
{
369+
if (!toProcess.Add(id))
370+
return;
368371
processQueue.Enqueue(id);
372+
373+
if (tree == null)
374+
return;
375+
376+
if (!tree.TryGetChildren(id, out var children))
377+
return;
378+
379+
foreach (var child in children!)
380+
{
381+
AddToQueue(child);
382+
}
369383
}
370384

371385
while (processQueue.TryDequeue(out var id))
372386
{
373-
var pushedSet = pushed.GetOrNew(kind);
374-
375-
if (tree.TryGetParents(id, out var parents))
387+
DebugTools.Assert(toProcess.Contains(id));
388+
if (tree != null)
376389
{
377-
var nonPushedParent = false;
378-
foreach (var parent in parents)
390+
if (tree.TryGetParents(id, out var parents))
379391
{
380-
//our parent has been reloaded and has not been added to the pushedSet yet
381-
if (modified[kind].Contains(parent) && !pushedSet.Contains(parent))
392+
DebugTools.Assert(parents.Length > 0);
393+
var nonPushedParent = false;
394+
foreach (var parent in parents)
382395
{
383-
//we re-queue ourselves at the end of the queue
396+
if (!toProcess.Contains(parent))
397+
continue;
398+
399+
// our parent has been modified, but has not yet been processed.
400+
// we re-queue ourselves at the end of the queue.
401+
DebugTools.Assert(processQueue.Contains(parent));
384402
processQueue.Enqueue(id);
385403
nonPushedParent = true;
386404
break;
387405
}
388-
}
389406

390-
if (nonPushedParent)
391-
continue;
407+
if (nonPushedParent)
408+
continue;
392409

393-
var parentMaps = new MappingDataNode[parents.Length];
394-
for (var i = 0; i < parentMaps.Length; i++)
410+
var parentMaps = new MappingDataNode[parents.Length];
411+
for (var i = 0; i < parentMaps.Length; i++)
412+
{
413+
parentMaps[i] = kindData.Results[parents[i]];
414+
}
415+
416+
kindData.Results[id] = _serializationManager.PushCompositionWithGenericNode(
417+
kind,
418+
parentMaps,
419+
kindData.RawResults[id]);
420+
}
421+
else
395422
{
396-
parentMaps[i] = kindData.Results[parents[i]];
423+
kindData.Results[id] = kindData.RawResults[id];
397424
}
398-
399-
kindData.Results[id] = _serializationManager.PushCompositionWithGenericNode(
400-
kind,
401-
parentMaps,
402-
kindData.Results[id]);
403425
}
404426

427+
toProcess.Remove(id);
405428

406429
var prototype = TryReadPrototype(kind, id, kindData.Results[id], SerializationHookContext.DontSkipHooks);
407-
if (prototype != null)
408-
{
409-
kindData.UnfrozenInstances ??= kindData.Instances.ToDictionary();
410-
kindData.UnfrozenInstances[id] = prototype;
411-
modifiedKinds.Add(kindData);
412-
}
430+
if (prototype == null)
431+
continue;
413432

414-
pushedSet.Add(id);
433+
kindData.UnfrozenInstances ??= kindData.Instances.ToDictionary();
434+
kindData.UnfrozenInstances[id] = prototype;
435+
modifiedInstances.Add(id, prototype);
415436
}
437+
438+
if (modifiedInstances.Count == 0)
439+
continue;
440+
441+
byType.Add(kindData.Type, new(modifiedInstances));
442+
modifiedKinds.Add(kindData);
416443
}
417444

418445
Freeze(modifiedKinds);
446+
419447
if (modifiedKinds.Any(x => x.Type == typeof(EntityPrototype) || x.Type == typeof(EntityCategoryPrototype)))
420448
UpdateCategories();
421-
#endif
422-
423-
//todo paul i hate it but i am not opening that can of worms in this refactor
424-
var byType = modified
425-
.ToDictionary(
426-
g => g.Key,
427-
g => new PrototypesReloadedEventArgs.PrototypeChangeSet(
428-
g.Value.Where(x => _kinds[g.Key].Instances.ContainsKey(x))
429-
.ToDictionary(a => a, a => _kinds[g.Key].Instances[a])));
430449

431450
var modifiedTypes = new HashSet<Type>(byType.Keys);
432451
if (removed != null)
@@ -592,11 +611,9 @@ private async Task PushKindInheritance(Type kind, KindData data)
592611

593612
// var sw = RStopwatch.StartNew();
594613

595-
var results = new Dictionary<string, InheritancePushDatum>(
596-
data.Results.Select(k => new KeyValuePair<string, InheritancePushDatum>(
597-
k.Key,
598-
new InheritancePushDatum(k.Value, tree.GetParentsCount(k.Key))))
599-
);
614+
var results = data.RawResults.ToDictionary(
615+
k => k.Key,
616+
k => new InheritancePushDatum(k.Value, tree.GetParentsCount(k.Key)));
600617

601618
using var countDown = new CountdownEvent(results.Count);
602619

@@ -1015,6 +1032,8 @@ private void RegisterKind(Type kind, Dictionary<Type, KindData> kinds)
10151032

10161033
if (kind.IsAssignableTo(typeof(IInheritingPrototype)))
10171034
kindData.Inheritance = new MultiRootInheritanceGraph<string>();
1035+
else
1036+
kindData.Results = kindData.RawResults;
10181037
}
10191038

10201039
/// <inheritdoc />
@@ -1026,7 +1045,13 @@ private sealed class KindData(Type kind, string name)
10261045

10271046
public FrozenDictionary<string, IPrototype> Instances = FrozenDictionary<string, IPrototype>.Empty;
10281047

1029-
public readonly Dictionary<string, MappingDataNode> Results = new();
1048+
public Dictionary<string, MappingDataNode> Results = new();
1049+
1050+
/// <summary>
1051+
/// Variant of <see cref="Results"/> prior to inheritance pushing. If the kind does not have inheritance,
1052+
/// then this is just the same dictionary.
1053+
/// </summary>
1054+
public readonly Dictionary<string, MappingDataNode> RawResults = new();
10301055

10311056
public readonly Type Type = kind;
10321057
public readonly string Name = name;

Robust.Shared/Serialization/Manager/SerializationManager.Composition.cs

+23-16
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,27 @@ private delegate DataNode PushCompositionDelegate(
2626

2727
public DataNode PushComposition(Type type, DataNode[] parents, DataNode child, ISerializationContext? context = null)
2828
{
29+
if (parents.Length == 0)
30+
return child.Copy();
31+
2932
DebugTools.Assert(parents.All(x => x.GetType() == child.GetType()));
3033

34+
// TODO SERIALIZATION
35+
// Change inheritance pushing so that it modifies the passed in child.
36+
// I.e., move the child.Clone() statement to the beginning here, then make the delegate modify the clone.
37+
// Currently pusing more than one parent requires multiple unnecessary clones.
38+
3139
var pusher = GetOrCreatePushCompositionDelegate(type, child);
3240

3341
var node = child;
34-
for (int i = 0; i < parents.Length; i++)
42+
foreach (var parent in parents)
3543
{
36-
node = pusher(type, parents[i], node, context);
44+
var newNode = pusher(type, parent, node, context);
45+
46+
// Currently delegate pusher should be returning a new instance, and not modifying the passed in child.
47+
DebugTools.Assert(!ReferenceEquals(newNode, node));
48+
49+
node = newNode;
3750
}
3851

3952
return node;
@@ -95,7 +108,7 @@ private PushCompositionDelegate GetOrCreatePushCompositionDelegate(Type type, Da
95108
Expression.Convert(parentParam, nodeType)),
96109
MappingDataNode => Expression.Call(
97110
instanceConst,
98-
nameof(PushInheritanceMapping),
111+
nameof(CombineMappings),
99112
Type.EmptyTypes,
100113
Expression.Convert(childParam, nodeType),
101114
Expression.Convert(parentParam, nodeType)),
@@ -117,32 +130,26 @@ private SequenceDataNode PushInheritanceSequence(SequenceDataNode child, Sequenc
117130
//todo implement different inheritancebehaviours for yamlfield
118131
// I have NFI what this comment means.
119132

120-
var result = new SequenceDataNode(child.Count + parent.Count);
133+
var result = child.Copy();
121134
foreach (var entry in parent)
122135
{
123-
result.Add(entry);
124-
}
125-
foreach (var entry in child)
126-
{
127-
result.Add(entry);
136+
result.Add(entry.Copy());
128137
}
129138

130139
return result;
131140
}
132141

133-
private MappingDataNode PushInheritanceMapping(MappingDataNode child, MappingDataNode parent)
142+
public MappingDataNode CombineMappings(MappingDataNode child, MappingDataNode parent)
134143
{
135144
//todo implement different inheritancebehaviours for yamlfield
136145
// I have NFI what this comment means.
146+
// I still don't know what it means, but if it's talking about the always/never push inheritance attributes,
147+
// make sure it doesn't break entity serialization.
137148

138-
var result = new MappingDataNode(child.Count + parent.Count);
149+
var result = child.Copy();
139150
foreach (var (k, v) in parent)
140151
{
141-
result[k] = v;
142-
}
143-
foreach (var (k, v) in child)
144-
{
145-
result[k] = v;
152+
result.TryAddCopy(k, v);
146153
}
147154

148155
return result;

Robust.Shared/Serialization/Markdown/Mapping/MappingDataNode.cs

+16
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Collections.Generic;
44
using System.Diagnostics.CodeAnalysis;
55
using System.Linq;
6+
using System.Runtime.InteropServices;
67
using System.Threading;
78
using Robust.Shared.Serialization.Markdown.Value;
89
using Robust.Shared.Utility;
@@ -396,5 +397,20 @@ public bool Remove(KeyValuePair<DataNode, DataNode> item)
396397

397398
public int Count => _children.Count;
398399
public bool IsReadOnly => false;
400+
401+
public bool TryAdd(DataNode key, DataNode value)
402+
{
403+
return _children.TryAdd(key, value);
404+
}
405+
406+
public bool TryAddCopy(DataNode key, DataNode value)
407+
{
408+
ref var entry = ref CollectionsMarshal.GetValueRefOrAddDefault(_children, key, out var exists);
409+
if (exists)
410+
return false;
411+
412+
entry = value.Copy();
413+
return true;
414+
}
399415
}
400416
}

Robust.Shared/Upload/SharedPrototypeLoadManager.cs

-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ protected virtual void LoadPrototypeData(GamePrototypeLoadMessage message)
4242

4343
var changed = new Dictionary<Type, HashSet<string>>();
4444
_prototypeManager.LoadString(data, true, changed);
45-
_prototypeManager.ResolveResults();
4645
_prototypeManager.ReloadPrototypes(changed);
4746
_localizationManager.ReloadLocalizations();
4847

0 commit comments

Comments
 (0)