2022-03-02 12:29:42 +13:00
using System.Collections.Generic ;
using System.Diagnostics.CodeAnalysis ;
using System.Linq ;
2021-12-30 22:56:10 +01:00
using Content.Client.Inventory ;
using Content.Shared.CharacterAppearance ;
using Content.Shared.Clothing ;
using Content.Shared.Inventory ;
using Content.Shared.Inventory.Events ;
2022-01-14 03:09:46 +13:00
using Content.Shared.Item ;
2022-03-06 11:27:27 +13:00
using Content.Shared.Tag ;
2021-12-30 22:56:10 +01:00
using Robust.Client.GameObjects ;
using Robust.Client.Graphics ;
using Robust.Client.ResourceManagement ;
2022-03-04 18:02:53 +13:00
using static Robust . Client . GameObjects . SpriteComponent ;
2022-03-02 12:29:42 +13:00
using static Robust . Shared . GameObjects . SharedSpriteComponent ;
2021-12-30 22:56:10 +01:00
namespace Content.Client.Clothing ;
2022-02-16 00:23:23 -07:00
public sealed class ClothingSystem : EntitySystem
2021-12-30 22:56:10 +01:00
{
/// <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" } ,
} ;
[Dependency] private IResourceCache _cache = default ! ;
[Dependency] private InventorySystem _inventorySystem = default ! ;
2022-03-06 11:27:27 +13:00
[Dependency] private TagSystem _tagSystem = default ! ;
2021-12-30 22:56:10 +01:00
public override void Initialize ( )
{
base . Initialize ( ) ;
SubscribeLocalEvent < ClothingComponent , GotEquippedEvent > ( OnGotEquipped ) ;
2022-01-14 03:09:46 +13:00
SubscribeLocalEvent < ClothingComponent , GotUnequippedEvent > ( OnGotUnequipped ) ;
2022-03-02 12:29:42 +13:00
SubscribeLocalEvent < SharedItemComponent , GetEquipmentVisualsEvent > ( OnGetVisuals ) ;
SubscribeLocalEvent < ClientInventoryComponent , VisualsChangedEvent > ( OnVisualsChanged ) ;
2021-12-30 22:56:10 +01:00
SubscribeLocalEvent < SpriteComponent , DidUnequipEvent > ( OnDidUnequip ) ;
}
2022-03-02 12:29:42 +13:00
private void OnGetVisuals ( EntityUid uid , SharedItemComponent item , GetEquipmentVisualsEvent args )
{
if ( ! TryComp ( args . Equipee , out ClientInventoryComponent ? 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 )
{
2022-03-21 11:29:20 +13:00
// using the $"{args.Slot}" layer key as the "bookmark" for layer ordering until layer draw depths get added
key = $"{args.Slot}-{i}" ;
2022-03-02 12:29:42 +13:00
i + + ;
}
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 , SharedItemComponent item , string slot , string? speciesId ,
[NotNullWhen(true)] out List < PrototypeLayerData > ? layers )
{
layers = null ;
RSI ? rsi = null ;
if ( item . RsiPath ! = null )
rsi = _cache . GetResource < RSIResource > ( TextureRoot / item . RsiPath ) . RSI ;
else if ( TryComp ( uid , out SpriteComponent ? sprite ) )
rsi = sprite . BaseRSI ;
if ( rsi = = null | | rsi . Path = = null )
return false ;
var correctedSlot = slot ;
TemporarySlotMap . TryGetValue ( correctedSlot , out correctedSlot ) ;
var state = ( item . EquippedPrefix = = null )
? $"equipped-{correctedSlot}"
: $"{item.EquippedPrefix}-equipped-{correctedSlot}" ;
// species specific
if ( speciesId ! = null & & rsi . TryGetState ( $"{state}-{speciesId}" , out _ ) )
{
state = $"{state}-{speciesId}" ;
}
else if ( ! rsi . TryGetState ( state , out _ ) )
{
return false ;
}
var layer = PrototypeLayerData . New ( ) ;
layer . RsiPath = rsi . Path . ToString ( ) ;
layer . State = state ;
layers = new ( ) { layer } ;
return true ;
}
private void OnVisualsChanged ( EntityUid uid , ClientInventoryComponent component , VisualsChangedEvent args )
2022-01-14 03:09:46 +13:00
{
if ( ! TryComp ( args . Item , out ClothingComponent ? clothing ) | | clothing . InSlot = = null )
return ;
RenderEquipment ( uid , args . Item , clothing . InSlot , component , null , clothing ) ;
}
private void OnGotUnequipped ( EntityUid uid , ClothingComponent component , GotUnequippedEvent args )
{
2022-03-06 11:27:27 +13:00
if ( component . InSlot = = "head"
& & _tagSystem . HasTag ( uid , "HidesHair" )
& & TryComp ( args . Equipee , out SpriteComponent ? sprite ) )
{
if ( sprite . LayerMapTryGet ( HumanoidVisualLayers . FacialHair , out var facial ) )
sprite [ facial ] . Visible = true ;
if ( sprite . LayerMapTryGet ( HumanoidVisualLayers . Hair , out var hair ) )
sprite [ hair ] . Visible = true ;
}
2022-01-14 03:09:46 +13:00
component . InSlot = null ;
}
2021-12-30 22:56:10 +01:00
private void OnDidUnequip ( EntityUid uid , SpriteComponent component , DidUnequipEvent args )
{
2022-03-02 12:29:42 +13:00
if ( ! TryComp ( uid , out ClientInventoryComponent ? inventory ) | | ! TryComp ( uid , out SpriteComponent ? sprite ) )
return ;
if ( ! inventory . 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 )
{
sprite . RemoveLayer ( layer ) ;
}
revealedLayers . Clear ( ) ;
2021-12-30 22:56:10 +01:00
}
public void InitClothing ( EntityUid uid , ClientInventoryComponent ? component = null , SpriteComponent ? sprite = null )
{
if ( ! _inventorySystem . TryGetSlots ( uid , out var slots , component ) | | ! Resolve ( uid , ref sprite , ref component ) ) return ;
foreach ( var slot in slots )
{
if ( ! _inventorySystem . TryGetSlotContainer ( uid , slot . Name , out var containerSlot , out _ , component ) | |
! containerSlot . ContainedEntity . HasValue ) continue ;
RenderEquipment ( uid , containerSlot . ContainedEntity . Value , slot . Name , component , sprite ) ;
}
}
private void OnGotEquipped ( EntityUid uid , ClothingComponent component , GotEquippedEvent args )
{
2022-01-14 03:09:46 +13:00
component . InSlot = args . Slot ;
2022-03-06 11:27:27 +13:00
if ( args . Slot = = "head"
& & _tagSystem . HasTag ( uid , "HidesHair" )
& & TryComp ( args . Equipee , out SpriteComponent ? sprite ) )
{
if ( sprite . LayerMapTryGet ( HumanoidVisualLayers . FacialHair , out var facial ) )
sprite [ facial ] . Visible = false ;
if ( sprite . LayerMapTryGet ( HumanoidVisualLayers . Hair , out var hair ) )
sprite [ hair ] . Visible = false ;
}
2022-03-02 12:29:42 +13:00
RenderEquipment ( args . Equipee , uid , args . Slot , clothingComponent : component ) ;
2021-12-30 22:56:10 +01:00
}
2022-03-02 12:29:42 +13:00
private void RenderEquipment ( EntityUid equipee , EntityUid equipment , string slot ,
ClientInventoryComponent ? inventory = null , SpriteComponent ? sprite = null , ClothingComponent ? clothingComponent = null )
2021-12-30 22:56:10 +01:00
{
2022-03-02 12:29:42 +13:00
if ( ! Resolve ( equipee , ref inventory , ref sprite ) | | ! Resolve ( equipment , ref clothingComponent , false ) )
2021-12-30 22:56:10 +01:00
return ;
if ( slot = = "jumpsuit" & & sprite . LayerMapTryGet ( HumanoidVisualLayers . StencilMask , out _ ) )
{
sprite . LayerSetState ( HumanoidVisualLayers . StencilMask , clothingComponent . FemaleMask switch
{
FemaleClothingMask . NoMask = > "female_none" ,
FemaleClothingMask . UniformTop = > "female_top" ,
_ = > "female_full" ,
} ) ;
}
2022-02-07 16:37:57 +13:00
2022-03-04 18:02:53 +13:00
if ( ! _inventorySystem . TryGetSlot ( equipee , slot , out var slotDef , inventory ) )
return ;
2022-03-02 12:29:42 +13:00
// 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 ( inventory . VisualLayerKeys . TryGetValue ( slot , out var revealedLayers ) )
2021-12-30 22:56:10 +01:00
{
2022-03-02 12:29:42 +13:00
foreach ( var key in revealedLayers )
2022-02-07 16:37:57 +13:00
{
2022-03-02 12:29:42 +13:00
sprite . RemoveLayer ( key ) ;
2022-02-07 16:37:57 +13:00
}
2022-03-02 12:29:42 +13:00
revealedLayers . Clear ( ) ;
}
else
{
revealedLayers = new ( ) ;
inventory . VisualLayerKeys [ slot ] = revealedLayers ;
}
var ev = new GetEquipmentVisualsEvent ( equipee , slot ) ;
RaiseLocalEvent ( equipment , ev , false ) ;
if ( ev . Layers . Count = = 0 )
{
RaiseLocalEvent ( equipment , new EquipmentVisualsUpdatedEvent ( equipee , slot , revealedLayers ) ) ;
return ;
2022-02-07 14:59:22 +11:00
}
2022-02-07 16:37:57 +13:00
2022-03-21 11:29:20 +13:00
// 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 ) ;
2022-03-02 12:29:42 +13:00
// add the new layers
foreach ( var ( key , layerData ) in ev . Layers )
2022-02-07 14:59:22 +11:00
{
2022-03-02 12:29:42 +13:00
if ( ! revealedLayers . Add ( key ) )
{
Logger . Warning ( $"Duplicate key for clothing visuals: {key}. Are multiple components attempting to modify the same layer? Equipment: {ToPrettyString(equipment)}" ) ;
continue ;
}
2022-03-21 11:29:20 +13:00
if ( slotLayerExists )
{
index + + ;
// note that every insertion requires reshuffling & remapping all the existing layers.
sprite . AddBlankLayer ( index ) ;
sprite . LayerMapSet ( key , index ) ;
}
else
index = sprite . LayerMapReserveBlank ( key ) ;
2022-03-04 18:02:53 +13:00
if ( sprite [ index ] is not Layer layer )
return ;
2022-03-02 12:29:42 +13:00
// 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
2022-03-04 18:02:53 +13:00
& & layer . RSI = = null
2022-03-02 12:29:42 +13:00
& & TryComp ( equipment , out SpriteComponent ? clothingSprite ) )
{
2022-03-04 18:02:53 +13:00
layer . SetRsi ( clothingSprite . BaseRSI ) ;
2022-03-02 12:29:42 +13:00
}
sprite . LayerSetData ( index , layerData ) ;
2022-03-04 18:02:53 +13:00
layer . Offset + = slotDef . Offset ;
2021-12-30 22:56:10 +01:00
}
2022-03-02 12:29:42 +13:00
RaiseLocalEvent ( equipment , new EquipmentVisualsUpdatedEvent ( equipee , slot , revealedLayers ) ) ;
2021-12-30 22:56:10 +01:00
}
}