using Content.Shared.Actions; using Content.Shared.Administration.Logs; using Content.Shared.Body; using Content.Shared.Changeling.Components; using Content.Shared.Cloning; using Content.Shared.Database; using Content.Shared.DoAfter; using Content.Shared.IdentityManagement; using Content.Shared.Popups; using Content.Shared.Storage; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Network; using Robust.Shared.Prototypes; namespace Content.Shared.Changeling.Systems; public sealed partial class ChangelingTransformSystem : EntitySystem { [Dependency] private INetManager _net = default!; [Dependency] private SharedActionsSystem _actions = default!; [Dependency] private SharedUserInterfaceSystem _ui = default!; [Dependency] private SharedDoAfterSystem _doAfter = default!; [Dependency] private MetaDataSystem _metaData = default!; [Dependency] private SharedPopupSystem _popup = default!; [Dependency] private ISharedAdminLogManager _adminLogger = default!; [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private SharedCloningSystem _cloning = default!; [Dependency] private SharedVisualBodySystem _visualBody = default!; [Dependency] private SharedContainerSystem _container = default!; [Dependency] private IdentitySystem _identity = default!; [Dependency] private SharedChangelingIdentitySystem _changelingIdentity = default!; private const string ChangelingBuiXmlGeneratedName = "ChangelingTransformBoundUserInterface"; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnTransformAction); SubscribeLocalEvent(OnTransformSelected); SubscribeLocalEvent(OnTransformDrop); SubscribeLocalEvent(OnSuccessfulTransform); SubscribeLocalEvent(OnShutdown); // Components that need special handling outside of cloning. SubscribeLocalEvent(StorageBeforeTransform); } private void OnMapInit(Entity ent, ref MapInitEvent init) { _actions.AddAction(ent, ref ent.Comp.ChangelingTransformActionEntity, ent.Comp.ChangelingTransformAction); var userInterfaceComp = EnsureComp(ent); _ui.SetUi((ent, userInterfaceComp), ChangelingTransformUiKey.Key, new InterfaceData(ChangelingBuiXmlGeneratedName)); } private void OnShutdown(Entity ent, ref ComponentShutdown args) { if (ent.Comp.ChangelingTransformActionEntity != null) { _actions.RemoveAction(ent.Owner, ent.Comp.ChangelingTransformActionEntity); } } private void OnTransformAction(Entity ent, ref ChangelingTransformActionEvent args) { if (!TryComp(ent, out var userInterfaceComp)) return; if (!HasComp(ent)) return; if (!_ui.IsUiOpen((ent, userInterfaceComp), ChangelingTransformUiKey.Key, args.Performer)) { _ui.OpenUi((ent, userInterfaceComp), ChangelingTransformUiKey.Key, args.Performer); } //TODO: Can add a Else here with TransformInto and CloseUI to make a quick switch, // issue right now is that Radials cover the Action buttons so clicking the action closes the UI (due to clicking off a radial causing it to close, even with UI) // but pressing the number does. } private void OnTransformSelected(Entity ent, ref ChangelingTransformIdentitySelectMessage args) { if (!TryGetEntity(args.TargetIdentity, out var targetIdentity)) return; if (!TryComp(ent, out var identity)) return; if (identity.CurrentIdentity == targetIdentity) return; // don't transform into ourselves if (!_changelingIdentity.TryGetDataFromIdentity((ent.Owner, identity), targetIdentity.Value, out _)) return; // this identity does not belong to this player TransformInto(ent.AsNullable(), targetIdentity.Value); } private void OnTransformDrop(Entity ent, ref ChangelingTransformIdentityDropMessage args) { if (!TryGetEntity(args.TargetIdentity, out var targetIdentity)) return; if (!TryComp(ent, out var identity)) return; if (!ent.Comp.ManualDrop) return; // can't drop identities in this mode if (identity.CurrentIdentity == targetIdentity) return; // don't drop our current identity if (!_changelingIdentity.TryGetDataFromIdentity((ent.Owner, identity), targetIdentity.Value, out _)) return; // this identity does not belong to this player _popup.PopupEntity(Loc.GetString("changeling-transform-bui-drop-identity-entity-popup", ("entity", targetIdentity.Value)), ent.Owner, ent.Owner, PopupType.Large); _changelingIdentity.DropStoredIdentity(ent.Owner, targetIdentity.Value); } /// /// Transform the changeling into another identity. /// This can be any cloneable humanoid and doesn't have to be stored in the ChangelingIdentityComponent, /// so make sure to validate the target before. /// public void TransformInto(Entity ent, EntityUid targetIdentity) { if (!Resolve(ent, ref ent.Comp)) return; var selfMessage = Loc.GetString("changeling-transform-attempt-self", ("user", Identity.Entity(ent.Owner, EntityManager))); var othersMessage = Loc.GetString("changeling-transform-attempt-others", ("user", Identity.Entity(ent.Owner, EntityManager))); _popup.PopupEntity( selfMessage, othersMessage, ent, ent, PopupType.MediumCaution); if (_net.IsServer) { ent.Comp.CurrentTransformSound = _audio.Stop(ent.Comp.CurrentTransformSound); // cancel any previous sounds first ent.Comp.CurrentTransformSound = _audio.PlayPvs(ent.Comp.TransformAttemptNoise, ent)?.Entity; } if (TryComp(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null) _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} begun an attempt to transform into \"{Name(targetIdentity)}\" ({storedIdentity.OriginalSession:player}) "); else _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} begun an attempt to transform into \"{Name(targetIdentity)}\""); _doAfter.TryStartDoAfter(new DoAfterArgs( EntityManager, ent, ent.Comp.TransformWindup, new ChangelingTransformDoAfterEvent(), ent, target: targetIdentity) { DuplicateCondition = DuplicateConditions.None, RequireCanInteract = false, DistanceThreshold = null, ExamineText = ent.Comp.TransformExamine == null ? null : Loc.GetString(ent.Comp.TransformExamine, ("user", Identity.Entity(ent, EntityManager))) }); } private void OnSuccessfulTransform(Entity ent, ref ChangelingTransformDoAfterEvent args) { args.Handled = true; if (args.Cancelled) { // Only stop the sound if we finish transforming successfully. ent.Comp.CurrentTransformSound = _audio.Stop(ent.Comp.CurrentTransformSound); return; } ent.Comp.CurrentTransformSound = null; if (!ProtoMan.Resolve(ent.Comp.TransformCloningSettings, out var settings)) return; if (args.Target is not { } targetIdentity) return; EntityUid? previousIdentity = null; if (TryComp(ent.Owner, out var identityComp) && !ent.Comp.ManualDrop) previousIdentity = identityComp.CurrentIdentity; var beforeTransformEvent = new BeforeChangelingTransformEvent(targetIdentity); RaiseLocalEvent(args.User, beforeTransformEvent); _visualBody.CopyAppearanceFrom(targetIdentity, args.User); _cloning.CloneComponents(targetIdentity, args.User, settings); if (settings.CopyStatusEffects) _cloning.CopyStatusEffects(targetIdentity, args.User, settings.StatusEffectWhitelist, settings.StatusEffectBlacklist); if (TryComp(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null) _adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent.Owner):player} successfully transformed into \"{Name(targetIdentity)}\" ({storedIdentity.OriginalSession:player})"); else _adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent.Owner):player} successfully transformed into \"{Name(targetIdentity)}\""); _metaData.SetEntityName(ent, Name(targetIdentity), raiseEvents: false); // Don't raise events because we don't want to rename the ID card. _metaData.SetEntityDescription(ent, Description(targetIdentity)); _identity.QueueIdentityUpdate(ent); // We have to manually refresh the identity because we did not raise events. Dirty(ent); if (TryComp(ent, out var identity)) // in case we ever get changelings that don't store identities { identity.CurrentIdentity = targetIdentity; Dirty(ent.Owner, identity); } var afterTransformEvent = new AfterChangelingTransformEvent(targetIdentity); RaiseLocalEvent(args.User, afterTransformEvent); if (previousIdentity != null) _changelingIdentity.DropStoredIdentity(ent.Owner, previousIdentity.Value); } private void StorageBeforeTransform(Entity ent, ref BeforeChangelingTransformEvent args) { if (HasComp(args.StoredIdentity)) return; // If we have a storage component and the target has one as well, then do nothing. // If the target identity does not have a storage anymore, drop all items inside our storage so that they don't become unreachable. _container.EmptyContainer(ent.Comp.Container); } }