diff --git a/Content.Client/Options/OptionsVisualizerComponent.cs b/Content.Client/Options/OptionsVisualizerComponent.cs new file mode 100644 index 000000000000..87999f381c14 --- /dev/null +++ b/Content.Client/Options/OptionsVisualizerComponent.cs @@ -0,0 +1,84 @@ +using Content.Shared.CCVar; + +namespace Content.Client.Options; + +/// +/// Allows specifying sprite alternatives depending on the client's accessibility options. +/// +/// +/// A list of layer mappings is given that the component applies to, +/// and it will pick one entry to apply based on the settings configuration. Example: +/// +/// +/// - type: Sprite +/// sprite: Effects/optionsvisualizertest.rsi +/// layers: +/// - state: none +/// map: [ "layer" ] +/// - type: OptionsVisualizer +/// visuals: +/// layer: +/// - options: Default +/// data: { state: none } +/// - options: Test +/// data: { state: test } +/// - options: ReducedMotion +/// data: { state: motion } +/// - options: [Test, ReducedMotion] +/// data: { state: both } +/// +/// +/// +/// +[RegisterComponent] +public sealed partial class OptionsVisualizerComponent : Component +{ + /// + /// A mapping storing data about which sprite layer keys should be controlled. + /// + /// + /// Each layer stores an array of possible options. The last entry with a + /// matching the active user preferences will be picked. + /// This allows choosing a priority if multiple entries are matched. + /// + [DataField(required: true)] + public Dictionary Visuals = default!; + + /// + /// A single option for a layer to be selected. + /// + [DataDefinition] + public sealed partial class LayerDatum + { + /// + /// Which options must be set by the user to make this datum match. + /// + [DataField] + public OptionVisualizerOptions Options { get; set; } + + /// + /// The sprite layer data to set on the sprite when this datum matches. + /// + [DataField] + public PrototypeLayerData Data { get; set; } + } +} + +[Flags] +public enum OptionVisualizerOptions +{ + /// + /// Corresponds to no special options being set, can be used as a "default" state. + /// + Default = 0, + + /// + /// Corresponds to the CVar being set. + /// + Test = 1 << 0, + + /// + /// Corresponds to the CVar being set. + /// + ReducedMotion = 1 << 1, +} diff --git a/Content.Client/Options/OptionsVisualizerSystem.cs b/Content.Client/Options/OptionsVisualizerSystem.cs new file mode 100644 index 000000000000..2a297e3802ad --- /dev/null +++ b/Content.Client/Options/OptionsVisualizerSystem.cs @@ -0,0 +1,97 @@ +using Content.Shared.CCVar; +using Robust.Client.GameObjects; +using Robust.Shared.Configuration; +using Robust.Shared.Reflection; + +namespace Content.Client.Options; + +/// +/// Implements . +/// +public sealed class OptionsVisualizerSystem : EntitySystem +{ + private static readonly (OptionVisualizerOptions, CVarDef)[] OptionVars = + { + (OptionVisualizerOptions.Test, CCVars.DebugOptionVisualizerTest), + (OptionVisualizerOptions.ReducedMotion, CCVars.ReducedMotion), + }; + + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IReflectionManager _reflection = default!; + + private OptionVisualizerOptions _currentOptions; + + public override void Initialize() + { + base.Initialize(); + + foreach (var (_, cvar) in OptionVars) + { + Subs.CVar(_cfg, cvar, _ => CVarChanged()); + } + + UpdateActiveOptions(); + + SubscribeLocalEvent(OnComponentStartup); + } + + private void CVarChanged() + { + UpdateActiveOptions(); + UpdateAllComponents(); + } + + private void UpdateActiveOptions() + { + _currentOptions = OptionVisualizerOptions.Default; + + foreach (var (value, cVar) in OptionVars) + { + if (_cfg.GetCVar(cVar)) + _currentOptions |= value; + } + } + + private void UpdateAllComponents() + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out _, out var component, out var sprite)) + { + UpdateComponent(component, sprite); + } + } + + private void OnComponentStartup(EntityUid uid, OptionsVisualizerComponent component, ComponentStartup args) + { + if (!TryComp(uid, out SpriteComponent? sprite)) + return; + + UpdateComponent(component, sprite); + } + + private void UpdateComponent(OptionsVisualizerComponent component, SpriteComponent sprite) + { + foreach (var (layerKeyRaw, layerData) in component.Visuals) + { + object layerKey = _reflection.TryParseEnumReference(layerKeyRaw, out var @enum) + ? @enum + : layerKeyRaw; + + OptionsVisualizerComponent.LayerDatum? matchedDatum = null; + foreach (var datum in layerData) + { + if ((datum.Options & _currentOptions) != datum.Options) + continue; + + matchedDatum = datum; + } + + if (matchedDatum == null) + continue; + + var layerIndex = sprite.LayerMapReserveBlank(layerKey); + sprite.LayerSetData(layerIndex, matchedDatum.Data); + } + } +} + diff --git a/Content.Server/Entry/IgnoredComponents.cs b/Content.Server/Entry/IgnoredComponents.cs index c1b646ec4f96..fe073da7a49a 100644 --- a/Content.Server/Entry/IgnoredComponents.cs +++ b/Content.Server/Entry/IgnoredComponents.cs @@ -19,6 +19,7 @@ public static class IgnoredComponents "InventorySlots", "LightFade", "HolidayRsiSwap", + "OptionsVisualizer", }; } } diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index aac3a17b6f8b..5d3eb1308763 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -2011,5 +2011,15 @@ public static readonly CVarDef public static readonly CVarDef GatewayGeneratorEnabled = CVarDef.Create("gateway.generator_enabled", true); + + /* + * DEBUG + */ + + /// + /// A simple toggle to test OptionsVisualizerComponent. + /// + public static readonly CVarDef DebugOptionVisualizerTest = + CVarDef.Create("debug.option_visualizer_test", false, CVar.CLIENTONLY); } } diff --git a/Resources/Prototypes/Entities/Debugging/options_visualizer.yml b/Resources/Prototypes/Entities/Debugging/options_visualizer.yml new file mode 100644 index 000000000000..229ffa00ccb6 --- /dev/null +++ b/Resources/Prototypes/Entities/Debugging/options_visualizer.yml @@ -0,0 +1,24 @@ +- type: entity + id: OptionsVisualizerTest + suffix: DEBUG + components: + - type: Tag + tags: + - Debug + - type: Sprite + sprite: Effects/optionsvisualizertest.rsi + layers: + - state: none + map: [ "layer" ] + - type: OptionsVisualizer + visuals: + layer: + - options: Default + data: { state: none } + - options: Test + data: { state: test } + - options: ReducedMotion + data: { state: motion } + - options: [Test, ReducedMotion] + data: { state: both } + diff --git a/Resources/Textures/Effects/optionsvisualizertest.rsi/both.png b/Resources/Textures/Effects/optionsvisualizertest.rsi/both.png new file mode 100644 index 000000000000..76237f5076ff Binary files /dev/null and b/Resources/Textures/Effects/optionsvisualizertest.rsi/both.png differ diff --git a/Resources/Textures/Effects/optionsvisualizertest.rsi/meta.json b/Resources/Textures/Effects/optionsvisualizertest.rsi/meta.json new file mode 100644 index 000000000000..e7f749513dc7 --- /dev/null +++ b/Resources/Textures/Effects/optionsvisualizertest.rsi/meta.json @@ -0,0 +1 @@ +{"version":1,"size":{"x":32,"y":32},"license":"CC-BY-SA-4.0","copyright":"Discord pjb","states":[{"name":"none"},{"name":"both"},{"name":"motion"},{"name":"test"}]} diff --git a/Resources/Textures/Effects/optionsvisualizertest.rsi/motion.png b/Resources/Textures/Effects/optionsvisualizertest.rsi/motion.png new file mode 100644 index 000000000000..edbbd5263650 Binary files /dev/null and b/Resources/Textures/Effects/optionsvisualizertest.rsi/motion.png differ diff --git a/Resources/Textures/Effects/optionsvisualizertest.rsi/none.png b/Resources/Textures/Effects/optionsvisualizertest.rsi/none.png new file mode 100644 index 000000000000..2f2dc7b3ff86 Binary files /dev/null and b/Resources/Textures/Effects/optionsvisualizertest.rsi/none.png differ diff --git a/Resources/Textures/Effects/optionsvisualizertest.rsi/test.png b/Resources/Textures/Effects/optionsvisualizertest.rsi/test.png new file mode 100644 index 000000000000..0078fd489c9f Binary files /dev/null and b/Resources/Textures/Effects/optionsvisualizertest.rsi/test.png differ