Decouple Lights from Toggleable Visuals (and headphone music notes bugfix) (#35341)

* - Combine enum keys `ToggleableLightVisuals` and `ToggleVisuals` into `ToggleableVisuals`
- Rename `ToggleableLightVisualsComponent` to `ToggleableVisualsComponent` and `ToggleableLightVisualsSystem` to `ToggleableVisualsSystem`
  - (The `SpriteLayer` field on the component is now required because the old default of `light` doesn't make sense anymore)
- Make it so that `ToggleableVisualsComponent` works even when there's not a light attached to the entity
  - (Amazingly this seems to have only applied to  Headphones, but I can only imagine there are many other things people would like to do with simple toggleable visuals)
- Explicitly make `ItemTogglePointLightComponent`'s purpose to make `ToggleVisualsComponent` apply to `PointLightComponent`s on the same entity.
  - Add field `ToggleableVisualsColorModulatesLights`, which makes the `Color` appearance value of `ToggleableVisuals` modulate the color of lights on the same entity
  - Lots of prototype updates to uptake the above

* fix bad merge

* unbork robust

* blindly letting rider reformat stuff

* I guess I never cleaned up these imports at all
This commit is contained in:
Centronias
2025-05-30 16:53:56 -07:00
committed by GitHub
parent 85636335ec
commit 9053c9692f
48 changed files with 327 additions and 305 deletions

View File

@@ -1,32 +0,0 @@
using Content.Shared.Hands.Components;
namespace Content.Client.Toggleable;
/// <summary>
/// Component that handles the toggling the visuals of some light emitting entity.
/// </summary>
/// <remarks>
/// This will toggle the visibility of layers on an entity's sprite, the in-hand visuals, and the clothing/equipment
/// visuals. This will modify the color of any attached point lights.
/// </remarks>
[RegisterComponent]
public sealed partial class ToggleableLightVisualsComponent : Component
{
/// <summary>
/// Sprite layer that will have its visibility toggled when this item is toggled.
/// </summary>
[DataField("spriteLayer")]
public string? SpriteLayer = "light";
/// <summary>
/// Layers to add to the sprite of the player that is holding this entity (while the component is toggled on).
/// </summary>
[DataField("inhandVisuals")]
public Dictionary<HandLocation, List<PrototypeLayerData>> InhandVisuals = new();
/// <summary>
/// Layers to add to the sprite of the player that is wearing this entity (while the component is toggled on).
/// </summary>
[DataField("clothingVisuals")]
public Dictionary<string, List<PrototypeLayerData>> ClothingVisuals = new();
}

View File

@@ -1,126 +0,0 @@
using Content.Client.Clothing;
using Content.Client.Items.Systems;
using Content.Shared.Clothing;
using Content.Shared.Hands;
using Content.Shared.Inventory;
using Content.Shared.Item;
using Content.Shared.Toggleable;
using Robust.Client.GameObjects;
using Robust.Shared.Utility;
using System.Linq;
namespace Content.Client.Toggleable;
public sealed class ToggleableLightVisualsSystem : VisualizerSystem<ToggleableLightVisualsComponent>
{
[Dependency] private readonly SharedItemSystem _itemSys = default!;
[Dependency] private readonly SharedPointLightSystem _lights = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ToggleableLightVisualsComponent, GetInhandVisualsEvent>(OnGetHeldVisuals, after: new[] { typeof(ItemSystem) });
SubscribeLocalEvent<ToggleableLightVisualsComponent, GetEquipmentVisualsEvent>(OnGetEquipmentVisuals, after: new[] { typeof(ClientClothingSystem) });
}
protected override void OnAppearanceChange(EntityUid uid, ToggleableLightVisualsComponent component, ref AppearanceChangeEvent args)
{
if (!AppearanceSystem.TryGetData<bool>(uid, ToggleableLightVisuals.Enabled, out var enabled, args.Component))
return;
var modulate = AppearanceSystem.TryGetData<Color>(uid, ToggleableLightVisuals.Color, out var color, args.Component);
// Update the item's sprite
if (args.Sprite != null && component.SpriteLayer != null && SpriteSystem.LayerMapTryGet((uid, args.Sprite), component.SpriteLayer, out var layer, false))
{
SpriteSystem.LayerSetVisible((uid, args.Sprite), layer, enabled);
if (modulate)
SpriteSystem.LayerSetColor((uid, args.Sprite), layer, color);
}
// Update any point-lights
if (TryComp(uid, out PointLightComponent? light))
{
DebugTools.Assert(!light.NetSyncEnabled, "light visualizers require point lights without net-sync");
_lights.SetEnabled(uid, enabled, light);
if (enabled && modulate)
{
_lights.SetColor(uid, color, light);
}
}
// update clothing & in-hand visuals.
_itemSys.VisualsChanged(uid);
}
/// <summary>
/// Add the unshaded light overlays to any clothing sprites.
/// </summary>
private void OnGetEquipmentVisuals(EntityUid uid, ToggleableLightVisualsComponent component, GetEquipmentVisualsEvent args)
{
if (!TryComp(uid, out AppearanceComponent? appearance)
|| !AppearanceSystem.TryGetData<bool>(uid, ToggleableLightVisuals.Enabled, out var enabled, appearance)
|| !enabled)
return;
if (!TryComp(args.Equipee, out InventoryComponent? inventory))
return;
List<PrototypeLayerData>? layers = null;
// attempt to get species specific data
if (inventory.SpeciesId != null)
component.ClothingVisuals.TryGetValue($"{args.Slot}-{inventory.SpeciesId}", out layers);
// No species specific data. Try to default to generic data.
if (layers == null && !component.ClothingVisuals.TryGetValue(args.Slot, out layers))
return;
var modulate = AppearanceSystem.TryGetData<Color>(uid, ToggleableLightVisuals.Color, out var color, appearance);
var i = 0;
foreach (var layer in layers)
{
var key = layer.MapKeys?.FirstOrDefault();
if (key == null)
{
key = i == 0 ? $"{args.Slot}-toggle" : $"{args.Slot}-toggle-{i}";
i++;
}
if (modulate)
layer.Color = color;
args.Layers.Add((key, layer));
}
}
private void OnGetHeldVisuals(EntityUid uid, ToggleableLightVisualsComponent component, GetInhandVisualsEvent args)
{
if (!TryComp(uid, out AppearanceComponent? appearance)
|| !AppearanceSystem.TryGetData<bool>(uid, ToggleableLightVisuals.Enabled, out var enabled, appearance)
|| !enabled)
return;
if (!component.InhandVisuals.TryGetValue(args.Location, out var layers))
return;
var modulate = AppearanceSystem.TryGetData<Color>(uid, ToggleableLightVisuals.Color, out var color, appearance);
var i = 0;
var defaultKey = $"inhand-{args.Location.ToString().ToLowerInvariant()}-toggle";
foreach (var layer in layers)
{
var key = layer.MapKeys?.FirstOrDefault();
if (key == null)
{
key = i == 0 ? defaultKey : $"{defaultKey}-{i}";
i++;
}
if (modulate)
layer.Color = color;
args.Layers.Add((key, layer));
}
}
}

View File

@@ -0,0 +1,30 @@
using Content.Shared.Hands.Components;
namespace Content.Client.Toggleable;
/// <summary>
/// Component that handles toggling the visuals of an entity, including layers on an entity's sprite,
/// the in-hand visuals, and the clothing/equipment visuals.
/// </summary>
/// <see cref="ToggleableVisualsSystem"/>
[RegisterComponent]
public sealed partial class ToggleableVisualsComponent : Component
{
/// <summary>
/// Sprite layer that will have its visibility toggled when this item is toggled.
/// </summary>
[DataField(required: true)]
public string? SpriteLayer;
/// <summary>
/// Layers to add to the sprite of the player that is holding this entity (while the component is toggled on).
/// </summary>
[DataField]
public Dictionary<HandLocation, List<PrototypeLayerData>> InhandVisuals = new();
/// <summary>
/// Layers to add to the sprite of the player that is wearing this entity (while the component is toggled on).
/// </summary>
[DataField]
public Dictionary<string, List<PrototypeLayerData>> ClothingVisuals = new();
}

View File

@@ -0,0 +1,140 @@
using System.Linq;
using Content.Client.Clothing;
using Content.Client.Items.Systems;
using Content.Shared.Clothing;
using Content.Shared.Hands;
using Content.Shared.Inventory;
using Content.Shared.Item;
using Content.Shared.Light.Components;
using Content.Shared.Toggleable;
using Robust.Client.GameObjects;
using Robust.Shared.Utility;
namespace Content.Client.Toggleable;
/// <summary>
/// Implements the behavior of <see cref="ToggleableVisualsComponent"/> by reacting to
/// <see cref="AppearanceChangeEvent"/>, for the sprite directly; <see cref="OnGetHeldVisuals"/> for the
/// in-hand visuals; and <see cref="OnGetEquipmentVisuals"/> for the clothing visuals.
/// </summary>
/// <see cref="ToggleableVisualsComponent"/>
public sealed class ToggleableVisualsSystem : VisualizerSystem<ToggleableVisualsComponent>
{
[Dependency] private readonly SharedItemSystem _item = default!;
[Dependency] private readonly SharedPointLightSystem _pointLight = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ToggleableVisualsComponent, GetInhandVisualsEvent>(OnGetHeldVisuals,
after: [typeof(ItemSystem)]);
SubscribeLocalEvent<ToggleableVisualsComponent, GetEquipmentVisualsEvent>(OnGetEquipmentVisuals,
after: [typeof(ClientClothingSystem)]);
}
protected override void OnAppearanceChange(EntityUid uid,
ToggleableVisualsComponent component,
ref AppearanceChangeEvent args)
{
if (!AppearanceSystem.TryGetData<bool>(uid, ToggleableVisuals.Enabled, out var enabled, args.Component))
return;
var modulateColor =
AppearanceSystem.TryGetData<Color>(uid, ToggleableVisuals.Color, out var color, args.Component);
// Update the item's sprite
if (args.Sprite != null && component.SpriteLayer != null &&
SpriteSystem.LayerMapTryGet((uid, args.Sprite), component.SpriteLayer, out var layer, false))
{
SpriteSystem.LayerSetVisible((uid, args.Sprite), layer, enabled);
if (modulateColor)
SpriteSystem.LayerSetColor((uid, args.Sprite), component.SpriteLayer, color);
}
// If there's a `ItemTogglePointLightComponent` that says to apply the color to attached lights, do so.
if (TryComp<ItemTogglePointLightComponent>(uid, out var toggleLights) &&
TryComp(uid, out PointLightComponent? light))
{
DebugTools.Assert(!light.NetSyncEnabled,
$"{typeof(ItemTogglePointLightComponent)} requires point lights without net-sync");
_pointLight.SetEnabled(uid, enabled, light);
if (modulateColor && toggleLights.ToggleableVisualsColorModulatesLights)
{
_pointLight.SetColor(uid, color, light);
}
}
// update clothing & in-hand visuals.
_item.VisualsChanged(uid);
}
private void OnGetEquipmentVisuals(EntityUid uid,
ToggleableVisualsComponent component,
GetEquipmentVisualsEvent args)
{
if (!TryComp(uid, out AppearanceComponent? appearance)
|| !AppearanceSystem.TryGetData<bool>(uid, ToggleableVisuals.Enabled, out var enabled, appearance)
|| !enabled)
return;
if (!TryComp(args.Equipee, out InventoryComponent? inventory))
return;
List<PrototypeLayerData>? layers = null;
// attempt to get species specific data
if (inventory.SpeciesId != null)
component.ClothingVisuals.TryGetValue($"{args.Slot}-{inventory.SpeciesId}", out layers);
// No species specific data. Try to default to generic data.
if (layers == null && !component.ClothingVisuals.TryGetValue(args.Slot, out layers))
return;
var modulateColor = AppearanceSystem.TryGetData<Color>(uid, ToggleableVisuals.Color, out var color, appearance);
var i = 0;
foreach (var layer in layers)
{
var key = layer.MapKeys?.FirstOrDefault();
if (key == null)
{
key = i == 0 ? $"{args.Slot}-toggle" : $"{args.Slot}-toggle-{i}";
i++;
}
if (modulateColor)
layer.Color = color;
args.Layers.Add((key, layer));
}
}
private void OnGetHeldVisuals(EntityUid uid, ToggleableVisualsComponent component, GetInhandVisualsEvent args)
{
if (!TryComp(uid, out AppearanceComponent? appearance)
|| !AppearanceSystem.TryGetData<bool>(uid, ToggleableVisuals.Enabled, out var enabled, appearance)
|| !enabled)
return;
if (!component.InhandVisuals.TryGetValue(args.Location, out var layers))
return;
var modulateColor = AppearanceSystem.TryGetData<Color>(uid, ToggleableVisuals.Color, out var color, appearance);
var i = 0;
var defaultKey = $"inhand-{args.Location.ToString().ToLowerInvariant()}-toggle";
foreach (var layer in layers)
{
var key = layer.MapKeys?.FirstOrDefault();
if (key == null)
{
key = i == 0 ? defaultKey : $"{defaultKey}-{i}";
i++;
}
if (modulateColor)
layer.Color = color;
args.Layers.Add((key, layer));
}
}
}