Add WeakEntityReference (#5577)

* Add WeakEntityReference

* Use NetEntity

* release notes

* A

* Fix merge conflicts

* comments

* A

* Add network serialization test

* Add ToPrettyString support for WeakEntityReference?

* inheritdoc

* Add GetWeakReference methods

* Not-nullable too

* Make EntitySystem proxy method signatures match EntityManager

* Add TryGetEntity

* interface

* fix test

* De-ref GetWeakReference methods

---------

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
This commit is contained in:
Leon Friedrich
2025-07-30 03:12:49 +12:00
committed by GitHub
parent 8498634993
commit c3489d4ded
19 changed files with 651 additions and 61 deletions

View File

@@ -39,7 +39,7 @@ END TEMPLATE-->
### New features
*None yet*
* Added a new `WeakEntityReference` struct that is intended to be used by component data-fields to refer to entities that may or may not still exist.
### Bugfixes

View File

@@ -33,7 +33,8 @@ namespace Robust.Shared.EntitySerialization;
public sealed class EntityDeserializer :
ISerializationContext,
ITypeSerializer<EntityUid, ValueDataNode>,
ITypeSerializer<NetEntity, ValueDataNode>
ITypeSerializer<NetEntity, ValueDataNode>,
ITypeSerializer<WeakEntityReference, ValueDataNode>
{
// See the comments around EntitySerializer's version const for information about the different versions.
// TBH version three isn't even really fully supported anymore, simply due to changes in engine component serialization.
@@ -1218,5 +1219,46 @@ public sealed class EntityDeserializer :
: new ValueDataNode("invalid");
}
WeakEntityReference ITypeReader<WeakEntityReference, ValueDataNode>.Read(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context,
ISerializationManager.InstantiationDelegate<WeakEntityReference>? instanceProvider)
{
var uid = serializationManager.Read<EntityUid>(node, context);
return EntMan.TryGetNetEntity(uid, out var nent)
? new(nent.Value)
: WeakEntityReference.Invalid;
}
DataNode ITypeWriter<WeakEntityReference>.Write(
ISerializationManager serializationManager,
WeakEntityReference value,
IDependencyCollection dependencies,
bool alwaysWrite,
ISerializationContext? context)
{
return value != WeakEntityReference.Invalid
? new ValueDataNode(value.Entity.Id.ToString(CultureInfo.InvariantCulture))
: new ValueDataNode("invalid");
}
ValidationNode ITypeValidator<WeakEntityReference, ValueDataNode>.Validate(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context)
{
if (node.Value is "invalid")
return new ValidatedValueNode(node);
if (!int.TryParse(node.Value, out _))
return new ErrorNode(node, "Invalid NetEntity");
return new ValidatedValueNode(node);
}
#endregion
}

View File

@@ -38,7 +38,8 @@ namespace Robust.Shared.EntitySerialization;
/// </remarks>
public sealed class EntitySerializer : ISerializationContext,
ITypeSerializer<EntityUid, ValueDataNode>,
ITypeSerializer<NetEntity, ValueDataNode>
ITypeSerializer<NetEntity, ValueDataNode>,
ITypeSerializer<WeakEntityReference, ValueDataNode>
{
public const int MapFormatVersion = 7;
// v6->v7: PR #5572 - Added more metadata, List maps/grids/orphans, include some life-stage information
@@ -868,12 +869,12 @@ public sealed class EntitySerializer : ISerializationContext,
return new ValidatedValueNode(node);
}
public DataNode Write(
DataNode ITypeWriter<EntityUid>.Write(
ISerializationManager serializationManager,
EntityUid value,
IDependencyCollection dependencies,
bool alwaysWrite = false,
ISerializationContext? context = null)
bool alwaysWrite,
ISerializationContext? context)
{
if (YamlUidMap.TryGetValue(value, out var yamlId))
return new ValueDataNode(yamlId.ToString(CultureInfo.InvariantCulture));
@@ -947,11 +948,11 @@ public sealed class EntitySerializer : ISerializationContext,
return node.Value == "invalid" ? EntityUid.Invalid : EntityUid.Parse(node.Value);
}
public ValidationNode Validate(
ValidationNode ITypeValidator<NetEntity, ValueDataNode>.Validate(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context = null)
ISerializationContext? context)
{
if (node.Value == "invalid")
return new ValidatedValueNode(node);
@@ -962,27 +963,68 @@ public sealed class EntitySerializer : ISerializationContext,
return new ValidatedValueNode(node);
}
public NetEntity Read(
NetEntity ITypeReader<NetEntity, ValueDataNode>.Read(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context = null,
ISerializationManager.InstantiationDelegate<NetEntity>? instanceProvider = null)
ISerializationContext? context,
ISerializationManager.InstantiationDelegate<NetEntity>? instanceProvider)
{
return node.Value == "invalid" ? NetEntity.Invalid : NetEntity.Parse(node.Value);
}
public DataNode Write(
DataNode ITypeWriter<NetEntity>.Write(
ISerializationManager serializationManager,
NetEntity value,
IDependencyCollection dependencies,
bool alwaysWrite = false,
ISerializationContext? context = null)
bool alwaysWrite,
ISerializationContext? context)
{
var uid = EntMan.GetEntity(value);
return serializationManager.WriteValue(uid, alwaysWrite, context);
}
ValidationNode ITypeValidator<WeakEntityReference, ValueDataNode>.Validate(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context)
{
if (node.Value == "invalid")
return new ValidatedValueNode(node);
if (!int.TryParse(node.Value, out _))
return new ErrorNode(node, "Invalid NetEntity");
return new ValidatedValueNode(node);
}
WeakEntityReference ITypeReader<WeakEntityReference, ValueDataNode>.Read(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context,
ISerializationManager.InstantiationDelegate<WeakEntityReference>? instanceProvider)
{
return node.Value == "invalid"
? WeakEntityReference.Invalid
: new(NetEntity.Parse(node.Value));
}
DataNode ITypeWriter<WeakEntityReference>.Write(
ISerializationManager serializationManager,
WeakEntityReference value,
IDependencyCollection dependencies,
bool alwaysWrite,
ISerializationContext? context)
{
if (EntMan.TryGetEntity(value.Entity, out var uid) && YamlUidMap.TryGetValue(uid.Value, out var yamlId))
return new ValueDataNode(yamlId.ToString(CultureInfo.InvariantCulture));
return new ValueDataNode("invalid");
}
#endregion
}

View File

@@ -1391,6 +1391,94 @@ namespace Robust.Shared.GameObjects
return new CompRegistryEntityEnumerator(this, trait1, registry);
}
#region WeakEntityReference
public WeakEntityReference GetWeakReference(EntityUid uid, MetaDataComponent? meta = null)
{
return new WeakEntityReference(GetNetEntity(uid, meta));
}
public WeakEntityReference? GetWeakReference(EntityUid? uid, MetaDataComponent? meta = null)
{
if (uid == null)
return null;
return new WeakEntityReference(GetNetEntity(uid.Value, meta));
}
/// <inheritdoc />
public EntityUid? Resolve(WeakEntityReference weakRef)
{
if (weakRef.Entity != NetEntity.Invalid
&& TryGetEntity(weakRef.Entity, out var ent))
{
return ent.Value;
}
return null;
}
/// <inheritdoc />
public EntityUid? Resolve(WeakEntityReference? weakRef)
{
return weakRef == null ? null : Resolve(weakRef.Value);
}
/// <inheritdoc />
public Entity<T>? Resolve<T>(WeakEntityReference<T> weakRef) where T : IComponent
{
if (weakRef.Entity != NetEntity.Invalid
&& TryGetEntity(weakRef.Entity, out var ent)
&& TryGetComponent(ent.Value, out T? comp))
{
return new(ent.Value, comp);
}
return null;
}
/// <inheritdoc />
public Entity<T>? Resolve<T>(WeakEntityReference<T>? weakRef) where T : IComponent
{
return weakRef == null ? null : Resolve(weakRef.Value);
}
public bool TryGetEntity(WeakEntityReference weakRef, [NotNullWhen(true)] out EntityUid? entity)
{
return TryGetEntity(weakRef.Entity, out entity);
}
public bool TryGetEntity([NotNullWhen(true)] WeakEntityReference? weakRef, [NotNullWhen(true)] out EntityUid? entity)
{
return TryGetEntity(weakRef?.Entity, out entity);
}
public bool TryGetEntity<T>(WeakEntityReference<T> weakRef, [NotNullWhen(true)] out Entity<T>? entity)
where T : IComponent
{
if (!TryGetEntity(weakRef.Entity, out var uid)
|| !TryGetComponent(uid.Value, out T? component))
{
entity = null;
return false;
}
entity = new(uid.Value, component);
return true;
}
public bool TryGetEntity<T>([NotNullWhen(true)] WeakEntityReference<T>? weakRef, [NotNullWhen(true)] out Entity<T>? entity)
where T : IComponent
{
if (weakRef != null)
return TryGetEntity(weakRef.Value, out entity);
entity = null;
return false;
}
#endregion
public AllEntityQueryEnumerator<IComponent> AllEntityQueryEnumerator(Type comp)
{
DebugTools.Assert(comp.IsAssignableTo(typeof(IComponent)));

View File

@@ -88,7 +88,7 @@ public partial class EntityManager
}
/// <inheritdoc />
public bool TryGetEntity(NetEntity? nEntity, [NotNullWhen(true)] out EntityUid? entity)
public bool TryGetEntity([NotNullWhen(true)] NetEntity? nEntity, [NotNullWhen(true)] out EntityUid? entity)
{
if (nEntity == null)
{
@@ -121,7 +121,7 @@ public partial class EntityManager
}
/// <inheritdoc />
public bool TryGetNetEntity(EntityUid? uid, [NotNullWhen(true)] out NetEntity? netEntity, MetaDataComponent? metadata = null)
public bool TryGetNetEntity([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(true)] out NetEntity? netEntity, MetaDataComponent? metadata = null)
{
if (uid == null)
{

View File

@@ -1040,6 +1040,19 @@ namespace Robust.Shared.GameObjects
return ToPrettyString(uid.Value, meta);
}
/// <inheritdoc />
[return: NotNullIfNotNull(nameof(weakRef))]
public EntityStringRepresentation? ToPrettyString(WeakEntityReference? weakRef)
{
return weakRef == null ? null : ToPrettyString(weakRef.Value);
}
/// <inheritdoc />
public EntityStringRepresentation ToPrettyString(WeakEntityReference weakRef)
{
return ToPrettyString(weakRef.Entity);
}
#endregion Entity Management
public virtual void RaisePredictiveEvent<T>(T msg) where T : EntityEventArgs

View File

@@ -423,6 +423,22 @@ public partial class EntitySystem
return EntityManager.ToPrettyString(netEntity);
}
/// <inheritdoc cref="IEntityManager.ToPrettyString(WeakEntityReference?)/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[return: NotNullIfNotNull(nameof(weakRef))]
protected EntityStringRepresentation? ToPrettyString(WeakEntityReference? weakRef)
{
return EntityManager.ToPrettyString(weakRef);
}
/// <inheritdoc cref="IEntityManager.ToPrettyString(WeakEntityReference?)/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[return: NotNullIfNotNull(nameof(weakRef))]
protected EntityStringRepresentation ToPrettyString(WeakEntityReference weakRef)
{
return EntityManager.ToPrettyString(weakRef);
}
/// <inheritdoc cref="IEntityManager.ToPrettyString(EntityUid, MetaDataComponent?)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected EntityStringRepresentation ToPrettyString(EntityUid uid, MetaDataComponent? metadata)
@@ -1606,4 +1622,76 @@ public partial class EntitySystem
}
#endregion
#region WeakEntityReference
/// <inheritdoc cref="IEntityManager.GetWeakReference(EntityUid, MetaDataComponent?)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected WeakEntityReference GetWeakReference(EntityUid uid, MetaDataComponent? meta = null)
{
return EntityManager.GetWeakReference(uid, meta);
}
/// <inheritdoc cref="IEntityManager.GetWeakReference(EntityUid?, MetaDataComponent?)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected WeakEntityReference? GetWeakReference(EntityUid? uid, MetaDataComponent? meta = null)
{
return EntityManager.GetWeakReference(uid, meta);
}
/// <inheritdoc cref="IEntityManager.Resolve(WeakEntityReference)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected EntityUid? Resolve(WeakEntityReference weakRef)
{
return EntityManager.Resolve(weakRef);
}
/// <inheritdoc cref="IEntityManager.Resolve(WeakEntityReference)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected EntityUid? Resolve(WeakEntityReference? weakRef)
{
return EntityManager.Resolve(weakRef);
}
/// <inheritdoc cref="IEntityManager.Resolve{T}(ref WeakEntityReference{T})"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected Entity<T>? Resolve<T>(WeakEntityReference<T> weakRef) where T : IComponent
{
return EntityManager.Resolve(weakRef);
}
/// <inheritdoc cref="IEntityManager.Resolve{T}(WeakEntityReference{T})"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected Entity<T>? Resolve<T>(WeakEntityReference<T>? weakRef) where T : IComponent
{
return EntityManager.Resolve(weakRef);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool TryGetEntity(WeakEntityReference weakRef, [NotNullWhen(true)] out EntityUid? entity)
{
return EntityManager.TryGetEntity(weakRef, out entity);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool TryGetEntity(WeakEntityReference? weakRef, [NotNullWhen(true)] out EntityUid? entity)
{
return EntityManager.TryGetEntity(weakRef, out entity);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool TryGetEntity<T>(WeakEntityReference<T> weakRef, [NotNullWhen(true)] out Entity<T>? entity)
where T : IComponent
{
return EntityManager.TryGetEntity(weakRef, out entity);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool TryGetEntity<T>(WeakEntityReference<T>? weakRef, [NotNullWhen(true)] out Entity<T>? entity)
where T : IComponent
{
return EntityManager.TryGetEntity(weakRef, out entity);
}
#endregion
}

View File

@@ -505,12 +505,56 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// <see cref="ComponentQueryEnumerator"/>
/// </summary>
public ComponentQueryEnumerator ComponentQueryEnumerator(ComponentRegistry registry);
ComponentQueryEnumerator ComponentQueryEnumerator(ComponentRegistry registry);
/// <summary>
/// <see cref="CompRegistryQueryEnumerator"/>
/// </summary>
public CompRegistryEntityEnumerator CompRegistryQueryEnumerator(ComponentRegistry registry);
CompRegistryEntityEnumerator CompRegistryQueryEnumerator(ComponentRegistry registry);
/// <summary>
/// Returns a <see cref="WeakEntityReference"/> pointing to the local entity.
/// </summary>
WeakEntityReference GetWeakReference(EntityUid uid, MetaDataComponent? meta = null);
/// <summary>
/// Returns a <see cref="WeakEntityReference"/> pointing to the local entity.
/// </summary>
WeakEntityReference? GetWeakReference(EntityUid? uid, MetaDataComponent? meta = null);
/// <summary>
/// Attempts to resolve the given <see cref="WeakEntityReference"/> into an <see cref="EntityUid"/> that
/// corresponds to an existing entity. If this fails, the entity has either been deleted, or for clients, the
/// entity may not yet have been sent to them.
/// </summary>
EntityUid? Resolve(WeakEntityReference weakRef);
/// <inheritdoc cref="Resolve(WeakEntityReference)"/>
EntityUid? Resolve(WeakEntityReference? weakRef);
bool TryGetEntity(WeakEntityReference weakRef, [NotNullWhen(true)] out EntityUid? entity);
bool TryGetEntity(
[NotNullWhen(true)] WeakEntityReference? weakRef,
[NotNullWhen(true)] out EntityUid? entity);
bool TryGetEntity<T>(WeakEntityReference<T> weakRef, [NotNullWhen(true)] out Entity<T>? entity)
where T : IComponent;
bool TryGetEntity<T>(
[NotNullWhen(true)] WeakEntityReference<T>? weakRef,
[NotNullWhen(true)] out Entity<T>? entity)
where T : IComponent;
/// <summary>
/// Attempts to resolve the given <see cref="WeakEntityReference"/> into an existing entity with the specified
/// component and return the <see cref="Entity{T}"/>. If this fails, the entity has either been deleted, doesn't
/// have the component, or for clients the entity may not yet have been sent to them.
/// </summary>
public Entity<T>? Resolve<T>(WeakEntityReference<T> weakRef) where T : IComponent;
/// <inheritdoc cref="Resolve{T}(WeakEntityReference{T})"/>
public Entity<T>? Resolve<T>(WeakEntityReference<T>? weakRef) where T : IComponent;
AllEntityQueryEnumerator<IComponent> AllEntityQueryEnumerator(Type comp);

View File

@@ -235,6 +235,11 @@ namespace Robust.Shared.GameObjects
/// </summary>
EntityStringRepresentation ToPrettyString(NetEntity netEntity);
/// <summary>
/// Returns a string representation of an entity with various information regarding it.
/// </summary>
EntityStringRepresentation ToPrettyString(WeakEntityReference weakRef);
/// <summary>
/// Returns a string representation of an entity with various information regarding it.
/// </summary>
@@ -247,6 +252,12 @@ namespace Robust.Shared.GameObjects
[return: NotNullIfNotNull("netEntity")]
EntityStringRepresentation? ToPrettyString(NetEntity? netEntity);
/// <summary>
/// Returns a string representation of an entity with various information regarding it.
/// </summary>
[return: NotNullIfNotNull(nameof(weakRef))]
EntityStringRepresentation? ToPrettyString(WeakEntityReference? weakRef);
#endregion Entity Management
/// <summary>

View File

@@ -0,0 +1,68 @@
using System;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Shared.GameObjects;
/// <summary>
/// This struct is just a wrapper around a <see cref="NetEntity"/> that is intended to be used to store references to
/// entities in a context where there is no expectation that the entity still exists (has not been deleted).
/// </summary>
/// <remarks>
/// The current convention is that a non-null EntityUid stored on a component should correspond to an
/// existing entity. Generally, if such an entity has since been deleted or had a relevant component removed, the
/// references to that entity should have been cleaned up by the component shutdown logic. If this is not done, this
/// generally results in errors being logged when an invalid EntityUid is passed around. This struct exists to for
/// cases where you want to store an entity reference, while making it clear that there is no expectation that it
/// should continue to be valid, which also means you do not need to clean up any references upon deletion or
/// component removal.
/// </remarks>
/// <remarks>
/// When saving a map, any weak references to entities that are not being included in the save file are automatically
/// ignored.
/// </remarks>
[CopyByRef, Serializable, NetSerializable]
public record struct WeakEntityReference(NetEntity Entity)
{
public override int GetHashCode() => Entity.GetHashCode();
public static readonly WeakEntityReference Invalid = new(NetEntity.Invalid);
public static WeakEntityReference Parse(ReadOnlySpan<char> uid) => new(NetEntity.Parse(uid));
public static bool TryParse(ReadOnlySpan<char> uid, out WeakEntityReference entity)
{
if (NetEntity.TryParse(uid, out var nent))
{
entity = new(nent);
return true;
}
entity = Invalid;
return false;
}
}
/// <summary>
/// Variant of <see cref="WeakEntityReference"/> that is only considered valid if the entity exists and still has the
/// specified component.
/// </summary>
[CopyByRef, Serializable]
public record struct WeakEntityReference<T>(NetEntity Entity) where T : IComponent
{
public override int GetHashCode() => Entity.GetHashCode();
public static readonly WeakEntityReference<T> Invalid = new(NetEntity.Invalid);
public static WeakEntityReference<T> Parse(ReadOnlySpan<char> uid) => new(NetEntity.Parse(uid));
public static bool TryParse(ReadOnlySpan<char> uid, out WeakEntityReference<T> entity)
{
if (NetEntity.TryParse(uid, out var nent))
{
entity = new(nent);
return true;
}
entity = Invalid;
return false;
}
}

View File

@@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Toolshed.TypeParsers;
@@ -16,7 +17,7 @@ namespace Robust.Shared.Prototypes;
/// This will be automatically validated by <see cref="EntProtoIdSerializer"/> if used in data fields.
/// </remarks>
/// <remarks><seealso cref="ProtoId{T}"/> for a wrapper of other prototype kinds.</remarks>
[Serializable, NetSerializable]
[Serializable, NetSerializable, CopyByRef]
public readonly record struct EntProtoId(string Id) : IEquatable<string>, IComparable<EntProtoId>, IAsType<string>,
IAsType<ProtoId<EntityPrototype>>
{
@@ -58,7 +59,7 @@ public readonly record struct EntProtoId(string Id) : IEquatable<string>, ICompa
}
/// <inheritdoc cref="EntProtoId"/>
[Serializable]
[Serializable, CopyByRef]
public readonly record struct EntProtoId<T>(string Id) : IEquatable<string>, IComparable<EntProtoId> where T : IComponent, new()
{
public static implicit operator string(EntProtoId<T> protoId)

View File

@@ -1,4 +1,5 @@
using System;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic;
using Robust.Shared.Toolshed.TypeParsers;
@@ -13,7 +14,7 @@ namespace Robust.Shared.Prototypes;
/// This will be automatically validated by <see cref="ProtoIdSerializer{T}"/> if used in data fields.
/// </remarks>
/// <remarks><seealso cref="EntProtoId"/> for an <see cref="EntityPrototype"/> alias.</remarks>
[Serializable]
[Serializable, CopyByRef]
[PreferOtherType(typeof(EntityPrototype), typeof(EntProtoId))]
public readonly record struct ProtoId<T>(string Id) :
IEquatable<string>,

View File

@@ -1,5 +1,4 @@
using System.Globalization;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
@@ -14,7 +13,8 @@ namespace Robust.Shared.Prototypes;
internal sealed class YamlValidationContext :
ISerializationContext,
ITypeSerializer<EntityUid, ValueDataNode>,
ITypeSerializer<NetEntity, ValueDataNode>
ITypeSerializer<NetEntity, ValueDataNode>,
ITypeSerializer<WeakEntityReference, ValueDataNode>
{
public SerializationManager.SerializerProvider SerializerProvider { get; } = new();
public bool WritingReadingPrototypes => true;
@@ -55,42 +55,71 @@ internal sealed class YamlValidationContext :
return EntityUid.Parse(node.Value);
}
public ValidationNode Validate(
ValidationNode ITypeValidator<NetEntity, ValueDataNode>.Validate(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context = null)
ISerializationContext? context)
{
if (node.Value == "invalid")
return new ValidatedValueNode(node);
return new ErrorNode(node, "Prototypes should not contain NetEntities");
return node.Value == "invalid"
? new ValidatedValueNode(node)
: new ErrorNode(node, "Prototypes should not contain NetEntities");
}
public NetEntity Read(
NetEntity ITypeReader<NetEntity, ValueDataNode>.Read(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context = null,
ISerializationManager.InstantiationDelegate<NetEntity>? instanceProvider = null)
ISerializationContext? context,
ISerializationManager.InstantiationDelegate<NetEntity>? instanceProvider)
{
if (node.Value == "invalid")
return NetEntity.Invalid;
return NetEntity.Parse(node.Value);
return node.Value == "invalid" ? NetEntity.Invalid : NetEntity.Parse(node.Value);
}
public DataNode Write(
DataNode ITypeWriter<NetEntity>.Write(
ISerializationManager serializationManager,
NetEntity value,
IDependencyCollection dependencies,
bool alwaysWrite = false,
ISerializationContext? context = null)
bool alwaysWrite,
ISerializationContext? context)
{
if (!value.Valid)
return new ValueDataNode("invalid");
return value.Valid
? new ValueDataNode(value.Id.ToString(CultureInfo.InvariantCulture))
: new ValueDataNode("invalid");
}
return new ValueDataNode(value.Id.ToString(CultureInfo.InvariantCulture));
ValidationNode ITypeValidator<WeakEntityReference, ValueDataNode>.Validate(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context)
{
return node.Value == "invalid"
? new ValidatedValueNode(node)
: new ErrorNode(node, "Prototypes should not contain WeakEntityReferences");
}
WeakEntityReference ITypeReader<WeakEntityReference, ValueDataNode>.Read(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context,
ISerializationManager.InstantiationDelegate<WeakEntityReference>? instanceProvider)
{
return node.Value == "invalid" ? WeakEntityReference.Invalid : new(NetEntity.Parse(node.Value));
}
DataNode ITypeWriter<WeakEntityReference>.Write(
ISerializationManager serializationManager,
WeakEntityReference value,
IDependencyCollection dependencies,
bool alwaysWrite,
ISerializationContext? context)
{
return !value.Entity.Valid
? new ValueDataNode("invalid")
: new ValueDataNode(value.Entity.Id.ToString(CultureInfo.InvariantCulture));
}
}

View File

@@ -1,8 +1,11 @@
using System;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.Manager.Exceptions;
public sealed class CopyToFailedException<T> : Exception
{
public override string Message => $"Failed performing CopyTo for Type {typeof(T)}";
public override string Message
=> $"Failed performing CopyTo for Type {typeof(T)}. Did you forget to create a {nameof(ITypeCopier<T>)} implementation? Or maybe {typeof(T)} should have the {nameof(CopyByRefAttribute)}?";
}

View File

@@ -17,7 +17,7 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations;
/// Serializer used automatically for <see cref="EntProtoId"/> types.
/// </summary>
[TypeSerializer]
public sealed class EntProtoIdSerializer : ITypeSerializer<EntProtoId, ValueDataNode>, ITypeCopyCreator<EntProtoId>
public sealed class EntProtoIdSerializer : ITypeSerializer<EntProtoId, ValueDataNode>
{
public ValidationNode Validate(ISerializationManager serialization, ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context = null)
{
@@ -37,18 +37,13 @@ public sealed class EntProtoIdSerializer : ITypeSerializer<EntProtoId, ValueData
{
return new ValueDataNode(value.Id);
}
public EntProtoId CreateCopy(ISerializationManager serializationManager, EntProtoId source, IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context = null)
{
return source;
}
}
/// <summary>
/// Serializer used automatically for <see cref="EntProtoId"/> types.
/// </summary>
[TypeSerializer]
public sealed class EntProtoIdSerializer<T> : ITypeSerializer<EntProtoId<T>, ValueDataNode>, ITypeCopyCreator<EntProtoId<T>> where T : IComponent, new()
public sealed class EntProtoIdSerializer<T> : ITypeSerializer<EntProtoId<T>, ValueDataNode> where T : IComponent, new()
{
public ValidationNode Validate(ISerializationManager serialization, ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context = null)
{
@@ -83,9 +78,4 @@ public sealed class EntProtoIdSerializer<T> : ITypeSerializer<EntProtoId<T>, Val
{
return new ValueDataNode(value.Id);
}
public EntProtoId<T> CreateCopy(ISerializationManager serializationManager, EntProtoId<T> source, IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context = null)
{
return source;
}
}

View File

@@ -15,7 +15,7 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic;
/// </summary>
/// <typeparam name="T">The type of the prototype for which the id is stored.</typeparam>
[TypeSerializer]
public sealed class ProtoIdSerializer<T> : ITypeSerializer<ProtoId<T>, ValueDataNode>, ITypeCopyCreator<ProtoId<T>> where T : class, IPrototype
public sealed class ProtoIdSerializer<T> : ITypeSerializer<ProtoId<T>, ValueDataNode> where T : class, IPrototype
{
public ValidationNode Validate(ISerializationManager serialization, ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context = null)
{
@@ -46,9 +46,4 @@ public sealed class ProtoIdSerializer<T> : ITypeSerializer<ProtoId<T>, ValueData
{
return new ValueDataNode(value.Id);
}
public ProtoId<T> CreateCopy(ISerializationManager serializationManager, ProtoId<T> source, IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context = null)
{
return source;
}
}

View File

@@ -0,0 +1,60 @@
using Robust.Shared.EntitySerialization;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
using static Robust.Shared.Serialization.Manager.ISerializationManager;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic;
// This specifically implements WeakEntityReference<T>, but not WeakEntityReference for the same reason that there is no
// EntityUid serializer: So that it can be implemented by the entity (de)serialization context.
// Ideally I'd also leave that there instead of here, but it needs generics...
[TypeSerializer]
public sealed class WeakEntityReferenceSerializer<T> :
ITypeSerializer<WeakEntityReference<T>, ValueDataNode>
where T : class, IComponent
{
public WeakEntityReference<T> Read(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context = null,
InstantiationDelegate<WeakEntityReference<T>>? instanceProvider = null)
{
var val = serializationManager.Read<WeakEntityReference>(node, hookCtx, context);
return new(val.Entity);
}
public DataNode Write(
ISerializationManager serializationManager,
WeakEntityReference<T> value,
IDependencyCollection dependencies,
bool alwaysWrite = false,
ISerializationContext? context = null)
{
NetEntity val = value.Entity;
if (context is EntitySerializer seri)
{
if (!seri.EntMan.TryGetEntity(val, out var uid) || !seri.EntMan.HasComponent<T>(uid))
val = NetEntity.Invalid;
}
return serializationManager.WriteValue(new WeakEntityReference(val), alwaysWrite, context);
}
public ValidationNode Validate(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context)
{
return serializationManager.ValidateNode<WeakEntityReference>(node, context);
}
}

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
@@ -42,6 +43,14 @@ public sealed partial class EntitySaveTestComponent : Component
}
}
[RegisterComponent]
[NetworkedComponent, AutoGenerateComponentState]
public sealed partial class WeakEntityReferenceTestComponent : Component
{
[DataField, AutoNetworkedField]
public WeakEntityReference Entity;
}
/// <summary>
/// Dummy tile definition for serializing grids.
/// </summary>

View File

@@ -0,0 +1,106 @@
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared;
using Robust.Shared.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.Player;
namespace Robust.UnitTesting.Shared.EntitySerialization;
public sealed partial class WeakEntityReferenceTest : RobustIntegrationTest
{
[Test]
public async Task TestWeakEntityReference()
{
var server = StartServer();
var client = StartClient();
await Task.WhenAll(server.WaitIdleAsync(), client.WaitIdleAsync());
var sEntMan = server.EntMan;
var sPlayerMan = server.ResolveDependency<ISharedPlayerManager>();
var cEntMan = client.EntMan;
var cNetMan = client.ResolveDependency<IClientNetManager>();
NetEntity netEntA = default;
NetEntity netEntB = default;
// Set up entities
await server.WaitPost(() =>
{
var entA = sEntMan.Spawn();
var entB = sEntMan.Spawn();
netEntA = sEntMan.GetNetEntity(entA);
netEntB = sEntMan.GetNetEntity(entB);
// Give A a weak reference to B
var comp = sEntMan.AddComponent<WeakEntityReferenceTestComponent>(entA);
comp.Entity = new WeakEntityReference(sEntMan.GetNetEntity(entB));
});
// Connect client.
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
await client.WaitPost(() => cNetMan.ClientConnect(null!, 0, null!));
// Disable PVS so everything gets networked
server.Post(() => server.CfgMan.SetCVar(CVars.NetPVS, false));
async Task RunTicks()
{
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
}
await RunTicks();
// Put the player into the game so they get entity data
await server.WaitAssertion(() =>
{
var session = sPlayerMan.Sessions.First();
sPlayerMan.JoinGame(session);
});
await RunTicks();
// Make sure the client got entity data
await client.WaitAssertion(() =>
{
Assert.That(cNetMan.IsConnected);
Assert.That(cEntMan.TryGetEntity(netEntA, out var entA));
Assert.That(cEntMan.TryGetEntity(netEntB, out var entB));
Assert.That(cEntMan.TryGetComponent<WeakEntityReferenceTestComponent>(entA, out var comp));
var referencedEnt = cEntMan.Resolve(comp!.Entity);
Assert.That(referencedEnt, Is.EqualTo(entB));
});
// Delete the referenced entity on the server
await server.WaitAssertion(() =>
{
var entB = sEntMan.GetEntity(netEntB);
sEntMan.DeleteEntity(entB);
});
await RunTicks();
// Make sure the client now resolves the reference to null
await client.WaitAssertion(() =>
{
Assert.That(cEntMan.TryGetEntity(netEntA, out var entA));
Assert.That(cEntMan.TryGetComponent<WeakEntityReferenceTestComponent>(entA, out var comp));
var referencedEnt = cEntMan.Resolve(comp!.Entity);
Assert.That(referencedEnt, Is.Null);
});
// Disconnect client
await client.WaitPost(() => cNetMan.ClientDisconnect(""));
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
// Reset cvar
// I love engine tests
server.Post(() => server.CfgMan.SetCVar(CVars.NetPVS, true));
}
}