From: Whatstone <166147148+whatston3@users.noreply.github.com> Date: Fri, 11 Jul 2025 00:36:57 +0000 (-0400) Subject: Make more objects spray paintable (Reviving #31328) (#37341) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=9ad99cfa64e0b2803b9c3f00736a0bdd7d5d3848;p=space-station-14.git Make more objects spray paintable (Reviving #31328) (#37341) * PaintableAirlockComponent and AirlockGroupPrototype have been replaced * Slightly redesigned SprayPainterSystem for greater versatility * Added handling of changes to the appearance of doors and storages * PaintableGroup prototypes have been created * Generating tabs with styles in the UI * Fix error with undiscovered layer * Slight improvement * Removed unnecessary property * The category for `PaintableGroup` was allocated to a separate prototype so that the engine itself would check if the category existed * Added canisters, but repainting doesn't work * Added localization to styles * Fix sprite changing * Added the ability to paint canisters * slight ui improvement * Fix yamllinter errors * Fix test * The UI now remembers which tab was open * Fix build (?) * Rename * Charges have been added to the spray painter * Added a charge texture for the spray painter * Now spray painter can paint decals * Increased number of charges * Spawning dummy objects has been replaced by PrototypeManager * added a signature about the painting of the object * fix * Code commenting * Fix upstream * Update Content.Shared/SprayPainter/Components/SprayPainterAmmo.cs Co-authored-by: pathetic meowmeow * review * Now decals can only be painted if the corresponding tab in the menu is open. * Fixed a bug with pipe and decal tabs not being remembered * Update EntityStorageVisualizerSystem.cs * record * loc * Cleanup * Revert electrified visuals * more cleanup, fix charges, del ammo4 * no empty file, remove meta component * closet exceptions, storage visualizer fixes * enable/disable decal through alt-verb * Fix missed merge conflicts * fix snap offset, button event handlers * simpler order, fix snap loc string * Remove PaintableViz.BaseRSI, no decal item, A-Z * State-respecting UI, BUI updates, FTL fixes * revert DecalPlacerWindow changes * revert unwanted changes, cleanup function order * Limit SprayPainterAmmo write access to AmmoSystem * Remove PaintedSystem * spray paint ammo lathe recipe, youtool listing * category as a list, groups as subtabs * Restore inhand copyright in meta.json * empty spray painter, recipe produces an empty one * allow alpha on spray painter decals * add comments * paintable wall lockers * Restrict painting more objects * Suggested event changes, event cleanup * component comments, fix ammo inhands * uncleanable decals, dirty styles on mapinit * organize paintables, separate emergency/closet grp * fix categories newline at EOF * airlock group whitespace cleanup * realphabetize * Clean up EntityStorageViz merge conflict markers * Apply requested changes * Apply suggestions from sowelipililimute's review Co-authored-by: pathetic meowmeow * betrayal most foul * Remove members from EntityPaintedEvent * No emerg. group, steelsec to secure, locker/closet * Enable repainting the medical wall locker * comments, no flags on PaintableVisuals * Remove locked variants from closets/wall closets * removable decals * off value consistency * can't paint away those bones * fix precedence * Remove AirlockDepartment, AirlockGroup protos Both unused. * whitelist consistency re: ammo component * add standing emergency closet styles * alphabetize the spray painter listings --------- Co-authored-by: Ertanic Co-authored-by: Эдуард <36124833+Ertanic@users.noreply.github.com> Co-authored-by: pathetic meowmeow --- diff --git a/Content.Client/Atmos/EntitySystems/GasCanisterAppearanceSystem.cs b/Content.Client/Atmos/EntitySystems/GasCanisterAppearanceSystem.cs new file mode 100644 index 0000000000..f16774ce24 --- /dev/null +++ b/Content.Client/Atmos/EntitySystems/GasCanisterAppearanceSystem.cs @@ -0,0 +1,28 @@ +using Content.Shared.Atmos.Piping.Unary.Components; +using Content.Shared.SprayPainter.Prototypes; +using Robust.Client.GameObjects; +using Robust.Shared.Prototypes; + +namespace Content.Client.Atmos.EntitySystems; + +/// +/// Used to change the appearance of gas canisters. +/// +public sealed class GasCanisterAppearanceSystem : VisualizerSystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + protected override void OnAppearanceChange(EntityUid uid, GasCanisterComponent component, ref AppearanceChangeEvent args) + { + if (!AppearanceSystem.TryGetData(uid, PaintableVisuals.Prototype, out var protoName, args.Component) || args.Sprite is not { } old) + return; + + if (!_prototypeManager.HasIndex(protoName)) + return; + + // Create the given prototype and get its first layer. + var tempUid = Spawn(protoName); + SpriteSystem.LayerSetRsiState(uid, 0, SpriteSystem.LayerGetRsiState(tempUid, 0)); + QueueDel(tempUid); + } +} diff --git a/Content.Client/Doors/DoorSystem.cs b/Content.Client/Doors/DoorSystem.cs index cb17cfaf21..3d9a3e2a9a 100644 --- a/Content.Client/Doors/DoorSystem.cs +++ b/Content.Client/Doors/DoorSystem.cs @@ -1,16 +1,17 @@ using Content.Shared.Doors.Components; using Content.Shared.Doors.Systems; +using Content.Shared.SprayPainter.Prototypes; using Robust.Client.Animations; using Robust.Client.GameObjects; -using Robust.Client.ResourceManagement; -using Robust.Shared.Serialization.TypeSerializers.Implementations; +using Robust.Shared.Prototypes; namespace Content.Client.Doors; public sealed class DoorSystem : SharedDoorSystem { [Dependency] private readonly AnimationPlayerSystem _animationSystem = default!; - [Dependency] private readonly IResourceCache _resourceCache = default!; + [Dependency] private readonly IComponentFactory _componentFactory = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly SpriteSystem _sprite = default!; public override void Initialize() @@ -85,8 +86,8 @@ public sealed class DoorSystem : SharedDoorSystem if (!AppearanceSystem.TryGetData(entity, DoorVisuals.State, out var state, args.Component)) state = DoorState.Closed; - if (AppearanceSystem.TryGetData(entity, DoorVisuals.BaseRSI, out var baseRsi, args.Component)) - UpdateSpriteLayers((entity.Owner, args.Sprite), baseRsi); + if (AppearanceSystem.TryGetData(entity, PaintableVisuals.Prototype, out var prototype, args.Component)) + UpdateSpriteLayers((entity.Owner, args.Sprite), prototype); if (_animationSystem.HasRunningAnimation(entity, DoorComponent.AnimationKey)) _animationSystem.Stop(entity.Owner, DoorComponent.AnimationKey); @@ -139,14 +140,14 @@ public sealed class DoorSystem : SharedDoorSystem } } - private void UpdateSpriteLayers(Entity sprite, string baseRsi) + private void UpdateSpriteLayers(Entity sprite, string targetProto) { - if (!_resourceCache.TryGetResource(SpriteSpecifierSerializer.TextureRoot / baseRsi, out var res)) - { - Log.Error("Unable to load RSI '{0}'. Trace:\n{1}", baseRsi, Environment.StackTrace); + if (!_prototypeManager.TryIndex(targetProto, out var target)) + return; + + if (!target.TryGetComponent(out SpriteComponent? targetSprite, _componentFactory)) return; - } - _sprite.SetBaseRsi(sprite.AsNullable(), res.RSI); + _sprite.SetBaseRsi(sprite.AsNullable(), targetSprite.BaseRSI); } } diff --git a/Content.Client/SprayPainter/SprayPainterSystem.cs b/Content.Client/SprayPainter/SprayPainterSystem.cs index 6a1d27e98b..8f7d7f0362 100644 --- a/Content.Client/SprayPainter/SprayPainterSystem.cs +++ b/Content.Client/SprayPainter/SprayPainterSystem.cs @@ -1,56 +1,129 @@ +using System.Linq; +using Content.Client.Items; +using Content.Client.Message; +using Content.Client.Stylesheets; +using Content.Shared.Decals; using Content.Shared.SprayPainter; -using Robust.Client.Graphics; -using Robust.Client.ResourceManagement; -using Robust.Shared.Serialization.TypeSerializers.Implementations; +using Content.Shared.SprayPainter.Components; +using Content.Shared.SprayPainter.Prototypes; +using Robust.Client.GameObjects; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; using Robust.Shared.Utility; -using System.Linq; -using Robust.Shared.Graphics; namespace Content.Client.SprayPainter; +/// +/// Client-side spray painter functions. Caches information for spray painter windows and updates the UI to reflect component state. +/// public sealed class SprayPainterSystem : SharedSprayPainterSystem { - [Dependency] private readonly IResourceCache _resourceCache = default!; + [Dependency] private readonly UserInterfaceSystem _ui = default!; + + public List Decals = []; + public Dictionary> PaintableGroupsByCategory = new(); + public Dictionary> PaintableStylesByGroup = new(); + + public override void Initialize() + { + base.Initialize(); - public List Entries { get; private set; } = new(); + Subs.ItemStatus(ent => new StatusControl(ent)); + SubscribeLocalEvent(OnStateUpdate); + SubscribeLocalEvent(OnPrototypesReloaded); - protected override void CacheStyles() + CachePrototypes(); + } + + private void OnStateUpdate(Entity ent, ref AfterAutoHandleStateEvent args) { - base.CacheStyles(); + UpdateUi(ent); + } - Entries.Clear(); - foreach (var style in Styles) + protected override void UpdateUi(Entity ent) + { + if (_ui.TryGetOpenUi(ent.Owner, SprayPainterUiKey.Key, out var bui)) + bui.Update(); + } + + private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) + { + if (!args.WasModified() || !args.WasModified() || !args.WasModified()) + return; + + CachePrototypes(); + } + + private void CachePrototypes() + { + PaintableGroupsByCategory.Clear(); + PaintableStylesByGroup.Clear(); + foreach (var category in Proto.EnumeratePrototypes().OrderBy(x => x.ID)) { - var name = style.Name; - string? iconPath = Groups - .FindAll(x => x.StylePaths.ContainsKey(name))? - .MaxBy(x => x.IconPriority)?.StylePaths[name]; - if (iconPath == null) + var groupList = new List(); + foreach (var groupId in category.Groups) { - Entries.Add(new SprayPainterEntry(name, null)); - continue; + if (!Proto.TryIndex(groupId, out var group)) + continue; + + groupList.Add(groupId); + PaintableStylesByGroup[groupId] = group.Styles; } - RSIResource doorRsi = _resourceCache.GetResource(SpriteSpecifierSerializer.TextureRoot / new ResPath(iconPath)); - if (!doorRsi.RSI.TryGetState("closed", out var icon)) - { - Entries.Add(new SprayPainterEntry(name, null)); + if (groupList.Count > 0) + PaintableGroupsByCategory[category.ID] = groupList; + } + + Decals.Clear(); + foreach (var decalPrototype in Proto.EnumeratePrototypes().OrderBy(x => x.ID)) + { + if (!decalPrototype.Tags.Contains("station") + && !decalPrototype.Tags.Contains("markings") + || decalPrototype.Tags.Contains("dirty")) continue; - } - Entries.Add(new SprayPainterEntry(name, icon.Frame0)); + Decals.Add(new SprayPainterDecalEntry(decalPrototype.ID, decalPrototype.Sprite)); } } -} -public sealed class SprayPainterEntry -{ - public string Name; - public Texture? Icon; - - public SprayPainterEntry(string name, Texture? icon) + private sealed class StatusControl : Control { - Name = name; - Icon = icon; + private readonly RichTextLabel _label; + private readonly Entity _entity; + private DecalPaintMode? _lastPaintingDecals = null; + + public StatusControl(Entity ent) + { + _entity = ent; + _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } }; + AddChild(_label); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + if (_entity.Comp.DecalMode == _lastPaintingDecals) + return; + + _lastPaintingDecals = _entity.Comp.DecalMode; + + string modeLocString = _entity.Comp.DecalMode switch + { + DecalPaintMode.Add => "spray-painter-item-status-add", + DecalPaintMode.Remove => "spray-painter-item-status-remove", + _ => "spray-painter-item-status-off" + }; + + _label.SetMarkupPermissive(Robust.Shared.Localization.Loc.GetString("spray-painter-item-status-label", + ("mode", Robust.Shared.Localization.Loc.GetString(modeLocString)))); + } } } + +/// +/// A spray paintable decal, mapped by ID. +/// +public sealed record SprayPainterDecalEntry(string Name, SpriteSpecifier Sprite); diff --git a/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs b/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs index 7d6a6cf2a5..701ec80bac 100644 --- a/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs +++ b/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs @@ -1,42 +1,96 @@ +using Content.Shared.Decals; using Content.Shared.SprayPainter; using Content.Shared.SprayPainter.Components; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Shared.Prototypes; namespace Content.Client.SprayPainter.UI; -public sealed class SprayPainterBoundUserInterface : BoundUserInterface +/// +/// A BUI for a spray painter. Allows selecting pipe colours, decals, and paintable object types sorted by category. +/// +public sealed class SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey) { [ViewVariables] private SprayPainterWindow? _window; - public SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + protected override void Open() { + base.Open(); + + if (_window == null) + { + _window = this.CreateWindow(); + + _window.OnSpritePicked += OnSpritePicked; + _window.OnSetPipeColor += OnSetPipeColor; + _window.OnTabChanged += OnTabChanged; + _window.OnDecalChanged += OnDecalChanged; + _window.OnDecalColorChanged += OnDecalColorChanged; + _window.OnDecalAngleChanged += OnDecalAngleChanged; + _window.OnDecalSnapChanged += OnDecalSnapChanged; + } + + var sprayPainter = EntMan.System(); + _window.PopulateCategories(sprayPainter.PaintableStylesByGroup, sprayPainter.PaintableGroupsByCategory, sprayPainter.Decals); + Update(); + + if (EntMan.TryGetComponent(Owner, out SprayPainterComponent? sprayPainterComp)) + _window.SetSelectedTab(sprayPainterComp.SelectedTab); } - protected override void Open() + public override void Update() { - base.Open(); + if (_window == null) + return; - _window = this.CreateWindow(); + if (!EntMan.TryGetComponent(Owner, out SprayPainterComponent? sprayPainter)) + return; - _window.OnSpritePicked = OnSpritePicked; - _window.OnColorPicked = OnColorPicked; + _window.PopulateColors(sprayPainter.ColorPalette); + if (sprayPainter.PickedColor != null) + _window.SelectColor(sprayPainter.PickedColor); + _window.SetSelectedStyles(sprayPainter.StylesByGroup); + _window.SetSelectedDecal(sprayPainter.SelectedDecal); + _window.SetDecalAngle(sprayPainter.SelectedDecalAngle); + _window.SetDecalColor(sprayPainter.SelectedDecalColor); + _window.SetDecalSnap(sprayPainter.SnapDecals); + } - if (EntMan.TryGetComponent(Owner, out SprayPainterComponent? comp)) - { - _window.Populate(EntMan.System().Entries, comp.Index, comp.PickedColor, comp.ColorPalette); - } + private void OnDecalSnapChanged(bool snap) + { + SendPredictedMessage(new SprayPainterSetDecalSnapMessage(snap)); + } + + private void OnDecalAngleChanged(int angle) + { + SendPredictedMessage(new SprayPainterSetDecalAngleMessage(angle)); + } + + private void OnDecalColorChanged(Color? color) + { + SendPredictedMessage(new SprayPainterSetDecalColorMessage(color)); + } + + private void OnDecalChanged(ProtoId protoId) + { + SendPredictedMessage(new SprayPainterSetDecalMessage(protoId)); + } + + private void OnTabChanged(int index, bool isSelectedTabWithDecals) + { + SendPredictedMessage(new SprayPainterTabChangedMessage(index, isSelectedTabWithDecals)); } - private void OnSpritePicked(ItemList.ItemListSelectedEventArgs args) + private void OnSpritePicked(string group, string style) { - SendMessage(new SprayPainterSpritePickedMessage(args.ItemIndex)); + SendPredictedMessage(new SprayPainterSetPaintableStyleMessage(group, style)); } - private void OnColorPicked(ItemList.ItemListSelectedEventArgs args) + private void OnSetPipeColor(ItemList.ItemListSelectedEventArgs args) { var key = _window?.IndexToColorKey(args.ItemIndex); - SendMessage(new SprayPainterColorPickedMessage(key)); + SendPredictedMessage(new SprayPainterSetPipeColorMessage(key)); } } diff --git a/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml new file mode 100644 index 0000000000..0d5c8e4f16 --- /dev/null +++ b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml @@ -0,0 +1,26 @@ + + +