From 4f323f6cca096fde1db44138fb05f66cb1260e01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:29:29 +0000 Subject: [PATCH 1/2] Initial plan From 400b18900b3df4178b659026553cdc3a5b589da2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:44:33 +0000 Subject: [PATCH 2/2] Port Starlight Plumbing System from ss14Starlight/space-station-14 - Add Content.Server/_Starlight/Plumbing/ (systems, components, nodes, nodegroups) - Add Content.Shared/_Starlight/Plumbing/ (components, shared state) - Add Content.Client/_Starlight/Plumbing/ (appearance system, UI windows) - Add Resources/Prototypes/_StarLight/Entities/Structures/Piping/Plumbing/ - Add Resources/Textures/_Starlight/Structures/Piping/Plumbing/ (RSI sprites) - Add Resources/Locale/en-US/_Starlight/plumbing/ (FTL strings) - Add NodeGroupID.Plumbing to NodeGroupID enum - Add rpld DataField to RCDDeconstructableComponent - Add PatchSolutionName to SharedChemMaster - Add BeakerBarChart UI control to Content.Client/Medical/Cryogenics - Add PatchComponent to Content.Shared/_Starlight/Medical/Items/Components - Add Patch and PlumbingDuct tags to tags.yml - Add minimal Patch entity prototype Co-authored-by: Koollan <23038489+Koollan@users.noreply.github.com> --- .../Medical/Cryogenics/BeakerBarChart.cs | 285 +++++++ .../PlumbingConnectorAppearanceSystem.cs | 184 +++++ .../UI/PlumbingFilterBoundUserInterface.cs | 57 ++ .../Plumbing/UI/PlumbingFilterWindow.xaml | 29 + .../Plumbing/UI/PlumbingFilterWindow.xaml.cs | 143 ++++ .../UI/PlumbingPillPressBoundUserInterface.cs | 50 ++ .../Plumbing/UI/PlumbingPillPressWindow.xaml | 61 ++ .../UI/PlumbingPillPressWindow.xaml.cs | 202 +++++ .../UI/PlumbingReactorBoundUserInterface.cs | 64 ++ .../Plumbing/UI/PlumbingReactorWindow.xaml | 39 + .../Plumbing/UI/PlumbingReactorWindow.xaml.cs | 186 +++++ ...lumbingSmartDispenserBoundUserInterface.cs | 31 + .../UI/PlumbingSmartDispenserWindow.xaml | 23 + .../UI/PlumbingSmartDispenserWindow.xaml.cs | 60 ++ .../PlumbingSynthesizerBoundUserInterface.cs | 45 + .../UI/PlumbingSynthesizerWindow.xaml | 22 + .../UI/PlumbingSynthesizerWindow.xaml.cs | 82 ++ .../Components/PlumbingDeviceComponent.cs | 50 ++ .../PlumbingConnectorAppearanceSystem.cs | 194 +++++ .../EntitySystems/PlumbingDeviceSystem.cs | 136 +++ .../EntitySystems/PlumbingFilterSystem.cs | 134 +++ .../EntitySystems/PlumbingInletSystem.cs | 64 ++ .../EntitySystems/PlumbingInputSystem.cs | 59 ++ .../EntitySystems/PlumbingOutputSystem.cs | 60 ++ .../EntitySystems/PlumbingPillPressSystem.cs | 335 ++++++++ .../EntitySystems/PlumbingPullSystem.cs | 317 +++++++ .../EntitySystems/PlumbingReactorSystem.cs | 326 ++++++++ .../PlumbingSmartDispenserSystem.cs | 368 +++++++++ .../PlumbingSynthesizerSystem.cs | 214 +++++ .../Plumbing/NodeGroups/PlumbingNet.cs | 18 + .../_Starlight/Plumbing/Nodes/PlumbingNode.cs | 20 + .../Plumbing/PlumbingPullAttemptEvent.cs | 41 + .../Plumbing/PlumbingPullIntoAttemptEvent.cs | 48 ++ Content.Shared/Chemistry/SharedChemMaster.cs | 1 + .../NodeContainer/NodeGroups/NodeGroupID.cs | 5 + .../Components/RCDDeconstructibleComponent.cs | 8 + .../Items/Components/PatchComponent.cs | 29 + .../PlumbingConnectorAppearanceComponent.cs | 36 + .../Components/PlumbingFilterComponent.cs | 48 ++ .../Components/PlumbingInletComponent.cs | 40 + .../Components/PlumbingInputComponent.cs | 16 + .../Components/PlumbingOutletComponent.cs | 40 + .../Components/PlumbingOutputComponent.cs | 15 + .../Components/PlumbingPillPressComponent.cs | 94 +++ .../Components/PlumbingReactorComponent.cs | 67 ++ .../PlumbingSmartDispenserComponent.cs | 24 + .../PlumbingSynthesizerComponent.cs | 46 ++ .../Components/PlungeableComponent.cs | 17 + .../_Starlight/Plumbing/PlumbingVisuals.cs | 55 ++ .../Plumbing/SharedPlumbingFilter.cs | 84 ++ .../Plumbing/SharedPlumbingPillPress.cs | 163 ++++ .../Plumbing/SharedPlumbingReactor.cs | 131 +++ .../Plumbing/SharedPlumbingSmartDispenser.cs | 47 ++ .../Plumbing/SharedPlumbingSynthesizer.cs | 91 ++ .../en-US/_Starlight/plumbing/pill-press.ftl | 15 + .../en-US/_Starlight/plumbing/plumbing.ftl | 56 ++ .../_Starlight/plumbing/smart-dispenser.ftl | 10 + .../Structures/Piping/Plumbing/ducts.yml | 189 +++++ .../Structures/Piping/Plumbing/patch.yml | 27 + .../Piping/Plumbing/plumbing_machines.yml | 780 ++++++++++++++++++ Resources/Prototypes/tags.yml | 8 + .../Plumbing/fluid_ducts.rsi/ductBend.png | Bin 0 -> 577 bytes .../fluid_ducts.rsi/ductConnector.png | Bin 0 -> 386 bytes .../Plumbing/fluid_ducts.rsi/ductFourway.png | Bin 0 -> 597 bytes .../Plumbing/fluid_ducts.rsi/ductStraight.png | Bin 0 -> 386 bytes .../fluid_ducts.rsi/ductTJunction.png | Bin 0 -> 588 bytes .../Piping/Plumbing/fluid_ducts.rsi/meta.json | 52 ++ .../Plumbing/fluid_ducts.rsi/storageBend.png | Bin 0 -> 300 bytes .../fluid_ducts.rsi/storageFourway.png | Bin 0 -> 368 bytes .../fluid_ducts.rsi/storageStraight.png | Bin 0 -> 239 bytes .../fluid_ducts.rsi/storageTjunction.png | Bin 0 -> 336 bytes .../Piping/Plumbing/plumbers.rsi/bottler.png | Bin 0 -> 1993 bytes .../Piping/Plumbing/plumbers.rsi/disposal.png | Bin 0 -> 374 bytes .../plumbers.rsi/disposal_working.png | Bin 0 -> 1036 bytes .../Plumbing/plumbers.rsi/ductConnector.png | Bin 0 -> 559 bytes .../plumbers.rsi/ductConnector_connected.png | Bin 0 -> 556 bytes .../Piping/Plumbing/plumbers.rsi/filter.png | Bin 0 -> 909 bytes .../Piping/Plumbing/plumbers.rsi/filterF.png | Bin 0 -> 892 bytes .../Piping/Plumbing/plumbers.rsi/meta.json | 114 +++ .../Plumbing/plumbers.rsi/pill_press.png | Bin 0 -> 1074 bytes .../Plumbing/plumbers.rsi/pill_press_off.png | Bin 0 -> 604 bytes .../Plumbing/plumbers.rsi/pipe_input.png | Bin 0 -> 487 bytes .../Plumbing/plumbers.rsi/pipe_output.png | Bin 0 -> 490 bytes .../plumbers.rsi/reaction_chamber.png | Bin 0 -> 713 bytes .../plumbers.rsi/reaction_chamber_on.png | Bin 0 -> 2534 bytes .../Plumbing/plumbers.rsi/synthesizer.png | Bin 0 -> 831 bytes .../plumbers.rsi/synthesizer_inactive.png | Bin 0 -> 935 bytes .../plumbers.rsi/synthesizer_overlay.png | Bin 0 -> 215 bytes .../Piping/Plumbing/plumbers.rsi/tank.png | Bin 0 -> 721 bytes .../Plumbing/plumbers.rsi/tap_output.png | Bin 0 -> 428 bytes 90 files changed, 6255 insertions(+) create mode 100644 Content.Client/Medical/Cryogenics/BeakerBarChart.cs create mode 100644 Content.Client/_Starlight/Plumbing/PlumbingConnectorAppearanceSystem.cs create mode 100644 Content.Client/_Starlight/Plumbing/UI/PlumbingFilterBoundUserInterface.cs create mode 100644 Content.Client/_Starlight/Plumbing/UI/PlumbingFilterWindow.xaml create mode 100644 Content.Client/_Starlight/Plumbing/UI/PlumbingFilterWindow.xaml.cs create mode 100644 Content.Client/_Starlight/Plumbing/UI/PlumbingPillPressBoundUserInterface.cs create mode 100644 Content.Client/_Starlight/Plumbing/UI/PlumbingPillPressWindow.xaml create mode 100644 Content.Client/_Starlight/Plumbing/UI/PlumbingPillPressWindow.xaml.cs create mode 100644 Content.Client/_Starlight/Plumbing/UI/PlumbingReactorBoundUserInterface.cs create mode 100644 Content.Client/_Starlight/Plumbing/UI/PlumbingReactorWindow.xaml create mode 100644 Content.Client/_Starlight/Plumbing/UI/PlumbingReactorWindow.xaml.cs create mode 100644 Content.Client/_Starlight/Plumbing/UI/PlumbingSmartDispenserBoundUserInterface.cs create mode 100644 Content.Client/_Starlight/Plumbing/UI/PlumbingSmartDispenserWindow.xaml create mode 100644 Content.Client/_Starlight/Plumbing/UI/PlumbingSmartDispenserWindow.xaml.cs create mode 100644 Content.Client/_Starlight/Plumbing/UI/PlumbingSynthesizerBoundUserInterface.cs create mode 100644 Content.Client/_Starlight/Plumbing/UI/PlumbingSynthesizerWindow.xaml create mode 100644 Content.Client/_Starlight/Plumbing/UI/PlumbingSynthesizerWindow.xaml.cs create mode 100644 Content.Server/_Starlight/Plumbing/Components/PlumbingDeviceComponent.cs create mode 100644 Content.Server/_Starlight/Plumbing/EntitySystems/PlumbingConnectorAppearanceSystem.cs create mode 100644 Content.Server/_Starlight/Plumbing/EntitySystems/PlumbingDeviceSystem.cs create mode 100644 Content.Server/_Starlight/Plumbing/EntitySystems/PlumbingFilterSystem.cs create mode 100644 Content.Server/_Starlight/Plumbing/EntitySystems/PlumbingInletSystem.cs create mode 100644 Content.Server/_Starlight/Plumbing/EntitySystems/PlumbingInputSystem.cs create mode 100644 Content.Server/_Starlight/Plumbing/EntitySystems/PlumbingOutputSystem.cs create mode 100644 Content.Server/_Starlight/Plumbing/EntitySystems/PlumbingPillPressSystem.cs create mode 100644 Content.Server/_Starlight/Plumbing/EntitySystems/PlumbingPullSystem.cs create mode 100644 Content.Server/_Starlight/Plumbing/EntitySystems/PlumbingReactorSystem.cs create mode 100644 Content.Server/_Starlight/Plumbing/EntitySystems/PlumbingSmartDispenserSystem.cs create mode 100644 Content.Server/_Starlight/Plumbing/EntitySystems/PlumbingSynthesizerSystem.cs create mode 100644 Content.Server/_Starlight/Plumbing/NodeGroups/PlumbingNet.cs create mode 100644 Content.Server/_Starlight/Plumbing/Nodes/PlumbingNode.cs create mode 100644 Content.Server/_Starlight/Plumbing/PlumbingPullAttemptEvent.cs create mode 100644 Content.Server/_Starlight/Plumbing/PlumbingPullIntoAttemptEvent.cs create mode 100644 Content.Shared/_Starlight/Medical/Items/Components/PatchComponent.cs create mode 100644 Content.Shared/_Starlight/Plumbing/Components/PlumbingConnectorAppearanceComponent.cs create mode 100644 Content.Shared/_Starlight/Plumbing/Components/PlumbingFilterComponent.cs create mode 100644 Content.Shared/_Starlight/Plumbing/Components/PlumbingInletComponent.cs create mode 100644 Content.Shared/_Starlight/Plumbing/Components/PlumbingInputComponent.cs create mode 100644 Content.Shared/_Starlight/Plumbing/Components/PlumbingOutletComponent.cs create mode 100644 Content.Shared/_Starlight/Plumbing/Components/PlumbingOutputComponent.cs create mode 100644 Content.Shared/_Starlight/Plumbing/Components/PlumbingPillPressComponent.cs create mode 100644 Content.Shared/_Starlight/Plumbing/Components/PlumbingReactorComponent.cs create mode 100644 Content.Shared/_Starlight/Plumbing/Components/PlumbingSmartDispenserComponent.cs create mode 100644 Content.Shared/_Starlight/Plumbing/Components/PlumbingSynthesizerComponent.cs create mode 100644 Content.Shared/_Starlight/Plumbing/Components/PlungeableComponent.cs create mode 100644 Content.Shared/_Starlight/Plumbing/PlumbingVisuals.cs create mode 100644 Content.Shared/_Starlight/Plumbing/SharedPlumbingFilter.cs create mode 100644 Content.Shared/_Starlight/Plumbing/SharedPlumbingPillPress.cs create mode 100644 Content.Shared/_Starlight/Plumbing/SharedPlumbingReactor.cs create mode 100644 Content.Shared/_Starlight/Plumbing/SharedPlumbingSmartDispenser.cs create mode 100644 Content.Shared/_Starlight/Plumbing/SharedPlumbingSynthesizer.cs create mode 100644 Resources/Locale/en-US/_Starlight/plumbing/pill-press.ftl create mode 100644 Resources/Locale/en-US/_Starlight/plumbing/plumbing.ftl create mode 100644 Resources/Locale/en-US/_Starlight/plumbing/smart-dispenser.ftl create mode 100644 Resources/Prototypes/_StarLight/Entities/Structures/Piping/Plumbing/ducts.yml create mode 100644 Resources/Prototypes/_StarLight/Entities/Structures/Piping/Plumbing/patch.yml create mode 100644 Resources/Prototypes/_StarLight/Entities/Structures/Piping/Plumbing/plumbing_machines.yml create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/fluid_ducts.rsi/ductBend.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/fluid_ducts.rsi/ductConnector.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/fluid_ducts.rsi/ductFourway.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/fluid_ducts.rsi/ductStraight.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/fluid_ducts.rsi/ductTJunction.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/fluid_ducts.rsi/meta.json create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/fluid_ducts.rsi/storageBend.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/fluid_ducts.rsi/storageFourway.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/fluid_ducts.rsi/storageStraight.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/fluid_ducts.rsi/storageTjunction.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/bottler.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/disposal.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/disposal_working.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/ductConnector.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/ductConnector_connected.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/filter.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/filterF.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/meta.json create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/pill_press.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/pill_press_off.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/pipe_input.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/pipe_output.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/reaction_chamber.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/reaction_chamber_on.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/synthesizer.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/synthesizer_inactive.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/synthesizer_overlay.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/tank.png create mode 100644 Resources/Textures/_Starlight/Structures/Piping/Plumbing/plumbers.rsi/tap_output.png diff --git a/Content.Client/Medical/Cryogenics/BeakerBarChart.cs b/Content.Client/Medical/Cryogenics/BeakerBarChart.cs new file mode 100644 index 00000000000..25301b52686 --- /dev/null +++ b/Content.Client/Medical/Cryogenics/BeakerBarChart.cs @@ -0,0 +1,285 @@ +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +// ReSharper disable CompareOfFloatsByEqualityOperator + +namespace Content.Client.Medical.Cryogenics; + + +public sealed class BeakerBarChart : Control +{ + private sealed class Entry + { + public float WidthFraction; // This entry's width as a fraction of the chart's total width (between 0 and 1) + public float TargetAmount; + public string Uid; // This UID is used to track entries between frames, for animation. + public string? Tooltip; + public Color Color; + public Label Label; + + public Entry(string uid, Label label) + { + Uid = uid; + Label = label; + } + } + + public float Capacity = 50; + + public Color NotchColor = new(1, 1, 1, 0.25f); + public Color BackgroundColor = new(0.1f, 0.1f, 0.1f); + + public int MediumNotchInterval = 5; + public int BigNotchInterval = 10; + + // When we have a very large beaker (i.e. bluespace beaker) we might need to increase the distance between notches. + // The distance between notches is increased by ScaleMultiplier when the distance between notches is less than + // MinSmallNotchScreenDistance in UI units. + public int MinSmallNotchScreenDistance = 2; + public int ScaleMultiplier = 10; + + public float SmallNotchHeight = 0.1f; + public float MediumNotchHeight = 0.25f; + public float BigNotchHeight = 1f; + + // We don't animate new entries until this control has been drawn at least once. + private bool _hasBeenDrawn = false; + + // This is used to keep the segments of the chart in the same order as the SetEntry calls. + // For example: In update 1 we might get cryox, alox, bic (in that order), and in update 2 we get alox, cryox, bic. + // To keep the order of the entries the same as the order of the SetEntry calls, we let the old cryox entry + // disappear and create a new cryox entry behind the alox entry. + private int _nextUpdateableEntry = 0; + + private readonly List _entries = new(); + + + public BeakerBarChart() + { + MouseFilter = MouseFilterMode.Pass; + TooltipSupplier = SupplyTooltip; + } + + public void Clear() + { + foreach (var entry in _entries) + { + entry.TargetAmount = 0; + } + + _nextUpdateableEntry = 0; + } + + /// + /// Either adds a new entry to the chart if the UID doesn't appear yet, or updates the amount of an existing entry. + /// + public void SetEntry( + string uid, + string label, + float amount, + Color color, + Color? textColor = null, + string? tooltip = null) + { + // If we can find an old entry we're allowed to update, update that one. + if (TryFindUpdateableEntry(uid, out var index)) + { + _entries[index].TargetAmount = amount; + _entries[index].Tooltip = tooltip; + _entries[index].Label.Text = label; + _nextUpdateableEntry = index + 1; + return; + } + + // Otherwise create a new entry. + if (amount <= 0) + return; + + // If no text color is provided, use either white or black depending on how dark the background is. + textColor ??= (color.R + color.G + color.B < 1.5f ? Color.White : Color.Black); + + var childLabel = new Label + { + Text = label, + ClipText = true, + FontColorOverride = textColor, + Margin = new Thickness(4, 0, 0, 0) + }; + AddChild(childLabel); + + _entries.Insert( + _nextUpdateableEntry, + new Entry(uid, childLabel) + { + WidthFraction = (_hasBeenDrawn ? 0 : amount / Capacity), + TargetAmount = amount, + Tooltip = tooltip, + Color = color + } + ); + + _nextUpdateableEntry += 1; + } + + private bool TryFindUpdateableEntry(string uid, out int index) + { + for (int i = _nextUpdateableEntry; i < _entries.Count; i++) + { + if (_entries[i].Uid == uid) + { + index = i; + return true; + } + } + + index = -1; + return false; + } + + private IEnumerable<(Entry, float xMin, float xMax)> EntryRanges(float? pixelWidth = null) + { + float chartWidth = pixelWidth ?? PixelWidth; + var xStart = 0f; + + foreach (var entry in _entries) + { + var entryWidth = entry.WidthFraction * chartWidth; + var xEnd = MathF.Min(xStart + entryWidth, chartWidth); + + yield return (entry, xStart, xEnd); + + xStart = xEnd; + } + } + + private bool TryFindEntry(float x, [NotNullWhen(true)] out Entry? entry) + { + foreach (var (currentEntry, xMin, xMax) in EntryRanges()) + { + if (xMin <= x && x < xMax) + { + entry = currentEntry; + return true; + } + } + + entry = null; + return false; + } + + protected override void FrameUpdate(FrameEventArgs args) + { + // Tween the amounts to their target amounts. + const float tweenInverseHalfLife = 8; // Half life of tween is 1/n + var hasChanged = false; + + foreach (var entry in _entries) + { + var targetWidthFraction = entry.TargetAmount / Capacity; + + if (entry.WidthFraction == targetWidthFraction) + continue; + + // Tween with lerp abuse interpolation + entry.WidthFraction = MathHelper.Lerp( + entry.WidthFraction, + targetWidthFraction, + MathHelper.Clamp01(tweenInverseHalfLife * args.DeltaSeconds) + ); + hasChanged = true; + + if (MathF.Abs(entry.WidthFraction - targetWidthFraction) < 0.0001f) + entry.WidthFraction = targetWidthFraction; + } + + if (!hasChanged) + return; + + InvalidateArrange(); + + // Remove old entries whose animations have finished. + foreach (var entry in _entries) + { + if (entry.WidthFraction == 0 && entry.TargetAmount == 0) + RemoveChild(entry.Label); + } + + _entries.RemoveAll(entry => entry.WidthFraction == 0 && entry.TargetAmount == 0); + } + + protected override void MouseMove(GUIMouseMoveEventArgs args) + { + HideTooltip(); + } + + protected override void Draw(DrawingHandleScreen handle) + { + handle.DrawRect(PixelSizeBox, BackgroundColor); + + // Draw the entry backgrounds + foreach (var (entry, xMin, xMax) in EntryRanges()) + { + if (xMin != xMax) + handle.DrawRect(new(xMin, 0, xMax, PixelHeight), entry.Color); + } + + // Draw notches + var unitWidth = PixelWidth / Capacity; + var unitsPerNotch = 1; + + while (unitWidth < MinSmallNotchScreenDistance) + { + // This is here for 1000u bluespace beakers. If the distance between small notches is so small that it would + // be very ugly, we reduce the amount of notches by ScaleMultiplier (currently a factor of 10). + // (I could use an analytical algorithm here, but it would be more difficult to read with pretty much no + // performance benefit, since it loops zero times normally and one time for the bluespace beaker) + unitWidth *= ScaleMultiplier; + unitsPerNotch *= ScaleMultiplier; + } + + for (int i = 0; i <= Capacity / unitsPerNotch; i++) + { + var x = i * unitWidth; + var height = (i % BigNotchInterval == 0 ? BigNotchHeight : + i % MediumNotchInterval == 0 ? MediumNotchHeight : + SmallNotchHeight) * PixelHeight; + var start = new Vector2(x, PixelHeight); + var end = new Vector2(x, PixelHeight - height); + handle.DrawLine(start, end, NotchColor); + } + + _hasBeenDrawn = true; + } + + protected override Vector2 ArrangeOverride(Vector2 finalSize) + { + foreach (var (entry, xMin, xMax) in EntryRanges(finalSize.X)) + { + entry.Label.Arrange(new((int)xMin, 0, (int)xMax, (int)finalSize.Y)); + } + + return finalSize; + } + + private Control? SupplyTooltip(Control sender) + { + var globalMousePos = UserInterfaceManager.MousePositionScaled.Position; + var mousePos = globalMousePos - GlobalPosition; + + if (!TryFindEntry(mousePos.X, out var entry) || entry.Tooltip == null) + return null; + + var msg = new FormattedMessage(); + msg.AddText(entry.Tooltip); + + var tooltip = new Tooltip(); + tooltip.SetMessage(msg); + return tooltip; + } +} diff --git a/Content.Client/_Starlight/Plumbing/PlumbingConnectorAppearanceSystem.cs b/Content.Client/_Starlight/Plumbing/PlumbingConnectorAppearanceSystem.cs new file mode 100644 index 00000000000..a165bdab176 --- /dev/null +++ b/Content.Client/_Starlight/Plumbing/PlumbingConnectorAppearanceSystem.cs @@ -0,0 +1,184 @@ +using System.Numerics; +using Content.Shared._Starlight.Plumbing; +using Content.Shared._Starlight.Plumbing.Components; +using Content.Client.SubFloor; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Piping; +using JetBrains.Annotations; +using Robust.Client.GameObjects; + +namespace Content.Client._Starlight.Plumbing; + +/// +/// Client system that creates and updates plumbing connector sprite layers. +/// Single layer per direction that switches between disconnected/connected sprites +/// Layers hide when covered by floor tiles (server sends CoveredByFloor state) +/// +[UsedImplicitly] +public sealed class PlumbingConnectorAppearanceSystem : EntitySystem +{ + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SpriteSystem _sprite = default!; + + private static readonly Color InletColor = new(1.0f, 0.35f, 0.35f); // Vibrant Red + private static readonly Color OutletColor = new(0.35f, 0.6f, 1.0f); // Vibrant Blue + private static readonly Color MixingInletColor = new(0.35f, 0.9f, 0.35f); // Vibrant Green + private static readonly PlumbingConnectionLayer[] ConnectionLayers = Enum.GetValues(); + + private EntityQuery _xformQuery; + + public override void Initialize() + { + base.Initialize(); + _xformQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnAppearanceChanged, after: [typeof(SubFloorHideSystem)]); + } + + private void OnInit(EntityUid uid, PlumbingConnectorAppearanceComponent component, ComponentInit args) + { + if (!TryComp(uid, out SpriteComponent? sprite)) + return; + + // Create one layer for each cardinal direction + // The layer will swap between disconnected/connected sprites based on connection state + // Insert at layer 0 so connectors render UNDER the plumbing machine sprite + foreach (var layerKey in ConnectionLayers) + { + var offset = GetDirectionOffset(layerKey, component.Offset); + + // Each insertion at 0 pushes previous layers up, so we use index 0 for all operations + var layerName = layerKey.ToString(); + _sprite.AddBlankLayer((uid, sprite), 0); + _sprite.LayerMapSet((uid, sprite), layerName, 0); + + // Disconnected connectors are offset from center to show under big machine sprites. + _sprite.LayerSetRsi((uid, sprite), 0, component.Disconnected.RsiPath); + _sprite.LayerSetRsiState((uid, sprite), 0, component.Disconnected.RsiState); + _sprite.LayerSetDirOffset((uid, sprite), 0, ToOffset(layerKey)); + _sprite.LayerSetVisible((uid, sprite), 0, false); + if (offset != Vector2.Zero) + _sprite.LayerSetOffset((uid, sprite), 0, offset); + } + } + + private static Vector2 GetDirectionOffset(PlumbingConnectionLayer layer, float offset) + { + return ((PipeDirection)layer).ToDirection().ToVec() * offset; + } + + private void OnAppearanceChanged(EntityUid uid, PlumbingConnectorAppearanceComponent component, ref AppearanceChangeEvent args) + { + if (args.Sprite == null) + return; + + if (!args.Sprite.Visible) + { + return; + } + + // Hide if no nodes exists somehow + if (!_appearance.TryGetData(uid, PlumbingVisuals.NodeDirections, out var nodeDirectionsInt, args.Component)) + { + HideAllLayers(uid, args.Sprite); + return; + } + + if (!_appearance.TryGetData(uid, PlumbingVisuals.ConnectedDirections, out var connectedDirectionsInt, args.Component)) + connectedDirectionsInt = 0; + + if (!_appearance.TryGetData(uid, PlumbingVisuals.InletDirections, out var inletDirectionsInt, args.Component)) + inletDirectionsInt = 0; + if (!_appearance.TryGetData(uid, PlumbingVisuals.OutletDirections, out var outletDirectionsInt, args.Component)) + outletDirectionsInt = 0; + if (!_appearance.TryGetData(uid, PlumbingVisuals.MixingInletDirections, out var mixingInletDirectionsInt, args.Component)) + mixingInletDirectionsInt = 0; + + if (!_appearance.TryGetData(uid, PlumbingVisuals.CoveredByFloor, out var coveredByFloor, args.Component)) + coveredByFloor = false; + + var nodeDirections = (PipeDirection)nodeDirectionsInt; + var connectedDirections = (PipeDirection)connectedDirectionsInt; + var inletDirections = (PipeDirection)inletDirectionsInt; + var outletDirections = (PipeDirection)outletDirectionsInt; + var mixingInletDirections = (PipeDirection)mixingInletDirectionsInt; + + // Get the entity's local rotation to transform world directions to local + if (!_xformQuery.TryGetComponent(uid, out var xform)) + return; + var localRotation = xform.LocalRotation; + var connectedDirectionsLocal = connectedDirections.RotatePipeDirection(-localRotation); + var nodeDirectionsLocal = nodeDirections.RotatePipeDirection(-localRotation); + var inletDirectionsLocal = inletDirections.RotatePipeDirection(-localRotation); + var outletDirectionsLocal = outletDirections.RotatePipeDirection(-localRotation); + var mixingInletDirectionsLocal = mixingInletDirections.RotatePipeDirection(-localRotation); + + + foreach (var layerKey in ConnectionLayers) + { + var dir = (PipeDirection)layerKey; + var hasNode = nodeDirectionsLocal.HasDirection(dir); + var isConnected = connectedDirectionsLocal.HasDirection(dir); + var isInlet = inletDirectionsLocal.HasDirection(dir); + var isOutlet = outletDirectionsLocal.HasDirection(dir); + var isMixingInlet = mixingInletDirectionsLocal.HasDirection(dir); + + // Determine color based on inlet/outlet/mixing + var color = isMixingInlet ? MixingInletColor : isInlet ? InletColor : isOutlet ? OutletColor : Color.White; + + var layerName = layerKey.ToString(); + if (_sprite.LayerMapTryGet((uid, args.Sprite), layerName, out var layerKey2, false)) + { + var layer = args.Sprite[layerKey2]; + layer.Visible = hasNode && !coveredByFloor; + + if (layer.Visible) + { + // Swap sprite based on connection state + if (isConnected) + { + _sprite.LayerSetRsiState((uid, args.Sprite), layerKey2, component.Connected.RsiState); + _sprite.LayerSetOffset((uid, args.Sprite), layerKey2, Vector2.Zero); + } + else + { + _sprite.LayerSetRsiState((uid, args.Sprite), layerKey2, component.Disconnected.RsiState); + _sprite.LayerSetOffset((uid, args.Sprite), layerKey2, GetDirectionOffset(layerKey, component.Offset)); + } + layer.Color = color; + } + } + } + } + + private void HideAllLayers(EntityUid uid, SpriteComponent sprite) + { + foreach (var layerKey in ConnectionLayers) + { + var layerName = layerKey.ToString(); + if (_sprite.LayerMapTryGet((uid, sprite), layerName, out var key, false)) + sprite[key].Visible = false; + } + } + + + private SpriteComponent.DirectionOffset ToOffset(PlumbingConnectionLayer layer) + { + return layer switch + { + PlumbingConnectionLayer.NorthConnection => SpriteComponent.DirectionOffset.Flip, + PlumbingConnectionLayer.EastConnection => SpriteComponent.DirectionOffset.CounterClockwise, + PlumbingConnectionLayer.WestConnection => SpriteComponent.DirectionOffset.Clockwise, + _ => SpriteComponent.DirectionOffset.None, // SouthConnection + }; + } + + private enum PlumbingConnectionLayer : byte + { + NorthConnection = PipeDirection.North, + SouthConnection = PipeDirection.South, + EastConnection = PipeDirection.East, + WestConnection = PipeDirection.West, + } +} diff --git a/Content.Client/_Starlight/Plumbing/UI/PlumbingFilterBoundUserInterface.cs b/Content.Client/_Starlight/Plumbing/UI/PlumbingFilterBoundUserInterface.cs new file mode 100644 index 00000000000..c35ca54913a --- /dev/null +++ b/Content.Client/_Starlight/Plumbing/UI/PlumbingFilterBoundUserInterface.cs @@ -0,0 +1,57 @@ +using Content.Shared._Starlight.Plumbing; +using JetBrains.Annotations; +using Robust.Client.UserInterface; + +namespace Content.Client._Starlight.Plumbing.UI; + +[UsedImplicitly] +public sealed class PlumbingFilterBoundUserInterface : BoundUserInterface +{ + private PlumbingFilterWindow? _window; + + public PlumbingFilterBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + } + + protected override void Open() + { + base.Open(); + + _window = this.CreateWindow(); + + _window.OnToggle += OnToggle; + _window.OnAddReagent += OnAddReagent; + _window.OnRemoveReagent += OnRemoveReagent; + _window.OnClear += OnClear; + } + + private void OnToggle(bool enabled) + { + SendMessage(new PlumbingFilterToggleMessage(enabled)); + } + + private void OnAddReagent(string reagentId) + { + SendMessage(new PlumbingFilterAddReagentMessage(reagentId)); + } + + private void OnRemoveReagent(string reagentId) + { + SendMessage(new PlumbingFilterRemoveReagentMessage(reagentId)); + } + + private void OnClear() + { + SendMessage(new PlumbingFilterClearMessage()); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + if (_window == null || state is not PlumbingFilterBoundUserInterfaceState cast) + return; + + _window.UpdateState(cast); + } +} diff --git a/Content.Client/_Starlight/Plumbing/UI/PlumbingFilterWindow.xaml b/Content.Client/_Starlight/Plumbing/UI/PlumbingFilterWindow.xaml new file mode 100644 index 00000000000..9a47109fde3 --- /dev/null +++ b/Content.Client/_Starlight/Plumbing/UI/PlumbingFilterWindow.xaml @@ -0,0 +1,29 @@ + + + + +