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