From eec720b840433bd88f1cf466365800ea61429a96 Mon Sep 17 00:00:00 2001 From: Menshin Date: Fri, 6 Mar 2026 19:45:59 +0100 Subject: [PATCH 1/7] Added support for specifying a sprite for the placement overlay of a given prototype. --- .../Components/Renderable/SpriteComponent.cs | 1 + Robust.Client/Placement/PlacementManager.cs | 32 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs index d1dbd6587c6..f1755f1f28d 100644 --- a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs +++ b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs @@ -167,6 +167,7 @@ public RSI? BaseRSI } [DataField("sprite", readOnly: true)] private string? rsi; + [DataField("placementOverlaySprite", readOnly: true)] public SpriteSpecifier? placementOverlaySprite; [DataField("layers", readOnly: true)] private List layerDatums = new(); [DataField(readOnly: true)] private string? state; diff --git a/Robust.Client/Placement/PlacementManager.cs b/Robust.Client/Placement/PlacementManager.cs index 8f92a3b04d0..99a8c2ec04b 100644 --- a/Robust.Client/Placement/PlacementManager.cs +++ b/Robust.Client/Placement/PlacementManager.cs @@ -743,7 +743,18 @@ private void PreparePlacement(string templateName) CurrentPrototype = prototype; IsActive = true; - CurrentPlacementOverlayEntity = EntityManager.SpawnEntity(templateName, MapCoordinates.Nullspace); + //does prototype uses a specific placement overlay sprite? + if (prototype.TryGetComponent(out var prototypeSprite, IoCManager.Resolve()) + && prototypeSprite.placementOverlaySprite != null) + { + CurrentPlacementOverlayEntity = EntityManager.SpawnEntity(null, MapCoordinates.Nullspace); + SetPlacementOverlaySprite(prototypeSprite, Sprite.RsiStateLike(prototypeSprite.placementOverlaySprite)); + } + else + { + CurrentPlacementOverlayEntity = EntityManager.SpawnEntity(templateName, MapCoordinates.Nullspace); + } + EntityManager.RunMapInit( CurrentPlacementOverlayEntity.Value, EntityManager.GetComponent(CurrentPlacementOverlayEntity.Value)); @@ -860,6 +871,25 @@ private void RequestPlacement(EntityCoordinates coordinates) _networkManager.ClientSendMessage(message); } + private void SetPlacementOverlaySprite(SpriteComponent prototypeSprite, IRsiStateLike overlayTexture) + { + if (CurrentPlacementOverlayEntity == null) + return; + + if (overlayTexture is not RSI.State overlayRSI) + { + //Fallback + Sprite.AddTextureLayer(CurrentPlacementOverlayEntity.Value, overlayTexture.Default); + return; + } + + EntityManager.EnsureComponent(CurrentPlacementOverlayEntity.Value, out var overlaySprite); + Sprite.AddRsiLayer(CurrentPlacementOverlayEntity.Value, overlayRSI.StateId, overlayRSI.RSI); + + overlaySprite.NoRotation = prototypeSprite.NoRotation; + Sprite.SetScale(CurrentPlacementOverlayEntity.Value, prototypeSprite.Scale); + } + public enum PlacementTypes : byte { None = 0, From f99329bd8138a0afa52171fcb53c6765745a3cd4 Mon Sep 17 00:00:00 2001 From: Menshin Date: Sat, 7 Mar 2026 09:28:22 +0100 Subject: [PATCH 2/7] Remove unnecessary key name for placementOverlaySprite. --- .../GameObjects/Components/Renderable/SpriteComponent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs index f1755f1f28d..c4dcf658633 100644 --- a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs +++ b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs @@ -167,7 +167,7 @@ public RSI? BaseRSI } [DataField("sprite", readOnly: true)] private string? rsi; - [DataField("placementOverlaySprite", readOnly: true)] public SpriteSpecifier? placementOverlaySprite; + [DataField(readOnly: true)] public SpriteSpecifier? placementOverlaySprite; [DataField("layers", readOnly: true)] private List layerDatums = new(); [DataField(readOnly: true)] private string? state; From 31800c90c36026c85349cd6273817e2be308ea36 Mon Sep 17 00:00:00 2001 From: Menshin Date: Sat, 7 Mar 2026 16:07:09 +0100 Subject: [PATCH 3/7] Made PlacementOverlay its own component. --- .../Components/Renderable/SpriteComponent.cs | 1 - Robust.Client/Placement/PlacementManager.cs | 16 +++++++++------- .../Placement/PlacementOverlayComponent.cs | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 Robust.Shared/Placement/PlacementOverlayComponent.cs diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs index c4dcf658633..d1dbd6587c6 100644 --- a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs +++ b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs @@ -167,7 +167,6 @@ public RSI? BaseRSI } [DataField("sprite", readOnly: true)] private string? rsi; - [DataField(readOnly: true)] public SpriteSpecifier? placementOverlaySprite; [DataField("layers", readOnly: true)] private List layerDatums = new(); [DataField(readOnly: true)] private string? state; diff --git a/Robust.Client/Placement/PlacementManager.cs b/Robust.Client/Placement/PlacementManager.cs index 99a8c2ec04b..2f83fc46f3f 100644 --- a/Robust.Client/Placement/PlacementManager.cs +++ b/Robust.Client/Placement/PlacementManager.cs @@ -20,6 +20,7 @@ using Robust.Shared.Timing; using Robust.Shared.Utility; using Robust.Shared.Log; +using Robust.Shared.Placement; using Direction = Robust.Shared.Maths.Direction; using Robust.Shared.Map.Components; using System.Linq; @@ -743,12 +744,10 @@ private void PreparePlacement(string templateName) CurrentPrototype = prototype; IsActive = true; - //does prototype uses a specific placement overlay sprite? - if (prototype.TryGetComponent(out var prototypeSprite, IoCManager.Resolve()) - && prototypeSprite.placementOverlaySprite != null) + if (prototype.TryGetComponent(out var placementOverlay, IoCManager.Resolve())) { CurrentPlacementOverlayEntity = EntityManager.SpawnEntity(null, MapCoordinates.Nullspace); - SetPlacementOverlaySprite(prototypeSprite, Sprite.RsiStateLike(prototypeSprite.placementOverlaySprite)); + SetPlacementOverlaySprite(prototype, Sprite.RsiStateLike(placementOverlay.sprite)); } else { @@ -871,7 +870,7 @@ private void RequestPlacement(EntityCoordinates coordinates) _networkManager.ClientSendMessage(message); } - private void SetPlacementOverlaySprite(SpriteComponent prototypeSprite, IRsiStateLike overlayTexture) + private void SetPlacementOverlaySprite(EntityPrototype prototype, IRsiStateLike overlayTexture) { if (CurrentPlacementOverlayEntity == null) return; @@ -886,8 +885,11 @@ private void SetPlacementOverlaySprite(SpriteComponent prototypeSprite, IRsiStat EntityManager.EnsureComponent(CurrentPlacementOverlayEntity.Value, out var overlaySprite); Sprite.AddRsiLayer(CurrentPlacementOverlayEntity.Value, overlayRSI.StateId, overlayRSI.RSI); - overlaySprite.NoRotation = prototypeSprite.NoRotation; - Sprite.SetScale(CurrentPlacementOverlayEntity.Value, prototypeSprite.Scale); + if(prototype.TryGetComponent(out var prototypeSprite, IoCManager.Resolve())) + { + overlaySprite.NoRotation = prototypeSprite.NoRotation; + Sprite.SetScale(CurrentPlacementOverlayEntity.Value, prototypeSprite.Scale); + } } public enum PlacementTypes : byte diff --git a/Robust.Shared/Placement/PlacementOverlayComponent.cs b/Robust.Shared/Placement/PlacementOverlayComponent.cs new file mode 100644 index 00000000000..d81fac9a2dc --- /dev/null +++ b/Robust.Shared/Placement/PlacementOverlayComponent.cs @@ -0,0 +1,14 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Utility; + +namespace Robust.Shared.Placement +{ + [RegisterComponent] + public sealed partial class PlacementOverlayComponent : Component + { /// + /// A SpriteSpecifier that will be used while in placement mode for this prototype + /// + [DataField(readOnly: true, required: true)] public SpriteSpecifier sprite; + } +} From 1cb46fe2729a0b6b0fcb971dcaf48fd1e1f83003 Mon Sep 17 00:00:00 2001 From: Menshin Date: Sat, 7 Mar 2026 17:41:18 +0100 Subject: [PATCH 4/7] Formatting --- Robust.Client/Placement/PlacementManager.cs | 2 +- .../Placement/PlacementOverlayComponent.cs | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Robust.Client/Placement/PlacementManager.cs b/Robust.Client/Placement/PlacementManager.cs index 2f83fc46f3f..769f90878ee 100644 --- a/Robust.Client/Placement/PlacementManager.cs +++ b/Robust.Client/Placement/PlacementManager.cs @@ -747,7 +747,7 @@ private void PreparePlacement(string templateName) if (prototype.TryGetComponent(out var placementOverlay, IoCManager.Resolve())) { CurrentPlacementOverlayEntity = EntityManager.SpawnEntity(null, MapCoordinates.Nullspace); - SetPlacementOverlaySprite(prototype, Sprite.RsiStateLike(placementOverlay.sprite)); + SetPlacementOverlaySprite(prototype, Sprite.RsiStateLike(placementOverlay.Sprite)); } else { diff --git a/Robust.Shared/Placement/PlacementOverlayComponent.cs b/Robust.Shared/Placement/PlacementOverlayComponent.cs index d81fac9a2dc..211568d2bd4 100644 --- a/Robust.Shared/Placement/PlacementOverlayComponent.cs +++ b/Robust.Shared/Placement/PlacementOverlayComponent.cs @@ -2,13 +2,16 @@ using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Utility; -namespace Robust.Shared.Placement +namespace Robust.Shared.Placement; + +/// +/// That component allows modifying the overlay used by the placement system. +/// +[RegisterComponent] +public sealed partial class PlacementOverlayComponent : Component { - [RegisterComponent] - public sealed partial class PlacementOverlayComponent : Component - { /// - /// A SpriteSpecifier that will be used while in placement mode for this prototype - /// - [DataField(readOnly: true, required: true)] public SpriteSpecifier sprite; - } + /// + /// Specific sprite for the placement overlay. + /// + [DataField(readOnly: true, required: true)] public SpriteSpecifier Sprite; } From 1de366f3b0f8c7cd504abbc7b1835b74bdd62865 Mon Sep 17 00:00:00 2001 From: Menshin Date: Sun, 8 Mar 2026 10:01:37 +0100 Subject: [PATCH 5/7] Added and used a component factory dependency. --- Robust.Client/Placement/PlacementManager.cs | 27 +++++++++++---------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/Robust.Client/Placement/PlacementManager.cs b/Robust.Client/Placement/PlacementManager.cs index 769f90878ee..939648ee29c 100644 --- a/Robust.Client/Placement/PlacementManager.cs +++ b/Robust.Client/Placement/PlacementManager.cs @@ -29,21 +29,22 @@ namespace Robust.Client.Placement { public sealed partial class PlacementManager : IPlacementManager, IDisposable, IEntityEventSubscriber { - [Dependency] private readonly ILogManager _logManager = default!; + [Dependency] private readonly IBaseClient _baseClient = default!; [Dependency] private readonly IClientNetManager _networkManager = default!; - [Dependency] internal readonly IPlayerManager PlayerManager = default!; - [Dependency] internal readonly IResourceCache ResourceCache = default!; - [Dependency] private readonly IReflectionManager _reflectionManager = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IGameTiming _time = default!; + [Dependency] internal readonly IClyde Clyde = default!; + [Dependency] private readonly IComponentFactory _componentFactory = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly IEyeManager _eyeManager = default!; + [Dependency] private readonly IGameTiming _time = default!; [Dependency] internal readonly IInputManager InputManager = default!; - [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; - [Dependency] private readonly IEntityManager _entityManager = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IBaseClient _baseClient = default!; + [Dependency] private readonly ILogManager _logManager = default!; + [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IOverlayManager _overlayManager = default!; - [Dependency] internal readonly IClyde Clyde = default!; + [Dependency] internal readonly IPlayerManager PlayerManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IReflectionManager _reflectionManager = default!; + [Dependency] internal readonly IResourceCache ResourceCache = default!; private static readonly ProtoId UnshadedShader = "unshaded"; @@ -744,7 +745,7 @@ private void PreparePlacement(string templateName) CurrentPrototype = prototype; IsActive = true; - if (prototype.TryGetComponent(out var placementOverlay, IoCManager.Resolve())) + if (prototype.TryGetComponent(out var placementOverlay, _componentFactory)) { CurrentPlacementOverlayEntity = EntityManager.SpawnEntity(null, MapCoordinates.Nullspace); SetPlacementOverlaySprite(prototype, Sprite.RsiStateLike(placementOverlay.Sprite)); @@ -885,7 +886,7 @@ private void SetPlacementOverlaySprite(EntityPrototype prototype, IRsiStateLike EntityManager.EnsureComponent(CurrentPlacementOverlayEntity.Value, out var overlaySprite); Sprite.AddRsiLayer(CurrentPlacementOverlayEntity.Value, overlayRSI.StateId, overlayRSI.RSI); - if(prototype.TryGetComponent(out var prototypeSprite, IoCManager.Resolve())) + if(prototype.TryGetComponent(out var prototypeSprite, _componentFactory)) { overlaySprite.NoRotation = prototypeSprite.NoRotation; Sprite.SetScale(CurrentPlacementOverlayEntity.Value, prototypeSprite.Scale); From ba57e6743d58a60bdcdd7b66fba281cfbbb989b6 Mon Sep 17 00:00:00 2001 From: Menshin Date: Wed, 11 Mar 2026 04:33:37 +0100 Subject: [PATCH 6/7] Decoupled the overlay placement sprite NoRotation and Scale from the prototype sprite. --- Robust.Client/Placement/PlacementManager.cs | 29 +++++++++---------- .../Placement/PlacementOverlayComponent.cs | 11 +++++++ 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/Robust.Client/Placement/PlacementManager.cs b/Robust.Client/Placement/PlacementManager.cs index 939648ee29c..a01a5ea6c01 100644 --- a/Robust.Client/Placement/PlacementManager.cs +++ b/Robust.Client/Placement/PlacementManager.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Numerics; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Input; @@ -11,19 +8,22 @@ using Robust.Shared.Input; using Robust.Shared.Input.Binding; using Robust.Shared.IoC; +using Robust.Shared.Log; using Robust.Shared.Map; +using Robust.Shared.Map.Components; using Robust.Shared.Maths; using Robust.Shared.Network; using Robust.Shared.Network.Messages; +using Robust.Shared.Placement; using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Timing; using Robust.Shared.Utility; -using Robust.Shared.Log; -using Robust.Shared.Placement; -using Direction = Robust.Shared.Maths.Direction; -using Robust.Shared.Map.Components; +using System; +using System.Collections.Generic; using System.Linq; +using System.Numerics; +using Direction = Robust.Shared.Maths.Direction; namespace Robust.Client.Placement { @@ -748,7 +748,7 @@ private void PreparePlacement(string templateName) if (prototype.TryGetComponent(out var placementOverlay, _componentFactory)) { CurrentPlacementOverlayEntity = EntityManager.SpawnEntity(null, MapCoordinates.Nullspace); - SetPlacementOverlaySprite(prototype, Sprite.RsiStateLike(placementOverlay.Sprite)); + SetPlacementOverlaySprite(placementOverlay); } else { @@ -871,26 +871,23 @@ private void RequestPlacement(EntityCoordinates coordinates) _networkManager.ClientSendMessage(message); } - private void SetPlacementOverlaySprite(EntityPrototype prototype, IRsiStateLike overlayTexture) + private void SetPlacementOverlaySprite(PlacementOverlayComponent placementOverlay) { if (CurrentPlacementOverlayEntity == null) return; - if (overlayTexture is not RSI.State overlayRSI) + if (Sprite.RsiStateLike(placementOverlay.Sprite) is not RSI.State overlayRSI) { //Fallback - Sprite.AddTextureLayer(CurrentPlacementOverlayEntity.Value, overlayTexture.Default); + Sprite.AddTextureLayer(CurrentPlacementOverlayEntity.Value, Sprite.RsiStateLike(placementOverlay.Sprite).Default); return; } EntityManager.EnsureComponent(CurrentPlacementOverlayEntity.Value, out var overlaySprite); Sprite.AddRsiLayer(CurrentPlacementOverlayEntity.Value, overlayRSI.StateId, overlayRSI.RSI); - if(prototype.TryGetComponent(out var prototypeSprite, _componentFactory)) - { - overlaySprite.NoRotation = prototypeSprite.NoRotation; - Sprite.SetScale(CurrentPlacementOverlayEntity.Value, prototypeSprite.Scale); - } + overlaySprite.NoRotation = placementOverlay.NoRotation; + Sprite.SetScale(CurrentPlacementOverlayEntity.Value, placementOverlay.Scale); } public enum PlacementTypes : byte diff --git a/Robust.Shared/Placement/PlacementOverlayComponent.cs b/Robust.Shared/Placement/PlacementOverlayComponent.cs index 211568d2bd4..86716906297 100644 --- a/Robust.Shared/Placement/PlacementOverlayComponent.cs +++ b/Robust.Shared/Placement/PlacementOverlayComponent.cs @@ -1,6 +1,7 @@ using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Utility; +using System.Numerics; namespace Robust.Shared.Placement; @@ -14,4 +15,14 @@ public sealed partial class PlacementOverlayComponent : Component /// Specific sprite for the placement overlay. /// [DataField(readOnly: true, required: true)] public SpriteSpecifier Sprite; + + /// + /// Whether the placement overlay should rotate (default: false). + /// + [DataField(readOnly: true)] public bool NoRotation; + + /// + /// Scaling vector for the placement overlay (default: Vector2.One). + /// + [DataField(readOnly: true)] public Vector2 Scale = Vector2.One; } From 898e564ff6828f812f21792fe4c1f9fcbc3fb4e2 Mon Sep 17 00:00:00 2001 From: Menshin Date: Sun, 22 Mar 2026 19:27:01 +0100 Subject: [PATCH 7/7] CI Poke