| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- using System.Diagnostics.CodeAnalysis;
- using System.Linq;
- using System.Numerics;
- using Content.Client.DisplacementMap;
- using Content.Client.Inventory;
- using Content.Shared.Clothing;
- using Content.Shared.Clothing.Components;
- using Content.Shared.Clothing.EntitySystems;
- using Content.Shared.DisplacementMap;
- using Content.Shared.Humanoid;
- using Content.Shared.Inventory;
- using Content.Shared.Inventory.Events;
- using Content.Shared.Item;
- using Robust.Client.GameObjects;
- using Robust.Client.Graphics;
- using Robust.Client.ResourceManagement;
- using Robust.Shared.Serialization.Manager;
- using Robust.Shared.Serialization.TypeSerializers.Implementations;
- using Robust.Shared.Utility;
- using static Robust.Client.GameObjects.SpriteComponent;
- namespace Content.Client.Clothing;
- public sealed class ClientClothingSystem : ClothingSystem
- {
- public const string Jumpsuit = "jumpsuit";
- /// <summary>
- /// This is a shitty hotfix written by me (Paul) to save me from renaming all files.
- /// For some context, im currently refactoring inventory. Part of that is slots not being indexed by a massive enum anymore, but by strings.
- /// Problem here: Every rsi-state is using the old enum-names in their state. I already used the new inventoryslots ALOT. tldr: its this or another week of renaming files.
- /// </summary>
- private static readonly Dictionary<string, string> TemporarySlotMap = new()
- {
- {"head", "HELMET"},
- {"eyes", "EYES"},
- {"ears", "EARS"},
- {"mask", "MASK"},
- {"outerClothing", "OUTERCLOTHING"},
- {Jumpsuit, "INNERCLOTHING"},
- {"neck", "NECK"},
- {"back", "BACKPACK"},
- {"belt", "BELT"},
- {"gloves", "HAND"},
- {"shoes", "FEET"},
- {"id", "IDCARD"},
- {"pocket1", "POCKET1"},
- {"pocket2", "POCKET2"},
- {"suitstorage", "SUITSTORAGE"},
- };
- [Dependency] private readonly IResourceCache _cache = default!;
- [Dependency] private readonly InventorySystem _inventorySystem = default!;
- [Dependency] private readonly DisplacementMapSystem _displacement = default!;
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent<ClothingComponent, GetEquipmentVisualsEvent>(OnGetVisuals);
- SubscribeLocalEvent<ClothingComponent, InventoryTemplateUpdated>(OnInventoryTemplateUpdated);
- SubscribeLocalEvent<InventoryComponent, VisualsChangedEvent>(OnVisualsChanged);
- SubscribeLocalEvent<SpriteComponent, DidUnequipEvent>(OnDidUnequip);
- SubscribeLocalEvent<InventoryComponent, AppearanceChangeEvent>(OnAppearanceUpdate);
- }
- private void OnAppearanceUpdate(EntityUid uid, InventoryComponent component, ref AppearanceChangeEvent args)
- {
- // May need to update displacement maps if the sex changed. Also required to properly set the stencil on init
- if (args.Sprite == null)
- return;
- UpdateAllSlots(uid, component);
- // No clothing equipped -> make sure the layer is hidden, though this should already be handled by on-unequip.
- if (args.Sprite.LayerMapTryGet(HumanoidVisualLayers.StencilMask, out var layer))
- {
- DebugTools.Assert(!args.Sprite[layer].Visible);
- args.Sprite.LayerSetVisible(layer, false);
- }
- }
- private void OnInventoryTemplateUpdated(Entity<ClothingComponent> ent, ref InventoryTemplateUpdated args)
- {
- UpdateAllSlots(ent.Owner, clothing: ent.Comp);
- }
- private void UpdateAllSlots(
- EntityUid uid,
- InventoryComponent? inventoryComponent = null,
- ClothingComponent? clothing = null)
- {
- var enumerator = _inventorySystem.GetSlotEnumerator((uid, inventoryComponent));
- while (enumerator.NextItem(out var item, out var slot))
- {
- RenderEquipment(uid, item, slot.Name, inventoryComponent, clothingComponent: clothing);
- }
- }
- private void OnGetVisuals(EntityUid uid, ClothingComponent item, GetEquipmentVisualsEvent args)
- {
- if (!TryComp(args.Equipee, out InventoryComponent? inventory))
- return;
- List<PrototypeLayerData>? layers = null;
- // first attempt to get species specific data.
- if (inventory.SpeciesId != null)
- item.ClothingVisuals.TryGetValue($"{args.Slot}-{inventory.SpeciesId}", out layers);
- // if that returned nothing, attempt to find generic data
- if (layers == null && !item.ClothingVisuals.TryGetValue(args.Slot, out layers))
- {
- // No generic data either. Attempt to generate defaults from the item's RSI & item-prefixes
- if (!TryGetDefaultVisuals(uid, item, args.Slot, inventory.SpeciesId, out layers))
- return;
- }
- // add each layer to the visuals
- var i = 0;
- foreach (var layer in layers)
- {
- var key = layer.MapKeys?.FirstOrDefault();
- if (key == null)
- {
- // using the $"{args.Slot}" layer key as the "bookmark" for layer ordering until layer draw depths get added
- key = $"{args.Slot}-{i}";
- i++;
- }
- item.MappedLayer = key;
- args.Layers.Add((key, layer));
- }
- }
- /// <summary>
- /// If no explicit clothing visuals were specified, this attempts to populate with default values.
- /// </summary>
- /// <remarks>
- /// Useful for lazily adding clothing sprites without modifying yaml. And for backwards compatibility.
- /// </remarks>
- private bool TryGetDefaultVisuals(EntityUid uid, ClothingComponent clothing, string slot, string? speciesId,
- [NotNullWhen(true)] out List<PrototypeLayerData>? layers)
- {
- layers = null;
- RSI? rsi = null;
- if (clothing.RsiPath != null)
- rsi = _cache.GetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / clothing.RsiPath).RSI;
- else if (TryComp(uid, out SpriteComponent? sprite))
- rsi = sprite.BaseRSI;
- if (rsi == null)
- return false;
- var correctedSlot = slot;
- TemporarySlotMap.TryGetValue(correctedSlot, out correctedSlot);
- var state = $"equipped-{correctedSlot}";
- if (!string.IsNullOrEmpty(clothing.EquippedPrefix))
- state = $"{clothing.EquippedPrefix}-equipped-{correctedSlot}";
- if (clothing.EquippedState != null)
- state = $"{clothing.EquippedState}";
- // species specific
- if (speciesId != null && rsi.TryGetState($"{state}-{speciesId}", out _))
- state = $"{state}-{speciesId}";
- else if (!rsi.TryGetState(state, out _))
- return false;
- var layer = new PrototypeLayerData();
- layer.RsiPath = rsi.Path.ToString();
- layer.State = state;
- layers = new() { layer };
- return true;
- }
- private void OnVisualsChanged(EntityUid uid, InventoryComponent component, VisualsChangedEvent args)
- {
- var item = GetEntity(args.Item);
- if (!TryComp(item, out ClothingComponent? clothing) || clothing.InSlot == null)
- return;
- RenderEquipment(uid, item, clothing.InSlot, component, null, clothing);
- }
- private void OnDidUnequip(EntityUid uid, SpriteComponent component, DidUnequipEvent args)
- {
- if (!TryComp(uid, out InventorySlotsComponent? inventorySlots))
- return;
- if (!inventorySlots.VisualLayerKeys.TryGetValue(args.Slot, out var revealedLayers))
- return;
- // Remove old layers. We could also just set them to invisible, but as items may add arbitrary layers, this
- // may eventually bloat the player with lots of invisible layers.
- foreach (var layer in revealedLayers)
- {
- component.RemoveLayer(layer);
- }
- revealedLayers.Clear();
- }
- public void InitClothing(EntityUid uid, InventoryComponent component)
- {
- if (!TryComp(uid, out SpriteComponent? sprite))
- return;
- var enumerator = _inventorySystem.GetSlotEnumerator((uid, component));
- while (enumerator.NextItem(out var item, out var slot))
- {
- RenderEquipment(uid, item, slot.Name, component, sprite);
- }
- }
- protected override void OnGotEquipped(EntityUid uid, ClothingComponent component, GotEquippedEvent args)
- {
- base.OnGotEquipped(uid, component, args);
- RenderEquipment(args.Equipee, uid, args.Slot, clothingComponent: component);
- }
- private void RenderEquipment(EntityUid equipee, EntityUid equipment, string slot,
- InventoryComponent? inventory = null, SpriteComponent? sprite = null, ClothingComponent? clothingComponent = null,
- InventorySlotsComponent? inventorySlots = null)
- {
- if (!Resolve(equipee, ref inventory, ref sprite, ref inventorySlots) ||
- !Resolve(equipment, ref clothingComponent, false))
- {
- return;
- }
- if (!_inventorySystem.TryGetSlot(equipee, slot, out var slotDef, inventory))
- return;
- // Remove old layers. We could also just set them to invisible, but as items may add arbitrary layers, this
- // may eventually bloat the player with lots of invisible layers.
- if (inventorySlots.VisualLayerKeys.TryGetValue(slot, out var revealedLayers))
- {
- foreach (var key in revealedLayers)
- {
- sprite.RemoveLayer(key);
- }
- revealedLayers.Clear();
- }
- else
- {
- revealedLayers = new();
- inventorySlots.VisualLayerKeys[slot] = revealedLayers;
- }
- var ev = new GetEquipmentVisualsEvent(equipee, slot);
- RaiseLocalEvent(equipment, ev);
- if (ev.Layers.Count == 0)
- {
- RaiseLocalEvent(equipment, new EquipmentVisualsUpdatedEvent(equipee, slot, revealedLayers), true);
- return;
- }
- // temporary, until layer draw depths get added. Basically: a layer with the key "slot" is being used as a
- // bookmark to determine where in the list of layers we should insert the clothing layers.
- bool slotLayerExists = sprite.LayerMapTryGet(slot, out var index);
- // Select displacement maps
- var displacementData = inventory.Displacements.GetValueOrDefault(slot); //Default unsexed map
- var equipeeSex = CompOrNull<HumanoidAppearanceComponent>(equipee)?.Sex;
- if (equipeeSex != null)
- {
- switch (equipeeSex)
- {
- case Sex.Male:
- if (inventory.MaleDisplacements.Count > 0)
- displacementData = inventory.MaleDisplacements.GetValueOrDefault(slot);
- break;
- case Sex.Female:
- if (inventory.FemaleDisplacements.Count > 0)
- displacementData = inventory.FemaleDisplacements.GetValueOrDefault(slot);
- break;
- }
- }
- // add the new layers
- foreach (var (key, layerData) in ev.Layers)
- {
- if (!revealedLayers.Add(key))
- {
- Log.Warning($"Duplicate key for clothing visuals: {key}. Are multiple components attempting to modify the same layer? Equipment: {ToPrettyString(equipment)}");
- continue;
- }
- if (slotLayerExists)
- {
- index++;
- // note that every insertion requires reshuffling & remapping all the existing layers.
- sprite.AddBlankLayer(index);
- sprite.LayerMapSet(key, index);
- if (layerData.Color != null)
- sprite.LayerSetColor(key, layerData.Color.Value);
- if (layerData.Scale != null)
- sprite.LayerSetScale(key, layerData.Scale.Value);
- }
- else
- index = sprite.LayerMapReserveBlank(key);
- if (sprite[index] is not Layer layer)
- continue;
- // In case no RSI is given, use the item's base RSI as a default. This cuts down on a lot of unnecessary yaml entries.
- if (layerData.RsiPath == null
- && layerData.TexturePath == null
- && layer.RSI == null
- && TryComp(equipment, out SpriteComponent? clothingSprite))
- {
- layer.SetRsi(clothingSprite.BaseRSI);
- }
- sprite.LayerSetData(index, layerData);
- layer.Offset += slotDef.Offset;
- if (displacementData is not null)
- {
- //Checking that the state is not tied to the current race. In this case we don't need to use the displacement maps.
- if (layerData.State is not null && inventory.SpeciesId is not null && layerData.State.EndsWith(inventory.SpeciesId))
- continue;
- if (_displacement.TryAddDisplacement(displacementData, sprite, index, key, revealedLayers))
- index++;
- }
- }
- RaiseLocalEvent(equipment, new EquipmentVisualsUpdatedEvent(equipee, slot, revealedLayers), true);
- }
- }
|