using System.Numerics; using Content.Shared.Changeling.Components; using Content.Shared.Cloning; using Content.Shared.Humanoid; using Content.Shared.NameModifier.EntitySystems; 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 class SharedChangelingIdentitySystem : EntitySystem { [Dependency] private readonly INetManager _net = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly MetaDataSystem _metaSystem = default!; [Dependency] private readonly NameModifierSystem _nameMod = default!; [Dependency] private readonly SharedCloningSystem _cloningSystem = default!; [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidSystem = default!; [Dependency] private readonly SharedMapSystem _map = default!; [Dependency] private readonly SharedPvsOverrideSystem _pvsOverrideSystem = default!; public MapId? PausedMapId; private int _numberOfStoredIdentities = 0; // TODO: remove this public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnPlayerAttached); SubscribeLocalEvent(OnPlayerDetached); SubscribeLocalEvent(OnStoredRemove); } 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. var clone = CloneToPausedMap(ent, ent.Owner); ent.Comp.CurrentIdentity = clone; } private void OnShutdown(Entity ent, ref ComponentShutdown args) { if (TryComp(ent, out var actor)) CleanupPvsOverride(ent, actor.PlayerSession); CleanupChangelingNullspaceIdentities(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); } } /// /// 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(CloningSettingsPrototype settings, EntityUid target) { // Don't create client side duplicate clones or a clientside map. if (_net.IsClient) return null; if (!TryComp(target, out var humanoid) || !_prototype.Resolve(humanoid.Species, out var speciesPrototype)) return null; EnsurePausedMap(); // TODO: Setting the spawn location is a shitty bandaid to prevent admins from crashing our servers. // Movercontrollers and mob collisions are currently being calculated even for paused entities. // Spawning all of them in the same spot causes severe performance problems. // Cryopods and Polymorph have the same problem. var clone = Spawn(speciesPrototype.Prototype, new MapCoordinates(new Vector2(2 * _numberOfStoredIdentities++, 0), PausedMapId!.Value)); var storedIdentity = EnsureComp(clone); storedIdentity.OriginalEntity = target; // TODO: network this once we have WeakEntityReference or the autonetworking source gen is fixed if (TryComp(target, out var actor)) storedIdentity.OriginalSession = actor.PlayerSession; _humanoidSystem.CloneAppearance(target, clone); _cloningSystem.CloneComponents(target, clone, settings); var targetName = _nameMod.GetBaseName(target); _metaSystem.SetEntityName(clone, targetName); 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. public EntityUid? CloneToPausedMap(Entity ent, EntityUid target) { if (!_prototype.Resolve(ent.Comp.IdentityCloningSettings, out var settings)) return null; var clone = CloneToPausedMap(settings, target); if (clone == null) return null; ent.Comp.ConsumedIdentities.Add(clone.Value); Dirty(ent); HandlePvsOverride(ent, clone.Value); return clone; } /// /// 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. /// private void CleanupPvsOverride(Entity ent, ICommonSession session) { foreach (var identity in ent.Comp.ConsumedIdentities) { _pvsOverrideSystem.RemoveSessionOverride(identity, 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) { _pvsOverrideSystem.AddSessionOverride(identity, session); } } /// /// 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); } }