Generic Clothing Equip Functions (#1407)

# Description

This PR was originally going to be called "Psionic Refactor V3 Part 2,
Items Of Power", but I began getting shakes and started vomiting when I
saw the list of Components that make items do things when equipped, and
I had a conniption about how much code there is constantly being
repeated. So instead of this PR adding Items Of Power, I added a
universal modular generic system for making clothing items do things
when equipped and unequipped. Which hooks into the library of
TraitFunctions that we've previously created. I also added a few
"Inverse" versions of trait functions that can be used by these new
clothing functions.

<!--
# Changelog

🆑
- add: Added a universal modular system for making clothing items DO
things when equipped and unequipped. This will be used for "Psionic
Artifacts" in conjunction with the Psionic Refactor V3.
-->

(cherry picked from commit 731e14f100d8161c0f8f7eb1bc5accc2abaa24da)
This commit is contained in:
VMSolidus
2025-01-02 23:49:42 +03:00
committed by Spatison
parent 4a2c85aea8
commit ec1803b44f
5 changed files with 118 additions and 26 deletions

View File

@@ -133,8 +133,8 @@ public sealed class ClientClothingSystem : ClothingSystem
RSI? rsi = null;
if (clothing.RsiPath != null)
rsi = _cache.GetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / clothing.RsiPath).RSI;
if (clothing.Sprite != null)
rsi = _cache.GetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / clothing.Sprite).RSI;
else if (TryComp(uid, out SpriteComponent? sprite))
rsi = sprite.BaseRSI;

View File

@@ -1,7 +1,35 @@
using Content.Shared.Clothing.Components;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.Inventory.Events;
using Robust.Shared.Serialization.Manager;
namespace Content.Server.Clothing;
public sealed class ServerClothingSystem : ClothingSystem
{
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly ISerializationManager _serializationManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ClothingComponent, DidEquipEvent>(OnClothingEquipped);
SubscribeLocalEvent<ClothingComponent, DidUnequipEvent>(OnClothingUnequipped);
}
private void OnClothingEquipped(EntityUid uid, ClothingComponent clothingComponent, DidEquipEvent args)
{
// Yes, this is using the trait system functions. I'm not going to write an entire third function library to do this.
// They're generic for a reason.
foreach (var function in clothingComponent.OnEquipFunctions)
function.OnPlayerSpawn(uid, _componentFactory, EntityManager, _serializationManager);
}
private void OnClothingUnequipped(EntityUid uid, ClothingComponent clothingComponent, DidUnequipEvent args)
{
// Yes, this is using the trait system functions. I'm not going to write an entire third function library to do this.
// They're generic for a reason.
foreach (var function in clothingComponent.OnUnequipFunctions)
function.OnPlayerSpawn(uid, _componentFactory, EntityManager, _serializationManager);
}
}

View File

@@ -137,6 +137,9 @@ public sealed partial class TraitAddPsionics : TraitFunction
[DataField, AlwaysPushInheritance]
public List<ProtoId<PsionicPowerPrototype>> PsionicPowers { get; private set; } = new();
[DataField, AlwaysPushInheritance]
public bool PlayFeedback;
public override void OnPlayerSpawn(EntityUid uid,
IComponentFactory factory,
IEntityManager entityManager,
@@ -147,7 +150,34 @@ public sealed partial class TraitAddPsionics : TraitFunction
foreach (var powerProto in PsionicPowers)
if (prototype.TryIndex(powerProto, out var psionicPower))
psionic.InitializePsionicPower(uid, psionicPower, false);
psionic.InitializePsionicPower(uid, psionicPower, PlayFeedback);
}
}
/// <summary>
/// This isn't actually used for any traits, surprise, other systems can use these functions!
/// This is used by Items of Power to remove a psionic power when unequipped.
/// </summary>
[UsedImplicitly]
public sealed partial class TraitRemovePsionics : TraitFunction
{
[DataField, AlwaysPushInheritance]
public List<ProtoId<PsionicPowerPrototype>> PsionicPowers { get; private set; } = new();
[DataField, AlwaysPushInheritance]
public bool Forced = true;
public override void OnPlayerSpawn(EntityUid uid,
IComponentFactory factory,
IEntityManager entityManager,
ISerializationManager serializationManager)
{
var prototype = IoCManager.Resolve<IPrototypeManager>();
var psionic = entityManager.System<PsionicAbilitiesSystem>();
foreach (var powerProto in PsionicPowers)
if (prototype.TryIndex(powerProto, out var psionicPower))
psionic.RemovePsionicPower(uid, psionicPower, Forced);
}
}
@@ -316,6 +346,25 @@ public sealed partial class TraitAddArmor : TraitFunction
}
}
[UsedImplicitly]
public sealed partial class TraitRemoveArmor : TraitFunction
{
[DataField, AlwaysPushInheritance]
public List<string> DamageModifierSets { get; private set; } = new();
public override void OnPlayerSpawn(EntityUid uid,
IComponentFactory factory,
IEntityManager entityManager,
ISerializationManager serializationManager)
{
if (!entityManager.TryGetComponent<DamageableComponent>(uid, out var damageableComponent))
return;
foreach (var modifierSet in DamageModifierSets)
damageableComponent.DamageModifierSets.Remove(modifierSet);
}
}
[UsedImplicitly]
public sealed partial class TraitAddSolutionContainer : TraitFunction
{

View File

@@ -1,6 +1,7 @@
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.DoAfter;
using Content.Shared.Inventory;
using Content.Shared.Traits;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
@@ -15,55 +16,69 @@ namespace Content.Shared.Clothing.Components;
[Access(typeof(ClothingSystem), typeof(InventorySystem))]
public sealed partial class ClothingComponent : Component
{
[DataField("clothingVisuals")]
[DataField]
[Access(typeof(ClothingSystem), typeof(InventorySystem), Other = AccessPermissions.ReadExecute)] // TODO remove execute permissions.
public Dictionary<string, List<PrototypeLayerData>> ClothingVisuals = new();
[ViewVariables(VVAccess.ReadWrite)]
[DataField("quickEquip")]
[DataField]
public bool QuickEquip = true;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("slots", required: true)]
[DataField(required: true)]
[Access(typeof(ClothingSystem), typeof(InventorySystem), Other = AccessPermissions.ReadExecute)]
public SlotFlags Slots = SlotFlags.NONE;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("equipSound")]
[DataField]
public SoundSpecifier? EquipSound;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("unequipSound")]
[DataField]
public SoundSpecifier? UnequipSound;
[Access(typeof(ClothingSystem))]
[ViewVariables(VVAccess.ReadWrite)]
[DataField("equippedPrefix")]
[DataField]
public string? EquippedPrefix;
/// <summary>
/// Allows the equipped state to be directly overwritten.
/// useful when prototyping INNERCLOTHING items into OUTERCLOTHING items without duplicating/modifying RSIs etc.
/// Allows the equipped state to be directly overwritten.
/// useful when prototyping INNERCLOTHING items into OUTERCLOTHING items without duplicating/modifying RSIs etc.
/// </summary>
[Access(typeof(ClothingSystem))]
[ViewVariables(VVAccess.ReadWrite)]
[DataField("equippedState")]
[DataField]
public string? EquippedState;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("sprite")]
public string? RsiPath;
[DataField]
public string? Sprite;
[DataField]
public ClothingMask MaleMask = ClothingMask.UniformFull;
[DataField]
public ClothingMask FemaleMask = ClothingMask.UniformFull;
[DataField]
public ClothingMask UnisexMask = ClothingMask.UniformFull;
/// <summary>
/// Name of the inventory slot the clothing is in.
/// Name of the inventory slot the clothing is in.
/// </summary>
public string? InSlot;
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public TimeSpan EquipDelay = TimeSpan.Zero;
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public TimeSpan UnequipDelay = TimeSpan.Zero;
/// <summary>
/// These functions are called when an entity equips an item with this component.
/// </summary>
[DataField(serverOnly: true)]
public TraitFunction[] OnEquipFunctions { get; private set; } = Array.Empty<TraitFunction>();
/// <summary>
/// These functions are called when an entity un-equips an item with this component.
/// </summary>
[DataField(serverOnly: true)]
public TraitFunction[] OnUnequipFunctions { get; private set; } = Array.Empty<TraitFunction>();
}
[Serializable, NetSerializable]
@@ -109,4 +124,3 @@ public sealed partial class ClothingUnequipDoAfterEvent : DoAfterEvent
public override DoAfterEvent Clone() => this;
}

View File

@@ -247,7 +247,8 @@ public abstract class ClothingSystem : EntitySystem
clothing.ClothingVisuals = otherClothing.ClothingVisuals;
clothing.EquippedPrefix = otherClothing.EquippedPrefix;
clothing.RsiPath = otherClothing.RsiPath;
clothing.Sprite = otherClothing.Sprite;
clothing.FemaleMask = otherClothing.FemaleMask;
_itemSys.VisualsChanged(uid);
Dirty(uid, clothing);