using Content.Shared.Actions; using Content.Shared.Clothing.Components; using Content.Shared.DoAfter; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Inventory; using Content.Shared.Inventory.Events; using Content.Shared.Popups; using Content.Shared.Strip; using Content.Shared.Verbs; using Robust.Shared.Containers; using Robust.Shared.Network; using Robust.Shared.Serialization; using Robust.Shared.Timing; using Robust.Shared.Utility; using System.Linq; namespace Content.Shared.Clothing.EntitySystems; // GOOBSTATION - MODSUITS - THIS SYSTEM FULLY CHANGED public sealed class ToggleableClothingSystem : EntitySystem { [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly INetManager _netMan = default!; [Dependency] private readonly SharedContainerSystem _containerSystem = default!; [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; [Dependency] private readonly ActionContainerSystem _actionContainer = default!; [Dependency] private readonly InventorySystem _inventorySystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedStrippableSystem _strippable = default!; [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnToggleableInit); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnToggleClothingAction); SubscribeLocalEvent(OnGetActions); SubscribeLocalEvent(OnRemoveToggleable); SubscribeLocalEvent(OnToggleableUnequip); SubscribeLocalEvent(OnToggleClothingMessage); SubscribeLocalEvent(OnToggleableUnequipAttempt); SubscribeLocalEvent(OnAttachedInit); SubscribeLocalEvent(OnInteractHand); SubscribeLocalEvent(OnAttachedUnequip); SubscribeLocalEvent(OnRemoveAttached); SubscribeLocalEvent(OnAttachedUnequipAttempt); SubscribeLocalEvent>>(GetRelayedVerbs); SubscribeLocalEvent>(OnGetVerbs); SubscribeLocalEvent>(OnGetAttachedStripVerbsEvent); SubscribeLocalEvent(OnDoAfterComplete); } private void GetRelayedVerbs(Entity toggleable, ref InventoryRelayedEvent> args) { OnGetVerbs(toggleable, ref args.Args); } private void OnGetVerbs(Entity toggleable, ref GetVerbsEvent args) { var comp = toggleable.Comp; if (!args.CanAccess || !args.CanInteract || args.Hands == null || comp.ClothingUids.Count == 0 || comp.Container == null) return; var text = comp.VerbText ?? (comp.ActionEntity == null ? null : Name(comp.ActionEntity.Value)); if (text == null) return; if (!_inventorySystem.InSlotWithFlags(toggleable.Owner, comp.RequiredFlags)) return; var wearer = Transform(toggleable).ParentUid; if (args.User != wearer && comp.StripDelay == null) return; var user = args.User; var verb = new EquipmentVerb() { Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")), Text = Loc.GetString(text), }; if (user == wearer) { verb.Act = () => ToggleClothing(user, toggleable); } else { verb.Act = () => StartDoAfter(user, toggleable, wearer); } args.Verbs.Add(verb); } private void StartDoAfter(EntityUid user, Entity toggleable, EntityUid wearer) { var comp = toggleable.Comp; if (comp.StripDelay == null) return; var (time, stealth) = _strippable.GetStripTimeModifiers(user, wearer, toggleable, comp.StripDelay.Value); bool hidden = stealth == ThievingStealth.Hidden; var args = new DoAfterArgs(EntityManager, user, time, new ToggleClothingDoAfterEvent(), toggleable, wearer, toggleable) { BreakOnDamage = true, BreakOnMove = true, DistanceThreshold = 2, }; if (!_doAfter.TryStartDoAfter(args)) return; if (!hidden) { var popup = Loc.GetString("strippable-component-alert-owner-interact", ("user", Identity.Entity(user, EntityManager)), ("item", toggleable)); _popupSystem.PopupEntity(popup, wearer, wearer, PopupType.Large); } } private void OnGetAttachedStripVerbsEvent(Entity attached, ref GetVerbsEvent args) { var comp = attached.Comp; if (!TryComp(comp.AttachedUid, out var toggleableComp)) return; // redirect to the attached entity. OnGetVerbs((comp.AttachedUid, toggleableComp), ref args); } private void OnDoAfterComplete(Entity toggleable, ref ToggleClothingDoAfterEvent args) { if (args.Cancelled) return; ToggleClothing(args.User, toggleable); } private void OnInteractHand(Entity attached, ref InteractHandEvent args) { var comp = attached.Comp; if (args.Handled) return; if (!TryComp(comp.AttachedUid, out ToggleableClothingComponent? toggleableComp) || toggleableComp.Container == null) return; // Get slot from dictionary of uid-slot if (!toggleableComp.ClothingUids.TryGetValue(attached.Owner, out var attachedSlot)) return; if (!_inventorySystem.TryUnequip(Transform(attached.Owner).ParentUid, attachedSlot, force: true)) return; _containerSystem.Insert(attached.Owner, toggleableComp.Container); args.Handled = true; } /// /// Prevents from unequipping entity if all attached not unequipped /// private void OnToggleableUnequipAttempt(Entity toggleable, ref BeingUnequippedAttemptEvent args) { var comp = toggleable.Comp; if (!comp.BlockUnequipWhenAttached) return; if (GetAttachedToggleStatus(toggleable) == ToggleableClothingAttachedStatus.NoneToggled) return; _popupSystem.PopupClient(Loc.GetString("toggleable-clothing-remove-all-attached-first"), args.Unequipee, args.Unequipee); args.Cancel(); } /// /// Called when the suit is unequipped, to ensure that the helmet also gets unequipped. /// private void OnToggleableUnequip(Entity toggleable, ref GotUnequippedEvent args) { var comp = toggleable.Comp; // If it's a part of PVS departure then don't handle it. if (_timing.ApplyingState) return; // Check if container exists and we have linked clothings if (comp.Container == null || comp.ClothingUids.Count == 0) return; var parts = comp.ClothingUids; foreach (var part in parts) { // Check if entity in container what means it already unequipped if (comp.Container.Contains(part.Key) || part.Value == null) continue; _inventorySystem.TryUnequip(args.Equipee, part.Value, force: true); } } private void OnRemoveToggleable(Entity toggleable, ref ComponentRemove args) { // If the parent/owner component of the attached clothing is being removed (entity getting deleted?) we will // delete the attached entity. We do this regardless of whether or not the attached entity is currently // "outside" of the container or not. This means that if a hardsuit takes too much damage, the helmet will also // automatically be deleted. var comp = toggleable.Comp; _actionsSystem.RemoveAction(comp.ActionEntity); if (comp.ClothingUids == null || _netMan.IsClient) return; foreach (var clothing in comp.ClothingUids.Keys) QueueDel(clothing); } private void OnAttachedUnequipAttempt(Entity attached, ref BeingUnequippedAttemptEvent args) { args.Cancel(); } private void OnRemoveAttached(Entity attached, ref ComponentRemove args) { // if the attached component is being removed (maybe entity is being deleted?) we will just remove the // toggleable clothing component. This means if you had a hard-suit helmet that took too much damage, you would // still be left with a suit that was simply missing a helmet. There is currently no way to fix a partially // broken suit like this. var comp = attached.Comp; if (!TryComp(comp.AttachedUid, out ToggleableClothingComponent? toggleableComp) || toggleableComp.LifeStage > ComponentLifeStage.Running) return; var clothingUids = toggleableComp.ClothingUids; if (!clothingUids.Remove(attached.Owner) || clothingUids.Count > 0) return; // If no attached clothing left - remove component and action if (clothingUids.Count > 0) return; _actionsSystem.RemoveAction(toggleableComp.ActionEntity); RemComp(comp.AttachedUid, toggleableComp); } /// /// Called if the clothing was unequipped, to ensure that it gets moved into the suit's container. /// private void OnAttachedUnequip(Entity attached, ref GotUnequippedEvent args) { var comp = attached.Comp; // Death told me to do this- if you need to figure out why each of these are here, idk, figure it out. if (_timing.ApplyingState || comp.LifeStage > ComponentLifeStage.Running || !TryComp(comp.AttachedUid, out ToggleableClothingComponent? toggleableComp) || toggleableComp.LifeStage > ComponentLifeStage.Running || !toggleableComp.ClothingUids.ContainsKey(attached.Owner)) return; if (toggleableComp.Container != null) _containerSystem.Insert(attached.Owner, toggleableComp.Container); } /// /// Equip or unequip toggle clothing with ui message /// private void OnToggleClothingMessage(Entity toggleable, ref ToggleableClothingUiMessage args) { var attachedUid = GetEntity(args.AttachedClothingUid); ToggleClothing(args.Actor, toggleable, attachedUid); } /// /// Equip or unequip the toggleable clothing. /// private void OnToggleClothingAction(Entity toggleable, ref ToggleClothingEvent args) { var comp = toggleable.Comp; if (args.Handled) return; if (comp.Container == null || comp.ClothingUids.Count == 0) return; args.Handled = true; // If clothing have only one attached clothing (like helmets) action will just toggle it // If it have more attached clothings, it'll open radial menu if (comp.ClothingUids.Count == 1) ToggleClothing(args.Performer, toggleable, comp.ClothingUids.First().Key); else _uiSystem.OpenUi(toggleable.Owner, ToggleClothingUiKey.Key, args.Performer); } /// /// Toggle function for single clothing /// private void ToggleClothing(EntityUid user, Entity toggleable, EntityUid attachedUid) { var comp = toggleable.Comp; var attachedClothings = comp.ClothingUids; var container = comp.Container; if (!CanToggleClothing(user, toggleable)) return; if (!attachedClothings.TryGetValue(attachedUid, out var slot) || string.IsNullOrEmpty(slot)) return; if (!container!.Contains(attachedUid)) UnequipClothing(user, toggleable, attachedUid, slot!); else EquipClothing(user, toggleable, attachedUid, slot!); } /// /// Toggle function for toggling multiple clothings at once /// private void ToggleClothing(EntityUid user, Entity toggleable) { var comp = toggleable.Comp; var attachedClothings = comp.ClothingUids; var container = comp.Container; if (!CanToggleClothing(user, toggleable)) return; if (GetAttachedToggleStatus(toggleable, comp) == ToggleableClothingAttachedStatus.NoneToggled) foreach (var clothing in attachedClothings) EquipClothing(user, toggleable, clothing.Key, clothing.Value); else foreach (var clothing in attachedClothings) if (!container!.Contains(clothing.Key)) UnequipClothing(user, toggleable, clothing.Key, clothing.Value); } private bool CanToggleClothing(EntityUid user, Entity toggleable) { var comp = toggleable.Comp; var attachedClothings = comp.ClothingUids; var container = comp.Container; if (container == null || attachedClothings.Count == 0) return false; var ev = new ToggleClothingAttemptEvent(user, toggleable); RaiseLocalEvent(toggleable, ev); return !ev.Cancelled; } private void UnequipClothing(EntityUid user, Entity toggleable, EntityUid clothing, string slot) { var parent = Transform(toggleable.Owner).ParentUid; _inventorySystem.TryUnequip(user, parent, slot, force: true); // If attached have clothing in container - equip it if (!TryComp(clothing, out var attachedComp) || attachedComp.ClothingContainer == null) return; var storedClothing = attachedComp.ClothingContainer.ContainedEntity; if (storedClothing != null) _inventorySystem.TryEquip(parent, storedClothing.Value, slot, force: true); } private void EquipClothing(EntityUid user, Entity toggleable, EntityUid clothing, string slot) { var parent = Transform(toggleable.Owner).ParentUid; var comp = toggleable.Comp; if (_inventorySystem.TryGetSlotEntity(parent, slot, out var currentClothing)) { // Check if we need to replace current clothing if (!TryComp(clothing, out var attachedComp) || !comp.ReplaceCurrentClothing) { _popupSystem.PopupClient(Loc.GetString("toggleable-clothing-remove-first", ("entity", currentClothing)), user, user); return; } // Check if attached clothing have container or this container not empty if (attachedComp.ClothingContainer == null || attachedComp.ClothingContainer.ContainedEntity != null) return; if (_inventorySystem.TryUnequip(user, parent, slot)) _containerSystem.Insert(currentClothing.Value, attachedComp.ClothingContainer); } _inventorySystem.TryEquip(user, parent, clothing, slot); } private void OnGetActions(Entity toggleable, ref GetItemActionsEvent args) { var comp = toggleable.Comp; if (comp.ClothingUids.Count == 0 || comp.ActionEntity == null || args.SlotFlags != comp.RequiredFlags) return; args.AddAction(comp.ActionEntity.Value); } private void OnToggleableInit(Entity toggleable, ref ComponentInit args) { var comp = toggleable.Comp; comp.Container = _containerSystem.EnsureContainer(toggleable, comp.ContainerId); } private void OnAttachedInit(Entity attached, ref ComponentInit args) { var comp = attached.Comp; comp.ClothingContainer = _containerSystem.EnsureContainer(attached, comp.ClothingContainerId); } /// /// On map init, either spawn the appropriate entity into the suit slot, or if it already exists, perform some /// sanity checks. Also updates the action icon to show the toggled-entity. /// private void OnMapInit(Entity toggleable, ref MapInitEvent args) { var comp = toggleable.Comp; if (comp.Container!.Count != 0) { DebugTools.Assert(comp.ClothingUids.Count != 0, "Unexpected entity present inside of a toggleable clothing container."); return; } if (comp.ClothingUids.Count != 0 && comp.ActionEntity != null) return; // Add prototype from ClothingPrototype and Slot field to ClothingPrototypes dictionary if (comp.ClothingPrototype != null && !string.IsNullOrEmpty(comp.Slot) && !comp.ClothingPrototypes.ContainsKey(comp.Slot)) { comp.ClothingPrototypes.Add(comp.Slot, comp.ClothingPrototype.Value); } var xform = Transform(toggleable.Owner); if (comp.ClothingPrototypes == null) return; var prototypes = comp.ClothingPrototypes; foreach (var prototype in prototypes) { var spawned = Spawn(prototype.Value, xform.Coordinates); var attachedClothing = EnsureComp(spawned); attachedClothing.AttachedUid = toggleable; EnsureComp(spawned); comp.ClothingUids.Add(spawned, prototype.Key); _containerSystem.Insert(spawned, comp.Container, containerXform: xform); Dirty(spawned, attachedClothing); } Dirty(toggleable, comp); if (_actionContainer.EnsureAction(toggleable, ref comp.ActionEntity, out var action, comp.Action)) _actionsSystem.SetEntityIcon(comp.ActionEntity.Value, toggleable, action); } // Checks status of all attached clothings toggle status public ToggleableClothingAttachedStatus GetAttachedToggleStatus(EntityUid toggleable, ToggleableClothingComponent? component = null) { if (!Resolve(toggleable, ref component)) return ToggleableClothingAttachedStatus.NoneToggled; var container = component.Container; var attachedClothings = component.ClothingUids; // If entity don't have any attached clothings it means none toggled if (container == null || attachedClothings.Count == 0) return ToggleableClothingAttachedStatus.NoneToggled; var toggledCount = attachedClothings.Count(c => !container.Contains(c.Key)); if (toggledCount == 0) return ToggleableClothingAttachedStatus.NoneToggled; if (toggledCount < attachedClothings.Count) return ToggleableClothingAttachedStatus.PartlyToggled; return ToggleableClothingAttachedStatus.AllToggled; } public List? GetAttachedClothingsList(EntityUid toggleable, ToggleableClothingComponent? component = null) { if (!Resolve(toggleable, ref component) || component.ClothingUids.Count == 0) return null; var newList = new List(); foreach (var attachee in component.ClothingUids) newList.Add(attachee.Key); return newList; } } public sealed partial class ToggleClothingEvent : InstantActionEvent { } [Serializable, NetSerializable] public sealed partial class ToggleClothingDoAfterEvent : SimpleDoAfterEvent { } /// /// Event raises on toggleable clothing when someone trying to toggle it /// public sealed class ToggleClothingAttemptEvent : CancellableEntityEventArgs { public EntityUid User { get; } public EntityUid Target { get; } public ToggleClothingAttemptEvent(EntityUid user, EntityUid target) { User = user; Target = target; } } /// /// Status of toggleable clothing attachee /// [Serializable, NetSerializable] public enum ToggleableClothingAttachedStatus : byte { NoneToggled, PartlyToggled, AllToggled }