using Content.Shared.Body.Part; using Content.Shared.Body.Systems; using Content.Shared.Clothing.Components; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Humanoid; using Content.Shared.Interaction.Events; using Content.Shared.Inventory; using Content.Shared.Inventory.Events; using Content.Shared.Item; using Content.Shared.Strip.Components; using Robust.Shared.Containers; using Robust.Shared.GameStates; namespace Content.Shared.Clothing.EntitySystems; public abstract class ClothingSystem : EntitySystem { [Dependency] private readonly SharedItemSystem _itemSys = default!; [Dependency] private readonly SharedContainerSystem _containerSys = default!; [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidSystem = default!; [Dependency] private readonly InventorySystem _invSystem = default!; [Dependency] private readonly SharedHandsSystem _handsSystem = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnUseInHand); SubscribeLocalEvent(OnGetState); SubscribeLocalEvent(OnHandleState); SubscribeLocalEvent(OnGotEquipped); SubscribeLocalEvent(OnGotUnequipped); SubscribeLocalEvent(OnMaskToggled); SubscribeLocalEvent(OnPickedUp); SubscribeLocalEvent(OnPartAttachedToBody, after: [typeof(SharedBodySystem)]); SubscribeLocalEvent(OnEquipDoAfter); SubscribeLocalEvent(OnUnequipDoAfter); SubscribeLocalEvent(OnItemStripped); } private void OnUseInHand(Entity ent, ref UseInHandEvent args) { if (args.Handled || !ent.Comp.QuickEquip) return; var user = args.User; if (!TryComp(user, out InventoryComponent? inv) || !TryComp(user, out HandsComponent? hands)) return; QuickEquip(ent, (user, inv, hands)); args.Handled = true; args.ApplyDelay = false; } private void QuickEquip( Entity toEquipEnt, Entity userEnt) { foreach (var slotDef in userEnt.Comp1.Slots) { // Do not attempt to quick-equip clothing in pocket slots. // We should probably add a special flag to SlotDefinition to skip quick equip if more similar slots get added. if (slotDef.SlotFlags.HasFlag(SlotFlags.POCKET)) continue; if (!_invSystem.CanEquip(userEnt, toEquipEnt, slotDef.Name, out _, slotDef, userEnt, toEquipEnt)) continue; if (_invSystem.TryGetSlotEntity(userEnt, slotDef.Name, out var slotEntity, userEnt)) { // Item in slot has to be quick equipable as well if (TryComp(slotEntity, out ClothingComponent? item) && !item.QuickEquip) continue; if (!_invSystem.TryUnequip(userEnt, slotDef.Name, true, inventory: userEnt, checkDoafter: true)) continue; if (!_invSystem.TryEquip(userEnt, toEquipEnt, slotDef.Name, true, inventory: userEnt, clothing: toEquipEnt, checkDoafter: true)) continue; _handsSystem.PickupOrDrop(userEnt, slotEntity.Value, handsComp: userEnt); } else { if (!_invSystem.TryEquip(userEnt, toEquipEnt, slotDef.Name, true, inventory: userEnt, clothing: toEquipEnt, checkDoafter: true)) continue; } break; } } private void ToggleVisualLayers(EntityUid equipee, HashSet layers, HashSet appearanceLayers, bool force = false) { foreach (HumanoidVisualLayers layer in layers) { if (!force && !appearanceLayers.Contains(layer)) continue; InventorySystem.InventorySlotEnumerator enumerator = _invSystem.GetSlotEnumerator(equipee); bool shouldLayerShow = true; while (enumerator.NextItem(out EntityUid item, out SlotDefinition? slot)) { if (TryComp(item, out HideLayerClothingComponent? comp)) { if (comp.Slots.Contains(layer)) { if (TryComp(item, out ClothingComponent? clothing) && clothing.Slots == slot.SlotFlags) { //Checks for mask toggling. TODO: Make a generic system for this if (comp.HideOnToggle && TryComp(item, out MaskComponent? mask)) { if (clothing.EquippedPrefix != mask.EquippedPrefix) { shouldLayerShow = false; break; } } else { shouldLayerShow = false; break; } } } } } _humanoidSystem.SetLayerVisibility(equipee, layer, shouldLayerShow); } } protected virtual void OnGotEquipped(EntityUid uid, ClothingComponent component, GotEquippedEvent args) { component.InSlot = args.Slot; CheckEquipmentForLayerHide(args.Equipment, args.Equipee); if ((component.Slots & args.SlotFlags) != SlotFlags.NONE) { var gotEquippedEvent = new ClothingGotEquippedEvent(args.Equipee, component); RaiseLocalEvent(uid, ref gotEquippedEvent); var didEquippedEvent = new ClothingDidEquippedEvent((uid, component)); RaiseLocalEvent(args.Equipee, ref didEquippedEvent); } } protected virtual void OnGotUnequipped(EntityUid uid, ClothingComponent component, GotUnequippedEvent args) { if ((component.Slots & args.SlotFlags) != SlotFlags.NONE) { var gotUnequippedEvent = new ClothingGotUnequippedEvent(args.Equipee, component); RaiseLocalEvent(uid, ref gotUnequippedEvent); var didUnequippedEvent = new ClothingDidUnequippedEvent((uid, component)); RaiseLocalEvent(args.Equipee, ref didUnequippedEvent); } component.InSlot = null; CheckEquipmentForLayerHide(args.Equipment, args.Equipee); } private void OnGetState(EntityUid uid, ClothingComponent component, ref ComponentGetState args) { args.State = new ClothingComponentState(component.EquippedPrefix); } private void OnHandleState(EntityUid uid, ClothingComponent component, ref ComponentHandleState args) { if (args.Current is ClothingComponentState state) { SetEquippedPrefix(uid, state.EquippedPrefix, component); if (component.InSlot != null && _containerSys.TryGetContainingContainer((uid, null, null), out var container)) { CheckEquipmentForLayerHide(uid, container.Owner); } } } private void OnMaskToggled(Entity ent, ref ItemMaskToggledEvent args) { //TODO: sprites for 'pulled down' state. defaults to invisible due to no sprite with this prefix SetEquippedPrefix(ent, args.IsToggled ? args.equippedPrefix : null, ent); CheckEquipmentForLayerHide(ent.Owner, args.Wearer); } private void OnPickedUp(Entity ent, ref GettingPickedUpAttemptEvent args) { // If this clothing is equipped by the performer of this action, and the clothing has an unequip delay, stop the attempt if (ent.Comp.UnequipDelay <= TimeSpan.Zero || !_invSystem.TryGetContainingSlot(ent.Owner, out var slot) || !_containerSys.TryGetContainingContainer(ent, out var container) || container.Owner != args.User) return; args.Cancel(); } // Yes, this is exclusive C# just so that high heels selected from loadouts still hide the feet layers // after Shitmed (SharedBodySystem.PartAppearance) initializes the feet parts setting their layer visibility to true. private void OnPartAttachedToBody(Entity ent, ref BodyPartAddedEvent args) { var enumerator = _invSystem.GetSlotEnumerator(ent.Owner); while (enumerator.NextItem(out var item)) { if (!TryComp(item, out var comp)) continue; CheckEquipmentForLayerHide(item, ent.Owner); } } private void OnEquipDoAfter(Entity ent, ref ClothingEquipDoAfterEvent args) { if (args.Handled || args.Cancelled || args.Target is not { } target) return; args.Handled = _invSystem.TryEquip(args.User, target, ent, args.Slot, clothing: ent.Comp, predicted: true, checkDoafter: false); } private void OnUnequipDoAfter(Entity ent, ref ClothingUnequipDoAfterEvent args) { if (args.Handled || args.Cancelled || args.Target is not { } target) return; args.Handled = _invSystem.TryUnequip(args.User, target, args.Slot, clothing: ent.Comp, predicted: true, checkDoafter: false); if (args.Handled) _handsSystem.TryPickup(args.User, ent); } private void OnItemStripped(Entity ent, ref BeforeItemStrippedEvent args) { args.Additive += ent.Comp.StripDelay; } private void CheckEquipmentForLayerHide(EntityUid equipment, EntityUid equipee) { if (TryComp(equipment, out HideLayerClothingComponent? clothesComp) && TryComp(equipee, out HumanoidAppearanceComponent? appearanceComp)) ToggleVisualLayers(equipee, clothesComp.Slots, appearanceComp.HideLayersOnEquip, clothesComp.Force); } #region Public API public void SetEquippedPrefix(EntityUid uid, string? prefix, ClothingComponent? clothing = null) { if (!Resolve(uid, ref clothing, false)) return; if (clothing.EquippedPrefix == prefix) return; clothing.EquippedPrefix = prefix; _itemSys.VisualsChanged(uid); Dirty(uid, clothing); } public void SetSlots(EntityUid uid, SlotFlags slots, ClothingComponent? clothing = null) { if (!Resolve(uid, ref clothing)) return; clothing.Slots = slots; Dirty(uid, clothing); } /// /// Copy all clothing specific visuals from another item. /// public void CopyVisuals(EntityUid uid, ClothingComponent otherClothing, ClothingComponent? clothing = null) { if (!Resolve(uid, ref clothing)) return; clothing.ClothingVisuals = otherClothing.ClothingVisuals; clothing.EquippedPrefix = otherClothing.EquippedPrefix; clothing.Sprite = otherClothing.Sprite; clothing.ClothingType = otherClothing.ClothingType; // WD EDIT _itemSys.VisualsChanged(uid); Dirty(uid, clothing); } public void SetLayerColor(ClothingComponent clothing, string slot, string mapKey, Color? color) { foreach (var layer in clothing.ClothingVisuals[slot]) { if (layer.MapKeys == null) return; if (!layer.MapKeys.Contains(mapKey)) continue; layer.Color = color; } } public void SetLayerState(ClothingComponent clothing, string slot, string mapKey, string state) { foreach (var layer in clothing.ClothingVisuals[slot]) { if (layer.MapKeys == null) return; if (!layer.MapKeys.Contains(mapKey)) continue; layer.State = state; } } #endregion }