Skip to content

Commit 6fa8ffd

Browse files
marc-pelletierArtisticRoombaPartmediachromiumboyBoaz1111
authored
Pipe layers and more (funky-station#1328)
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Co-authored-by: Kevin Zheng <kevinz5000@gmail.com> Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com> Co-authored-by: Boaz1111 <boazwand@gmail.com> Co-authored-by: Steve <marlumpy@gmail.com> Co-authored-by: funkystationbot <funky@funkystation.org>
1 parent 10474fa commit 6fa8ffd

154 files changed

Lines changed: 6451 additions & 641 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// SPDX-FileCopyrightText: 2025 ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
2+
// SPDX-FileCopyrightText: 2025 chromiumboy <50505512+chromiumboy@users.noreply.github.com>
3+
//
4+
// SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
using Content.Client.Construction;
7+
using Content.Shared.Atmos.Components;
8+
using Content.Shared.Atmos.EntitySystems;
9+
using Content.Shared.Construction.Prototypes;
10+
using Robust.Client.GameObjects;
11+
using Robust.Client.Graphics;
12+
using Robust.Client.Placement;
13+
using Robust.Client.Placement.Modes;
14+
using Robust.Client.Utility;
15+
using Robust.Shared.Enums;
16+
using Robust.Shared.Map;
17+
using Robust.Shared.Map.Components;
18+
using Robust.Shared.Prototypes;
19+
using Robust.Shared.Utility;
20+
using System.Numerics;
21+
using static Robust.Client.Placement.PlacementManager;
22+
23+
namespace Content.Client.Atmos;
24+
25+
/// <summary>
26+
/// Allows users to place atmos pipes on different layers depending on how the mouse cursor is positioned within a grid tile.
27+
/// </summary>
28+
/// <remarks>
29+
/// This placement mode is not on the engine because it is content specific.
30+
/// </remarks>
31+
public sealed class AlignAtmosPipeLayers : SnapgridCenter
32+
{
33+
[Dependency] private readonly IEntityManager _entityManager = default!;
34+
[Dependency] private readonly IPrototypeManager _protoManager = default!;
35+
[Dependency] private readonly IMapManager _mapManager = default!;
36+
[Dependency] private readonly IEyeManager _eyeManager = default!;
37+
38+
private readonly SharedMapSystem _mapSystem;
39+
private readonly SharedTransformSystem _transformSystem;
40+
private readonly SharedAtmosPipeLayersSystem _pipeLayersSystem;
41+
private readonly SpriteSystem _spriteSystem;
42+
43+
private const float SearchBoxSize = 2f;
44+
private EntityCoordinates _unalignedMouseCoords = default;
45+
private const float MouseDeadzoneRadius = 0.25f;
46+
47+
private Color _guideColor = new Color(0, 0, 0.5785f);
48+
private const float GuideRadius = 0.1f;
49+
private const float GuideOffset = 0.21875f;
50+
51+
public AlignAtmosPipeLayers(PlacementManager pMan) : base(pMan)
52+
{
53+
IoCManager.InjectDependencies(this);
54+
55+
_mapSystem = _entityManager.System<SharedMapSystem>();
56+
_transformSystem = _entityManager.System<SharedTransformSystem>();
57+
_pipeLayersSystem = _entityManager.System<SharedAtmosPipeLayersSystem>();
58+
_spriteSystem = _entityManager.System<SpriteSystem>();
59+
}
60+
61+
/// <inheritdoc/>
62+
public override void Render(in OverlayDrawArgs args)
63+
{
64+
var gridUid = _entityManager.System<SharedTransformSystem>().GetGrid(MouseCoords);
65+
66+
if (gridUid == null || Grid == null)
67+
return;
68+
69+
// Draw guide circles for each pipe layer if we are not in line/grid placing mode
70+
if (pManager.PlacementType == PlacementTypes.None)
71+
{
72+
var gridRotation = _transformSystem.GetWorldRotation(gridUid.Value);
73+
var worldPosition = _mapSystem.LocalToWorld(gridUid.Value, Grid, MouseCoords.Position);
74+
var direction = (_eyeManager.CurrentEye.Rotation + gridRotation + Math.PI / 2).GetCardinalDir();
75+
var multi = (direction == Direction.North || direction == Direction.South) ? -1f : 1f;
76+
77+
args.WorldHandle.DrawCircle(worldPosition, GuideRadius, _guideColor);
78+
args.WorldHandle.DrawCircle(worldPosition + gridRotation.RotateVec(new Vector2(multi * GuideOffset, GuideOffset)), GuideRadius, _guideColor);
79+
args.WorldHandle.DrawCircle(worldPosition - gridRotation.RotateVec(new Vector2(multi * GuideOffset, GuideOffset)), GuideRadius, _guideColor);
80+
}
81+
82+
base.Render(args);
83+
}
84+
85+
/// <inheritdoc/>
86+
public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
87+
{
88+
_unalignedMouseCoords = ScreenToCursorGrid(mouseScreen);
89+
base.AlignPlacementMode(mouseScreen);
90+
91+
// Exit early if we are in line/grid placing mode
92+
if (pManager.PlacementType != PlacementTypes.None)
93+
return;
94+
95+
MouseCoords = _unalignedMouseCoords.AlignWithClosestGridTile(SearchBoxSize, _entityManager, _mapManager);
96+
97+
var gridId = _transformSystem.GetGrid(MouseCoords);
98+
99+
if (!_entityManager.TryGetComponent<MapGridComponent>(gridId, out var mapGrid))
100+
return;
101+
102+
var gridRotation = _transformSystem.GetWorldRotation(gridId.Value);
103+
CurrentTile = _mapSystem.GetTileRef(gridId.Value, mapGrid, MouseCoords);
104+
105+
float tileSize = mapGrid.TileSize;
106+
GridDistancing = tileSize;
107+
108+
MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2 + pManager.PlacementOffset.X,
109+
CurrentTile.Y + tileSize / 2 + pManager.PlacementOffset.Y));
110+
111+
// Calculate the position of the mouse cursor with respect to the center of the tile to determine which layer to use
112+
var mouseCoordsDiff = _unalignedMouseCoords.Position - MouseCoords.Position;
113+
var layer = AtmosPipeLayer.Primary;
114+
115+
if (mouseCoordsDiff.Length() > MouseDeadzoneRadius)
116+
{
117+
// Determine the direction of the mouse is relative to the center of the tile, adjusting for the player eye and grid rotation
118+
var direction = (new Angle(mouseCoordsDiff) + _eyeManager.CurrentEye.Rotation + gridRotation + Math.PI / 2).GetCardinalDir();
119+
layer = (direction == Direction.North || direction == Direction.East) ? AtmosPipeLayer.Secondary : AtmosPipeLayer.Tertiary;
120+
}
121+
122+
// Update the construction menu placer
123+
if (pManager.Hijack != null)
124+
UpdateHijackedPlacer(layer, mouseScreen);
125+
126+
// Otherwise update the debug placer
127+
else
128+
UpdatePlacer(layer);
129+
}
130+
131+
private void UpdateHijackedPlacer(AtmosPipeLayer layer, ScreenCoordinates mouseScreen)
132+
{
133+
// Try to get alternative prototypes from the construction prototype
134+
var constructionSystem = (pManager.Hijack as ConstructionPlacementHijack)?.CurrentConstructionSystem;
135+
var altPrototypes = (pManager.Hijack as ConstructionPlacementHijack)?.CurrentPrototype?.AlternativePrototypes;
136+
137+
if (constructionSystem == null || altPrototypes == null || (int)layer >= altPrototypes.Length)
138+
return;
139+
140+
var newProtoId = altPrototypes[(int)layer];
141+
142+
if (!_protoManager.TryIndex(newProtoId, out var newProto))
143+
return;
144+
145+
if (newProto.Type != ConstructionType.Structure)
146+
{
147+
pManager.Clear();
148+
return;
149+
}
150+
151+
if (newProto.ID == (pManager.Hijack as ConstructionPlacementHijack)?.CurrentPrototype?.ID)
152+
return;
153+
154+
// Start placing
155+
pManager.BeginPlacing(new PlacementInformation()
156+
{
157+
IsTile = false,
158+
PlacementOption = newProto.PlacementMode,
159+
}, new ConstructionPlacementHijack(constructionSystem, newProto));
160+
161+
if (pManager.CurrentMode is AlignAtmosPipeLayers { } newMode)
162+
newMode.RefreshGrid(mouseScreen);
163+
164+
// Update construction guide
165+
constructionSystem.GetGuide(newProto);
166+
}
167+
168+
private void UpdatePlacer(AtmosPipeLayer layer)
169+
{
170+
// Try to get alternative prototypes from the entity atmos pipe layer component
171+
if (pManager.CurrentPermission?.EntityType == null)
172+
return;
173+
174+
if (!_protoManager.TryIndex<EntityPrototype>(pManager.CurrentPermission.EntityType, out var currentProto))
175+
return;
176+
177+
if (!currentProto.TryGetComponent<AtmosPipeLayersComponent>(out var atmosPipeLayers, _entityManager.ComponentFactory))
178+
return;
179+
180+
if (!_pipeLayersSystem.TryGetAlternativePrototype(atmosPipeLayers, layer, out var newProtoId))
181+
return;
182+
183+
if (_protoManager.TryIndex<EntityPrototype>(newProtoId, out var newProto))
184+
{
185+
// Update the placed prototype
186+
pManager.CurrentPermission.EntityType = newProtoId;
187+
188+
// Update the appearance of the ghost sprite
189+
if (newProto.TryGetComponent<SpriteComponent>(out var sprite, _entityManager.ComponentFactory))
190+
{
191+
var textures = new List<IDirectionalTextureProvider>();
192+
193+
foreach (var spriteLayer in sprite.AllLayers)
194+
{
195+
if (spriteLayer.ActualRsi?.Path != null && spriteLayer.RsiState.Name != null)
196+
textures.Add(_spriteSystem.RsiStateLike(new SpriteSpecifier.Rsi(spriteLayer.ActualRsi.Path, spriteLayer.RsiState.Name)));
197+
}
198+
199+
pManager.CurrentTextures = textures;
200+
}
201+
}
202+
}
203+
204+
private void RefreshGrid(ScreenCoordinates mouseScreen)
205+
{
206+
base.AlignPlacementMode(mouseScreen);
207+
}
208+
}

Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// SPDX-FileCopyrightText: 2024 MilenVolf <63782763+MilenVolf@users.noreply.github.com>
22
// SPDX-FileCopyrightText: 2024 Tadeo <td12233a@gmail.com>
3+
// SPDX-FileCopyrightText: 2025 ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
4+
// SPDX-FileCopyrightText: 2025 chromiumboy <50505512+chromiumboy@users.noreply.github.com>
35
// SPDX-FileCopyrightText: 2025 taydeo <td12233a@gmail.com>
46
//
57
// SPDX-License-Identifier: MIT
@@ -23,6 +25,10 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
2325
public int? FocusNetId = null;
2426

2527
private const int ChunkSize = 4;
28+
private const float ScaleModifier = 4f;
29+
30+
private readonly float[] _layerFraction = { 0.5f, 0.75f, 0.25f };
31+
private const float LineThickness = 0.05f;
2632

2733
private readonly Color _basePipeNetColor = Color.LightGray;
2834
private readonly Color _unfocusedPipeNetColor = Color.DimGray;
@@ -101,23 +107,23 @@ private void DrawPipeNetwork(DrawingHandleScreen handle, List<AtmosMonitoringCon
101107
foreach (var chunkedLine in atmosPipeNetwork)
102108
{
103109
var leftTop = ScalePosition(new Vector2
104-
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
105-
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
110+
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - LineThickness,
111+
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - LineThickness)
106112
- offset);
107113

108114
var rightTop = ScalePosition(new Vector2
109-
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
110-
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
115+
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + LineThickness,
116+
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - LineThickness)
111117
- offset);
112118

113119
var leftBottom = ScalePosition(new Vector2
114-
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
115-
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
120+
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - LineThickness,
121+
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + LineThickness)
116122
- offset);
117123

118124
var rightBottom = ScalePosition(new Vector2
119-
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
120-
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
125+
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + LineThickness,
126+
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + LineThickness)
121127
- offset);
122128

123129
if (!pipeVertexUVs.TryGetValue(chunkedLine.Color, out var pipeVertexUV))
@@ -148,7 +154,7 @@ private List<AtmosMonitoringConsoleLine> GetDecodedAtmosPipeChunks(Dictionary<Ve
148154
if (chunks == null || grid == null)
149155
return decodedOutput;
150156

151-
// Clear stale look up table values
157+
// Clear stale look up table values
152158
_horizLines.Clear();
153159
_horizLinesReversed.Clear();
154160
_vertLines.Clear();
@@ -164,7 +170,7 @@ private List<AtmosMonitoringConsoleLine> GetDecodedAtmosPipeChunks(Dictionary<Ve
164170
{
165171
var list = new List<AtmosMonitoringConsoleLine>();
166172

167-
foreach (var ((netId, hexColor), atmosPipeData) in chunk.AtmosPipeData)
173+
foreach (var ((netId, layer, hexColor), atmosPipeData) in chunk.AtmosPipeData)
168174
{
169175
// Determine the correct coloration for the pipe
170176
var color = Color.FromHex(hexColor) * _basePipeNetColor;
@@ -197,6 +203,9 @@ private List<AtmosMonitoringConsoleLine> GetDecodedAtmosPipeChunks(Dictionary<Ve
197203
_vertLinesReversed[color] = vertLinesReversed;
198204
}
199205

206+
var layerFraction = _layerFraction[(int)layer];
207+
var origin = new Vector2(grid.TileSize * layerFraction, -grid.TileSize * layerFraction);
208+
200209
// Loop over the chunk
201210
for (var tileIdx = 0; tileIdx < ChunkSize * ChunkSize; tileIdx++)
202211
{
@@ -214,21 +223,22 @@ private List<AtmosMonitoringConsoleLine> GetDecodedAtmosPipeChunks(Dictionary<Ve
214223

215224
// Calculate the draw point offsets
216225
var vertLineOrigin = (atmosPipeData & northMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
217-
new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 1f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
226+
new Vector2(grid.TileSize * layerFraction, -grid.TileSize * 1f) : origin;
218227

219228
var vertLineTerminus = (atmosPipeData & southMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
220-
new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
229+
new Vector2(grid.TileSize * layerFraction, -grid.TileSize * 0f) : origin;
221230

222231
var horizLineOrigin = (atmosPipeData & eastMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
223-
new Vector2(grid.TileSize * 1f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
232+
new Vector2(grid.TileSize * 1f, -grid.TileSize * layerFraction) : origin;
224233

225234
var horizLineTerminus = (atmosPipeData & westMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
226-
new Vector2(grid.TileSize * 0f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
235+
new Vector2(grid.TileSize * 0f, -grid.TileSize * layerFraction) : origin;
227236

228-
// Since we can have pipe lines that have a length of a half tile,
229-
// double the vectors and convert to vector2i so we can merge them
230-
AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + horizLineOrigin, 2), ConvertVector2ToVector2i(tile + horizLineTerminus, 2), horizLines, horizLinesReversed);
231-
AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + vertLineOrigin, 2), ConvertVector2ToVector2i(tile + vertLineTerminus, 2), vertLines, vertLinesReversed);
237+
// Scale up the vectors and convert to vector2i so we can merge them
238+
AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + horizLineOrigin, ScaleModifier),
239+
ConvertVector2ToVector2i(tile + horizLineTerminus, ScaleModifier), horizLines, horizLinesReversed);
240+
AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + vertLineOrigin, ScaleModifier),
241+
ConvertVector2ToVector2i(tile + vertLineTerminus, ScaleModifier), vertLines, vertLinesReversed);
232242
}
233243
}
234244
}
@@ -241,7 +251,7 @@ private List<AtmosMonitoringConsoleLine> GetDecodedAtmosPipeChunks(Dictionary<Ve
241251

242252
foreach (var (origin, terminal) in horizLines)
243253
decodedOutput.Add(new AtmosMonitoringConsoleLine
244-
(ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB));
254+
(ConvertVector2iToVector2(origin, 1f / ScaleModifier), ConvertVector2iToVector2(terminal, 1f / ScaleModifier), sRGB));
245255
}
246256

247257
foreach (var (color, vertLines) in _vertLines)
@@ -251,7 +261,7 @@ private List<AtmosMonitoringConsoleLine> GetDecodedAtmosPipeChunks(Dictionary<Ve
251261

252262
foreach (var (origin, terminal) in vertLines)
253263
decodedOutput.Add(new AtmosMonitoringConsoleLine
254-
(ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB));
264+
(ConvertVector2iToVector2(origin, 1f / ScaleModifier), ConvertVector2iToVector2(terminal, 1f / ScaleModifier), sRGB));
255265
}
256266

257267
return decodedOutput;

Content.Client/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// SPDX-FileCopyrightText: 2024 MilenVolf <63782763+MilenVolf@users.noreply.github.com>
22
// SPDX-FileCopyrightText: 2024 Tadeo <td12233a@gmail.com>
3+
// SPDX-FileCopyrightText: 2025 ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
4+
// SPDX-FileCopyrightText: 2025 chromiumboy <50505512+chromiumboy@users.noreply.github.com>
35
// SPDX-FileCopyrightText: 2025 taydeo <td12233a@gmail.com>
46
//
57
// SPDX-License-Identifier: MIT
@@ -21,7 +23,7 @@ public override void Initialize()
2123

2224
private void OnHandleState(EntityUid uid, AtmosMonitoringConsoleComponent component, ref ComponentHandleState args)
2325
{
24-
Dictionary<Vector2i, Dictionary<(int, string), ulong>> modifiedChunks;
26+
Dictionary<Vector2i, Dictionary<AtmosMonitoringConsoleSubnet, ulong>> modifiedChunks;
2527
Dictionary<NetEntity, AtmosDeviceNavMapData> atmosDevices;
2628

2729
switch (args.Current)
@@ -60,7 +62,7 @@ private void OnHandleState(EntityUid uid, AtmosMonitoringConsoleComponent compon
6062
foreach (var (origin, chunk) in modifiedChunks)
6163
{
6264
var newChunk = new AtmosPipeChunk(origin);
63-
newChunk.AtmosPipeData = new Dictionary<(int, string), ulong>(chunk);
65+
newChunk.AtmosPipeData = new Dictionary<AtmosMonitoringConsoleSubnet, ulong>(chunk);
6466

6567
component.AtmosPipeChunks[origin] = newChunk;
6668
}

0 commit comments

Comments
 (0)