using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.Changeling.Components; using Content.Shared.Cloning; using Content.Shared.Mind; using Content.Shared.Mobs; using Content.Shared.Roles.Jobs; using Robust.Shared.GameStates; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Prototypes; namespace Content.Shared.Changeling.Systems; public abstract partial class SharedChangelingIdentitySystem : EntitySystem { [Dependency] private INetManager _net = default!; [Dependency] private MetaDataSystem _metaSystem = default!; [Dependency] private SharedCloningSystem _cloningSystem = default!; [Dependency] private SharedMapSystem _map = default!; [Dependency] private SharedPvsOverrideSystem _pvsOverrideSystem = default!; [Dependency] private SharedMindSystem _mind = default!; [Dependency] private SharedJobSystem _job = default!; public MapId? PausedMapId; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnPlayerAttached); SubscribeLocalEvent(OnPlayerDetached); SubscribeLocalEvent(OnDevouredEntity); SubscribeLocalEvent(OnStoredRemove); SubscribeLocalEvent(OnDevouredShutdown); SubscribeLocalEvent(OnRecentlyDevouredMobState); } private void OnDevouredEntity(Entity ent, ref ChangelingDevouredEvent args) { if (args.ObtainedIdentity) { GrantIdentity(ent.Owner, args.Devoured, true); } if (args.GrantedDna && TryGetDataFromOriginal(ent.AsNullable(), args.Devoured, out var data)) { data.GrantedDna = true; } } private void OnPlayerAttached(Entity ent, ref PlayerAttachedEvent args) { HandOverPvsOverride(ent, args.Player); } private void OnPlayerDetached(Entity ent, ref PlayerDetachedEvent args) { CleanupPvsOverride(ent, args.Player); } private void OnMapInit(Entity ent, ref MapInitEvent args) { // Make a backup of our current identity so we can transform back. GrantIdentity(ent.Owner, ent.Owner); if (!TryGetDataFromOriginal(ent.AsNullable(), ent, out var data)) return; ent.Comp.CurrentIdentity = data.Identity; } private void OnShutdown(Entity ent, ref ComponentShutdown args) { if (TryComp(ent, out var actor)) CleanupPvsOverride(ent, actor.PlayerSession); CleanupChangelingNullspaceIdentities(ent); CleanupDevouredReferences(ent); } // Set all references to this entity to null to prevent PVS errors when networking. private void OnDevouredShutdown(Entity ent, ref ComponentShutdown args) { foreach (var ling in ent.Comp.DevouredBy) { if (!TryComp(ling, out var identityComp)) continue; RemoveOriginalReference((ling, identityComp), ent); } } private void OnRecentlyDevouredMobState(Entity ent, ref MobStateChangedEvent args) { // Once we are revived the body is no longer recently devoured. if (args.NewMobState != MobState.Alive) return; RemCompDeferred(ent); } private void OnStoredRemove(Entity ent, ref ComponentRemove args) { // The last stored identity is being deleted, we can clean up the map. if (_net.IsServer && PausedMapId != null && Count() <= 1) _map.QueueDeleteMap(PausedMapId.Value); } /// /// Cleanup all nullspaced Identities when the changeling no longer exists /// /// the changeling public void CleanupChangelingNullspaceIdentities(Entity ent) { if (_net.IsClient) return; foreach (var consumedIdentity in ent.Comp.ConsumedIdentities) { QueueDel(consumedIdentity.Identity); } } /// /// Removes all references to the owning changeling from ChangelingDevouredComponents. /// /// The changeling entity private void CleanupDevouredReferences(Entity ent) { foreach (var devouredUid in ent.Comp.ConsumedIdentities) { if (!TryComp(devouredUid.Original, out var devouredComp)) continue; if (devouredComp.DevouredBy.Remove(ent.Owner)) Dirty(devouredUid.Original.Value, devouredComp); } } /// /// Removes reference to an original entity from . /// /// The changeling. /// The entity to remove from identity originals. private void RemoveOriginalReference(Entity ent, EntityUid original) { foreach (var identity in ent.Comp.ConsumedIdentities) { if (identity.Original == original) identity.Original = null; } Dirty(ent); } /// /// Clone a target humanoid to a paused map. /// It creates a perfect copy of the target and can be used to pull components down for future use. /// /// The settings to use for cloning. /// The target to clone. public EntityUid? CloneToPausedMap(ProtoId settings, EntityUid target) { // Don't create client side duplicate clones or a clientside map. if (_net.IsClient) return null; EnsurePausedMap(); if (PausedMapId == null) return null; var mapCoords = new MapCoordinates(0, 0, PausedMapId.Value); if (!_cloningSystem.TryCloning(target, mapCoords, settings, out var clone)) return null; var storedIdentity = EnsureComp(clone.Value); storedIdentity.OriginalEntity = target; // TODO: network this once we have a relations system so that this does not cause PVS errors. if (TryComp(target, out var actor)) storedIdentity.OriginalSession = actor.PlayerSession; return clone; } /// /// Clone a target humanoid to a paused map and add it to the Changelings list of identities. /// It creates a perfect copy of the target and can be used to pull components down for future use. /// /// The Changeling. /// The target to clone. /// Whether this identity was granted via devouring, aka granted DNA to the changeling. public EntityUid? GrantIdentity(Entity ent, EntityUid target, bool devoured = false) { if (!Resolve(ent.Owner, ref ent.Comp)) return null; var clone = CloneToPausedMap(ent.Comp.IdentityCloningSettings, target); if (clone == null) return null; var isNew = false; // We see if we already have a identity slot for this entity. // This can happen if we devoured them before, but then dropped their stored identity. if (!TryGetDataFromOriginal(ent.AsNullable(), target, out var newIdentity)) { newIdentity = new ChangelingIdentityData(); ent.Comp.ConsumedIdentities.Add(newIdentity); isNew = true; // Data didn't exist before so its not an update. } UpdateIdentityData(newIdentity, clone.Value, target); AddDevouredReference(ent!, target); newIdentity.GrantedDna = devoured; if (newIdentity.Original == ent) { // This is an identity of us, we don't wanna count it for objectives n stuff. newIdentity.Starting = true; newIdentity.GrantedDna = true; } var ev = new ChangelingGainedOrUpdatedIdentityEvent(ent, newIdentity, isNew); RaiseLocalEvent(ent, ref ev, true); // Broadcast it to allow the mind tracker to update. HandlePvsOverride(ent, clone.Value); Dirty(ent); return clone; } /// /// Marks that the changeling has successfully devoured the target. /// public void AddDevouredReference(Entity ent, EntityUid target) { var targetDevoured = EnsureComp(target); targetDevoured.DevouredBy.Add(ent.Owner); Dirty(target, targetDevoured); } /// /// Drop a stored identity from the changeling's storage. /// public void DropStoredIdentity(Entity ent, EntityUid identity) { if (!Resolve(ent, ref ent.Comp)) return; if (!HasComp(identity)) return; // Not a stored identity. var toDrop = ent.Comp.ConsumedIdentities.Where(data => data.Identity == identity).ToList(); foreach (var dropped in toDrop) { if (TryComp(dropped.Original, out var devoured)) { if (devoured.DevouredBy.Remove(ent)) Dirty(dropped.Original.Value, devoured); } dropped.Identity = null; } PredictedQueueDel(identity); if (toDrop.Count > 0) { Dirty(ent); } } /// /// Simple helper to add a PVS override to a nullspace identity. /// /// The actor that should get the override. /// The identity stored in nullspace. private void HandlePvsOverride(EntityUid uid, EntityUid identity) { if (!TryComp(uid, out var actor)) return; _pvsOverrideSystem.AddSessionOverride(identity, actor.PlayerSession); } /// /// Cleanup all PVS overrides for the owner of the ChangelingIdentity /// /// The changeling storing the identities. /// The session you wish to remove the overrides from. private void CleanupPvsOverride(Entity ent, ICommonSession session) { foreach (var identity in ent.Comp.ConsumedIdentities) { if (identity.Identity == null) continue; _pvsOverrideSystem.RemoveSessionOverride(identity.Identity.Value, session); } } /// /// Inform another session of the entities stored for transformation. /// /// The changeling storing the identities. /// The session you wish to inform. public void HandOverPvsOverride(Entity ent, ICommonSession session) { foreach (var identity in ent.Comp.ConsumedIdentities) { if (identity.Identity == null) continue; _pvsOverrideSystem.AddSessionOverride(identity.Identity.Value, session); } } /// /// Returns whether the changeling has space to store another disguise. /// public bool HasFreeDisguiseSlot(Entity ent) { if (!Resolve(ent, ref ent.Comp, false)) return false; return ent.Comp.ConsumedIdentities.Count(data => data.Identity != null) < ent.Comp.MaxStoredDisguises; } /// /// Whether the given changeling has a valid identity of the given entity. /// public bool HasIdentity(Entity changeling, EntityUid devoured) { if (!Resolve(changeling, ref changeling.Comp, false)) return false; return changeling.Comp.ConsumedIdentities.FirstOrDefault(data => data.Original == devoured && data.Identity != null) != null; } /// /// Create a paused map for storing devoured identities as a clone of the player. /// private void EnsurePausedMap() { if (_map.MapExists(PausedMapId)) return; var mapUid = _map.CreateMap(out var newMapId); _metaSystem.SetEntityName(mapUid, Loc.GetString("changeling-paused-map-name")); PausedMapId = newMapId; _map.SetPaused(mapUid, true); } /// /// Creates a ChangelingIdentityData for given entities. /// /// The created identity this is supposed to refer to. /// The original entity this is supposed to refer to. /// Identity data based on the given parameters. public ChangelingIdentityData CreateIdentityData(EntityUid identity, EntityUid original) { ChangelingIdentityData identityData = new ChangelingIdentityData(); UpdateIdentityData(identityData, identity, original); return identityData; } /// /// Updates an existing identity data with information from a new identity and original entity. /// /// The existing data. /// The changeling identity to use. /// The original entity of the identity. public void UpdateIdentityData(ChangelingIdentityData data, EntityUid identity, EntityUid original) { data.Identity = identity; data.Original = original; data.OriginalName = Name(original); var foundMind = _mind.TryGetMind(original, out var mindId, out _); data.OriginalMind = foundMind ? mindId : null; if (foundMind) { _job.MindTryGetJobId(mindId, out var jobId); data.OriginalJob = jobId; } } /// /// Fetches the relevant from an entity's based on the identity's EntityUid. /// /// The changeling entity. /// The identity's EntityUid. /// The returned if one is found. /// True if identity data is found, otherwise False. public bool TryGetDataFromIdentity(Entity ent, EntityUid identity, [NotNullWhen(true)] out ChangelingIdentityData? identityData) { identityData = null; if (!Resolve(ent, ref ent.Comp, false)) return false; identityData = ent.Comp.ConsumedIdentities.FirstOrDefault(data => data.Identity == identity); return identityData != null; } /// /// Fetches the relevant from an entity's based on the original entity's EntityUid. /// /// The changeling entity. /// The original entity's EntityUid. /// The returned if one is found. /// True if identity data is found, otherwise False. public bool TryGetDataFromOriginal(Entity ent, EntityUid original, [NotNullWhen(true)] out ChangelingIdentityData? identityData) { identityData = null; if (!Resolve(ent, ref ent.Comp, false)) return false; identityData = ent.Comp.ConsumedIdentities.FirstOrDefault(data => data.Original == original); return identityData != null; } /// /// Fetches the from an entity's based on the identity they are currently using. /// /// The changeling entity. /// The returned for the current identity if one is found. /// True if identity data is found, otherwise False. public bool TryGetCurrentIdentityData(Entity ent, [NotNullWhen(true)] out ChangelingIdentityData? identityData) { identityData = null; if (!Resolve(ent, ref ent.Comp, false)) return false; if (ent.Comp.CurrentIdentity == null) return false; identityData = ent.Comp.ConsumedIdentities.FirstOrDefault(data => data.Identity == ent.Comp.CurrentIdentity); return identityData != null; } }