]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Improve crayon UI to not be stuck in 1996 (#33101)
authorSaphire Lattice <lattice@saphi.re>
Sat, 16 Nov 2024 03:25:06 +0000 (10:25 +0700)
committerGitHub <noreply@github.com>
Sat, 16 Nov 2024 03:25:06 +0000 (21:25 -0600)
* Improve crayon UI to not be stuck in 1996

* Make a horrifying crayon spaghetti

* Crayon

* Undeprecate the crayon, describe the crayon

Content.Client/Crayon/UI/CrayonBoundUserInterface.cs
Content.Client/Crayon/UI/CrayonWindow.xaml
Content.Client/Crayon/UI/CrayonWindow.xaml.cs
Content.Server/Crayon/CrayonSystem.cs
Content.Shared/Crayon/SharedCrayonComponent.cs
Resources/Locale/en-US/crayon/crayon-component.ftl
Resources/Prototypes/Decals/crayons.yml

index e5be0b1811f11b8d39150ee3cb908192f2319ec1..44501767dd49c9e9456c2b8d6ba846c48e409330 100644 (file)
@@ -31,7 +31,7 @@ namespace Content.Client.Crayon.UI
         private void PopulateCrayons()
         {
             var crayonDecals = _protoManager.EnumeratePrototypes<DecalPrototype>().Where(x => x.Tags.Contains("crayon"));
-            _menu?.Populate(crayonDecals);
+            _menu?.Populate(crayonDecals.ToList());
         }
 
         public override void OnProtoReload(PrototypesReloadedEventArgs args)
@@ -44,6 +44,16 @@ namespace Content.Client.Crayon.UI
             PopulateCrayons();
         }
 
+        protected override void ReceiveMessage(BoundUserInterfaceMessage message)
+        {
+            base.ReceiveMessage(message);
+
+            if (_menu is null || message is not CrayonUsedMessage crayonMessage)
+                return;
+
+            _menu.AdvanceState(crayonMessage.DrawnDecal);
+        }
+
         protected override void UpdateState(BoundUserInterfaceState state)
         {
             base.UpdateState(state);
index 7729318ae7ffaca1a25b41a08638de7d0bc67a6d..7acb22551b7d0c5bae3c2acb6b14aa70ca28a586 100644 (file)
@@ -1,14 +1,13 @@
 <DefaultWindow xmlns="https://spacestation14.io"
             Title="{Loc 'crayon-window-title'}"
-            MinSize="250 300"
-            SetSize="250 300">
+            MinSize="450 500"
+            SetSize="450 500">
     <BoxContainer Orientation="Vertical">
         <ColorSelectorSliders Name="ColorSelector" Visible="False" />
-        <LineEdit Name="Search" />
+        <LineEdit Name="Search" Margin="0 0 0 8" PlaceHolder="{Loc 'crayon-window-placeholder'}" />
         <ScrollContainer VerticalExpand="True">
-            <GridContainer Name="Grid" Columns="6">
-                <!-- Crayon decals get added here by code -->
-            </GridContainer>
+            <BoxContainer Name="Grids" Orientation="Vertical">
+            </BoxContainer>
         </ScrollContainer>
     </BoxContainer>
 </DefaultWindow>
index 6ef282d219a260715f50c221dc3593fef86511e1..88475562c674b462912b522b098ae8724af9b768 100644 (file)
@@ -1,8 +1,10 @@
 using System.Collections.Generic;
+using System.Linq;
 using Content.Client.Stylesheets;
 using Content.Shared.Crayon;
 using Content.Shared.Decals;
 using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
 using Robust.Client.Graphics;
 using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.CustomControls;
@@ -18,7 +20,12 @@ namespace Content.Client.Crayon.UI
     [GenerateTypedNameReferences]
     public sealed partial class CrayonWindow : DefaultWindow
     {
-        private Dictionary<string, Texture>? _decals;
+        [Dependency] private readonly IEntitySystemManager _entitySystem = default!;
+        private readonly SpriteSystem _spriteSystem = default!;
+
+        private Dictionary<string, List<(string Name, Texture Texture)>>? _decals;
+        private List<string>? _allDecals;
+        private string? _autoSelected;
         private string? _selected;
         private Color _color;
 
@@ -28,8 +35,10 @@ namespace Content.Client.Crayon.UI
         public CrayonWindow()
         {
             RobustXamlLoader.Load(this);
+            IoCManager.InjectDependencies(this);
+            _spriteSystem = _entitySystem.GetEntitySystem<SpriteSystem>();
 
-            Search.OnTextChanged += _ => RefreshList();
+            Search.OnTextChanged += SearchChanged;
             ColorSelector.OnColorChanged += SelectColor;
         }
 
@@ -44,51 +53,94 @@ namespace Content.Client.Crayon.UI
         private void RefreshList()
         {
             // Clear
-            Grid.DisposeAllChildren();
-            if (_decals == null)
+            Grids.DisposeAllChildren();
+
+            if (_decals == null || _allDecals == null)
                 return;
 
             var filter = Search.Text;
-            foreach (var (decal, tex) in _decals)
+            var comma = filter.IndexOf(',');
+            var first = (comma == -1 ? filter : filter[..comma]).Trim();
+
+            var names = _decals.Keys.ToList();
+            names.Sort((a, b) => a == "random" ? 1 : b == "random" ? -1 : a.CompareTo(b));
+
+            if (_autoSelected != null && first != _autoSelected && _allDecals.Contains(first))
+            {
+                _selected = first;
+                _autoSelected = _selected;
+                OnSelected?.Invoke(_selected);
+            }
+
+            foreach (var categoryName in names)
             {
-                if (!decal.Contains(filter))
+                var locName = Loc.GetString("crayon-category-" + categoryName);
+                var category = _decals[categoryName].Where(d => locName.Contains(first) || d.Name.Contains(first)).ToList();
+
+                if (category.Count == 0)
                     continue;
 
-                var button = new TextureButton()
+                var label = new Label
                 {
-                    TextureNormal = tex,
-                    Name = decal,
-                    ToolTip = decal,
-                    Modulate = _color,
+                    Text = locName
                 };
-                button.OnPressed += ButtonOnPressed;
-                if (_selected == decal)
+
+                var grid = new GridContainer
                 {
-                    var panelContainer = new PanelContainer()
+                    Columns = 6,
+                    Margin = new Thickness(0, 0, 0, 16)
+                };
+
+                Grids.AddChild(label);
+                Grids.AddChild(grid);
+
+                foreach (var (name, texture) in category)
+                {
+                    var button = new TextureButton()
                     {
-                        PanelOverride = new StyleBoxFlat()
-                        {
-                            BackgroundColor = StyleNano.ButtonColorDefault,
-                        },
-                        Children =
-                        {
-                            button,
-                        },
+                        TextureNormal = texture,
+                        Name = name,
+                        ToolTip = name,
+                        Modulate = _color,
+                        Scale = new System.Numerics.Vector2(2, 2)
                     };
-                    Grid.AddChild(panelContainer);
-                }
-                else
-                {
-                    Grid.AddChild(button);
+                    button.OnPressed += ButtonOnPressed;
+
+                    if (_selected == name)
+                    {
+                        var panelContainer = new PanelContainer()
+                        {
+                            PanelOverride = new StyleBoxFlat()
+                            {
+                                BackgroundColor = StyleNano.ButtonColorDefault,
+                            },
+                            Children =
+                            {
+                                button,
+                            },
+                        };
+                        grid.AddChild(panelContainer);
+                    }
+                    else
+                    {
+                        grid.AddChild(button);
+                    }
                 }
             }
         }
 
+        private void SearchChanged(LineEdit.LineEditEventArgs obj)
+        {
+            _autoSelected = ""; // Placeholder to kick off the auto-select in refreshlist()
+            RefreshList();
+        }
+
         private void ButtonOnPressed(ButtonEventArgs obj)
         {
             if (obj.Button.Name == null) return;
 
             _selected = obj.Button.Name;
+            _autoSelected = null;
             OnSelected?.Invoke(_selected);
             RefreshList();
         }
@@ -107,12 +159,38 @@ namespace Content.Client.Crayon.UI
             RefreshList();
         }
 
-        public void Populate(IEnumerable<DecalPrototype> prototypes)
+        public void AdvanceState(string drawnDecal)
         {
-            _decals = new Dictionary<string, Texture>();
+            var filter = Search.Text;
+            if (!filter.Contains(',') || !filter.Contains(drawnDecal))
+                return;
+
+            var first = filter[..filter.IndexOf(',')].Trim();
+
+            if (first.Equals(drawnDecal, StringComparison.InvariantCultureIgnoreCase))
+            {
+                Search.Text = filter[(filter.IndexOf(',') + 1)..].Trim();
+                _autoSelected = first;
+            }
+
+            RefreshList();
+        }
+
+        public void Populate(List<DecalPrototype> prototypes)
+        {
+            _decals = [];
+            _allDecals = [];
+
+            prototypes.Sort((a, b) => a.ID.CompareTo(b.ID));
+
             foreach (var decalPrototype in prototypes)
             {
-                _decals.Add(decalPrototype.ID, decalPrototype.Sprite.Frame0());
+                var category = "random";
+                if (decalPrototype.Tags.Count > 1 && decalPrototype.Tags[1].StartsWith("crayon-"))
+                    category = decalPrototype.Tags[1].Replace("crayon-", "");
+                var list = _decals.GetOrNew(category);
+                list.Add((decalPrototype.ID, _spriteSystem.Frame0(decalPrototype.Sprite)));
+                _allDecals.Add(decalPrototype.ID);
             }
 
             RefreshList();
index 07a13d8a34a476a441ad876fe034743c0b3f6011..4257c436c23c6d2cc11e6f29aab326ef7eb4d60e 100644 (file)
@@ -82,6 +82,8 @@ public sealed class CrayonSystem : SharedCrayonSystem
 
         if (component.DeleteEmpty && component.Charges <= 0)
             UseUpCrayon(uid, args.User);
+        else
+            _uiSystem.ServerSendUiMessage(uid, SharedCrayonComponent.CrayonUiKey.Key, new CrayonUsedMessage(component.SelectedState));
     }
 
     private void OnCrayonUse(EntityUid uid, CrayonComponent component, UseInHandEvent args)
index f8e88b218dedbfbc182b0c9c68ea9781b9b4c2ce..a9c21988ea695fb5fcda68881608e4df8c3915ec 100644 (file)
@@ -3,12 +3,23 @@ using Robust.Shared.Serialization;
 
 namespace Content.Shared.Crayon
 {
+
+    /// <summary>
+    /// Component holding the state of a crayon-like component
+    /// </summary>
     [NetworkedComponent, ComponentProtoName("Crayon"), Access(typeof(SharedCrayonSystem))]
     public abstract partial class SharedCrayonComponent : Component
     {
+        /// <summary>
+        /// The ID of currently selected decal prototype that will be placed when the crayon is used
+        /// </summary>
         public string SelectedState { get; set; } = string.Empty;
 
-        [DataField("color")] public Color Color;
+        /// <summary>
+        /// Color with which the crayon will draw
+        /// </summary>
+        [DataField("color")]
+        public Color Color;
 
         [Serializable, NetSerializable]
         public enum CrayonUiKey : byte
@@ -17,6 +28,9 @@ namespace Content.Shared.Crayon
         }
     }
 
+    /// <summary>
+    /// Used by the client to notify the server about the selected decal ID
+    /// </summary>
     [Serializable, NetSerializable]
     public sealed class CrayonSelectMessage : BoundUserInterfaceMessage
     {
@@ -27,6 +41,9 @@ namespace Content.Shared.Crayon
         }
     }
 
+    /// <summary>
+    /// Sets the color of the crayon, used by Rainbow Crayon
+    /// </summary>
     [Serializable, NetSerializable]
     public sealed class CrayonColorMessage : BoundUserInterfaceMessage
     {
@@ -37,13 +54,25 @@ namespace Content.Shared.Crayon
         }
     }
 
+    /// <summary>
+    /// Server to CLIENT. Notifies the BUI that a decal with given ID has been drawn.
+    /// Allows the client UI to advance forward in the client-only ephemeral queue,
+    /// preventing the crayon from becoming a magic text storage device.
+    /// </summary>
     [Serializable, NetSerializable]
-    public enum CrayonVisuals
+    public sealed class CrayonUsedMessage : BoundUserInterfaceMessage
     {
-        State,
-        Color
+        public readonly string DrawnDecal;
+
+        public CrayonUsedMessage(string drawn)
+        {
+            DrawnDecal = drawn;
+        }
     }
 
+    /// <summary>
+    /// Component state, describes how many charges are left in the crayon in the near-hand UI
+    /// </summary>
     [Serializable, NetSerializable]
     public sealed class CrayonComponentState : ComponentState
     {
@@ -60,10 +89,17 @@ namespace Content.Shared.Crayon
             Capacity = capacity;
         }
     }
+
+    /// <summary>
+    /// The state of the crayon UI as sent by the server
+    /// </summary>
     [Serializable, NetSerializable]
     public sealed class CrayonBoundUserInterfaceState : BoundUserInterfaceState
     {
         public string Selected;
+        /// <summary>
+        /// Whether or not the color can be selected
+        /// </summary>
         public bool SelectableColor;
         public Color Color;
 
index 444ffa4c45ef71202f41ad858d0bbbaa8f55e24a..e13bf76941cb0a5e0c0b09c020a88695dc136ea1 100644 (file)
@@ -8,3 +8,10 @@ crayon-interact-invalid-location = Can't reach there!
 
 ## UI
 crayon-window-title = Crayon
+crayon-window-placeholder = Search, or queue a comma-separated list of names
+crayon-category-1-brushes = Brushes
+crayon-category-2-alphanum = Numbers and letters
+crayon-category-3-symbols = Symbols
+crayon-category-4-info = Signs
+crayon-category-5-graffiti = Graffiti
+crayon-category-random = Random
index 3be2ec2426156c5e9e31155725d73508b16faa14..a15619d483cce001fe641d4a9cecb1425d247984 100644 (file)
@@ -1,6 +1,6 @@
 - type: decal
   id: 0
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
@@ -10,7 +10,7 @@
 
 - type: decal
   id: 1
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
@@ -20,7 +20,7 @@
 
 - type: decal
   id: 2
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
@@ -30,7 +30,7 @@
 
 - type: decal
   id: 3
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
@@ -40,7 +40,7 @@
 
 - type: decal
   id: 4
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
@@ -50,7 +50,7 @@
 
 - type: decal
   id: 5
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
@@ -60,7 +60,7 @@
 
 - type: decal
   id: 6
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
@@ -70,7 +70,7 @@
 
 - type: decal
   id: 7
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
@@ -80,7 +80,7 @@
 
 - type: decal
   id: 8
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
@@ -90,7 +90,7 @@
 
 - type: decal
   id: 9
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: Blasto
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: Clandestine
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: Cyber
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: Diablo
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: Donk
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: Gene
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: Gib
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: Max
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: Newton
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: North
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: Omni
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: Osiron
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: Prima
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: Psyke
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: Sirius
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: Tunnel
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: Waffle
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
     sprite: Effects/crayondecals.rsi
     state: Waffle
 
-- type: decal
-  id: a
-  tags: ["crayon"]
-  defaultCleanable: true
-  defaultCustomColor: true
-  defaultSnap: false
-  sprite:
-    sprite: Effects/crayondecals.rsi
-    state: a
-
 - type: decal
   id: ampersand
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-3-symbols"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: amyjon
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: arrow
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-3-symbols"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
     sprite: Effects/crayondecals.rsi
     state: arrow
 
-- type: decal
-  id: b
-  tags: ["crayon"]
-  defaultCleanable: true
-  defaultCustomColor: true
-  defaultSnap: false
-  sprite:
-    sprite: Effects/crayondecals.rsi
-    state: b
-
 - type: decal
   id: beepsky
   tags: ["crayon"]
 
 - type: decal
   id: biohazard
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-4-info"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: brush
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-1-brushes"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
     sprite: Effects/crayondecals.rsi
     state: brush
 
-- type: decal
-  id: c
-  tags: ["crayon"]
-  defaultCleanable: true
-  defaultCustomColor: true
-  defaultSnap: false
-  sprite:
-    sprite: Effects/crayondecals.rsi
-    state: c
-
 - type: decal
   id: carp
   tags: ["crayon"]
 
 - type: decal
   id: chevron
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-3-symbols"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: comma
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-3-symbols"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: credit
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-3-symbols"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: cyka
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
     sprite: Effects/crayondecals.rsi
     state: cyka
 
-- type: decal
-  id: d
-  tags: ["crayon"]
-  defaultCleanable: true
-  defaultCustomColor: true
-  defaultSnap: false
-  sprite:
-    sprite: Effects/crayondecals.rsi
-    state: d
-
 - type: decal
   id: danger
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-4-info"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: dot
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-3-symbols"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
     sprite: Effects/crayondecals.rsi
     state: dwarf
 
-- type: decal
-  id: e
-  tags: ["crayon"]
-  defaultCleanable: true
-  defaultCustomColor: true
-  defaultSnap: false
-  sprite:
-    sprite: Effects/crayondecals.rsi
-    state: e
-
 - type: decal
   id: electricdanger
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-4-info"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: end
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: engie
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: equals
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-3-symbols"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: evac
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-4-info"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: exclamationmark
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-3-symbols"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
     sprite: Effects/crayondecals.rsi
     state: exclamationmark
 
-- type: decal
-  id: f
-  tags: ["crayon"]
-  defaultCleanable: true
-  defaultCustomColor: true
-  defaultSnap: false
-  sprite:
-    sprite: Effects/crayondecals.rsi
-    state: f
-
 - type: decal
   id: face
   tags: ["crayon"]
 
 - type: decal
   id: firedanger
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-4-info"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: food
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-4-info"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
     state: footprint
 
 - type: decal
-  id: g
+  id: ghost
   tags: ["crayon"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: g
+    state: ghost
 
 - type: decal
-  id: ghost
+  id: guy
   tags: ["crayon"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: ghost
+    state: guy
 
 - type: decal
-  id: guy
-  tags: ["crayon"]
+  id: heart
+  tags: ["crayon", "crayon-3-symbols"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: guy
+    state: heart
 
 - type: decal
-  id: h
-  tags: ["crayon"]
+  id: largebrush
+  tags: ["crayon", "crayon-1-brushes"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: h
+    state: largebrush
 
 - type: decal
-  id: heart
-  tags: ["crayon"]
+  id: like
+  tags: ["crayon", "crayon-3-symbols"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: heart
+    state: like
 
 - type: decal
-  id: i
-  tags: ["crayon"]
+  id: line
+  tags: ["crayon", "crayon-1-brushes"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: i
+    state: line
 
 - type: decal
-  id: j
-  tags: ["crayon"]
+  id: matt
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: j
+    state: matt
 
 - type: decal
-  id: k
-  tags: ["crayon"]
+  id: med
+  tags: ["crayon", "crayon-4-info"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: k
+    state: med
 
 - type: decal
-  id: l
-  tags: ["crayon"]
+  id: minus
+  tags: ["crayon", "crayon-3-symbols"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: l
+    state: minus
 
 - type: decal
-  id: largebrush
-  tags: ["crayon"]
+  id: nay
+  tags: ["crayon", "crayon-4-info"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: largebrush
+    state: nay
 
 - type: decal
-  id: like
+  id: pawprint
   tags: ["crayon"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: like
+    state: pawprint
 
 - type: decal
-  id: line
-  tags: ["crayon"]
+  id: peace
+  tags: ["crayon", "crayon-4-info"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: line
+    state: peace
 
 - type: decal
-  id: m
-  tags: ["crayon"]
+  id: percent
+  tags: ["crayon", "crayon-3-symbols"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: m
+    state: percent
 
 - type: decal
-  id: matt
-  tags: ["crayon"]
+  id: plus
+  tags: ["crayon", "crayon-3-symbols"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: matt
+    state: plus
 
 - type: decal
-  id: med
-  tags: ["crayon"]
+  id: pound
+  tags: ["crayon", "crayon-3-symbols"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: med
+    state: pound
 
 - type: decal
-  id: minus
-  tags: ["crayon"]
+  id: prolizard
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: minus
+    state: prolizard
 
 - type: decal
-  id: n
-  tags: ["crayon"]
+  id: questionmark
+  tags: ["crayon", "crayon-3-symbols"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: n
+    state: questionmark
 
 - type: decal
-  id: nay
-  tags: ["crayon"]
+  id: radiation
+  tags: ["crayon", "crayon-4-info"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: nay
+    state: radiation
 
 - type: decal
-  id: o
-  tags: ["crayon"]
+  id: revolution
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: o
+    state: revolution
 
 - type: decal
-  id: p
-  tags: ["crayon"]
+  id: rune1
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: p
+    state: rune1
 
 - type: decal
-  id: pawprint
-  tags: ["crayon"]
+  id: rune2
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: pawprint
+    state: rune2
 
 - type: decal
-  id: peace
-  tags: ["crayon"]
+  id: rune3
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: peace
+    state: rune3
 
 - type: decal
-  id: percent
-  tags: ["crayon"]
+  id: rune4
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: percent
+    state: rune4
 
 - type: decal
-  id: plus
-  tags: ["crayon"]
+  id: rune5
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: plus
+    state: rune5
 
 - type: decal
-  id: pound
-  tags: ["crayon"]
+  id: rune6
+  tags: ["crayon", "crayon-5-graffiti"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: pound
+    state: rune6
 
 - type: decal
-  id: prolizard
-  tags: ["crayon"]
+  id: safe
+  tags: ["crayon", "crayon-4-info"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: prolizard
+    state: safe
 
 - type: decal
-  id: q
+  id: scroll
   tags: ["crayon"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: q
+    state: scroll
 
 - type: decal
-  id: questionmark
-  tags: ["crayon"]
+  id: shop
+  tags: ["crayon", "crayon-4-info"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: questionmark
+    state: shop
 
 - type: decal
-  id: r
-  tags: ["crayon"]
+  id: shortline
+  tags: ["crayon", "crayon-1-brushes"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: r
+    state: shortline
 
 - type: decal
-  id: radiation
+  id: shotgun
   tags: ["crayon"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: radiation
+    state: shotgun
 
 - type: decal
-  id: revolution
-  tags: ["crayon"]
+  id: skull
+  tags: ["crayon", "crayon-4-info"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: revolution
+    state: skull
 
 - type: decal
-  id: rune1
-  tags: ["crayon"]
+  id: slash
+  tags: ["crayon", "crayon-3-symbols"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: rune1
+    state: slash
 
 - type: decal
-  id: rune2
-  tags: ["crayon"]
+  id: smallbrush
+  tags: ["crayon", "crayon-1-brushes"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: rune2
+    state: smallbrush
 
 - type: decal
-  id: rune3
+  id: snake
   tags: ["crayon"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: rune3
+    state: snake
 
 - type: decal
-  id: rune4
-  tags: ["crayon"]
+  id: space
+  tags: ["crayon", "crayon-4-info"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: rune4
+    state: space
 
 - type: decal
-  id: rune5
+  id: splatter
   tags: ["crayon"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: rune5
+    state: splatter
 
 - type: decal
-  id: rune6
-  tags: ["crayon"]
+  id: star
+  tags: ["crayon", "crayon-3-symbols"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: rune6
+    state: star
 
 - type: decal
-  id: s
+  id: stickman
   tags: ["crayon"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: s
+    state: stickman
 
 - type: decal
-  id: safe
+  id: taser
   tags: ["crayon"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: safe
+    state: taser
 
 - type: decal
-  id: scroll
-  tags: ["crayon"]
+  id: thinline
+  tags: ["crayon", "crayon-1-brushes"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: scroll
+    state: thinline
 
 - type: decal
-  id: shop
+  id: toilet
   tags: ["crayon"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: shop
+    state: toilet
 
 - type: decal
-  id: shortline
+  id: toolbox
   tags: ["crayon"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: shortline
+    state: toolbox
 
 - type: decal
-  id: shotgun
-  tags: ["crayon"]
+  id: trade
+  tags: ["crayon", "crayon-4-info"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: shotgun
+    state: trade
 
 - type: decal
-  id: skull
+  id: uboa
   tags: ["crayon"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: skull
+    state: uboa
 
 - type: decal
-  id: slash
-  tags: ["crayon"]
+  id: a
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: slash
+    state: a
 
 - type: decal
-  id: smallbrush
-  tags: ["crayon"]
+  id: b
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: smallbrush
+    state: b
 
 - type: decal
-  id: snake
-  tags: ["crayon"]
+  id: c
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: snake
+    state: c
 
 - type: decal
-  id: space
-  tags: ["crayon"]
+  id: d
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: space
+    state: d
 
 - type: decal
-  id: splatter
-  tags: ["crayon"]
+  id: e
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: splatter
+    state: e
 
 - type: decal
-  id: star
-  tags: ["crayon"]
+  id: f
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: star
+    state: f
 
 - type: decal
-  id: stickman
-  tags: ["crayon"]
+  id: g
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: stickman
+    state: g
 
 - type: decal
-  id: t
-  tags: ["crayon"]
+  id: h
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: t
+    state: h
 
 - type: decal
-  id: taser
-  tags: ["crayon"]
+  id: i
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: taser
+    state: i
 
 - type: decal
-  id: thinline
-  tags: ["crayon"]
+  id: j
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: thinline
+    state: j
 
 - type: decal
-  id: toilet
-  tags: ["crayon"]
+  id: k
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: toilet
+    state: k
 
 - type: decal
-  id: toolbox
-  tags: ["crayon"]
+  id: l
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: toolbox
+    state: l
 
 - type: decal
-  id: trade
-  tags: ["crayon"]
+  id: m
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: trade
+    state: m
 
 - type: decal
-  id: u
-  tags: ["crayon"]
+  id: n
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: u
+    state: n
 
 - type: decal
-  id: uboa
-  tags: ["crayon"]
+  id: o
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
   sprite:
     sprite: Effects/crayondecals.rsi
-    state: uboa
+    state: o
+
+- type: decal
+  id: p
+  tags: ["crayon", "crayon-2-alphanum"]
+  defaultCleanable: true
+  defaultCustomColor: true
+  defaultSnap: false
+  sprite:
+    sprite: Effects/crayondecals.rsi
+    state: p
+
+- type: decal
+  id: q
+  tags: ["crayon", "crayon-2-alphanum"]
+  defaultCleanable: true
+  defaultCustomColor: true
+  defaultSnap: false
+  sprite:
+    sprite: Effects/crayondecals.rsi
+    state: q
+
+- type: decal
+  id: r
+  tags: ["crayon", "crayon-2-alphanum"]
+  defaultCleanable: true
+  defaultCustomColor: true
+  defaultSnap: false
+  sprite:
+    sprite: Effects/crayondecals.rsi
+    state: r
+
+- type: decal
+  id: s
+  tags: ["crayon", "crayon-2-alphanum"]
+  defaultCleanable: true
+  defaultCustomColor: true
+  defaultSnap: false
+  sprite:
+    sprite: Effects/crayondecals.rsi
+    state: s
+
+- type: decal
+  id: t
+  tags: ["crayon", "crayon-2-alphanum"]
+  defaultCleanable: true
+  defaultCustomColor: true
+  defaultSnap: false
+  sprite:
+    sprite: Effects/crayondecals.rsi
+    state: t
+
+- type: decal
+  id: u
+  tags: ["crayon", "crayon-2-alphanum"]
+  defaultCleanable: true
+  defaultCustomColor: true
+  defaultSnap: false
+  sprite:
+    sprite: Effects/crayondecals.rsi
+    state: u
 
 - type: decal
   id: v
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: w
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: x
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: y
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false
 
 - type: decal
   id: z
-  tags: ["crayon"]
+  tags: ["crayon", "crayon-2-alphanum"]
   defaultCleanable: true
   defaultCustomColor: true
   defaultSnap: false