diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs index 8b5844380dc..12e1db30656 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs @@ -1,3 +1,4 @@ +using Content.Server.Administration.Managers; using Content.Server.Atmos.EntitySystems; using Content.Server.Body.Systems; using Content.Server.Electrocution; @@ -96,6 +97,7 @@ public sealed partial class AdminVerbSystem [Dependency] private readonly GibbingSystem _gibbing = default!; [Dependency] private readonly DamageableSystem _damageable = default!; + private readonly EntProtoId _actionViewLawsProtoId = "ActionViewLaws"; private readonly ProtoId _crewsimovLawset = "Crewsimov"; diff --git a/Content.Server/ImmovableRod/ImmovableRodSystem.cs b/Content.Server/ImmovableRod/ImmovableRodSystem.cs index 013f0294764..ff2b2c90777 100644 --- a/Content.Server/ImmovableRod/ImmovableRodSystem.cs +++ b/Content.Server/ImmovableRod/ImmovableRodSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Destructible; using Content.Server.Polymorph.Components; using Content.Server.Popups; +using Content.Shared._Persistence14.PersistentIdentifier; using Content.Shared.Administration.Logs; using Content.Shared.Body; using Content.Shared.Damage.Systems; @@ -31,6 +32,7 @@ public sealed class ImmovableRodSystem : EntitySystem [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedMapSystem _map = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private PersistentIdentifierSystem _pid = default!; public override void Update(float frameTime) { @@ -111,8 +113,7 @@ private void OnCollide(EntityUid uid, ImmovableRodComponent component, ref Start // dont delete/hurt self if polymoprhed into a rod if (TryComp(uid, out var polymorphed)) { - if (polymorphed.Parent == ent) - return; + return; } // gib or damage em diff --git a/Content.Server/Polymorph/Components/PolymorphedEntityComponent.cs b/Content.Server/Polymorph/Components/PolymorphedEntityComponent.cs index 03ab7b6c1fd..2dbfa26944c 100644 --- a/Content.Server/Polymorph/Components/PolymorphedEntityComponent.cs +++ b/Content.Server/Polymorph/Components/PolymorphedEntityComponent.cs @@ -1,10 +1,12 @@ using Content.Server.Polymorph.Systems; +using Content.Shared._Persistence14.PersistentIdentifier; +using Content.Shared._Persistence14.PersistentIdentifier.Reference; using Content.Shared.Polymorph; namespace Content.Server.Polymorph.Components; [RegisterComponent] -[Access(typeof(PolymorphSystem))] +[Access(typeof(PolymorphSystem), typeof(PersistentIdentifierSystem))] public sealed partial class PolymorphedEntityComponent : Component { /// @@ -17,8 +19,8 @@ public sealed partial class PolymorphedEntityComponent : Component /// /// The original entity that the player will revert back into /// - [DataField(required: true)] - public EntityUid? Parent; + [DataField] + public string ParentPersistentId; /// /// Whether this polymorph has been reverted. diff --git a/Content.Server/Polymorph/Systems/PolymorphSystem.cs b/Content.Server/Polymorph/Systems/PolymorphSystem.cs index 6b769e4e5fc..19d437e3196 100644 --- a/Content.Server/Polymorph/Systems/PolymorphSystem.cs +++ b/Content.Server/Polymorph/Systems/PolymorphSystem.cs @@ -1,6 +1,12 @@ using Content.Server.Actions; +using Content.Server.Administration.Managers; +using Content.Server.Administration.Systems; +using Content.Server.Database; using Content.Server.Inventory; using Content.Server.Polymorph.Components; +using Content.Server._Persistence14.EntityVoid; +using Content.Shared._Persistence14.PersistentIdentifier; +using Content.Shared.Administration; using Content.Shared.Body; using Content.Shared.Buckle; using Content.Shared.Coordinates; @@ -15,12 +21,17 @@ using Content.Shared.Nutrition; using Content.Shared.Polymorph; using Content.Shared.Popups; +using Content.Shared.Verbs; using Robust.Server.Audio; using Robust.Server.Containers; using Robust.Server.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.Shared.Utility; +using Content.Shared._Persistence14.EntityVoid; namespace Content.Server.Polymorph.Systems; @@ -43,6 +54,11 @@ public sealed partial class PolymorphSystem : EntitySystem [Dependency] private readonly SharedVisualBodySystem _visualBody = default!; [Dependency] private readonly SharedMindSystem _mindSystem = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private PersistentIdentifierSystem _pid = default!; + [Dependency] private AdminVerbSystem _adminVerb = default!; + [Dependency] private readonly IAdminManager _adminManager = default!; + [Dependency] private readonly ILogManager _log = default!; + [Dependency] private EntityVoidSystem _void = default!; private const string RevertPolymorphId = "ActionRevertPolymorph"; @@ -58,6 +74,8 @@ public override void Initialize() SubscribeLocalEvent(OnDestruction); SubscribeLocalEvent(OnPolymorphedTerminating); + SubscribeLocalEvent>(AddPolymorphVerbs); + InitializeMap(); } @@ -104,11 +122,13 @@ private void OnMapInit(Entity ent, ref MapInitEvent if (component.Configuration.Forced) return; - if (_actions.AddAction(uid, ref component.Action, out var action, RevertPolymorphId)) + /* + if (_actions.AddAction(uid, ref component.Action, out var action, RevertPolymorphId) && + _pid.TryResolveId(component.Parent, out var parentEnt)) { - _actions.SetEntityIcon((component.Action.Value, action), component.Parent); + _actions.SetEntityIcon((component.Action.Value, action), parentEnt); _actions.SetUseDelay(component.Action.Value, TimeSpan.FromSeconds(component.Configuration.Delay)); - } + }*/ } private void OnPolymorphActionEvent(Entity ent, ref PolymorphActionEvent args) @@ -155,10 +175,6 @@ private void OnPolymorphedTerminating(Entity ent, re if (ent.Comp.Configuration.RevertOnDelete) Revert(ent.AsNullable()); - - // Remove our original entity too - // Note that Revert will set Parent to null, so reverted entities will not be deleted - QueueDel(ent.Comp.Parent); } /// @@ -180,6 +196,9 @@ private void OnPolymorphedTerminating(Entity ent, re /// The new entity, or null if the polymorph failed. public EntityUid? PolymorphEntity(EntityUid uid, PolymorphConfiguration configuration) { + if (HasComp(uid)) + return null; + // If they're morphed, check their current config to see if they can be // morphed again if (!configuration.IgnoreAllowRepeatedMorphs @@ -194,6 +213,8 @@ private void OnPolymorphedTerminating(Entity ent, re _gameTiming.CurTime < polymorphableComponent.LastPolymorphEnd + configuration.Cooldown) return null; + var pid = _pid.EnsureId(uid, out var pidEntity); + // mostly just for vehicles _buckle.TryUnbuckle(uid, uid, true); @@ -213,8 +234,8 @@ private void OnPolymorphedTerminating(Entity ent, re _mindSystem.MakeSentient(child); var polymorphedComp = Factory.GetComponent(); - polymorphedComp.Parent = uid; polymorphedComp.Configuration = configuration; + polymorphedComp.ParentPersistentId = pid; AddComp(child, polymorphedComp); var childXform = Transform(child); @@ -268,10 +289,13 @@ private void OnPolymorphedTerminating(Entity ent, re if (_mindSystem.TryGetMind(uid, out var mindId, out var mind)) _mindSystem.TransferTo(mindId, child, mind: mind); - //Ensures a map to banish the entity to - EnsurePausedMap(); - if (PausedMap != null) - _transform.SetParent(uid, targetTransformComp, PausedMap.Value); + if (!_void.TryVoidEntity(uid)) + { + if (_mindSystem.TryGetMind(child, out var childMindId, out var childMind)) + _mindSystem.TransferTo(child, childMindId, mind: childMind); + QueueDel(child); + return null; + } // Raise an event to inform anything that wants to know about the entity swap var ev = new PolymorphedEvent(uid, child, false); @@ -298,42 +322,40 @@ private void OnPolymorphedTerminating(Entity ent, re if (Deleted(uid)) return null; - if (component.Parent is not { } parent) - return null; - - if (Deleted(parent)) + if (ent.Comp?.ParentPersistentId is not { } pid) return null; var uidXform = Transform(uid); - var parentXform = Transform(parent); // Don't swap back onto a terminating grid if (TerminatingOrDeleted(uidXform.ParentUid)) return null; + var coords = _transform.ToMapCoordinates(uidXform.Coordinates); + + if (!_void.TryReconstructEntity(pid, coords, out var parentEnt)) + return null; + if (component.Configuration.ExitPolymorphSound != null) _audio.PlayPvs(component.Configuration.ExitPolymorphSound, uidXform.Coordinates); - _transform.SetParent(parent, parentXform, uidXform.ParentUid); - _transform.SetCoordinates(parent, parentXform, uidXform.Coordinates, uidXform.LocalRotation); - component.Reverted = true; if (component.Configuration.TransferDamage && - TryComp(parent, out var damageParent) && - _mobThreshold.GetScaledDamage(uid, parent, out var damage) && + TryComp(parentEnt, out var damageParent) && + _mobThreshold.GetScaledDamage(uid, parentEnt, out var damage) && damage != null) { - _damageable.SetDamage((parent, damageParent), damage); + _damageable.SetDamage((parentEnt, damageParent), damage); } if (component.Configuration.Inventory == PolymorphInventoryChange.Transfer) { - _inventory.TransferEntityInventories(uid, parent); + _inventory.TransferEntityInventories(uid, parentEnt); foreach (var held in _hands.EnumerateHeld(uid)) { _hands.TryDrop(uid, held); - _hands.TryPickupAnyHand(parent, held, checkActionBlocker: false); + _hands.TryPickupAnyHand(parentEnt, held, checkActionBlocker: false); } } else if (component.Configuration.Inventory == PolymorphInventoryChange.Drop) @@ -353,30 +375,32 @@ private void OnPolymorphedTerminating(Entity ent, re } if (_mindSystem.TryGetMind(uid, out var mindId, out var mind)) - _mindSystem.TransferTo(mindId, parent, mind: mind); + _mindSystem.TransferTo(mindId, parentEnt, mind: mind); - if (TryComp(parent, out var polymorphableComponent)) + if (TryComp(parentEnt, out var polymorphableComponent)) polymorphableComponent.LastPolymorphEnd = _gameTiming.CurTime; + var parentXform = Transform(parentEnt); + // if an item polymorph was picked up, put it back down after reverting - _transform.AttachToGridOrMap(parent, parentXform); + _transform.AttachToGridOrMap(parentEnt, parentXform); // Raise an event to inform anything that wants to know about the entity swap - var ev = new PolymorphedEvent(uid, parent, true); + var ev = new PolymorphedEvent(uid, parentEnt, true); RaiseLocalEvent(uid, ref ev); // visual effect spawn if (component.Configuration.EffectProto != null) - SpawnAttachedTo(component.Configuration.EffectProto, parent.ToCoordinates()); + SpawnAttachedTo(component.Configuration.EffectProto, parentEnt.ToCoordinates()); if (component.Configuration.ExitPolymorphPopup != null) _popup.PopupEntity(Loc.GetString(component.Configuration.ExitPolymorphPopup, ("parent", Identity.Entity(uid, EntityManager)), - ("child", Identity.Entity(parent, EntityManager))), - parent); + ("child", Identity.Entity(parentEnt, EntityManager))), + parentEnt); QueueDel(uid); - return parent; + return parentEnt; } /// @@ -420,4 +444,25 @@ public void RemovePolymorphAction(ProtoId id, Entity args) + { + if (!TryComp(args.User, out ActorComponent? actor)) + return; + + var player = actor.PlayerSession; + + if (!_adminManager.HasAdminFlag(player, AdminFlags.Admin)) + return; + + if (HasComp(args.Target) || HasComp(args.Target)) + return; + + args.Verbs.Add(new Verb + { + Text = "revert-polymorph-verb", + Category = VerbCategory.Admin, + Act = () => Revert((uid, component)), + }); + } } diff --git a/Content.Server/_Persistence14/EntityVoid/EntityVoidSystem.cs b/Content.Server/_Persistence14/EntityVoid/EntityVoidSystem.cs new file mode 100644 index 00000000000..f4e44e5370b --- /dev/null +++ b/Content.Server/_Persistence14/EntityVoid/EntityVoidSystem.cs @@ -0,0 +1,88 @@ +using Content.Shared._Persistence14.EntityVoid; +using Content.Shared._Persistence14.PersistentIdentifier; +using Robust.Server.GameObjects; +using Robust.Shared.ContentPack; +using Robust.Shared.EntitySerialization.Systems; +using Robust.Shared.Map; +using Robust.Shared.Utility; + +namespace Content.Server._Persistence14.EntityVoid; + +/// +/// A system for shucking entities into the void to be handled later. Set up to allow persistent reference to these entities through a GUID. +/// +public sealed partial class EntityVoidSystem : EntitySystem +{ + [Dependency] private PersistentIdentifierSystem _pid = default!; + [Dependency] private MapLoaderSystem _mapLoader = default!; + [Dependency] private SharedTransformSystem _transform = default!; + [Dependency] private IResourceManager _resourceManager = default!; + [Dependency] private PhysicsSystem _physics = default!; + + /// + /// Attempts to serialize an entity and remove it from the world for storage. + /// + + public bool TryVoidEntity(EntityUid uid) + { + if (!Exists(uid)) + return false; + + if (HasComp(uid)) // Don't void twice, if voided already... something is wrong. + return false; + + AddComp(uid); + + var id = _pid.EnsureId(uid); // Piggy backing off of PID for the GUID system. Keeps things symetric. + var path = BuildPath(id); + + if (!_mapLoader.TrySaveEntity(uid, path)) + return false; + + QueueDel(uid); + return true; + } + + /// + /// Attempts to regenerate a given ID's entity from the serialized file and place it at a given set of coordiantes. + /// + public bool TryReconstructEntity(string persistentId, MapCoordinates coordinates, out EntityUid uid) + { + uid = EntityUid.Invalid; + + + var path = BuildPath(persistentId); + + if (!_mapLoader.TryLoadEntity(path, out var entity)) + return false; + + + uid = entity.Value.Owner; + _transform.SetMapCoordinates(uid, coordinates); + _physics.WakeBody(uid); + + if (HasComp(uid)) + RemComp(uid); + + + RemoveEntity(persistentId); + + return true; + } + + /// + /// Turns an id into a path to be consumed by other void functions. + /// + private ResPath BuildPath(string persistentId) => new ResPath($"/EntityVoid/{persistentId}"); + + /// + /// Removes an entity file from storage, functionally deleting it. + /// + public void RemoveEntity(string persistentId) + { + var path = BuildPath(persistentId); + + if (_resourceManager.UserData.Exists(path)) + _resourceManager.UserData.Delete(path); + } +} \ No newline at end of file diff --git a/Content.Shared/_Persistence14/EntityVoid/VoidedComponent.cs b/Content.Shared/_Persistence14/EntityVoid/VoidedComponent.cs new file mode 100644 index 00000000000..0b117664e48 --- /dev/null +++ b/Content.Shared/_Persistence14/EntityVoid/VoidedComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Shared._Persistence14.EntityVoid; + +/// +/// A simple component identifying an entity as voided. Used in double-call protection attempts for the +/// +[RegisterComponent] +public sealed partial class VoidedComponent : Component { } \ No newline at end of file diff --git a/Content.Shared/_Persistence14/PersistentIdentifier/PersistentIdentifierComponent.cs b/Content.Shared/_Persistence14/PersistentIdentifier/PersistentIdentifierComponent.cs index d33df2e275a..dc4b04e9885 100644 --- a/Content.Shared/_Persistence14/PersistentIdentifier/PersistentIdentifierComponent.cs +++ b/Content.Shared/_Persistence14/PersistentIdentifier/PersistentIdentifierComponent.cs @@ -12,7 +12,7 @@ public sealed partial class PersistentIdentifierComponent : Component /// A serialized identifier used to reference the entity this component is attached to between runtimes. /// [DataField, ViewVariables(VVAccess.ReadOnly), AutoNetworkedField] - public string Id = Guid.Empty.ToString(); + public string Id = PersistentIdentifierSystem.EmptyId; /// /// Shows the current initialization state of the ID and allows for basic manipulation of that ID.

@@ -20,5 +20,5 @@ public sealed partial class PersistentIdentifierComponent : Component /// Returns false when the ID is null. ///
[ViewVariables(VVAccess.ReadOnly)] - public bool IdInit => Id != Guid.Empty.ToString(); + public bool IdInit => Id != PersistentIdentifierSystem.EmptyId; } \ No newline at end of file diff --git a/Content.Shared/_Persistence14/PersistentIdentifier/PersistentIdentifierSystem.cs b/Content.Shared/_Persistence14/PersistentIdentifier/PersistentIdentifierSystem.cs index 419234a4500..7e8bc454870 100644 --- a/Content.Shared/_Persistence14/PersistentIdentifier/PersistentIdentifierSystem.cs +++ b/Content.Shared/_Persistence14/PersistentIdentifier/PersistentIdentifierSystem.cs @@ -1,3 +1,5 @@ +using Content.Shared._Persistence14.PersistentIdentifier.Reference; + namespace Content.Shared._Persistence14.PersistentIdentifier; public sealed partial class PersistentIdentifierSystem : EntitySystem @@ -8,7 +10,9 @@ public sealed partial class PersistentIdentifierSystem : EntitySystem /// /// The Sawmill key for all ID related log messages. /// - public const string Sawmill = "Persistent ID"; + public const string Sawmill = "persistent-id"; + + public static string EmptyId = Guid.Empty.ToString(); /// public override void Initialize() @@ -27,9 +31,11 @@ public string EnsureId(Entity ent) return id; } - public string EnsureId(EntityUid uid) + public string EnsureId(EntityUid uid) => EnsureId(uid, out _); + public string EnsureId(EntityUid uid, out Entity ent) { EnsureComp(uid, out var idComp); + ent = (uid, idComp); return EnsureId((uid, idComp)); } @@ -45,7 +51,7 @@ public bool TryGetId(Entity ent, out string id) return true; } - id = Guid.Empty.ToString(); + id = EmptyId; return false; } @@ -74,7 +80,7 @@ public void ClearId(Entity ent, PersistentIdChang if (!ent.Comp.IdInit) return; var oldId = ent.Comp.Id; - ent.Comp.Id = Guid.Empty.ToString(); + ent.Comp.Id = EmptyId; Dirty(ent); var ev = new PersistentIdChangedEvent(ent, oldId, ent.Comp.Id, behaviour); @@ -85,7 +91,7 @@ public bool OverrideId(Entity ent, string id, Per { if (id == ent.Comp.Id) return false; - if (id == Guid.Empty.ToString()) + if (IsEmptyId(id)) { _log.GetSawmill(Sawmill).Warning("Unable to override ID to empty. Use ClearId instead."); return false; @@ -140,6 +146,13 @@ public bool TryResolveId( return false; } + /// + /// Attempts to resolve a provided into an entity. + /// + /// True if the reference sucessfully resolved into an entity, otherwise false. + public bool TryResolveId(PersistentEntityReference reference, out Entity ent) + => reference.TryResolve(this, out ent, _log.GetSawmill(Sawmill)); + /// /// Fetches an id from all existing . Attempts to add valid ids to an existing /// @@ -152,12 +165,13 @@ public bool TryFetchId( string id, out Entity ent, Func, bool>? conditional = null, - PersistentIdRegisterComponent? registry = null) + PersistentIdRegisterComponent? registry = null, + bool sendLogs = true) { ent = default!; conditional ??= _ => true; - _log.GetSawmill(Sawmill).Info($"Attempting to fetch persistent id: {id}"); + if (sendLogs) _log.GetSawmill(Sawmill).Info($"Attempting to fetch persistent id: {id}"); var lookup = EntityQueryEnumerator(); @@ -167,12 +181,12 @@ public bool TryFetchId( { ent = (uid, idComp); if (registry is not null) registry.TryRegister(ent, _entMan); - _log.GetSawmill(Sawmill).Info($"Entity found: {uid}"); + if (sendLogs) _log.GetSawmill(Sawmill).Info($"Entity found: {uid}"); return true; } } - _log.GetSawmill(Sawmill).Warning($"Unable to find entity with matching pid."); + if (sendLogs) _log.GetSawmill(Sawmill).Warning($"Unable to find entity with matching pid."); return false; } @@ -181,7 +195,7 @@ private void OnRegisterIdChange(EntityUid uid, PersistentIdRegisterComponent reg // Culling will remove the existing reference as the new ID will not match that stored in the key. register.CullStaleEntities(_entMan); - if (args.NewId == Guid.Empty.ToString() || args.NewId == args.OldId || args.Behaviour == PersistentIdChangeBehaviour.Sever) + if (IsEmptyId(args.NewId) || args.NewId == args.OldId || args.Behaviour == PersistentIdChangeBehaviour.Sever) return; // Nothing more to do. if (!TryComp(args.Uid, out var idComp)) @@ -189,4 +203,31 @@ private void OnRegisterIdChange(EntityUid uid, PersistentIdRegisterComponent reg register.TryRegister((args.Uid, idComp), _entMan); } + + private bool IsEmptyId(string id) => id == EmptyId; + + public bool CompareId(PersistentEntityReference reference, string id) + => !IsEmptyId(id) && reference.TargetId == id; + public bool CompareId(PersistentEntityReference reference, Entity ent) + => ent.Comp.IdInit && reference.TargetId == ent.Comp.Id; + public bool CompareId(PersistentEntityReference reference, EntityUid uid) + { + var id = EnsureId(uid); + return CompareId(reference, id); + } + + public void AssignIdReference(ref PersistentEntityReference reference, string id) + { + _log.GetSawmill(Sawmill).Info($"Assigning id ({id}) to entity reference."); + reference.TargetId = id; + } + public void AssignIdReference(ref PersistentEntityReference reference, Entity ent) { + _log.GetSawmill(Sawmill).Info($"Assigning id from {ToPrettyString(ent)}"); + AssignIdReference(ref reference, ent.Comp.Id); + } + public void AssignIdReference(ref PersistentEntityReference reference, EntityUid uid) + { + var id = EnsureId(uid); + AssignIdReference(ref reference, id); + } } \ No newline at end of file diff --git a/Content.Shared/_Persistence14/PersistentIdentifier/Reference/PersistentEntitityReference.cs b/Content.Shared/_Persistence14/PersistentIdentifier/Reference/PersistentEntitityReference.cs new file mode 100644 index 00000000000..86b8ffef446 --- /dev/null +++ b/Content.Shared/_Persistence14/PersistentIdentifier/Reference/PersistentEntitityReference.cs @@ -0,0 +1,46 @@ +namespace Content.Shared._Persistence14.PersistentIdentifier.Reference; + +[DataDefinition] +public partial struct PersistentEntityReference +{ + [DataField(readOnly: true), ViewVariables(VVAccess.ReadOnly)] + public string TargetId = PersistentIdentifierSystem.EmptyId; + + [ViewVariables(VVAccess.ReadOnly)] + private Entity? _cachedEntity = null; + + public PersistentEntityReference() { } + + /// + /// Attempts to resolve the reference into an entity with a matching ID. + /// + /// True if the reference succesfully resolves into an entity, otherwise false. + public bool TryResolve(PersistentIdentifierSystem pid, out Entity entity, ISawmill? sawmill = null) + { + if (sawmill is { }) sawmill.Info("Attempting to resolve ID reference."); + if (_cachedEntity is { } cached && cached.Comp.Id == TargetId) + { + entity = cached; + if (sawmill is { }) sawmill.Info("Cached entity found, returning."); + return true; + } + if (!pid.TryFetchId(TargetId, out entity, sendLogs: true)) + { + return false; + } + if (entity.Comp.Id != TargetId) + { + if (sawmill is { }) sawmill.Info($"Fetched ID does not match target id... somehow (Expected:{TargetId}, Actual: {entity.Comp.Id})."); + return false; + } + + _cachedEntity = entity; + if (sawmill is { }) sawmill.Info("Entity cached and valid."); + return true; + } + public void Reset() + { + TargetId = PersistentIdentifierSystem.EmptyId; + _cachedEntity = null; + } +} \ No newline at end of file diff --git a/Resources/Locale/en-US/_Persistence14/polymorph.ftl b/Resources/Locale/en-US/_Persistence14/polymorph.ftl new file mode 100644 index 00000000000..c1e6b133450 --- /dev/null +++ b/Resources/Locale/en-US/_Persistence14/polymorph.ftl @@ -0,0 +1 @@ +revert-polymorph-verb = revert polymorph \ No newline at end of file