Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype reload + other enhancements #5371

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ END TEMPLATE-->

### Breaking changes

*None yet*
* Prototype reloads now also re-apply modified components. For example this means sprite or physics changes are reflected on existing prototypes.

### New features

* Console completion options now have a new flags for preventing suggestions from being escaped or quoted.
* AddComponent now has an overload for ComponentRegistryEntry.

### Bugfixes

Expand Down
73 changes: 44 additions & 29 deletions Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.IoC;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;

namespace Robust.Shared.GameObjects;
Expand All @@ -12,7 +9,6 @@ namespace Robust.Shared.GameObjects;
internal sealed class PrototypeReloadSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypes = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;

public override void Initialize()
{
Expand All @@ -36,43 +32,62 @@ private void OnPrototypesReloaded(PrototypesReloadedEventArgs eventArgs)
}
}

private bool IsIgnored(EntityPrototype.ComponentRegistryEntry entry)
{
var compType = entry.Component.GetType();

if (compType == typeof(TransformComponent) || compType == typeof(MetaDataComponent))
return true;

return false;
}

private void UpdateEntity(EntityUid entity, MetaDataComponent metaData, EntityPrototype newPrototype)
{
var oldPrototype = metaData.EntityPrototype;
var modified = false;

var oldPrototypeComponents = oldPrototype?.Components.Keys
.Where(n => n != "Transform" && n != "MetaData")
.Select(name => (name, _componentFactory.GetRegistration(name).Type))
.ToList() ?? new List<(string name, Type Type)>();
if (oldPrototype != null)
{
foreach (var oldComp in oldPrototype.Components)
{
if (IsIgnored(oldComp.Value))
continue;

var newPrototypeComponents = newPrototype.Components.Keys
.Where(n => n != "Transform" && n != "MetaData")
.Select(name => (name, _componentFactory.GetRegistration(name).Type))
.ToList();
// Removed
if (!newPrototype.Components.TryGetValue(oldComp.Key, out var newComp))
{
modified = true;
RemComp(entity, oldComp.Value.Component.GetType());
continue;
}

var ignoredComponents = new List<string>();
// Modified
if (!newComp.Mapping.Equals(oldComp.Value.Mapping))
{
modified = true;
EntityManager.AddComponent(entity, newComp, overwrite: true, metaData);
}
}
Comment on lines +65 to +70
Copy link
Member

@ElectroJr ElectroJr Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes that the data on an entity's component matches the data on the prototype's component, which isn't necessarily true, especially when reloading prototypes in a post map-init map.

What this would have to do instead is closer to map loading. I.e., using methods added in #5571, I guess it'd look something like this (though this is kinda pseudo code):

var compNode = _serManager.Write(EntitysCurrentComponentInstance)
var delta = compNode.Except(oldComp.Value.Mapping)
var newNode = _serManager.CombineMappings(delta, newComp.Mapping);
var comp = (IComponent) _serManager.Read(newComp.Component.GetType(), newNode, _context)                    EntityManager.AddComponent(entity, comp, overwrite: true);

Or alternatively, instead of creating a new instance, use serialization manager to load directly onto the entity's existing comp instance, though you'd probably still want to raise the comp remove & add events to ensure that its fully refreshed and changes get networked.

}

// Find components to be removed, and remove them
foreach (var (name, type) in oldPrototypeComponents.Except(newPrototypeComponents))
foreach (var newComp in newPrototype.Components)
{
if (newPrototype.Components.ContainsKey(name))
{
ignoredComponents.Add(name);
if (IsIgnored(newComp.Value))
continue;
}

RemComp(entity, type);
}
// Existing component, handled above
if (oldPrototype?.Components.ContainsKey(newComp.Key) == true)
continue;

EntityManager.CullRemovedComponents();
// Added
modified = true;
EntityManager.AddComponent(entity, newComp.Value, overwrite: true, metadata: metaData);
}

// Add new components
foreach (var (name, _) in newPrototypeComponents.Where(t => !ignoredComponents.Contains(t.name))
.Except(oldPrototypeComponents))
if (modified)
{
var data = newPrototype.Components[name];
var component = _componentFactory.GetComponent(name);
EntityManager.AddComponent(entity, component);
EntityManager.CullRemovedComponents();
}

// Update entity metadata
Expand Down
122 changes: 119 additions & 3 deletions Robust.Shared/Prototypes/EntityPrototype.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
Expand All @@ -21,7 +22,7 @@ namespace Robust.Shared.Prototypes
/// Prototype that represents game entities.
/// </summary>
[Prototype("entity", -1)]
public sealed partial class EntityPrototype : IPrototype, IInheritingPrototype, ISerializationHooks
public sealed partial class EntityPrototype : IPrototype, IInheritingPrototype, ISerializationHooks, IEquatable<EntityPrototype>
{
private ILocalizationManager _loc = default!;

Expand Down Expand Up @@ -288,7 +289,7 @@ public override string ToString()
public record ComponentRegistryEntry(IComponent Component, MappingDataNode Mapping);

[DataDefinition]
public sealed partial class EntityPlacementProperties
public sealed partial class EntityPlacementProperties : IEquatable<EntityPlacementProperties>
{
public bool PlacementOverriden { get; private set; }
public bool SnapOverriden { get; private set; }
Expand Down Expand Up @@ -332,6 +333,27 @@ public HashSet<string> SnapFlags
_snapFlags = value;
}
}

public bool Equals(EntityPlacementProperties? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return _placementMode == other._placementMode &&
_placementOffset.Equals(other._placementOffset) &&
Equals(MountingPoints, other.MountingPoints) &&
PlacementRange == other.PlacementRange &&
_snapFlags.SetEquals(other._snapFlags);
}

public override bool Equals(object? obj)
{
return ReferenceEquals(this, obj) || obj is EntityPlacementProperties other && Equals(other);
}

public override int GetHashCode()
{
return HashCode.Combine(_placementMode, _placementOffset, MountingPoints, PlacementRange, _snapFlags);
}
}
/*private class PrototypeSerializationContext : YamlObjectSerializer.Context
{
Expand Down Expand Up @@ -400,9 +422,71 @@ public override bool TryGetDataCache(string field, out object? value)
return prototype.DataCache.TryGetValue(field, out value);
}
}*/
public bool Equals(EntityPrototype? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
var result = ID == other.ID &&
Equals(_locPropertiesSet, other._locPropertiesSet) &&
Equals(CategoriesInternal, other.CategoriesInternal) &&
PlacementProperties.Equals(other.PlacementProperties) &&
SetName == other.SetName &&
SetDesc == other.SetDesc &&
SetSuffix == other.SetSuffix &&
CustomLocalizationID == other.CustomLocalizationID &&
HideSpawnMenu == other.HideSpawnMenu &&
MapSavable == other.MapSavable &&
Abstract == other.Abstract;

if (!result)
return false;

if ((Parents == null && other.Parents != null) ||
(Parents != null && other.Parents == null))
{
return false;
}

if (Parents != null && other.Parents != null)
{
for (var i = 0; i < Parents.Length; i++)
{
if (Parents[i] != other.Parents[i])
return false;
}
}

return Components.Equals(other.Components);
}

public override bool Equals(object? obj)
{
return ReferenceEquals(this, obj) || obj is EntityPrototype other && Equals(other);
}

public override int GetHashCode()
{
var hashCode = new HashCode();
hashCode.Add(_loc);
hashCode.Add(_locPropertiesSet);
hashCode.Add(CategoriesInternal);
hashCode.Add(PlacementProperties);
hashCode.Add(ID);
hashCode.Add(SetName);
hashCode.Add(SetDesc);
hashCode.Add(SetSuffix);
hashCode.Add(Categories);
hashCode.Add(CustomLocalizationID);
hashCode.Add(HideSpawnMenu);
hashCode.Add(MapSavable);
hashCode.Add(Parents);
hashCode.Add(Abstract);
hashCode.Add(Components);
return hashCode.ToHashCode();
}
}

public sealed class ComponentRegistry : Dictionary<string, EntityPrototype.ComponentRegistryEntry>, IEntityLoadContext, ISerializationContext
public sealed class ComponentRegistry : Dictionary<string, EntityPrototype.ComponentRegistryEntry>, IEntityLoadContext, ISerializationContext, IEquatable<ComponentRegistry>
{
public ComponentRegistry()
{
Expand Down Expand Up @@ -432,5 +516,37 @@ public bool ShouldSkipComponent(string compName)

public SerializationManager.SerializerProvider SerializerProvider { get; } = new();
public bool WritingReadingPrototypes { get; } = true;

public bool Equals(ComponentRegistry? other)
{
Comment on lines +520 to +521
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This equality override, and thus also the EntityPrototype one, can actually pretty slow, because they go via MappingDataNode.Except(). This could suddenly make anything that uses a HashSet<EntityPrototype> or dictionary much slower.

Assuming these were just added specifically to do the equivalence check for prototype reloading, I'd rather that just be a separate method that has to be intentionally called. Removing the hashcode overrides would also remove all the Non-readonly field referenced in 'GetHashCode()' warnings/suggestions.

if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;

if (other.Count != Count)
return false;

foreach (var (id, component) in this)
{
if (!other.TryGetValue(id, out var otherComponent))
continue;

if (!component.Mapping.Equals(otherComponent.Mapping))
{
return false;
}
}

return true;
}

public override bool Equals(object? obj)
{
return ReferenceEquals(this, obj) || obj is ComponentRegistry other && Equals(other);
}

public override int GetHashCode()
{
return HashCode.Combine(SerializerProvider, WritingReadingPrototypes);
}
}
}
Loading
Loading