From 1ce21553152199e3d97a8d02c11922fb8db5fd52 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Sat, 17 Feb 2024 21:52:11 +0100 Subject: [PATCH] Add new "OptionsVisualizer" (#25128) This is a visualizer somewhat similar to the Generic. It allows configuring appearance info based on specific CVars the user has set. This allows YAML to easily configure alternatives for accessibility CVars like reduced motion. --- .../Options/OptionsVisualizerComponent.cs | 84 +++++++++++++++ .../Options/OptionsVisualizerSystem.cs | 97 ++++++++++++++++++ Content.Server/Entry/IgnoredComponents.cs | 1 + Content.Shared/CCVar/CCVars.cs | 10 ++ .../Entities/Debugging/options_visualizer.yml | 24 +++++ .../optionsvisualizertest.rsi/both.png | Bin 0 -> 316 bytes .../optionsvisualizertest.rsi/meta.json | 1 + .../optionsvisualizertest.rsi/motion.png | Bin 0 -> 336 bytes .../optionsvisualizertest.rsi/none.png | Bin 0 -> 251 bytes .../optionsvisualizertest.rsi/test.png | Bin 0 -> 317 bytes 10 files changed, 217 insertions(+) create mode 100644 Content.Client/Options/OptionsVisualizerComponent.cs create mode 100644 Content.Client/Options/OptionsVisualizerSystem.cs create mode 100644 Resources/Prototypes/Entities/Debugging/options_visualizer.yml create mode 100644 Resources/Textures/Effects/optionsvisualizertest.rsi/both.png create mode 100644 Resources/Textures/Effects/optionsvisualizertest.rsi/meta.json create mode 100644 Resources/Textures/Effects/optionsvisualizertest.rsi/motion.png create mode 100644 Resources/Textures/Effects/optionsvisualizertest.rsi/none.png create mode 100644 Resources/Textures/Effects/optionsvisualizertest.rsi/test.png diff --git a/Content.Client/Options/OptionsVisualizerComponent.cs b/Content.Client/Options/OptionsVisualizerComponent.cs new file mode 100644 index 0000000000..87999f381c --- /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 0000000000..2a297e3802 --- /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 c1b646ec4f..fe073da7a4 100644 --- a/Content.Server/Entry/IgnoredComponents.cs +++ b/Content.Server/Entry/IgnoredComponents.cs @@ -19,6 +19,7 @@ namespace Content.Server.Entry "InventorySlots", "LightFade", "HolidayRsiSwap", + "OptionsVisualizer", }; } } diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index fb6b25b57f..e8f5e44a61 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -2011,5 +2011,15 @@ namespace Content.Shared.CCVar 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 0000000000..229ffa00cc --- /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 0000000000000000000000000000000000000000..76237f5076fffb9e3de2965a2285eaa0b0b9862b GIT binary patch literal 316 zcmV-C0mJ@@P)Px#_DMuRR9J=WR_zLaFbGuX{U2HTWti!{+K6BWQCYj)P8~xL5%>`F<>Xrc0Kj2i zqH;jQG(H122b44-N5tZH5ebEM33!VUSMV2YIBM^Z0L`_v6A^)k9x=T{5X1-#~eBCu>ooaRMh=N3PktqDL7iY(5-2WTS6$QNmmaqhM$)Yx{T7XVD%bOQcnl$Q;W&yL%BH&g7E3|0JGVY=S*2>4gq|I7ZZBgukZ0C!F zj-n_aBJMdeycUolJ1;Cf57hd2 O0000F@hKX literal 0 HcmV?d00001 diff --git a/Resources/Textures/Effects/optionsvisualizertest.rsi/meta.json b/Resources/Textures/Effects/optionsvisualizertest.rsi/meta.json new file mode 100644 index 0000000000..e7f749513d --- /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 0000000000000000000000000000000000000000..edbbd52636503deb3774b71419aa34dd44acc367 GIT binary patch literal 336 zcmV-W0k8gvP)Px$3Q0skR9J=WmeCHwAP7Z;^#4DZyC*tI0jo3XlJzEgP*yz1)s_ID-6+0u@du!_ z);2qc>_9~6{0bd*K$AwviKvVN2tkJcKGn>w(9a#iXe(#8cM9O!3IUjTuNc6=d^)eD zw0*a8K9*_A0?gb*WKY8UX0;#+C?b2Sv;g0_2qe_oiUE5@ z&0;42PpXyo`kBRhV5tYJ`E;xcWw8;k5+E~^{crAxSH5rSfff$!Kf-dyWqQI4~(%#`8UzJNP_0`J)~9}-SXKFiI((DzL_ihWXMoqLJ! zhed&flP(tPx#_en%SR9J=WmO&20AP7aJ>HUxFpA`)vsOXrciGS6>4m?1t1OVwpU2g_I04b$( z%OBQuAR^cP4;2+)r6CZZxJ0Q0{+RRHXqoyx3qE4V5WDyq;J$}IU=LwcEe zb`wBnSiOjQ|ua*ui3*D^kr0a0acfq1BGaG&H9`kDxw$ zWYAeP0W8OnJHs-7C-1(n^*+%1B1U`9GLK@GcIaik0`ZLt$pJV32Vkdu2Jp)efnPl^ P00000NkvXXu0mjfSu%kU literal 0 HcmV?d00001 -- 2.52.0