]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
spray painter rework (#23287)
authordeltanedas <39013340+deltanedas@users.noreply.github.com>
Thu, 1 Feb 2024 22:30:46 +0000 (22:30 +0000)
committerGitHub <noreply@github.com>
Thu, 1 Feb 2024 22:30:46 +0000 (09:30 +1100)
* refactor and add Department to PaintableAirlock, move it to server dir since its in namespace

* add departments to doors, cleanup

* add style -> departments mapping

* AirlockDepartmentsPrototype

* update shared spray stuff to have department

* name file the same as the class name

* department optional

* refactor spray painter system + send department

* fixy

* client

* no need to rewrite ActivateableUi

* pro ops

* the reckoning

* hiss

* .

* :trollface:

* add standard atmos colors to palette

* Update Content.Shared/SprayPainter/SharedSprayPainterSystem.cs

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
15 files changed:
Content.Client/SprayPainter/SprayPainterSystem.cs
Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs
Content.Server/SprayPainter/SprayPainterSystem.cs
Content.Shared/SprayPainter/Components/PaintableAirlockComponent.cs
Content.Shared/SprayPainter/Components/SprayPainterComponent.cs
Content.Shared/SprayPainter/Prototypes/AirlockDepartmentsPrototype.cs [new file with mode: 0644]
Content.Shared/SprayPainter/SharedDevicePainterSystem.cs [deleted file]
Content.Shared/SprayPainter/SharedSprayPainterSystem.cs [new file with mode: 0644]
Content.Shared/SprayPainter/SprayPainterEvents.cs
Resources/Prototypes/Entities/Objects/Tools/spray_painter.yml
Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml
Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml
Resources/Prototypes/Entities/Structures/Doors/Airlocks/external.yml
Resources/Prototypes/Entities/Structures/Doors/Airlocks/shuttle.yml
Resources/Prototypes/Entities/Structures/Doors/airlock_groups.yml

index 4476e2a90ae0a79947fefdff5f77c72cdddd33a6..6a1d27e98b7f998e092eb1e5c1a282529ec6d951 100644 (file)
@@ -14,29 +14,31 @@ public sealed class SprayPainterSystem : SharedSprayPainterSystem
 
     public List<SprayPainterEntry> Entries { get; private set; } = new();
 
-    public override void Initialize()
+    protected override void CacheStyles()
     {
-        base.Initialize();
+        base.CacheStyles();
 
-        foreach (string style in Styles)
+        Entries.Clear();
+        foreach (var style in Styles)
         {
+            var name = style.Name;
             string? iconPath = Groups
-              .FindAll(x => x.StylePaths.ContainsKey(style))?
-              .MaxBy(x => x.IconPriority)?.StylePaths[style];
+              .FindAll(x => x.StylePaths.ContainsKey(name))?
+              .MaxBy(x => x.IconPriority)?.StylePaths[name];
             if (iconPath == null)
             {
-                Entries.Add(new SprayPainterEntry(style, null));
+                Entries.Add(new SprayPainterEntry(name, null));
                 continue;
             }
 
             RSIResource doorRsi = _resourceCache.GetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / new ResPath(iconPath));
             if (!doorRsi.RSI.TryGetState("closed", out var icon))
             {
-                Entries.Add(new SprayPainterEntry(style, null));
+                Entries.Add(new SprayPainterEntry(name, null));
                 continue;
             }
 
-            Entries.Add(new SprayPainterEntry(style, icon.Frame0));
+            Entries.Add(new SprayPainterEntry(name, icon.Frame0));
         }
     }
 }
index d5c57a601f00054b4515a47583795063642f935c..e8442d239086faaa9855d0ddf7904c6f488bdd36 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.SprayPainter;
+using Content.Shared.SprayPainter.Components;
 using Robust.Client.GameObjects;
 using Robust.Client.UserInterface.Controls;
 
@@ -20,14 +21,20 @@ public sealed class SprayPainterBoundUserInterface : BoundUserInterface
     {
         base.Open();
 
+        if (!EntMan.TryGetComponent<SprayPainterComponent>(Owner, out var comp))
+            return;
+
         _window = new SprayPainterWindow();
 
         _painter = EntMan.System<SprayPainterSystem>();
 
-        _window.OpenCentered();
         _window.OnClose += Close;
         _window.OnSpritePicked = OnSpritePicked;
         _window.OnColorPicked = OnColorPicked;
+
+        _window.Populate(_painter.Entries, comp.Index, comp.PickedColor, comp.ColorPalette);
+
+        _window.OpenCentered();
     }
 
     protected override void Dispose(bool disposing)
@@ -37,25 +44,6 @@ public sealed class SprayPainterBoundUserInterface : BoundUserInterface
         _window?.Dispose();
     }
 
-    protected override void UpdateState(BoundUserInterfaceState state)
-    {
-        base.UpdateState(state);
-
-        if (_window == null)
-            return;
-
-        if (_painter == null)
-            return;
-
-        if (state is not SprayPainterBoundUserInterfaceState stateCast)
-            return;
-
-        _window.Populate(_painter.Entries,
-                         stateCast.SelectedStyle,
-                         stateCast.SelectedColorKey,
-                         stateCast.Palette);
-    }
-
     private void OnSpritePicked(ItemList.ItemListSelectedEventArgs args)
     {
         SendMessage(new SprayPainterSpritePickedMessage(args.ItemIndex));
index 4f8f1cda2ea0d98c3a884d272034f89330b95746..e49c49c1da059218364e36b019d29a07952ab6f7 100644 (file)
-using System.Linq;
-using Content.Server.Administration.Logs;
 using Content.Server.Atmos.Piping.Components;
 using Content.Server.Atmos.Piping.EntitySystems;
-using Content.Server.Popups;
-using Content.Shared.Database;
 using Content.Shared.DoAfter;
-using Content.Shared.Doors.Components;
+using Content.Shared.Interaction;
 using Content.Shared.SprayPainter;
 using Content.Shared.SprayPainter.Components;
-using Content.Shared.SprayPainter.Prototypes;
-using Content.Shared.Interaction;
-using JetBrains.Annotations;
-using Robust.Server.GameObjects;
-using Robust.Shared.Audio;
-using Robust.Shared.Audio.Systems;
-using Robust.Shared.Player;
 
 namespace Content.Server.SprayPainter;
 
 /// <summary>
-/// A system for painting airlocks and pipes using enginner painter
+/// Handles spraying pipes using a spray painter.
+/// Airlocks are handled in shared.
 /// </summary>
-[UsedImplicitly]
 public sealed class SprayPainterSystem : SharedSprayPainterSystem
 {
-    [Dependency] private readonly IAdminLogManager _adminLogger = default!;
-    [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
-    [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
-    [Dependency] private readonly PopupSystem _popupSystem = default!;
-    [Dependency] private readonly SharedAudioSystem _audio = default!;
-    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
-    [Dependency] private readonly AtmosPipeColorSystem _pipeColorSystem = default!;
+    [Dependency] private readonly AtmosPipeColorSystem _pipeColor = default!;
 
     public override void Initialize()
     {
         base.Initialize();
 
-        SubscribeLocalEvent<SprayPainterComponent, ComponentInit>(OnInit);
-        SubscribeLocalEvent<SprayPainterComponent, AfterInteractEvent>(AfterInteractOn);
-        SubscribeLocalEvent<SprayPainterComponent, ActivateInWorldEvent>(OnActivate);
-        SubscribeLocalEvent<SprayPainterComponent, SprayPainterSpritePickedMessage>(OnSpritePicked);
-        SubscribeLocalEvent<SprayPainterComponent, SprayPainterColorPickedMessage>(OnColorPicked);
-        SubscribeLocalEvent<SprayPainterComponent, SprayPainterDoAfterEvent>(OnDoAfter);
-    }
-
-    private void OnInit(EntityUid uid, SprayPainterComponent component, ComponentInit args)
-    {
-        if (component.ColorPalette.Count == 0)
-            return;
+        SubscribeLocalEvent<SprayPainterComponent, SprayPainterPipeDoAfterEvent>(OnPipeDoAfter);
 
-        SetColor(uid, component, component.ColorPalette.First().Key);
+        SubscribeLocalEvent<AtmosPipeColorComponent, InteractUsingEvent>(OnPipeInteract);
     }
 
-    private void OnDoAfter(EntityUid uid, SprayPainterComponent component, SprayPainterDoAfterEvent args)
+    private void OnPipeDoAfter(Entity<SprayPainterComponent> ent, ref SprayPainterPipeDoAfterEvent args)
     {
-        component.IsSpraying = false;
-
         if (args.Handled || args.Cancelled)
             return;
 
-        if (args.Args.Target == null)
+        if (args.Args.Target is not {} target)
             return;
 
-        EntityUid target = (EntityUid) args.Args.Target;
-
-        _audio.PlayPvs(component.SpraySound, uid);
-
-        if (TryComp<AtmosPipeColorComponent>(target, out var atmosPipeColorComp))
-        {
-            _pipeColorSystem.SetColor(target, atmosPipeColorComp, args.Color ?? Color.White);
-        } else { // Target is an airlock
-            if (args.Sprite != null)
-            {
-                _appearance.SetData(target, DoorVisuals.BaseRSI, args.Sprite);
-                _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.Args.User):user} painted {ToPrettyString(args.Args.Target.Value):target}");
-            }
-        }
-
-        args.Handled = true;
-    }
-
-    private void OnActivate(EntityUid uid, SprayPainterComponent component, ActivateInWorldEvent args)
-    {
-        if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
+        if (!TryComp<AtmosPipeColorComponent>(target, out var color))
             return;
-        DirtyUI(uid, component);
 
-        _userInterfaceSystem.TryOpen(uid, SprayPainterUiKey.Key, actor.PlayerSession);
-        args.Handled = true;
-    }
-
-    private void AfterInteractOn(EntityUid uid, SprayPainterComponent component, AfterInteractEvent args)
-    {
-        if (component.IsSpraying || args.Target is not { Valid: true } target || !args.CanReach)
-            return;
+        Audio.PlayPvs(ent.Comp.SpraySound, ent);
 
-        if (EntityManager.TryGetComponent<PaintableAirlockComponent>(target, out var airlock))
-        {
-            if (!_prototypeManager.TryIndex<AirlockGroupPrototype>(airlock.Group, out var grp))
-            {
-                Log.Error("Group not defined: %s", airlock.Group);
-                return;
-            }
-
-            string style = Styles[component.Index];
-            if (!grp.StylePaths.TryGetValue(style, out var sprite))
-            {
-                string msg = Loc.GetString("spray-painter-style-not-available");
-                _popupSystem.PopupEntity(msg, args.User, args.User);
-                return;
-            }
-            component.IsSpraying = true;
-
-            var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.AirlockSprayTime, new SprayPainterDoAfterEvent(sprite, null), uid, target: target, used: uid)
-            {
-                BreakOnTargetMove = true,
-                BreakOnUserMove = true,
-                BreakOnDamage = true,
-                NeedHand = true,
-            };
-            _doAfterSystem.TryStartDoAfter(doAfterEventArgs);
-
-            // Log attempt
-            _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.User):user} is painting {ToPrettyString(uid):target} to '{style}' at {Transform(uid).Coordinates:targetlocation}");
-        } else { // Painting pipes
-            if(component.PickedColor is null)
-                return;
-
-            if (!EntityManager.HasComponent<AtmosPipeColorComponent>(target))
-                return;
-
-            if(!component.ColorPalette.TryGetValue(component.PickedColor, out var color))
-                return;
-
-            var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.PipeSprayTime, new SprayPainterDoAfterEvent(null, color), uid, target, uid)
-            {
-                BreakOnTargetMove = true,
-                BreakOnUserMove = true,
-                BreakOnDamage = true,
-                CancelDuplicate = true,
-                DuplicateCondition = DuplicateConditions.SameTarget,
-                NeedHand = true,
-            };
-
-            _doAfterSystem.TryStartDoAfter(doAfterEventArgs);
-        }
-    }
+        _pipeColor.SetColor(target, color, args.Color);
 
-    private void OnColorPicked(EntityUid uid, SprayPainterComponent component, SprayPainterColorPickedMessage args)
-    {
-        SetColor(uid, component, args.Key);
-    }
-
-    private void OnSpritePicked(EntityUid uid, SprayPainterComponent component, SprayPainterSpritePickedMessage args)
-    {
-        component.Index = args.Index;
-        DirtyUI(uid, component);
+        args.Handled = true;
     }
 
-    private void SetColor(EntityUid uid, SprayPainterComponent component, string? paletteKey)
+    private void OnPipeInteract(Entity<AtmosPipeColorComponent> ent, ref InteractUsingEvent args)
     {
-        if (paletteKey == null)
+        if (args.Handled)
             return;
 
-        if (!component.ColorPalette.ContainsKey(paletteKey) || paletteKey == component.PickedColor)
+        if (!TryComp<SprayPainterComponent>(args.Used, out var painter) || painter.PickedColor is not {} colorName)
             return;
 
-        component.PickedColor = paletteKey;
-        DirtyUI(uid, component);
-    }
-
-    private void DirtyUI(EntityUid uid, SprayPainterComponent? component = null)
-    {
-        if (!Resolve(uid, ref component))
+        if (!painter.ColorPalette.TryGetValue(colorName, out var color))
             return;
 
-        _userInterfaceSystem.TrySetUiState(
-            uid,
-            SprayPainterUiKey.Key,
-            new SprayPainterBoundUserInterfaceState(
-                component.Index,
-                component.PickedColor,
-                component.ColorPalette));
+        var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, painter.PipeSprayTime, new SprayPainterPipeDoAfterEvent(color), args.Used, target: ent, used: args.Used)
+        {
+            BreakOnTargetMove = true,
+            BreakOnUserMove = true,
+            BreakOnDamage = true,
+            CancelDuplicate = true,
+            // multiple pipes can be sprayed at once just not the same one
+            DuplicateCondition = DuplicateConditions.SameTarget,
+            NeedHand = true
+        };
+
+        args.Handled = DoAfter.TryStartDoAfter(doAfterEventArgs);
     }
 }
index 10fd72434e644f8655cc926896886153a61e94eb..fdd0aeeb7f970e491ad43a66179cd24e8823bfc5 100644 (file)
@@ -1,11 +1,24 @@
+using Content.Shared.Roles;
 using Content.Shared.SprayPainter.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
 
-namespace Content.Server.SprayPainter;
+namespace Content.Shared.SprayPainter.Components;
 
-[RegisterComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
 public sealed partial class PaintableAirlockComponent : Component
 {
-    [DataField("group", customTypeSerializer:typeof(PrototypeIdSerializer<AirlockGroupPrototype>))]
-    public string Group = default!;
+    /// <summary>
+    /// Group of styles this airlock can be painted with, e.g. glass, standard or external.
+    /// </summary>
+    [DataField(required: true), AutoNetworkedField]
+    public ProtoId<AirlockGroupPrototype> Group = string.Empty;
+
+    /// <summary>
+    /// Department this airlock is painted as, or none.
+    /// Must be specified in prototypes for turf war to work.
+    /// To better catch any mistakes, you need to explicitly state a non-styled airlock has a null department.
+    /// </summary>
+    [DataField(required: true), AutoNetworkedField]
+    public ProtoId<DepartmentPrototype>? Department;
 }
index e4581527b796ea348080f903647e3a8316badb9d..1742b13f8d32d5e9125551cd0d9299155b4bd207 100644 (file)
@@ -1,29 +1,44 @@
+using Content.Shared.DoAfter;
 using Robust.Shared.Audio;
 using Robust.Shared.GameStates;
 
 namespace Content.Shared.SprayPainter.Components;
 
-[RegisterComponent, NetworkedComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
 public sealed partial class SprayPainterComponent : Component
 {
-    [DataField("spraySound")]
+    [DataField]
     public SoundSpecifier SpraySound = new SoundPathSpecifier("/Audio/Effects/spray2.ogg");
 
-    [DataField("airlockSprayTime")]
-    public float AirlockSprayTime = 3.0f;
+    [DataField]
+    public TimeSpan AirlockSprayTime = TimeSpan.FromSeconds(3);
 
-    [DataField("pipeSprayTime")]
-    public float PipeSprayTime = 1.0f;
+    [DataField]
+    public TimeSpan PipeSprayTime = TimeSpan.FromSeconds(1);
 
-    [DataField("isSpraying")]
-    public bool IsSpraying = false;
+    /// <summary>
+    /// DoAfterId for airlock spraying.
+    /// Pipes do not track doafters so you can spray multiple at once.
+    /// </summary>
+    [DataField]
+    public DoAfterId? AirlockDoAfter;
 
-    [ViewVariables(VVAccess.ReadWrite)]
+    /// <summary>
+    /// Pipe color chosen to spray with.
+    /// </summary>
+    [DataField, AutoNetworkedField]
     public string? PickedColor;
 
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("colorPalette")]
+    /// <summary>
+    /// Pipe colors that can be selected.
+    /// </summary>
+    [DataField]
     public Dictionary<string, Color> ColorPalette = new();
 
-    public int Index = default!;
+    /// <summary>
+    /// Airlock style index selected.
+    /// After prototype reload this might not be the same style but it will never be out of bounds.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public int Index;
 }
diff --git a/Content.Shared/SprayPainter/Prototypes/AirlockDepartmentsPrototype.cs b/Content.Shared/SprayPainter/Prototypes/AirlockDepartmentsPrototype.cs
new file mode 100644 (file)
index 0000000..3553597
--- /dev/null
@@ -0,0 +1,21 @@
+using Content.Shared.Roles;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.SprayPainter.Prototypes;
+
+/// <summary>
+/// Maps airlock style names to department ids.
+/// </summary>
+[Prototype("airlockDepartments")]
+public sealed class AirlockDepartmentsPrototype : IPrototype
+{
+    [IdDataField]
+    public string ID { get; private set; } = default!;
+
+    /// <summary>
+    /// Dictionary of style names to department ids.
+    /// If a style does not have a department (e.g. external) it is set to null.
+    /// </summary>
+    [DataField(required: true)]
+    public Dictionary<string, ProtoId<DepartmentPrototype>> Departments = new();
+}
diff --git a/Content.Shared/SprayPainter/SharedDevicePainterSystem.cs b/Content.Shared/SprayPainter/SharedDevicePainterSystem.cs
deleted file mode 100644 (file)
index ff43b11..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-using System.Linq;
-using Content.Shared.SprayPainter.Prototypes;
-using Robust.Shared.Prototypes;
-
-namespace Content.Shared.SprayPainter;
-
-public abstract class SharedSprayPainterSystem : EntitySystem
-{
-    [Dependency] protected readonly IPrototypeManager _prototypeManager = default!;
-
-    public List<string> Styles { get; private set; } = new();
-    public List<AirlockGroupPrototype> Groups { get; private set; } = new();
-
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        SortedSet<string> styles = new();
-        foreach (AirlockGroupPrototype grp in _prototypeManager.EnumeratePrototypes<AirlockGroupPrototype>())
-        {
-            Groups.Add(grp);
-            foreach (string style in grp.StylePaths.Keys)
-            {
-                styles.Add(style);
-            }
-        }
-        Styles = styles.ToList();
-    }
-}
diff --git a/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs b/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs
new file mode 100644 (file)
index 0000000..c0f0f6d
--- /dev/null
@@ -0,0 +1,202 @@
+using Content.Shared.Administration.Logs;
+using Content.Shared.Database;
+using Content.Shared.DoAfter;
+using Content.Shared.Doors.Components;
+using Content.Shared.Interaction;
+using Content.Shared.Popups;
+using Content.Shared.SprayPainter.Components;
+using Content.Shared.SprayPainter.Prototypes;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Prototypes;
+using System.Linq;
+
+namespace Content.Shared.SprayPainter;
+
+/// <summary>
+/// System for painting airlocks using a spray painter.
+/// Pipes are handled serverside since AtmosPipeColorSystem is server only.
+/// </summary>
+public abstract class SharedSprayPainterSystem : EntitySystem
+{
+    [Dependency] protected readonly IPrototypeManager Proto = default!;
+    [Dependency] private   readonly ISharedAdminLogManager _adminLogger = default!;
+    [Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
+    [Dependency] protected readonly SharedAudioSystem Audio = default!;
+    [Dependency] protected readonly SharedDoAfterSystem DoAfter = default!;
+    [Dependency] private   readonly SharedPopupSystem _popup = default!;
+    [Dependency] private   readonly SharedUserInterfaceSystem _ui = default!;
+
+    public List<AirlockStyle> Styles { get; private set; } = new();
+    public List<AirlockGroupPrototype> Groups { get; private set; } = new();
+
+    [ValidatePrototypeId<AirlockDepartmentsPrototype>]
+    private const string Departments = "Departments";
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        CacheStyles();
+
+        SubscribeLocalEvent<SprayPainterComponent, MapInitEvent>(OnMapInit);
+        SubscribeLocalEvent<SprayPainterComponent, SprayPainterDoorDoAfterEvent>(OnDoorDoAfter);
+        Subs.BuiEvents<SprayPainterComponent>(SprayPainterUiKey.Key, subs =>
+        {
+            subs.Event<SprayPainterSpritePickedMessage>(OnSpritePicked);
+            subs.Event<SprayPainterColorPickedMessage>(OnColorPicked);
+        });
+
+        SubscribeLocalEvent<PaintableAirlockComponent, InteractUsingEvent>(OnAirlockInteract);
+
+        SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
+    }
+
+    private void OnMapInit(Entity<SprayPainterComponent> ent, ref MapInitEvent args)
+    {
+        if (ent.Comp.ColorPalette.Count == 0)
+            return;
+
+        SetColor(ent, ent.Comp.ColorPalette.First().Key);
+    }
+
+    private void OnDoorDoAfter(Entity<SprayPainterComponent> ent, ref SprayPainterDoorDoAfterEvent args)
+    {
+        ent.Comp.AirlockDoAfter = null;
+
+        if (args.Handled || args.Cancelled)
+            return;
+
+        if (args.Args.Target is not {} target)
+            return;
+
+        if (!TryComp<PaintableAirlockComponent>(target, out var airlock))
+            return;
+
+        airlock.Department = args.Department;
+        Dirty(target, airlock);
+
+        Audio.PlayPredicted(ent.Comp.SpraySound, ent, args.Args.User);
+        Appearance.SetData(target, DoorVisuals.BaseRSI, args.Sprite);
+        _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.Args.User):user} painted {ToPrettyString(args.Args.Target.Value):target}");
+
+        args.Handled = true;
+    }
+
+    #region UI messages
+
+    private void OnColorPicked(Entity<SprayPainterComponent> ent, ref SprayPainterColorPickedMessage args)
+    {
+        SetColor(ent, args.Key);
+    }
+
+    private void OnSpritePicked(Entity<SprayPainterComponent> ent, ref SprayPainterSpritePickedMessage args)
+    {
+        if (args.Index >= Styles.Count)
+            return;
+
+        ent.Comp.Index = args.Index;
+        Dirty(ent, ent.Comp);
+    }
+
+    private void SetColor(Entity<SprayPainterComponent> ent, string? paletteKey)
+    {
+        if (paletteKey == null || paletteKey == ent.Comp.PickedColor)
+            return;
+
+        if (!ent.Comp.ColorPalette.ContainsKey(paletteKey))
+            return;
+
+        ent.Comp.PickedColor = paletteKey;
+        Dirty(ent, ent.Comp);
+    }
+
+    #endregion
+
+    private void OnAirlockInteract(Entity<PaintableAirlockComponent> ent, ref InteractUsingEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        if (!TryComp<SprayPainterComponent>(args.Used, out var painter) || painter.AirlockDoAfter != null)
+            return;
+
+        var group = Proto.Index<AirlockGroupPrototype>(ent.Comp.Group);
+
+        var style = Styles[painter.Index];
+        if (!group.StylePaths.TryGetValue(style.Name, out var sprite))
+        {
+            string msg = Loc.GetString("spray-painter-style-not-available");
+            _popup.PopupEntity(msg, args.User, args.User);
+            return;
+        }
+
+        var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, painter.AirlockSprayTime, new SprayPainterDoorDoAfterEvent(sprite, style.Department), args.Used, target: ent, used: args.Used)
+        {
+            BreakOnTargetMove = true,
+            BreakOnUserMove = true,
+            BreakOnDamage = true,
+            NeedHand = true
+        };
+        if (!DoAfter.TryStartDoAfter(doAfterEventArgs, out var id))
+            return;
+
+        // since we are now spraying an airlock prevent spraying more at the same time
+        // pipes ignore this
+        painter.AirlockDoAfter = id;
+        args.Handled = true;
+
+        // Log the attempt
+        _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.User):user} is painting {ToPrettyString(ent):target} to '{style.Name}' at {Transform(ent).Coordinates:targetlocation}");
+    }
+
+    #region Style caching
+
+    private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
+    {
+        if (!args.WasModified<AirlockGroupPrototype>() && !args.WasModified<AirlockDepartmentsPrototype>())
+            return;
+
+        Styles.Clear();
+        Groups.Clear();
+        CacheStyles();
+
+        // style index might be invalid now so check them all
+        var max = Styles.Count - 1;
+        var query = AllEntityQuery<SprayPainterComponent>();
+        while (query.MoveNext(out var uid, out var comp))
+        {
+            if (comp.Index > max)
+            {
+                comp.Index = max;
+                Dirty(uid, comp);
+            }
+        }
+    }
+
+    protected virtual void CacheStyles()
+    {
+        // collect every style's name
+        var names = new SortedSet<string>();
+        foreach (var group in Proto.EnumeratePrototypes<AirlockGroupPrototype>())
+        {
+            Groups.Add(group);
+            foreach (var style in group.StylePaths.Keys)
+            {
+                names.Add(style);
+            }
+        }
+
+        // get their department ids too for the final style list
+        var departments = Proto.Index<AirlockDepartmentsPrototype>(Departments);
+        Styles.Capacity = names.Count;
+        foreach (var name in names)
+        {
+            departments.Departments.TryGetValue(name, out var department);
+            Styles.Add(new AirlockStyle(name, department));
+        }
+    }
+
+    #endregion
+}
+
+public record struct AirlockStyle(string Name, string? Department);
index f0e02086104331ea4aeb4914a2aea771f5a4a829..b88b054ad14f7fb6ee556a9f0a2092f66ccc65c5 100644 (file)
@@ -12,7 +12,7 @@ public enum SprayPainterUiKey
 [Serializable, NetSerializable]
 public sealed class SprayPainterSpritePickedMessage : BoundUserInterfaceMessage
 {
-    public int Index { get; }
+    public readonly int Index;
 
     public SprayPainterSpritePickedMessage(int index)
     {
@@ -23,7 +23,7 @@ public sealed class SprayPainterSpritePickedMessage : BoundUserInterfaceMessage
 [Serializable, NetSerializable]
 public sealed class SprayPainterColorPickedMessage : BoundUserInterfaceMessage
 {
-    public string? Key { get; }
+    public readonly string? Key;
 
     public SprayPainterColorPickedMessage(string? key)
     {
@@ -32,36 +32,40 @@ public sealed class SprayPainterColorPickedMessage : BoundUserInterfaceMessage
 }
 
 [Serializable, NetSerializable]
-public sealed class SprayPainterBoundUserInterfaceState : BoundUserInterfaceState
+public sealed partial class SprayPainterDoorDoAfterEvent : DoAfterEvent
 {
-    public int SelectedStyle { get; }
-    public string? SelectedColorKey { get; }
-    public Dictionary<string, Color> Palette { get; }
+    /// <summary>
+    /// Base RSI path to set for the door sprite.
+    /// </summary>
+    [DataField]
+    public string Sprite;
 
-    public SprayPainterBoundUserInterfaceState(int selectedStyle, string? selectedColorKey, Dictionary<string, Color> palette)
+    /// <summary>
+    /// Department id to set for the door, if the style has one.
+    /// </summary>
+    [DataField]
+    public string? Department;
+
+    public SprayPainterDoorDoAfterEvent(string sprite, string? department)
     {
-        SelectedStyle = selectedStyle;
-        SelectedColorKey = selectedColorKey;
-        Palette = palette;
+        Sprite = sprite;
+        Department = department;
     }
+
+    public override DoAfterEvent Clone() => this;
 }
 
 [Serializable, NetSerializable]
-public sealed partial class SprayPainterDoAfterEvent : DoAfterEvent
+public sealed partial class SprayPainterPipeDoAfterEvent : DoAfterEvent
 {
-    [DataField("sprite")]
-    public string? Sprite = null;
-
-    [DataField("color")]
-    public Color? Color = null;
+    /// <summary>
+    /// Color of the pipe to set.
+    /// </summary>
+    [DataField]
+    public Color Color;
 
-    private SprayPainterDoAfterEvent()
+    public SprayPainterPipeDoAfterEvent(Color color)
     {
-    }
-
-    public SprayPainterDoAfterEvent(string? sprite, Color? color)
-    {
-        Sprite = sprite;
         Color = color;
     }
 
index 8a8c569510dc620ec6e96928bd62e3900f61d680..903b8d3f9062e6438e2b8d0a3f3c5be2f0330448 100644 (file)
@@ -4,24 +4,31 @@
   name: spray painter
   description: A spray painter for painting airlocks and pipes.
   components:
-    - type: Sprite
-      sprite: Objects/Tools/spray_painter.rsi
-      state: spray_painter
-    - type: Item
-      sprite: Objects/Tools/spray_painter.rsi
-    - type: UserInterface
-      interfaces:
-        - key: enum.SprayPainterUiKey.Key
-          type: SprayPainterBoundUserInterface
-    - type: SprayPainter
-      colorPalette:
-        red: '#FF1212FF'
-        yellow: '#B3A234FF'
-        brown: '#947507FF'
-        green: '#3AB334FF'
-        cyan: '#03FCD3FF'
-        blue: '#0335FCFF'
-        white: '#FFFFFFFF'
-        black: '#333333FF'
-    - type: StaticPrice
-      price: 40
+  - type: Sprite
+    sprite: Objects/Tools/spray_painter.rsi
+    state: spray_painter
+  - type: Item
+    sprite: Objects/Tools/spray_painter.rsi
+  - type: ActivatableUI
+    key: enum.SprayPainterUiKey.Key
+  - type: UserInterface
+    interfaces:
+    - key: enum.SprayPainterUiKey.Key
+      type: SprayPainterBoundUserInterface
+  - type: SprayPainter
+    colorPalette:
+      red: '#FF1212FF'
+      yellow: '#B3A234FF'
+      brown: '#947507FF'
+      green: '#3AB334FF'
+      cyan: '#03FCD3FF'
+      blue: '#0335FCFF'
+      white: '#FFFFFFFF'
+      black: '#333333FF'
+      # standard atmos pipes
+      waste: '#990000'
+      distro: '#0055cc'
+      air: '#03fcd3'
+      mix: '#947507'
+  - type: StaticPrice
+    price: 40
index 549a42c26411615edbebf4d9a37f9aa3f6e2bb9a..ce2b32612945a068e89e72fc4cf2a484bb9e58b1 100644 (file)
   components:
   - type: Sprite
     sprite: Structures/Doors/Airlocks/Standard/engineering.rsi
+  - type: PaintableAirlock
+    department: Engineering
 
 - type: entity
-  parent: Airlock
+  parent: AirlockEngineering
   id: AirlockAtmospherics
   suffix: Atmospherics
   components:
@@ -29,6 +31,8 @@
   components:
   - type: Sprite
     sprite: Structures/Doors/Airlocks/Standard/cargo.rsi
+  - type: PaintableAirlock
+    department: Cargo
 
 - type: entity
   parent: Airlock
   components:
   - type: Sprite
     sprite: Structures/Doors/Airlocks/Standard/medical.rsi
+  - type: PaintableAirlock
+    department: Medical
 
 - type: entity
-  parent: Airlock
+  parent: AirlockMedical
   id: AirlockVirology
   suffix: Virology
   components:
     sprite: Structures/Doors/Airlocks/Standard/virology.rsi
 
 - type: entity
-  parent: Airlock
+  parent: AirlockMedical
   id: AirlockChemistry
   suffix: Chemistry
-  components:
-  - type: Sprite
-    sprite: Structures/Doors/Airlocks/Standard/medical.rsi
 
 - type: entity
   parent: Airlock
@@ -61,6 +64,8 @@
   components:
   - type: Sprite
     sprite: Structures/Doors/Airlocks/Standard/science.rsi
+  - type: PaintableAirlock
+    department: Science
 
 - type: entity
   parent: Airlock
@@ -71,6 +76,8 @@
     sprite: Structures/Doors/Airlocks/Standard/command.rsi
   - type: WiresPanelSecurity
     securityLevel: medSecurity
+  - type: PaintableAirlock
+    department: Command
 
 - type: entity
   parent: Airlock
@@ -79,6 +86,8 @@
   components:
   - type: Sprite
     sprite: Structures/Doors/Airlocks/Standard/security.rsi
+  - type: PaintableAirlock
+    department: Security
 
 - type: entity
   parent: Airlock
@@ -89,7 +98,7 @@
     sprite: Structures/Doors/Airlocks/Standard/maint.rsi
 
 - type: entity
-  parent: Airlock
+  parent: AirlockSecurity # if you get syndie door somehow it counts as sec
   id: AirlockSyndicate
   suffix: Syndicate
   components:
     sprite: Structures/Doors/Airlocks/Standard/syndicate.rsi
 
 - type: entity
-  parent: Airlock
+  parent: AirlockCargo
   id: AirlockMining
   suffix: Mining(Salvage)
   components:
     sprite: Structures/Doors/Airlocks/Standard/mining.rsi
 
 - type: entity
-  parent: Airlock
+  parent: AirlockCommand # if you get centcom door somehow it counts as command, also inherit panel
   id: AirlockCentralCommand
   suffix: Central Command
   components:
   - type: Sprite
     sprite: Structures/Doors/Airlocks/Standard/centcomm.rsi
-  - type: WiresPanelSecurity
-    securityLevel: medSecurity
 
 - type: entity
   parent: Airlock
   - type: Sprite
     sprite: Structures/Doors/Airlocks/Glass/engineering.rsi
   - type: PaintableAirlock
-    group: Glass
+    department: Engineering
 
 - type: entity
   parent: AirlockGlass
   components:
   - type: Sprite
     sprite: Structures/Doors/Airlocks/Glass/maint.rsi
-  - type: PaintableAirlock
-    group: Glass
 
 - type: entity
-  parent: AirlockGlass
+  parent: AirlockEngineeringGlass
   id: AirlockAtmosphericsGlass
   suffix: Atmospherics
   components:
   - type: Sprite
     sprite: Structures/Doors/Airlocks/Glass/atmospherics.rsi
-  - type: PaintableAirlock
-    group: Glass
 
 - type: entity
   parent: AirlockGlass
   - type: Sprite
     sprite: Structures/Doors/Airlocks/Glass/cargo.rsi
   - type: PaintableAirlock
-    group: Glass
-
-- type: entity
-  parent: AirlockGlass
-  id: AirlockChemistryGlass
-  suffix: Chemistry
-  components:
-  - type: Sprite
-    sprite: Structures/Doors/Airlocks/Glass/medical.rsi
-  - type: PaintableAirlock
-    group: Glass
+    department: Cargo
 
 - type: entity
   parent: AirlockGlass
   - type: Sprite
     sprite: Structures/Doors/Airlocks/Glass/medical.rsi
   - type: PaintableAirlock
-    group: Glass
+    department: Medical
 
 - type: entity
-  parent: AirlockGlass
+  parent: AirlockMedicalGlass
+  id: AirlockChemistryGlass
+  suffix: Chemistry
+
+- type: entity
+  parent: AirlockMedicalGlass
   id: AirlockVirologyGlass
   suffix: Virology
   components:
   - type: Sprite
     sprite: Structures/Doors/Airlocks/Glass/virology.rsi
-  - type: PaintableAirlock
-    group: Glass
 
 - type: entity
   parent: AirlockGlass
   - type: Sprite
     sprite: Structures/Doors/Airlocks/Glass/science.rsi
   - type: PaintableAirlock
-    group: Glass
+    department: Science
 
 - type: entity
   parent: AirlockGlass
   - type: Sprite
     sprite: Structures/Doors/Airlocks/Glass/command.rsi
   - type: PaintableAirlock
-    group: Glass
+    department: Command
   - type: WiresPanelSecurity
     securityLevel: medSecurity
 
   - type: Sprite
     sprite: Structures/Doors/Airlocks/Glass/security.rsi
   - type: PaintableAirlock
-    group: Glass
+    department: Security
 
 - type: entity
-  parent: AirlockGlass
+  parent: AirlockSecurityGlass # see standard
   id: AirlockSyndicateGlass
   suffix: Syndicate
   components:
   - type: Sprite
     sprite: Structures/Doors/Airlocks/Glass/syndicate.rsi
-  - type: PaintableAirlock
-    group: Glass
 
 - type: entity
-  parent: AirlockGlass
+  parent: AirlockCargoGlass
   id: AirlockMiningGlass
   suffix: Mining(Salvage)
   components:
     sprite: Structures/Doors/Airlocks/Glass/mining.rsi
 
 - type: entity
-  parent: AirlockGlass
+  parent: AirlockCommandGlass # see standard
   id: AirlockCentralCommandGlass
   suffix: Central Command
   components:
     sprite: Structures/Doors/Airlocks/Glass/centcomm.rsi
   - type: WiresPanelSecurity
     securityLevel: medSecurity
-
index d610fb25fba42fc0bb201ba2a44fa2a99558206d..9930e6631de4b33d0247d59649ec960b5675aae7 100644 (file)
     mode: NoSprite
   - type: PaintableAirlock
     group: Standard
+    department: Civilian
   - type: AccessReader
   - type: StaticPrice
     price: 150
index 5e2eb5689fbb6c1dcdd25c66d106ce0aa9874cd5..75b23f7071902b91009b6367d7f79cd8a0043eeb 100644 (file)
@@ -18,6 +18,7 @@
     sprite: Structures/Doors/Airlocks/Standard/external.rsi
   - type: PaintableAirlock
     group: External
+    department: null
 
 - type: entity
   parent: AirlockExternal
index 8c5aa49b8e762140fde751fb34900d96cdb2f1da..21d485be0c89de47b7c7565f0f219dd6a18a2437 100644 (file)
@@ -64,6 +64,7 @@
       - ForceNoFixRotations
   - type: PaintableAirlock
     group: Shuttle
+    department: null
   - type: Construction
     graph: AirlockShuttle
     node: airlock
index b22d11826ce7c2b37b8071db48b971fce0eb34c8..09ce1a05d94d6c9eabfdfdf73572e1e907f64a61 100644 (file)
   iconPriority: 40
   stylePaths:
     shuttle:     Structures/Doors/Airlocks/Glass/shuttle.rsi
+
+# fun
+- type: airlockDepartments
+  id: Departments
+  departments:
+    atmospherics: Engineering
+    basic: Civilian
+    cargo: Cargo
+    command: Command
+    engineering: Engineering
+    freezer: Civilian
+    maintenance: Civilian
+    medical: Medical
+    science: Science
+    security: Security
+    virology: Medical