From e47ce26377fa69141d56561b5dfc9fb761d34dee Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Fri, 27 Mar 2026 20:46:15 -0400 Subject: [PATCH 001/126] Remove `[Virtual]` from some classes that shouldn't have it (#43347) * Remove [Virtual] from some classes that shouldn't have it * Back to virtual with you --- .../Stylesheets/Stylesheets/NanotrasenStylesheet.Palettes.cs | 2 +- Content.Client/UserInterface/Controls/RadialMenu.cs | 1 - Content.Client/UserInterface/Controls/SlotControl.cs | 1 - .../Systems/Inventory/Controls/ItemSlotUIContainer.cs | 1 - 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Content.Client/Stylesheets/Stylesheets/NanotrasenStylesheet.Palettes.cs b/Content.Client/Stylesheets/Stylesheets/NanotrasenStylesheet.Palettes.cs index df190154f8..097d54d581 100644 --- a/Content.Client/Stylesheets/Stylesheets/NanotrasenStylesheet.Palettes.cs +++ b/Content.Client/Stylesheets/Stylesheets/NanotrasenStylesheet.Palettes.cs @@ -2,7 +2,7 @@ namespace Content.Client.Stylesheets.Stylesheets; -public sealed partial class NanotrasenStylesheet +public partial class NanotrasenStylesheet { public override ColorPalette PrimaryPalette => Palettes.Navy; public override ColorPalette SecondaryPalette => Palettes.Slate; diff --git a/Content.Client/UserInterface/Controls/RadialMenu.cs b/Content.Client/UserInterface/Controls/RadialMenu.cs index 959a60ef4f..0cc207dd89 100644 --- a/Content.Client/UserInterface/Controls/RadialMenu.cs +++ b/Content.Client/UserInterface/Controls/RadialMenu.cs @@ -228,7 +228,6 @@ public class RadialMenu : BaseWindow /// Base class for radial menu buttons. Excludes all actions except clicks and alt-clicks /// from interactions. /// -[Virtual] public abstract class RadialMenuButtonBase : BaseButton { /// diff --git a/Content.Client/UserInterface/Controls/SlotControl.cs b/Content.Client/UserInterface/Controls/SlotControl.cs index 2b43f2397d..d55b4acf55 100644 --- a/Content.Client/UserInterface/Controls/SlotControl.cs +++ b/Content.Client/UserInterface/Controls/SlotControl.cs @@ -9,7 +9,6 @@ using Robust.Shared.Prototypes; namespace Content.Client.UserInterface.Controls { - [Virtual] public abstract class SlotControl : Control, IEntityControl { public static int DefaultButtonSize = 64; diff --git a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemSlotUIContainer.cs b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemSlotUIContainer.cs index 8094a77b6b..cd18c3e696 100644 --- a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemSlotUIContainer.cs +++ b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemSlotUIContainer.cs @@ -11,7 +11,6 @@ public interface IItemslotUIContainer public bool TryAddButton(SlotControl control); } -[Virtual] public abstract class ItemSlotUIContainer : GridContainer, IItemslotUIContainer where T : SlotControl { private readonly Dictionary _buttons = new(); From c10a2ae2da7082b17df437ab26b64ed3352743e0 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Fri, 27 Mar 2026 23:09:19 -0400 Subject: [PATCH 002/126] Update RT to 275.1.0 (#43368) --- RobustToolbox | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RobustToolbox b/RobustToolbox index dad56301e1..a12555988a 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit dad56301e115f79f03852e3a8dfe485f0db667c3 +Subproject commit a12555988ae75a6c77b916df5c448b9a5d19f519 From b582f2e156a454d860a210a4d02fa885da6a64f6 Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Sat, 28 Mar 2026 05:15:41 +0100 Subject: [PATCH 003/126] cleanup GasPrototype and some other atmos code (#43318) * cleanup * fix some localizations * fix typo * review and test fix * rename * minus one --------- Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> --- .../Consoles/AtmosAlarmEntryContainer.xaml.cs | 5 +- .../AtmosMonitoringEntryContainer.xaml.cs | 5 +- .../EntitySystems/AtmosphereSystem.Gases.cs | 2 +- .../Overlays/GasTileVisibleGasOverlay.cs | 12 +- .../Atmos/UI/GasAnalyzerBoundUserInterface.cs | 52 +++-- .../Atmos/UI/GasAnalyzerWindow.xaml.cs | 33 ++-- .../Medical/Cryogenics/CryoPodWindow.xaml.cs | 17 +- .../Tests/Atmos/ConstantsTest.cs | 3 - .../Tests/Atmos/SharedGasSpecificHeatsTest.cs | 4 +- .../EntitySystems/AtmosphereSystem.Gases.cs | 2 +- .../EntitySystems/AtmosphereSystem.LINDA.cs | 2 +- .../Atmos/EntitySystems/GasAnalyzerSystem.cs | 23 ++- Content.Shared/Atmos/Atmospherics.cs | 20 +- .../Atmos/Components/GasAnalyzerComponent.cs | 170 ++++++++-------- .../SharedAtmosphereSystem.Gases.cs | 10 +- .../SharedGasTileOverlaySystem.cs | 3 +- .../Atmos/Prototypes/GasPrototype.cs | 187 ++++++++++-------- .../Effects/Atmos/CreateGasEntityEffect.cs | 2 +- .../Medical/Cryogenics/CryoPodComponent.cs | 4 +- .../en-US/atmos/gas-analyzer-component.ftl | 1 - Resources/Locale/en-US/atmos/gases.ftl | 20 +- Resources/Locale/en-US/gases/gases.ftl | 9 - Resources/Prototypes/Atmospherics/gases.yml | 90 +++++---- 23 files changed, 335 insertions(+), 341 deletions(-) delete mode 100644 Resources/Locale/en-US/gases/gases.ftl diff --git a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs index f0e4b13356..e192a66fcc 100644 --- a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs +++ b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs @@ -1,6 +1,7 @@ using Content.Client.Stylesheets; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; using Content.Shared.Atmos.Monitor; using Content.Shared.FixedPoint; using Content.Shared.Temperature; @@ -22,6 +23,7 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer private readonly IEntityManager _entManager; private readonly IResourceCache _cache; + private readonly SharedAtmosphereSystem _atmosphere; private Dictionary _alarmStrings = new Dictionary() { @@ -37,6 +39,7 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer _entManager = IoCManager.Resolve(); _cache = IoCManager.Resolve(); + _atmosphere = _entManager.System(); NetEntity = uid; Coordinates = coordinates; @@ -149,7 +152,7 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer foreach ((var gas, (var mol, var percent, var alert)) in keyValuePairs) { FixedPoint2 gasPercent = percent * 100f; - var gasAbbreviation = Atmospherics.GasAbbreviations.GetValueOrDefault(gas, Loc.GetString("gas-unknown-abbreviation")); + var gasAbbreviation = Loc.GetString(_atmosphere.GetGas(gas).Abbreviation); var gasLabel = new Label() { diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml.cs index 0ce0c9c880..cf9a89d1a5 100644 --- a/Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml.cs +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml.cs @@ -1,6 +1,7 @@ using Content.Client.Stylesheets; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; using Content.Shared.FixedPoint; using Content.Shared.Temperature; using Robust.Client.AutoGenerated; @@ -19,12 +20,14 @@ public sealed partial class AtmosMonitoringEntryContainer : BoxContainer private readonly IEntityManager _entManager; private readonly IResourceCache _cache; + private readonly SharedAtmosphereSystem _atmosphere; public AtmosMonitoringEntryContainer(AtmosMonitoringConsoleEntry data) { RobustXamlLoader.Load(this); _entManager = IoCManager.Resolve(); _cache = IoCManager.Resolve(); + _atmosphere = _entManager.System(); Data = data; @@ -132,7 +135,7 @@ public sealed partial class AtmosMonitoringEntryContainer : BoxContainer var gasPercent = (FixedPoint2)0f; gasPercent = percent * 100f; - var gasAbbreviation = Atmospherics.GasAbbreviations.GetValueOrDefault(gas, Loc.GetString("gas-unknown-abbreviation")); + var gasAbbreviation = Loc.GetString(_atmosphere.GetGas(gas).Abbreviation); var gasLabel = new Label() { diff --git a/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs index d950108922..de544fba99 100644 --- a/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs +++ b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs @@ -51,7 +51,7 @@ public sealed partial class AtmosphereSystem // though this isnt the hottest code path so it should be fine // the gc can eat a little as a treat var tmp = new float[moles.Length]; - NumericsHelpers.Multiply(moles, GasSpecificHeats, tmp); + NumericsHelpers.Multiply(moles, GasMolarHeatCapacities, tmp); // Adjust heat capacity by speedup, because this is primarily what // determines how quickly gases heat up/cool. return MathF.Max(NumericsHelpers.HorizontalAdd(tmp), Atmospherics.MinimumHeatCapacity); diff --git a/Content.Client/Atmos/Overlays/GasTileVisibleGasOverlay.cs b/Content.Client/Atmos/Overlays/GasTileVisibleGasOverlay.cs index 37298b95fd..4d909c9fc8 100644 --- a/Content.Client/Atmos/Overlays/GasTileVisibleGasOverlay.cs +++ b/Content.Client/Atmos/Overlays/GasTileVisibleGasOverlay.cs @@ -72,17 +72,7 @@ public sealed class GasTileVisibleGasOverlay : Overlay { var gasPrototype = _atmosphereSystem.GetGas(_gasTileOverlaySystem.VisibleGasId[i]); - SpriteSpecifier overlay; - - if (!string.IsNullOrEmpty(gasPrototype.GasOverlaySprite) && - !string.IsNullOrEmpty(gasPrototype.GasOverlayState)) - overlay = new SpriteSpecifier.Rsi(new(gasPrototype.GasOverlaySprite), gasPrototype.GasOverlayState); - else if (!string.IsNullOrEmpty(gasPrototype.GasOverlayTexture)) - overlay = new SpriteSpecifier.Texture(new(gasPrototype.GasOverlayTexture)); - else - continue; - - switch (overlay) + switch (gasPrototype.GasOverlaySprite) { case SpriteSpecifier.Rsi animated: var rsi = _resourceCache.GetResource(animated.RsiPath).RSI; diff --git a/Content.Client/Atmos/UI/GasAnalyzerBoundUserInterface.cs b/Content.Client/Atmos/UI/GasAnalyzerBoundUserInterface.cs index 3a5df3f9a8..c8f2091d13 100644 --- a/Content.Client/Atmos/UI/GasAnalyzerBoundUserInterface.cs +++ b/Content.Client/Atmos/UI/GasAnalyzerBoundUserInterface.cs @@ -1,41 +1,33 @@ -using Robust.Client.GameObjects; using Robust.Client.UserInterface; -using static Content.Shared.Atmos.Components.GasAnalyzerComponent; +using Content.Shared.Atmos.Components; -namespace Content.Client.Atmos.UI +namespace Content.Client.Atmos.UI; + +public sealed class GasAnalyzerBoundUserInterface : BoundUserInterface { - public sealed class GasAnalyzerBoundUserInterface : BoundUserInterface + [ViewVariables] + private GasAnalyzerWindow? _window; + + public GasAnalyzerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { - [ViewVariables] - private GasAnalyzerWindow? _window; + } - public GasAnalyzerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) - { - } + protected override void Open() + { + base.Open(); - protected override void Open() - { - base.Open(); + _window = this.CreateWindowCenteredLeft(); + _window.OnClose += Close; + } - _window = this.CreateWindowCenteredLeft(); - _window.OnClose += Close; - } + protected override void ReceiveMessage(BoundUserInterfaceMessage message) + { + if (_window == null) + return; - protected override void ReceiveMessage(BoundUserInterfaceMessage message) - { - if (_window == null) - return; - if (message is not GasAnalyzerUserMessage cast) - return; - _window.Populate(cast); - } + if (message is not GasAnalyzerUserMessage cast) + return; - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - _window?.Dispose(); - } + _window.Populate(cast); } } diff --git a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs index 63b4e6b0c6..94a9f23745 100644 --- a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs +++ b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs @@ -1,6 +1,8 @@ using System.Numerics; using Content.Client.UserInterface.Controls; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; using Content.Shared.Temperature; using Robust.Client.Graphics; using Robust.Client.UserInterface; @@ -8,7 +10,6 @@ using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.XAML; -using static Content.Shared.Atmos.Components.GasAnalyzerComponent; using Direction = Robust.Shared.Maths.Direction; namespace Content.Client.Atmos.UI @@ -16,25 +17,17 @@ namespace Content.Client.Atmos.UI [GenerateTypedNameReferences] public sealed partial class GasAnalyzerWindow : DefaultWindow { + private readonly SharedAtmosphereSystem _atmosphere; private NetEntity _currentEntity = NetEntity.Invalid; public GasAnalyzerWindow() { RobustXamlLoader.Load(this); + _atmosphere = IoCManager.Resolve().System(); } public void Populate(GasAnalyzerUserMessage msg) { - if (msg.Error != null) - { - CTopBox.AddChild(new Label - { - Text = Loc.GetString("gas-analyzer-window-error-text", ("errorText", msg.Error)), - FontColorOverride = Color.Red - }); - return; - } - if (msg.NodeGasMixes.Length == 0) { CTopBox.AddChild(new Label @@ -329,31 +322,31 @@ namespace Content.Client.Atmos.UI for (var j = 0; j < gasMix.Gases.Length; j++) { - var gas = gasMix.Gases[j]; - var color = Color.FromHex($"#{gas.Color}", Color.White); + var gasEntry = gasMix.Gases[j]; + var gasProto = _atmosphere.GetGas(gasEntry.Gas); // Add to the table tableKey.AddChild(new Label { - Text = Loc.GetString(gas.Name) + Text = Loc.GetString(gasProto.Name) }); tableVal.AddChild(new Label { Text = Loc.GetString("gas-analyzer-window-molarity-text", - ("mol", $"{gas.Amount:0.00}")), + ("mol", $"{gasEntry.Amount:0.00}")), Align = Label.AlignMode.Right, }); tablePercent.AddChild(new Label { Text = Loc.GetString("gas-analyzer-window-percentage-text", - ("percentage", $"{(gas.Amount / totalGasAmount * 100):0.0}")), + ("percentage", $"{(gasEntry.Amount / totalGasAmount * 100):0.0}")), Align = Label.AlignMode.Right }); // Add to the gas bar //TODO: highlight the currently hover one - gasBar.AddEntry(gas.Amount, color, tooltip: Loc.GetString("gas-analyzer-window-molarity-percentage-text", - ("gasName", gas.Name), - ("amount", $"{gas.Amount:0.##}"), - ("percentage", $"{(gas.Amount / totalGasAmount * 100):0.#}"))); + gasBar.AddEntry(gasEntry.Amount, gasProto.Color, tooltip: Loc.GetString("gas-analyzer-window-molarity-percentage-text", + ("gasName", Loc.GetString(gasProto.Name)), + ("amount", $"{gasEntry.Amount:0.##}"), + ("percentage", $"{(gasEntry.Amount / totalGasAmount * 100):0.#}"))); } dataContainer.AddChild(gasBar); diff --git a/Content.Client/Medical/Cryogenics/CryoPodWindow.xaml.cs b/Content.Client/Medical/Cryogenics/CryoPodWindow.xaml.cs index 51ff5c1a00..211225119b 100644 --- a/Content.Client/Medical/Cryogenics/CryoPodWindow.xaml.cs +++ b/Content.Client/Medical/Cryogenics/CryoPodWindow.xaml.cs @@ -2,9 +2,8 @@ using System.Linq; using System.Numerics; using Content.Client.UserInterface.Controls; using Content.Shared.Atmos; +using Content.Shared.Atmos.EntitySystems; using Content.Shared.Chemistry.Reagent; -using Content.Shared.Damage.Components; -using Content.Shared.Damage.Systems; using Content.Shared.EntityConditions.Conditions; using Content.Shared.FixedPoint; using Content.Shared.Medical.Cryogenics; @@ -20,6 +19,7 @@ public sealed partial class CryoPodWindow : FancyWindow { [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + private readonly SharedAtmosphereSystem _atmosphere = default!; public event Action? OnEjectPatientPressed; public event Action? OnEjectBeakerPressed; @@ -29,6 +29,7 @@ public sealed partial class CryoPodWindow : FancyWindow { IoCManager.InjectDependencies(this); RobustXamlLoader.Load(this); + _atmosphere = _entityManager.System(); EjectPatientButton.OnPressed += _ => OnEjectPatientPressed?.Invoke(); EjectBeakerButton.OnPressed += _ => OnEjectBeakerPressed?.Invoke(); Inject1.OnPressed += _ => OnInjectPressed?.Invoke(1); @@ -71,16 +72,16 @@ public sealed partial class CryoPodWindow : FancyWindow { var totalGasAmount = msg.GasMix.Gases.Sum(gas => gas.Amount); - foreach (var gas in msg.GasMix.Gases) + foreach (var gasEntry in msg.GasMix.Gases) { - var color = Color.FromHex($"#{gas.Color}", Color.White); - var percent = gas.Amount / totalGasAmount * 100; - var localizedName = Loc.GetString(gas.Name); + var gasProto = _atmosphere.GetGas(gasEntry.Gas); + var percent = gasEntry.Amount / totalGasAmount * 100; + var localizedName = Loc.GetString(gasProto.Name); var tooltip = Loc.GetString("gas-analyzer-window-molarity-percentage-text", ("gasName", localizedName), - ("amount", $"{gas.Amount:0.##}"), + ("amount", $"{gasEntry.Amount:0.##}"), ("percentage", $"{percent:0.#}")); - GasMixChart.AddEntry(gas.Amount, color, tooltip: tooltip); + GasMixChart.AddEntry(gasEntry.Amount, gasProto.Color, tooltip: tooltip); } } diff --git a/Content.IntegrationTests/Tests/Atmos/ConstantsTest.cs b/Content.IntegrationTests/Tests/Atmos/ConstantsTest.cs index 6481e377c9..4112acac50 100644 --- a/Content.IntegrationTests/Tests/Atmos/ConstantsTest.cs +++ b/Content.IntegrationTests/Tests/Atmos/ConstantsTest.cs @@ -34,9 +34,6 @@ public sealed class ConstantsTest // enum mapping gases to their Id Assert.That(Enum.GetValues(), Has.Length.EqualTo(Atmospherics.TotalNumberOfGases), $"Gas enum size is not equal to TotalNumberOfGases."); - // localized abbreviations for UI purposes - Assert.That(Atmospherics.GasAbbreviations, Has.Count.EqualTo(Atmospherics.TotalNumberOfGases), - $"GasAbbreviations size is not equal to TotalNumberOfGases."); // the ID for each gas has to correspond to a value in the Gas enum (converted to a string) foreach (var gas in gasProtos) diff --git a/Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs b/Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs index ac80f4a105..406f7fa10d 100644 --- a/Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs +++ b/Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs @@ -62,12 +62,12 @@ public sealed class SharedGasSpecificHeatsTest var clientSpecificHeats = Array.Empty(); await Server.WaitPost(delegate { - serverSpecificHeats = _sAtmos.GasSpecificHeats; + serverSpecificHeats = _sAtmos.GasMolarHeatCapacities; }); await Client.WaitPost(delegate { - clientSpecificHeats = _cAtmos.GasSpecificHeats; + clientSpecificHeats = _cAtmos.GasMolarHeatCapacities; }); Assert.That(serverSpecificHeats, diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs index 639a71ec60..1fae93ef94 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs @@ -38,7 +38,7 @@ namespace Content.Server.Atmos.EntitySystems } Span tmp = stackalloc float[moles.Length]; - NumericsHelpers.Multiply(moles, GasSpecificHeats, tmp); + NumericsHelpers.Multiply(moles, GasMolarHeatCapacities, tmp); // Adjust heat capacity by speedup, because this is primarily what // determines how quickly gases heat up/cool. return MathF.Max(NumericsHelpers.HorizontalAdd(tmp), Atmospherics.MinimumHeatCapacity); diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs index ad19770bfe..7ece546e4c 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs @@ -229,7 +229,7 @@ namespace Content.Server.Atmos.EntitySystems if (!(MathF.Abs(delta) >= Atmospherics.GasMinMoles)) continue; if (absTemperatureDelta > Atmospherics.MinimumTemperatureDeltaToConsider) { - var gasHeatCapacity = delta * GasSpecificHeats[i]; + var gasHeatCapacity = delta * GasMolarHeatCapacities[i]; if (delta > 0) { heatCapacityToSharer += gasHeatCapacity; diff --git a/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs b/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs index a3fe2400d0..9dcb4d13a2 100644 --- a/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs +++ b/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs @@ -8,7 +8,6 @@ using Content.Shared.Interaction; using Content.Shared.NodeContainer; using JetBrains.Annotations; using Robust.Server.GameObjects; -using static Content.Shared.Atmos.Components.GasAnalyzerComponent; namespace Content.Server.Atmos.EntitySystems; @@ -101,6 +100,7 @@ public sealed class GasAnalyzerSystem : EntitySystem _popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), user.Value, user.Value); entity.Comp.Enabled = false; + entity.Comp.User = null; Dirty(entity); _appearance.SetData(entity.Owner, GasAnalyzerVisuals.Enabled, entity.Comp.Enabled); RemCompDeferred(entity.Owner); @@ -139,15 +139,15 @@ public sealed class GasAnalyzerSystem : EntitySystem return false; // check if the user has walked away from what they scanned - if (component.Target.HasValue) + if (component.Target.HasValue && component.User.HasValue) { // Listen! Even if you don't want the Gas Analyzer to work on moving targets, you should use // this code to determine if the object is still generally in range so that the check is consistent with the code // in OnAfterInteract() and also consistent with interaction code in general. - if (!_interactionSystem.InRangeUnobstructed((component.User, null), (component.Target.Value, null))) + if (!_interactionSystem.InRangeUnobstructed((component.User.Value, null), (component.Target.Value, null))) { - if (component.User is { } userId && component.Enabled) - _popup.PopupEntity(Loc.GetString("gas-analyzer-object-out-of-range"), userId, userId); + if (component.Enabled) + _popup.PopupEntity(Loc.GetString("gas-analyzer-object-out-of-range"), component.User.Value, component.User.Value); component.Target = null; } @@ -256,18 +256,17 @@ public sealed class GasAnalyzerSystem : EntitySystem { var gases = new List(); + if (mixture == null) + return []; + for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) { - var gas = _atmo.GetGas(i); + var gas = (Gas)i; - if (mixture?[i] <= UIMinMoles) + if (mixture[i] <= UIMinMoles) continue; - if (mixture != null) - { - var gasName = Loc.GetString(gas.Name); - gases.Add(new GasEntry(gasName, mixture[i], gas.Color)); - } + gases.Add(new GasEntry(gas, mixture[i])); } var gasesOrdered = gases.OrderByDescending(gas => gas.Amount); diff --git a/Content.Shared/Atmos/Atmospherics.cs b/Content.Shared/Atmos/Atmospherics.cs index 10d303cd1d..02d4a7deb8 100644 --- a/Content.Shared/Atmos/Atmospherics.cs +++ b/Content.Shared/Atmos/Atmospherics.cs @@ -170,22 +170,6 @@ namespace Content.Shared.Atmos /// public const float SpaceHeatCapacity = 7000f; - /// - /// Dictionary of chemical abbreviations for - /// - public static Dictionary GasAbbreviations = new Dictionary() - { - [Gas.Ammonia] = Loc.GetString("gas-ammonia-abbreviation"), - [Gas.CarbonDioxide] = Loc.GetString("gas-carbon-dioxide-abbreviation"), - [Gas.Frezon] = Loc.GetString("gas-frezon-abbreviation"), - [Gas.Nitrogen] = Loc.GetString("gas-nitrogen-abbreviation"), - [Gas.NitrousOxide] = Loc.GetString("gas-nitrous-oxide-abbreviation"), - [Gas.Oxygen] = Loc.GetString("gas-oxygen-abbreviation"), - [Gas.Plasma] = Loc.GetString("gas-plasma-abbreviation"), - [Gas.Tritium] = Loc.GetString("gas-tritium-abbreviation"), - [Gas.WaterVapor] = Loc.GetString("gas-water-vapor-abbreviation"), - }; - #region Excited Groups /// @@ -235,8 +219,8 @@ namespace Content.Shared.Atmos public const float SuperSaturationEnds = SuperSaturationThreshold / 3; public const float OxygenBurnRateBase = 1.4f; - public const float PlasmaMinimumBurnTemperature = (100f+T0C); - public const float PlasmaUpperTemperature = (1370f+T0C); + public const float PlasmaMinimumBurnTemperature = 100f + T0C; + public const float PlasmaUpperTemperature = 1370f + T0C; public const float PlasmaOxygenFullburn = 10f; public const float PlasmaBurnRateDelta = 9f; diff --git a/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs b/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs index 115cb54892..cc3914fe67 100644 --- a/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs +++ b/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs @@ -1,101 +1,103 @@ using Robust.Shared.GameStates; -using Robust.Shared.Map; using Robust.Shared.Serialization; namespace Content.Shared.Atmos.Components; +/// +/// Used for gas analyzers, an item that shows players the gas contents of an atmos +/// device they use it on or of the tile they are standing on. +/// [RegisterComponent, NetworkedComponent] public sealed partial class GasAnalyzerComponent : Component { - [ViewVariables] + /// + /// The target entity currently being analyzed. + /// + [DataField] public EntityUid? Target; - [ViewVariables] - public EntityUid User; + /// + /// The current user of the gas analyzer. + /// + [DataField] + public EntityUid? User; - [DataField("enabled"), ViewVariables(VVAccess.ReadWrite)] + /// + /// Is the analyzer currently active? + /// + [DataField] public bool Enabled; - - [Serializable, NetSerializable] - public enum GasAnalyzerUiKey - { - Key, - } - - /// - /// Atmospheric data is gathered in the system and sent to the user - /// - [Serializable, NetSerializable] - public sealed class GasAnalyzerUserMessage : BoundUserInterfaceMessage - { - public string DeviceName; - public NetEntity DeviceUid; - public bool DeviceFlipped; - public string? Error; - public GasMixEntry[] NodeGasMixes; - public GasAnalyzerUserMessage(GasMixEntry[] nodeGasMixes, string deviceName, NetEntity deviceUid, bool deviceFlipped, string? error = null) - { - NodeGasMixes = nodeGasMixes; - DeviceName = deviceName; - DeviceUid = deviceUid; - DeviceFlipped = deviceFlipped; - Error = error; - } - } - - /// - /// Contains information on a gas mix entry, turns into a tab in the UI - /// - [Serializable, NetSerializable] - public struct GasMixEntry - { - /// - /// Name of the tab in the UI - /// - public readonly string Name; - public readonly float Volume; - public readonly float Pressure; - public readonly float Temperature; - public readonly GasEntry[]? Gases; - - public GasMixEntry(string name, float volume, float pressure, float temperature, GasEntry[]? gases = null) - { - Name = name; - Volume = volume; - Pressure = pressure; - Temperature = temperature; - Gases = gases; - } - } - - /// - /// Individual gas entry data for populating the UI - /// - [Serializable, NetSerializable] - public struct GasEntry - { - public readonly string Name; - public readonly float Amount; - public readonly string Color; - - public GasEntry(string name, float amount, string color) - { - Name = name; - Amount = amount; - Color = color; - } - - public override string ToString() - { - // e.g. "Plasma: 2000 mol" - return Loc.GetString( - "gas-entry-info", - ("gasName", Name), - ("gasAmount", Amount)); - } - } } +/// +/// Atmospheric data is gathered in the system and sent to the user. +/// +[Serializable, NetSerializable] +public sealed class GasAnalyzerUserMessage(GasMixEntry[] nodeGasMixes, string deviceName, NetEntity deviceUid, bool deviceFlipped) : BoundUserInterfaceMessage +{ + public string DeviceName = deviceName; + public NetEntity DeviceUid = deviceUid; + public bool DeviceFlipped = deviceFlipped; + public GasMixEntry[] NodeGasMixes = nodeGasMixes; +} + +/// +/// Contains information on a gas mix entry, turns into a tab in the UI. +/// +[Serializable, NetSerializable] +public readonly record struct GasMixEntry(string Name, float Volume, float Pressure, float Temperature, GasEntry[]? Gases = null) +{ + /// + /// Name of the tab in the UI. + /// + public readonly string Name = Name; + /// + /// Volume of this gas mixture. + /// + public readonly float Volume = Volume; + /// + /// Pressure of this gas mixture. + /// + public readonly float Pressure = Pressure; + /// + /// Temperature of this gas mixture. + /// + public readonly float Temperature = Temperature; + /// + /// The gases contained in this gas mixture. + /// The gases below a certain mol threshold are not included. + /// + public readonly GasEntry[]? Gases = Gases; +} + +/// +/// Individual gas entry data for populating the UI. +/// +[Serializable, NetSerializable] +public readonly record struct GasEntry(Gas Gas, float Amount) +{ + /// + /// The gas this entry represents. + /// + public readonly Gas Gas = Gas; + /// + /// The gas amount in mol. + /// + public readonly float Amount = Amount; +} + +/// +/// Key for the GasAnalyzerBoundUserInterface. +/// +[Serializable, NetSerializable] +public enum GasAnalyzerUiKey +{ + Key, +} + +/// +/// Individual gas entry data for populating the UI +/// [Serializable, NetSerializable] public enum GasAnalyzerVisuals : byte { diff --git a/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.Gases.cs b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.Gases.cs index c7267393dd..99b2942f13 100644 --- a/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.Gases.cs +++ b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.Gases.cs @@ -16,10 +16,10 @@ public abstract partial class SharedAtmosphereSystem */ /// - /// Cached array of gas specific heats. + /// Cached array of molar heat capacities of the gases. /// - public float[] GasSpecificHeats => _gasSpecificHeats; - private float[] _gasSpecificHeats = new float[Atmospherics.TotalNumberOfGases]; + public float[] GasMolarHeatCapacities => _gasMolarHeatCapacities; + private float[] _gasMolarHeatCapacities = new float[Atmospherics.TotalNumberOfGases]; /// /// Mask used to determine if a gas is flammable or not. @@ -69,7 +69,7 @@ public abstract partial class SharedAtmosphereSystem GasReagents[idx] = gasPrototype.Reagent; } - Array.Resize(ref _gasSpecificHeats, MathHelper.NextMultipleOf(Atmospherics.TotalNumberOfGases, 4)); + Array.Resize(ref _gasMolarHeatCapacities, MathHelper.NextMultipleOf(Atmospherics.TotalNumberOfGases, 4)); for (var i = 0; i < GasPrototypes.Length; i++) { @@ -81,7 +81,7 @@ public abstract partial class SharedAtmosphereSystem If you would like the unscaled specific heat, you'd need to multiply by HeatScale again. TODO ATMOS: please just make this 2 separate arrays instead of invoking multiplication every time. */ - _gasSpecificHeats[i] = GasPrototypes[i].SpecificHeat / HeatScale; + _gasMolarHeatCapacities[i] = GasPrototypes[i].MolarHeatCapacity / HeatScale; // """Mask""" built here. Used to determine if a gas is fuel/oxidizer or not decently quickly and clearly. GasFuelMask[i] = GasPrototypes[i].IsFuel ? 1 : 0; diff --git a/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs b/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs index 23ea8fa8fa..e9c697c53c 100644 --- a/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs +++ b/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs @@ -31,8 +31,7 @@ public abstract class SharedGasTileOverlaySystem : EntitySystem for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) { var gasPrototype = _atmosphere.GetGas(i); - if (!string.IsNullOrEmpty(gasPrototype.GasOverlayTexture) || - (!string.IsNullOrEmpty(gasPrototype.GasOverlaySprite) && !string.IsNullOrEmpty(gasPrototype.GasOverlayState))) + if (gasPrototype.GasOverlaySprite != null) visibleGases.Add(i); } VisibleGasId = visibleGases.ToArray(); diff --git a/Content.Shared/Atmos/Prototypes/GasPrototype.cs b/Content.Shared/Atmos/Prototypes/GasPrototype.cs index 86304dfdda..bb92d0307d 100644 --- a/Content.Shared/Atmos/Prototypes/GasPrototype.cs +++ b/Content.Shared/Atmos/Prototypes/GasPrototype.cs @@ -1,103 +1,122 @@ -using Content.Shared.Chemistry.Reagent; +using Content.Shared.CCVar; +using Content.Shared.Chemistry.Reagent; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Utility; -namespace Content.Shared.Atmos.Prototypes +namespace Content.Shared.Atmos.Prototypes; + +/// +/// Prototype defining a gas for atmospherics. +/// +/// +/// The total number of gases is hardcoded in a bunch of places. +/// If you add any new ones, make sure to also adjust the constants in accordingly. +/// +[Prototype] +public sealed partial class GasPrototype : IPrototype { - [Prototype] - public sealed partial class GasPrototype : IPrototype - { - [DataField("name")] public string Name { get; set; } = ""; + // TODO: Add interfaces for gas behaviours e.g. breathing, burning - // TODO: Control gas amount necessary for overlay to appear - // TODO: Add interfaces for gas behaviours e.g. breathing, burning + /// + [IdDataField] + public string ID { get; private set; } = default!; - [ViewVariables] - [IdDataField] - public string ID { get; private set; } = default!; + /// + /// The name of the gas as shown to the player. + /// + [DataField(required: true)] + public LocId Name; - /// - /// Specific heat for gas. - /// - [DataField("specificHeat")] - public float SpecificHeat { get; private set; } + /// + /// The abbreviation of the name. For example O₂ for Oxygen. + /// Used for UI purposes. + /// + [DataField(required: true)] + public LocId Abbreviation; - /// - /// Heat capacity ratio for gas - /// - [DataField("heatCapacityRatio")] - public float HeatCapacityRatio { get; private set; } = 1.4f; + /// + /// The molar heat capacity of this gas, in J/(K * mol). + /// Describes how much heat energy is needed to heat up this gas by one Kelvin. + /// Or in other words, the higher this number is the more energy this gas can store. + /// + /// + /// This will be divided by the cvar. + /// + [DataField] + public float MolarHeatCapacity; - /// - /// Molar mass of gas - /// - [DataField("molarMass")] - public float MolarMass { get; set; } = 1f; + /// + /// Heat capacity ratio for gas. + /// TODO: Make gas pumps do proper adiabatic compression so that this is actually used. + /// + [DataField] + public float HeatCapacityRatio = 1.4f; + + /// + /// Molar mass of the gas. + /// TODO: This is not used anywhere, do we even need this? + /// + [DataField] + public float MolarMass = 1f; - /// - /// Minimum amount of moles for this gas to be visible. - /// - [DataField("gasMolesVisible")] - public float GasMolesVisible { get; private set; } = 0.25f; + /// + /// Minimum amount of moles for this gas to be visible. + /// + [DataField] + public float GasMolesVisible = 0.25f; - /// - /// Visibility for this gas will be max after this value. - /// - public float GasMolesVisibleMax => GasMolesVisible * GasVisibilityFactor; + /// + /// Visibility for this gas will be max after this value. + /// + [ViewVariables] + public float GasMolesVisibleMax => GasMolesVisible * GasVisibilityFactor; - [DataField("gasVisbilityFactor")] - public float GasVisibilityFactor = Atmospherics.FactorGasVisibleMax; + /// + /// Multiplier that decides when a gas will be at maximum visibility. + /// + [DataField] + public float GasVisibilityFactor = Atmospherics.FactorGasVisibleMax; - /// - /// If this reagent is in gas form, this is the path to the overlay that will be used to make the gas visible. - /// - [DataField("gasOverlayTexture")] - public string GasOverlayTexture { get; private set; } = string.Empty; + /// + /// Sprite to show in the gas overlay if this gas is present on a tile. + /// If null the gas will be invisible. + /// + [DataField] + public SpriteSpecifier? GasOverlaySprite; - /// - /// If this reagent is in gas form, this will be the path to the RSI sprite that will be used to make the gas visible. - /// - [DataField("gasOverlayState")] - public string GasOverlayState { get; set; } = string.Empty; + /// + /// The reagent that this gas will turn into when inhaled or condensed. + /// + [DataField] + public ProtoId? Reagent; - /// - /// State for the gas RSI overlay. - /// - [DataField("gasOverlaySprite")] - public string GasOverlaySprite { get; set; } = string.Empty; + /// + /// The color of the gas used for UI purposes. + /// + [DataField] + public Color Color = Color.White; - /// - /// Path to the tile overlay used when this gas appears visible. - /// - [DataField("overlayPath")] - public string OverlayPath { get; private set; } = string.Empty; + /// + /// The price per mole when this gas is sold at cargo. + /// The final price will also depend on the purity of the gas mixture. + /// + [DataField] + public float PricePerMole = 0; - /// - /// The reagent that this gas will turn into when inhaled. - /// - [DataField("reagent", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string? Reagent { get; private set; } = default!; + /// + /// Whether the gas is considered to be flammable. + /// This is used generically across Atmospherics to determine + /// if things like hotspots are allowed to ignite if an + /// oxidizer is present. + /// + [DataField] + public bool IsFuel; - [DataField("color")] public string Color { get; private set; } = string.Empty; - - [DataField("pricePerMole")] - public float PricePerMole { get; set; } = 0; - - /// - /// Whether the gas is considered to be flammable. - /// This is used generically across Atmospherics to determine - /// if things like hotspots are allowed to ignite if an - /// oxidizer is present. - /// - [DataField] - public bool IsFuel; - - /// - /// Whether the gas is considered to be an oxidizer. - /// Same reasoning as but for oxidizers. - /// - [DataField] - public bool IsOxidizer; - } + /// + /// Whether the gas is considered to be an oxidizer. + /// Same reasoning as but for oxidizers. + /// + [DataField] + public bool IsOxidizer; } diff --git a/Content.Shared/EntityEffects/Effects/Atmos/CreateGasEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Atmos/CreateGasEntityEffect.cs index aa5132e596..9614933247 100644 --- a/Content.Shared/EntityEffects/Effects/Atmos/CreateGasEntityEffect.cs +++ b/Content.Shared/EntityEffects/Effects/Atmos/CreateGasEntityEffect.cs @@ -30,6 +30,6 @@ public sealed partial class CreateGas : EntityEffectBase return Loc.GetString("entity-effect-guidebook-create-gas", ("chance", Probability), ("moles", Moles), - ("gas", gasProto.Name)); + ("gas", Loc.GetString(gasProto.Name))); } } diff --git a/Content.Shared/Medical/Cryogenics/CryoPodComponent.cs b/Content.Shared/Medical/Cryogenics/CryoPodComponent.cs index 386d0989d9..6dbe11776d 100644 --- a/Content.Shared/Medical/Cryogenics/CryoPodComponent.cs +++ b/Content.Shared/Medical/Cryogenics/CryoPodComponent.cs @@ -127,7 +127,7 @@ public enum CryoPodUiKey : byte [Serializable, NetSerializable] public sealed class CryoPodUserMessage : BoundUserInterfaceMessage { - public GasAnalyzerComponent.GasMixEntry GasMix; + public GasMixEntry GasMix; public HealthAnalyzerUiState Health; public FixedPoint2? BeakerCapacity; public List? Beaker; @@ -135,7 +135,7 @@ public sealed class CryoPodUserMessage : BoundUserInterfaceMessage public bool HasDamage; public CryoPodUserMessage( - GasAnalyzerComponent.GasMixEntry gasMix, + GasMixEntry gasMix, HealthAnalyzerUiState health, FixedPoint2? beakerCapacity, List? beaker, diff --git a/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl b/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl index a2cb5301b2..57ed361248 100644 --- a/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl +++ b/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl @@ -11,7 +11,6 @@ gas-analyzer-window-tab-title-capitalized = {CAPITALIZE($title)} gas-analyzer-window-refresh-button = Refresh gas-analyzer-window-no-data = No Data gas-analyzer-window-no-gas-text = No Gases -gas-analyzer-window-error-text = Error: {$errorText} gas-analyzer-window-volume-text = Volume: gas-analyzer-window-volume-val-text = {$volume} L gas-analyzer-window-pressure-text = Pressure: diff --git a/Resources/Locale/en-US/atmos/gases.ftl b/Resources/Locale/en-US/atmos/gases.ftl index 5c540c46df..b5d9ed6aae 100644 --- a/Resources/Locale/en-US/atmos/gases.ftl +++ b/Resources/Locale/en-US/atmos/gases.ftl @@ -1,10 +1,18 @@ -gas-ammonia-abbreviation = NH₃ -gas-carbon-dioxide-abbreviation = CO₂ -gas-frezon-abbreviation = F -gas-nitrogen-abbreviation = N₂ -gas-nitrous-oxide-abbreviation = N₂O +gas-oxygen = Oxygen gas-oxygen-abbreviation = O₂ +gas-nitrogen = Nitrogen +gas-nitrogen-abbreviation = N₂ +gas-carbon-dioxide = Carbon Dioxide +gas-carbon-dioxide-abbreviation = CO₂ +gas-plasma = Plasma gas-plasma-abbreviation = P +gas-tritium = Tritium gas-tritium-abbreviation = T +gas-water-vapor = Water Vapor gas-water-vapor-abbreviation = H₂O -gas-unknown-abbreviation = X +gas-ammonia = Ammonia +gas-ammonia-abbreviation = NH₃ +gas-nitrous-oxide = Nitrous Oxide +gas-nitrous-oxide-abbreviation = N₂O +gas-frezon = Frezon +gas-frezon-abbreviation = F diff --git a/Resources/Locale/en-US/gases/gases.ftl b/Resources/Locale/en-US/gases/gases.ftl deleted file mode 100644 index e41aa4fc99..0000000000 --- a/Resources/Locale/en-US/gases/gases.ftl +++ /dev/null @@ -1,9 +0,0 @@ -gases-oxygen = Oxygen -gases-nitrogen = Nitrogen -gases-co2 = Carbon Dioxide -gases-plasma = Plasma -gases-tritium = Tritium -gases-water-vapor = Water Vapor -gases-ammonia = Ammonia -gases-n2o = Nitrous Oxide -gases-frezon = Frezon diff --git a/Resources/Prototypes/Atmospherics/gases.yml b/Resources/Prototypes/Atmospherics/gases.yml index f27bab9074..2108b68e53 100644 --- a/Resources/Prototypes/Atmospherics/gases.yml +++ b/Resources/Prototypes/Atmospherics/gases.yml @@ -1,105 +1,119 @@ - type: gas id: Oxygen - name: gases-oxygen - specificHeat: 20 + name: gas-oxygen + abbreviation: gas-oxygen-abbreviation + molarHeatCapacity: 20 heatCapacityRatio: 1.4 molarMass: 32 - color: 2887E8 + color: '#2887E8' reagent: Oxygen pricePerMole: 0 isOxidizer: true - type: gas id: Nitrogen - name: gases-nitrogen - specificHeat: 30 + name: gas-nitrogen + abbreviation: gas-nitrogen-abbreviation + molarHeatCapacity: 30 heatCapacityRatio: 1.4 molarMass: 28 - color: DA1010 + color: '#DA1010' reagent: Nitrogen pricePerMole: 0 - type: gas id: CarbonDioxide - name: gases-co2 - specificHeat: 30 + name: gas-carbon-dioxide + abbreviation: gas-carbon-dioxide-abbreviation + molarHeatCapacity: 30 heatCapacityRatio: 1.3 molarMass: 44 - color: 4e4e4e + color: '#4e4e4e' reagent: CarbonDioxide pricePerMole: 0 - type: gas id: Plasma - name: gases-plasma - specificHeat: 200 + name: gas-plasma + abbreviation: gas-plasma-abbreviation + molarHeatCapacity: 200 heatCapacityRatio: 1.7 molarMass: 120 - gasOverlaySprite: /Textures/Effects/atmospherics.rsi - gasOverlayState: plasma - color: FF3300 + gasOverlaySprite: + sprite: /Textures/Effects/atmospherics.rsi + state: plasma + color: '#FF3300' reagent: Plasma pricePerMole: 0 isFuel: true - type: gas id: Tritium - name: gases-tritium - specificHeat: 10 + name: gas-tritium + abbreviation: gas-tritium-abbreviation + molarHeatCapacity: 10 heatCapacityRatio: 1.3 molarMass: 6 - gasOverlaySprite: /Textures/Effects/atmospherics.rsi - gasOverlayState: tritium - color: 13FF4B + gasOverlaySprite: + sprite: /Textures/Effects/atmospherics.rsi + state: tritium + color: '#13FF4B' reagent: Tritium pricePerMole: 2.5 isFuel: true - type: gas id: WaterVapor - name: gases-water-vapor - specificHeat: 40 + name: gas-water-vapor + abbreviation: gas-water-vapor-abbreviation + molarHeatCapacity: 40 heatCapacityRatio: 1.33 molarMass: 18 - gasOverlaySprite: /Textures/Effects/atmospherics.rsi - gasOverlayState: water_vapor - color: bffffd + gasOverlaySprite: + sprite: /Textures/Effects/atmospherics.rsi + state: water_vapor + color: '#bffffd' reagent: Water pricePerMole: 0 - type: gas id: Ammonia - name: gases-ammonia - specificHeat: 20 + name: gas-ammonia + abbreviation: gas-ammonia-abbreviation + molarHeatCapacity: 20 heatCapacityRatio: 1.4 molarMass: 44 - gasOverlaySprite: /Textures/Effects/atmospherics.rsi - gasOverlayState: miasma + gasOverlaySprite: + sprite: /Textures/Effects/atmospherics.rsi + state: miasma gasMolesVisible: 2 - gasVisbilityFactor: 3.5 - color: 56941E + gasVisibilityFactor: 3.5 + color: '#56941E' reagent: Ammonia pricePerMole: 0.15 - type: gas id: NitrousOxide - name: gases-n2o - specificHeat: 40 + name: gas-nitrous-oxide + abbreviation: gas-nitrous-oxide-abbreviation + molarHeatCapacity: 40 heatCapacityRatio: 1.3 molarMass: 44 - color: 8F00FF + color: '#8F00FF' reagent: NitrousOxide pricePerMole: 0.1 - type: gas id: Frezon - name: gases-frezon - specificHeat: 600 # Strongest by far + name: gas-frezon + abbreviation: gas-frezon-abbreviation + molarHeatCapacity: 600 # Strongest by far heatCapacityRatio: 1.33 molarMass: 50 - gasOverlaySprite: /Textures/Effects/atmospherics.rsi - gasOverlayState: frezon + gasOverlaySprite: + sprite: /Textures/Effects/atmospherics.rsi + state: frezon gasMolesVisible: 0.6 - color: 3a758c + color: '#3a758c' reagent: Frezon pricePerMole: 1 From 067bd9f43c4dc78d46c9c3644f300b29dd48c335 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Sat, 28 Mar 2026 06:29:55 -0400 Subject: [PATCH 004/126] Fix `TrackingIssueAttribute` blocking orgs/repos containing numbers (#43369) Fix TrackingIssueAttribute blocking orgs/repos containing numbers --- .../Fixtures/Attributes/TrackingIssueAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.IntegrationTests/Fixtures/Attributes/TrackingIssueAttribute.cs b/Content.IntegrationTests/Fixtures/Attributes/TrackingIssueAttribute.cs index 02f9e4f7e4..4acbe8e87b 100644 --- a/Content.IntegrationTests/Fixtures/Attributes/TrackingIssueAttribute.cs +++ b/Content.IntegrationTests/Fixtures/Attributes/TrackingIssueAttribute.cs @@ -28,7 +28,7 @@ public sealed class TrackingIssueAttribute : PropertyAttribute "github.com" ]; - private static readonly Regex GithubStyleIssueMatch = new(@"^\/[a-z\-\$\#]*\/[a-z\-\$\#]*\/(issues|pulls)\/\d*$", + private static readonly Regex GithubStyleIssueMatch = new(@"^\/[a-z\d\-\$\#]*\/[a-z\d\-\$\#]*\/(issues|pulls)\/\d*$", RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.IgnoreCase); public TrackingIssueAttribute([StringSyntax(StringSyntaxAttribute.Uri)] string url) : base(url) From 8e87fd3e58ccbdf4631569b70d9a18a3307d127d Mon Sep 17 00:00:00 2001 From: Gentleman-Bird Date: Sat, 28 Mar 2026 11:01:23 -0400 Subject: [PATCH 005/126] Fix: Make clockwork glass grindable (#43371) make cGlass grindable --- .../Prototypes/Entities/Objects/Materials/Sheets/glass.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Prototypes/Entities/Objects/Materials/Sheets/glass.yml b/Resources/Prototypes/Entities/Objects/Materials/Sheets/glass.yml index 532d0b9cca..70c823b765 100644 --- a/Resources/Prototypes/Entities/Objects/Materials/Sheets/glass.yml +++ b/Resources/Prototypes/Entities/Objects/Materials/Sheets/glass.yml @@ -500,7 +500,7 @@ - !type:DoActsBehavior acts: [ "Destruction" ] - type: Extractable - grindableSolutionName: brassglass + grindableSolutionName: cglass - type: SolutionContainerManager solutions: cglass: From 937b5f955d657a7960f93b14d99a5c0c06beb67f Mon Sep 17 00:00:00 2001 From: PJBot Date: Sat, 28 Mar 2026 15:15:29 +0000 Subject: [PATCH 006/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 9dd07d2c84..7ef90b977a 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: aada - changes: - - message: Cannabis no longer stacks infinitely. Sorry, botanists! - type: Fix - id: 9080 - time: '2025-10-12T01:29:01.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/38412 - author: GovnokradZXC changes: - message: New hair named Pigtail (Over Eye) @@ -4033,3 +4026,10 @@ id: 9591 time: '2026-03-27T23:00:25.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43361 +- author: Gentleman-Bird + changes: + - message: Clockwork Glass is now grindable + type: Fix + id: 9592 + time: '2026-03-28T15:14:21.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43371 From e0eae8628f7a4a80efeb627f494fbbb1f8ace88b Mon Sep 17 00:00:00 2001 From: Minerva <218184747+mnva0@users.noreply.github.com> Date: Sat, 28 Mar 2026 11:49:03 -0400 Subject: [PATCH 007/126] Makes singularities respect reduced motion option (#43362) * Makes singularities respect reduced motion option * This seems better * Apply suggestion from @slarticodefast * Apply suggestion from @slarticodefast * Apply suggestion from @slarticodefast --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --- Content.Client/Singularity/SingularityOverlay.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Content.Client/Singularity/SingularityOverlay.cs b/Content.Client/Singularity/SingularityOverlay.cs index 39b0758bf1..fd888fb6dd 100644 --- a/Content.Client/Singularity/SingularityOverlay.cs +++ b/Content.Client/Singularity/SingularityOverlay.cs @@ -1,9 +1,11 @@ +using System.Numerics; +using Content.Shared.CCVar; using Content.Shared.Singularity.Components; using Robust.Client.Graphics; using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.Prototypes; -using System.Numerics; namespace Content.Client.Singularity { @@ -13,6 +15,7 @@ namespace Content.Client.Singularity [Dependency] private readonly IEntityManager _entMan = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IConfigurationManager _configManager = default!; private SharedTransformSystem? _xformSystem = null; /// @@ -28,6 +31,8 @@ namespace Content.Client.Singularity private readonly ShaderInstance _shader; + private bool _reducedMotion; + public SingularityOverlay() { IoCManager.InjectDependencies(this); @@ -35,6 +40,8 @@ namespace Content.Client.Singularity _shader.SetParameter("maxDistance", MaxDistance * EyeManager.PixelsPerMeter); _entMan.EventBus.SubscribeEvent(EventSource.Local, this, OnProjectFromScreenToMap); ZIndex = 101; // Should be drawn after the placement overlay so admins placing items near the singularity can tell where they're going. + + _configManager.OnValueChanged(CCVars.ReducedMotion, (b) => { _reducedMotion = b; }, invokeImmediately: true); } private readonly Vector2[] _positions = new Vector2[MaxCount]; @@ -44,6 +51,8 @@ namespace Content.Client.Singularity protected override bool BeforeDraw(in OverlayDrawArgs args) { + if (_reducedMotion) + return false; if (args.Viewport.Eye == null) return false; if (_xformSystem is null && !_entMan.TrySystem(out _xformSystem)) @@ -102,6 +111,8 @@ namespace Content.Client.Singularity /// private void OnProjectFromScreenToMap(ref PixelToMapEvent args) { // Mostly copypasta from the singularity shader. + if (_reducedMotion) + return; if (args.Viewport.Eye == null) return; var maxDistance = MaxDistance * EyeManager.PixelsPerMeter; From 54bbdde221d392146977a98e8138f86131867e69 Mon Sep 17 00:00:00 2001 From: PJBot Date: Sat, 28 Mar 2026 16:02:24 +0000 Subject: [PATCH 008/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 7ef90b977a..91cb9a8c82 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: GovnokradZXC - changes: - - message: New hair named Pigtail (Over Eye) - type: Add - id: 9081 - time: '2025-10-12T05:45:59.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/39850 - author: Princess-Cheeseballs changes: - message: Meat Kudzu gasps less often. @@ -4033,3 +4026,11 @@ id: 9592 time: '2026-03-28T15:14:21.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43371 +- author: Minerva + changes: + - message: Singularity distortion effects now respect the reduced motion accessibility + option. + type: Tweak + id: 9593 + time: '2026-03-28T16:01:16.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43362 From a87a4d3fb3f27983e01d15d1aae053a26d8609c6 Mon Sep 17 00:00:00 2001 From: Marchy <89603088+M4rchy-S@users.noreply.github.com> Date: Sat, 28 Mar 2026 23:06:30 +0100 Subject: [PATCH 009/126] "Reduce motion of visual effects" now works for blood loss effect (#41878) * "Reduce motion of visual effects" now works for blood loss effect * Improve strenght * Make motion less strong * Stop motion effect * cleanup and rebalancing * fix warning --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --- Content.Client/Drunk/DrunkOverlay.cs | 37 +++++++++++++++++++-------- Content.Client/Drunk/DrunkSystem.cs | 6 +++++ Resources/Textures/Shaders/drunk.swsl | 29 +++++++++++++-------- 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/Content.Client/Drunk/DrunkOverlay.cs b/Content.Client/Drunk/DrunkOverlay.cs index 692232776a..44d82f3794 100644 --- a/Content.Client/Drunk/DrunkOverlay.cs +++ b/Content.Client/Drunk/DrunkOverlay.cs @@ -1,8 +1,8 @@ +using Content.Shared.CCVar; using Content.Shared.Drunk; -using Content.Shared.StatusEffect; -using Content.Shared.StatusEffectNew; using Robust.Client.Graphics; using Robust.Client.Player; +using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.Prototypes; using Robust.Shared.Timing; @@ -11,19 +11,23 @@ namespace Content.Client.Drunk; public sealed class DrunkOverlay : Overlay { - private static readonly ProtoId Shader = "Drunk"; + private static readonly ProtoId DrunkShader = "Drunk"; [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IEntitySystemManager _sysMan = default!; [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IConfigurationManager _configManager = default!; + private readonly Shared.StatusEffectNew.StatusEffectsSystem _statusEffectsSystem; public override OverlaySpace Space => OverlaySpace.WorldSpace; public override bool RequestScreenTexture => true; private readonly ShaderInstance _drunkShader; public float CurrentBoozePower = 0.0f; + // Starting phase for the rotation effect. + // Needed so it doesn't always look the same for 0 motion. + public float Phase = 0f; private const float VisualThreshold = 10.0f; private const float PowerDivisor = 250.0f; @@ -37,12 +41,22 @@ public sealed class DrunkOverlay : Overlay private const float BoozePowerScale = 8f; - private float _visualScale = 0; + private float _visualScale = 0f; + private float _timeScale = 1f; + private float _distortionScale = 1f; public DrunkOverlay() { IoCManager.InjectDependencies(this); - _drunkShader = _prototypeManager.Index(Shader).InstanceUnique(); + _statusEffectsSystem = _entityManager.System(); + _drunkShader = _prototypeManager.Index(DrunkShader).InstanceUnique(); + _configManager.OnValueChanged(CCVars.ReducedMotion, OnReducedMotionChanged, invokeImmediately: true); + } + + private void OnReducedMotionChanged(bool reducedMotion) + { + _timeScale = reducedMotion ? 0.0f : 1.0f; + _distortionScale = reducedMotion ? 4.0f : 1.0f; // Make the offset stronger to compensate the lack of motion. } protected override void FrameUpdate(FrameEventArgs args) @@ -53,15 +67,14 @@ public sealed class DrunkOverlay : Overlay if (playerEntity == null) return; - var statusSys = _sysMan.GetEntitySystem(); - if (!statusSys.TryGetMaxTime(playerEntity.Value, out var status)) + if (!_statusEffectsSystem.TryGetMaxTime(playerEntity.Value, out var status)) return; var time = status.Item2; - var power = time == null ? MaxBoozePower : (float) Math.Min((time - _timing.CurTime).Value.TotalSeconds, MaxBoozePower); + var power = time == null ? MaxBoozePower : (float)Math.Min((time - _timing.CurTime).Value.TotalSeconds, MaxBoozePower); - CurrentBoozePower += BoozePowerScale * (power - CurrentBoozePower) * args.DeltaSeconds / (power+1); + CurrentBoozePower += BoozePowerScale * (power - CurrentBoozePower) * args.DeltaSeconds / (power + 1); } protected override bool BeforeDraw(in OverlayDrawArgs args) @@ -82,8 +95,12 @@ public sealed class DrunkOverlay : Overlay return; var handle = args.WorldHandle; + _drunkShader.SetParameter("SCREEN_TEXTURE", ScreenTexture); _drunkShader.SetParameter("boozePower", _visualScale); + _drunkShader.SetParameter("timeScale", _timeScale); + _drunkShader.SetParameter("distortionScale", _distortionScale); + _drunkShader.SetParameter("phase", Phase); handle.UseShader(_drunkShader); handle.DrawRect(args.WorldBounds, Color.White); handle.UseShader(null); diff --git a/Content.Client/Drunk/DrunkSystem.cs b/Content.Client/Drunk/DrunkSystem.cs index c5fab75e7d..2e1f8157aa 100644 --- a/Content.Client/Drunk/DrunkSystem.cs +++ b/Content.Client/Drunk/DrunkSystem.cs @@ -3,6 +3,7 @@ using Content.Shared.StatusEffectNew; using Robust.Client.Graphics; using Robust.Client.Player; using Robust.Shared.Player; +using Robust.Shared.Random; namespace Content.Client.Drunk; @@ -10,6 +11,7 @@ public sealed class DrunkSystem : SharedDrunkSystem { [Dependency] private readonly IPlayerManager _player = default!; [Dependency] private readonly IOverlayManager _overlayMan = default!; + [Dependency] private readonly IRobustRandom _random = default!; private DrunkOverlay _overlay = default!; @@ -29,7 +31,10 @@ public sealed class DrunkSystem : SharedDrunkSystem private void OnStatusApplied(Entity entity, ref StatusEffectAppliedEvent args) { if (!_overlayMan.HasOverlay()) + { + _overlay.Phase = _random.NextFloat(MathF.Tau); // random starting phase for movement effect _overlayMan.AddOverlay(_overlay); + } } private void OnStatusRemoved(Entity entity, ref StatusEffectRemovedEvent args) @@ -47,6 +52,7 @@ public sealed class DrunkSystem : SharedDrunkSystem private void OnPlayerAttached(Entity entity, ref StatusEffectRelayedEvent args) { _overlayMan.AddOverlay(_overlay); + } private void OnPlayerDetached(Entity entity, ref StatusEffectRelayedEvent args) diff --git a/Resources/Textures/Shaders/drunk.swsl b/Resources/Textures/Shaders/drunk.swsl index c2657c18a5..1410baa823 100644 --- a/Resources/Textures/Shaders/drunk.swsl +++ b/Resources/Textures/Shaders/drunk.swsl @@ -1,22 +1,29 @@ uniform sampler2D SCREEN_TEXTURE; uniform highp float boozePower; -const highp float TimeScale = 0.5; -const highp float DistortionScale = 0.01; +// How fast to do the rotating motion. +// 1 for normal effect. +// 0 for for no motion. +uniform highp float timeScale; +// Starting phase for the rotation effect. +// Needed so it doesn't always look the same for 0 motion. +uniform highp float phase; +// Multiplier for the amplitude of the offset. 1 for normal strength. +uniform highp float distortionScale; void fragment() { - highp float mod = mix(0.0, DistortionScale, boozePower); + highp float amplitudeMod = mix(0.0, distortionScale * 0.01, boozePower); highp vec2 coord = FRAGCOORD.xy * SCREEN_PIXEL_SIZE.xy; - highp float time = TIME * TimeScale; + highp float time = TIME * timeScale * 0.5 + phase; - highp vec2 offset = vec2((mod * 1.5) * sin(time * 1.5), (mod * 2.0) * cos(time * 1.5 - 0.2)); + highp vec2 offset = vec2((amplitudeMod * 1.5) * sin(time * 1.5), (amplitudeMod * 2.0) * cos(time * 1.5 - 0.2)); highp vec4 tex1 = zTextureSpec(SCREEN_TEXTURE, coord + offset); - - if (boozePower > 0.5) { - offset = vec2((mod * 2.0 - DistortionScale) * sin(time * 0.333 - 0.2), (mod * 2.0 - DistortionScale) * cos(time * 0.333)); - tex1 = mix(tex1, zTextureSpec(SCREEN_TEXTURE, coord + offset), mix(0.0, 0.3, boozePower*2.0-1.0)); + + if (boozePower > 0.25) { + offset = vec2((amplitudeMod * 2.0) * sin(time * 0.333 - 0.2), (amplitudeMod * 2.0) * cos(time * 0.333)); + tex1 = mix(tex1, zTextureSpec(SCREEN_TEXTURE, coord + offset), mix(0.0, 0.3, boozePower * 2.0 - 0.5)); } - - offset = vec2((mod * 1.0) * sin(time * 1.0 + 0.1), (mod * 1.0) * cos(time * 1.0)); + + offset = vec2(amplitudeMod * sin(time * 1.0 + 0.1), amplitudeMod * cos(time * 1.0)); COLOR = mix(tex1, zTextureSpec(SCREEN_TEXTURE, coord + offset), mix(0.0, 0.5, boozePower)); } From 9598347e81efcac3baeb704e980f8621f8ca696a Mon Sep 17 00:00:00 2001 From: PJBot Date: Sat, 28 Mar 2026 22:20:27 +0000 Subject: [PATCH 010/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 91cb9a8c82..f56657827b 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Princess-Cheeseballs - changes: - - message: Meat Kudzu gasps less often. - type: Tweak - id: 9082 - time: '2025-10-12T10:47:30.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/39304 - author: PJB3005 changes: - message: The patrons list in the in-game credits works again. @@ -4034,3 +4027,11 @@ id: 9593 time: '2026-03-28T16:01:16.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43362 +- author: M4rchy-S + changes: + - message: '"Reduce motion of visual effects" now works for drunk and blood loss + status' + type: Add + id: 9594 + time: '2026-03-28T22:19:17.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/41878 From 1f24878cf6d5e7d7cdcaeabfffa67fcd0ec25103 Mon Sep 17 00:00:00 2001 From: InsoPL Date: Sun, 29 Mar 2026 04:23:40 +0200 Subject: [PATCH 011/126] Heat distortion shader (#42973) * revert of the revert * tests * changes * more fun * test * ccvvvar * works but bad * now its better * more fixes * more cleanup * cleaning * last fixes before move to glasses activ * x * glasses only * working * fix toolbox * cleanup * ThermalByte added * small fix * small optimalisations * float bux fix * comments add * more comments * more comments * last fix * revert cvar delete * wrong blue shades * cvar refactor * Update Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * Update Content.Client/Atmos/Overlays/GasTileDangerousTemperatureOverlay.cs Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * tweak to TryGetTemperature comment * Factors are now const * renames * Interface for ThermalByte * tile color vaccum and more comments * saving yeeted * integration test * rename and cleanup * fix * cleanup * switch * UT fix (hopefully) * small bug+ rename * vaccum limit + space is now invalid * typo * typo * fix * init, works * move method around * renames and split * renames * heatblur * cleanup * more docs * resource cache * No dynamic perlin noise generation [no fun allowed] and new blur softening method * magic numbers and rename * misc cleanup * removed init values because display compat mode broken * misc cleanup --------- Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> --- .../GasTileHeatBlurOverlaySystem.cs | 30 ++ .../Atmos/Overlays/GasTileHeatBlurOverlay.cs | 261 ++++++++++++++++++ Resources/Prototypes/Shaders/shaders.yml | 5 + .../Effects/HeatBlur/perlin_noise.png | Bin 0 -> 77641 bytes .../Textures/Effects/HeatBlur/soft_circle.png | Bin 0 -> 1616 bytes Resources/Textures/Shaders/heatBlur.swsl | 34 +++ Tools/generate_heat_distortion_textures.py | 54 ++++ 7 files changed, 384 insertions(+) create mode 100644 Content.Client/Atmos/EntitySystems/GasTileHeatBlurOverlaySystem.cs create mode 100644 Content.Client/Atmos/Overlays/GasTileHeatBlurOverlay.cs create mode 100644 Resources/Textures/Effects/HeatBlur/perlin_noise.png create mode 100644 Resources/Textures/Effects/HeatBlur/soft_circle.png create mode 100644 Resources/Textures/Shaders/heatBlur.swsl create mode 100644 Tools/generate_heat_distortion_textures.py diff --git a/Content.Client/Atmos/EntitySystems/GasTileHeatBlurOverlaySystem.cs b/Content.Client/Atmos/EntitySystems/GasTileHeatBlurOverlaySystem.cs new file mode 100644 index 0000000000..a80208620b --- /dev/null +++ b/Content.Client/Atmos/EntitySystems/GasTileHeatBlurOverlaySystem.cs @@ -0,0 +1,30 @@ +using Content.Client.Atmos.Overlays; +using JetBrains.Annotations; +using Robust.Client.Graphics; + +namespace Content.Client.Atmos.EntitySystems; + +/// +/// System responsible for rendering heat distortion using . +/// +[UsedImplicitly] +public sealed class GasTileHeatBlurOverlaySystem : EntitySystem +{ + [Dependency] private readonly IOverlayManager _overlayMan = default!; + + private GasTileHeatBlurOverlay _gasTileHeatBlurOverlay = default!; + + public override void Initialize() + { + base.Initialize(); + + _gasTileHeatBlurOverlay = new GasTileHeatBlurOverlay(); + _overlayMan.AddOverlay(_gasTileHeatBlurOverlay); + } + + public override void Shutdown() + { + base.Shutdown(); + _overlayMan.RemoveOverlay(); + } +} diff --git a/Content.Client/Atmos/Overlays/GasTileHeatBlurOverlay.cs b/Content.Client/Atmos/Overlays/GasTileHeatBlurOverlay.cs new file mode 100644 index 0000000000..2f540b6360 --- /dev/null +++ b/Content.Client/Atmos/Overlays/GasTileHeatBlurOverlay.cs @@ -0,0 +1,261 @@ +using Content.Client.Atmos.EntitySystems; +using Content.Client.Graphics; +using Content.Client.Resources; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; +using Content.Shared.CCVar; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Shared.Configuration; +using Robust.Shared.Enums; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Prototypes; +using System.Numerics; +using Color = Robust.Shared.Maths.Color; +using Texture = Robust.Client.Graphics.Texture; + +namespace Content.Client.Atmos.Overlays; + +/// +/// Overlay responsible for rendering heat distortion shader. +/// +public sealed class GasTileHeatBlurOverlay : Overlay +{ + public override bool RequestScreenTexture { get; set; } = true; + private static readonly ProtoId UnshadedShader = "unshaded"; + private static readonly ProtoId HeatOverlayShader = "HeatBlur"; + + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IClyde _clyde = default!; + [Dependency] private readonly IConfigurationManager _configManager = default!; + [Dependency] private readonly IResourceCache _resourceCache = default!; + + private readonly SharedTransformSystem _xformSys; + private readonly ShaderInstance _shader; + + private readonly Texture _noiseTexture; + private readonly Texture _heatGradientTexture; + private List> _intersectingGrids = new(); + private readonly OverlayResourceCache _resources = new(); + + // Overlay settings + private const float + ShaderSpilling = 2.5f; // for example 4f - spills shader one tile from hotspot, 2.5f - spills it half tile + + private const float ShaderStrength = 0.04f; // Makes waves stronger + private const float ShaderScale = 1f; // Makes more waves + private const float ShaderSpeed = 0.4f; // Makes waves run faster + + // Overlay settings for reduced motion setting + private const float ShaderStrengthForReducedMotion = 0.01f; + private const float ShaderScaleReducedMotion = 0.5f; + private const float ShaderSpeedReducedMotion = 0.25f; + + private const int MinDistortionTemp = 300; // Distortion starts to show up at this temperature in Kelvins + private const int MaxDistortionTemp = 2000; // Maximum distortion strength at this temperature in Kelvins + + public override OverlaySpace Space => OverlaySpace.WorldSpace; + + public GasTileHeatBlurOverlay() + { + IoCManager.InjectDependencies(this); + _xformSys = _entManager.System(); + + _noiseTexture = _resourceCache.GetTexture("/Textures/Effects/HeatBlur/perlin_noise.png"); + _heatGradientTexture = _resourceCache.GetTexture("/Textures/Effects/HeatBlur/soft_circle.png"); + + _shader = _proto.Index(HeatOverlayShader).InstanceUnique(); + _configManager.OnValueChanged(CCVars.ReducedMotion, SetReducedMotion, invokeImmediately: true); + } + + private void SetReducedMotion(bool reducedMotion) + { + _shader.SetParameter("strength_scale", reducedMotion ? ShaderStrengthForReducedMotion : ShaderStrength); + _shader.SetParameter("spatial_scale", reducedMotion ? ShaderScaleReducedMotion : ShaderScale); + _shader.SetParameter("speed_scale", reducedMotion ? ShaderSpeedReducedMotion : ShaderSpeed); + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + if (args.MapId == MapId.Nullspace) + return false; + + var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources()); + + var target = args.Viewport.RenderTarget; + + // Probably the resolution of the game window changed, remake the textures. + if (res.HeatTarget?.Texture.Size != target.Size) + { + res.HeatTarget?.Dispose(); + res.HeatTarget = _clyde.CreateRenderTarget( + target.Size, + new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), + name: nameof(GasTileHeatBlurOverlaySystem)); + } + + if (res.HeatBlurTarget?.Texture.Size != target.Size) + { + res.HeatBlurTarget?.Dispose(); + res.HeatBlurTarget = _clyde.CreateRenderTarget( + target.Size, + new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), + name: $"{nameof(GasTileHeatBlurOverlaySystem)}-blur"); + } + + var overlayQuery = _entManager.GetEntityQuery(); + + args.WorldHandle.UseShader(_proto.Index(UnshadedShader).Instance()); + + var mapId = args.MapId; + var worldAABB = args.WorldAABB; + var worldBounds = args.WorldBounds; + var worldHandle = args.WorldHandle; + var worldToViewportLocal = args.Viewport.GetWorldToLocalMatrix(); + + // If there is no distortion after checking all visible tiles, we can bail early + var anyDistortion = false; + + // We're rendering in the context of the heat target texture, which will encode data as to where and how strong + // the heat distortion will be + args.WorldHandle.RenderInRenderTarget(res.HeatTarget, + () => + { + _intersectingGrids.Clear(); + _mapManager.FindGridsIntersecting(mapId, worldAABB, ref _intersectingGrids); + foreach (var grid in _intersectingGrids) + { + if (!overlayQuery.TryGetComponent(grid.Owner, out var comp)) + continue; + + var gridEntToWorld = _xformSys.GetWorldMatrix(grid.Owner); + var gridEntToViewportLocal = gridEntToWorld * worldToViewportLocal; + + if (!Matrix3x2.Invert(gridEntToViewportLocal, out var viewportLocalToGridEnt)) + continue; + + var uvToUi = Matrix3Helpers.CreateScale(res.HeatTarget.Size.X, -res.HeatTarget.Size.Y); + var uvToGridEnt = uvToUi * viewportLocalToGridEnt; + + // Because we want the actual distortion to be calculated based on the grid coordinates*, we need + // to pass a matrix transformation to go from the viewport coordinates to grid coordinates. + // * (why? because otherwise the effect would shimmer like crazy as you moved around, think + // moving a piece of warped glass above a picture instead of placing the warped glass on the + // paper and moving them together) + _shader.SetParameter("grid_ent_from_viewport_local", uvToGridEnt); + + // Draw commands (like DrawRect) will be using grid coordinates from here + worldHandle.SetTransform(gridEntToViewportLocal); + + // We only care about tiles that fit in these bounds + var floatBounds = worldToViewportLocal.TransformBox(worldBounds).Enlarged(grid.Comp.TileSize); + var localBounds = new Box2i( + (int)MathF.Floor(floatBounds.Left), + (int)MathF.Floor(floatBounds.Bottom), + (int)MathF.Ceiling(floatBounds.Right), + (int)MathF.Ceiling(floatBounds.Top)); + + // for each tile and its gas ---> + foreach (var chunk in comp.Chunks.Values) + { + var enumerator = new GasChunkEnumerator(chunk); + + while (enumerator.MoveNext(out var tileGas)) + { + // Check and make sure the tile is within the viewport/screen + var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y); + if (!localBounds.Contains(tilePosition)) + continue; + + // Get the distortion strength from the temperature and bail if it's not hot enough + var strength = GetHeatDistortionStrength(tileGas.ByteGasTemperature); + if (strength <= 0f) + continue; + + anyDistortion = true; + + // Encode the strength in the red channel + // alpha set to 1 as tile is active + worldHandle.DrawTextureRect( + _heatGradientTexture, + Box2.CenteredAround(tilePosition + grid.Comp.TileSizeHalfVector, + grid.Comp.TileSizeVector * ShaderSpilling), + new Color(strength, 0f, 0f)); + } + } + } + }, + // This clears the buffer to all zero first... + new Color(0, 0, 0, 0)); + + // no distortion, no need to render + if (!anyDistortion) + { + args.WorldHandle.UseShader(null); + args.WorldHandle.SetTransform(Matrix3x2.Identity); + return false; + } + + return true; + } + + protected override void Draw(in OverlayDrawArgs args) + { + var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources()); + + if (ScreenTexture is null || res.HeatTarget is null || res.HeatBlurTarget is null) + return; + + _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture); + _shader.SetParameter("NOISE_TEXTURE", _noiseTexture); + + args.WorldHandle.UseShader(_shader); + args.WorldHandle.DrawTextureRect(res.HeatTarget.Texture, args.WorldBounds); + + args.WorldHandle.UseShader(null); + args.WorldHandle.SetTransform(Matrix3x2.Identity); + } + + protected override void DisposeBehavior() + { + _resources.Dispose(); + + _configManager.UnsubValueChanged(CCVars.ReducedMotion, SetReducedMotion); + base.DisposeBehavior(); + } + + /// + /// Gets the strength of the heat distortion effect based on the temperature of the tile. + /// The strength is a value between 0 and 1, where 0 means no distortion and 1 means maximum distortion. + /// + /// The temperature of the tile. + /// The strength of the heat distortion effect. + /// + private static float GetHeatDistortionStrength(ThermalByte temp) + { + if (!temp.TryGetTemperature(out var kelvinTemp)) + { + return 0f; + } + + var strength = (kelvinTemp - MinDistortionTemp) / (MaxDistortionTemp - MinDistortionTemp); + + return MathHelper.Clamp01(strength); + } + + internal sealed class CachedResources : IDisposable + { + public IRenderTexture? HeatTarget; + public IRenderTexture? HeatBlurTarget; + + public void Dispose() + { + HeatTarget?.Dispose(); + HeatBlurTarget?.Dispose(); + } + } +} diff --git a/Resources/Prototypes/Shaders/shaders.yml b/Resources/Prototypes/Shaders/shaders.yml index 057abf0ac2..bf740ba1f6 100644 --- a/Resources/Prototypes/Shaders/shaders.yml +++ b/Resources/Prototypes/Shaders/shaders.yml @@ -115,3 +115,8 @@ id: Hologram kind: source path: "/Textures/Shaders/hologram.swsl" + +- type: shader + id: HeatBlur + kind: source + path: "/Textures/Shaders/heatBlur.swsl" diff --git a/Resources/Textures/Effects/HeatBlur/perlin_noise.png b/Resources/Textures/Effects/HeatBlur/perlin_noise.png new file mode 100644 index 0000000000000000000000000000000000000000..f1422d52a8818317dd84ebc86cd1be4e68c7b68c GIT binary patch literal 77641 zcmV)iK%&2iP)q4FV7BtiksKwn+{B;TAp%m=oYDi_gKTx;g# z%uf|B7jLBt^(y;aY*?(>9;mtLJ>{gTV`g*8?RibblI;bqm*&nfD$cGh(kuC$e0M&m zT5Rl@xpDf^JZCkb>{tGQnX5xhCdb>UXIRev!PuTuS``ZDAP3aI5Lj&R({s z2iGUg|N8ao|MRQal#MjHKE}_6shmQ+XP#<$=YC`R)YokO9LMIM?N+Q@(-`4@lKeo8&PpYok~B;%Xuy3Rq_Fwd_z)HTrqd&G0V%T^VA>%o^5YI z`!+FhI*%?@hi^+;(+-%P5CD`9zgP^eE;=IO_6Ktm!x zYttlK!l2kGj&b*=9@UUr636bTa|N0#7qL+=QpDDC_NMWyKpUIY{U@RtX##(ikLmhm zueZ6a$ay0!&(!$&{>DCRr2&5Sl&i+F{&srg0eRGYfcYFd3~+ulBxL3Ts{v-`^k-YY zXdgR1!bm3yt0xrfi!L@zuTj13Mo#Sid;-uc?2K?aQo_u4XYgEKy-wn=O>6_Ez=$78_i@ zz+S-SWBoKehspGz>0#f*_l6L%+H(EY*{?8`iPEEbf~w_TsW26gDg&EFqnuHjp_-@z z*C!2zH7`77;z&s%061UiXXkAE4S3JFNe_$FoQqB5kTUgT;e2g?U(I)MXX2VCo9{?~ zvuVC#J*k{`b~Mt~Cd)xpUycMg-`Ty?9B*^L=~K1S>Sbe_9AnoU=&RosakYx~PyNl0 z|35MbK|>ZEN@*t*1gQxd~+p8}})o3+$E*BEe&6IVP_hg#q1ZGB-r zF&kX&sKVITJoTjpRjeP=eLlW6IT=!`nF!0~Tc|)MoexfMcOT>@~<|FGbEMF7;q}M;605Av9 znZ&r5(af>7fx9(XW17c_gt$hHeyRAJdrcE&jZ^vP?An0;4e(FhYJ6LGOo#dhB15ek+E+ZPHWzfK%(rk>!4s3QW7 zd^oI8w*L79pmmmw-!dPS9Ot?MZnTFn8oZXkv~BY29*c=%TWze-cO)80hav#1k#5#j z(vT;2T@^`c$ga7xvBfoUy|#I!Tw#TJ-We#RoOC20<=>o>#{J!W9bYGIC7AUNmZxr*Nt@c$}+Q~5D}yn^^M zdpP!US0C-#O--}&KYs>4nOk!mui>k0TPKu`I~d~fq;3+Qf)^6QNlu)R^ah5MCi zR@uz<97XLtyUxZm0COYBhD2I0=9t%wovMyn^1*Cy#DeWJ%NaHoU+&YB>DyQ6-!3N8 z#-n$B%EvzSo5!i=seNGQ4d~Y|Y3j^0icd{>y!M$`js|Xf*ch_#f-I0Zj zq`Y-xAmzWi?)0G_uIZlpuAZ>rHw56Omhrk;yS3kesLD&`g9EFE1Tmm*Mb-D_)%yK) z(ZlgiDe?W>V|9OgneTWV#o@9e2}djezy(lA+gvblT=6(((Gg}3v(4G%-cuZ$env1_ zP{;XrO(jQ)p8{|t>}S>Jk4m`I0MZyWzMX@1-hg}}gh~{c&pmbZqX;gu!HscqZfd1% zh;4IM`=as)Yp(&?Ex@2I*CQ0TY;u&X*aHT*iBNy{SvJP7;i1%4J1_+`gne zLlVVob9LzSHlWV>g&VseP03D2gd5;@HnnRrd*?CQ<_L|;(+2q2x^90^z5vVSpD5q% zoBF8#+*2_?J#G(`9UHs9CO*Q5$2fMDAqh0+t|8Fk4hNVz?>W)8@6^g{xtcSNPxPI z^IOu7Yu`u$Q`Hx6j_Y4@9p_6L*Z9$1$1$qsroC-I_(?CvYtF|+0-E){0*OxY89yJI z7!knrTyK3_R2+Ue0A%un04Ro&m|9yLvytMlveOl_YRc!lVY=$i-x4?~P<9Q*5tc16 zVjo#CQF_}wL*Ljt>Q(!q8V+7hzO>DXjZwq|ZQeA3M8J}4ns2MG0b-VGcz*-rtd<$c znl>R_I@+2C1%?mICanQTk?qd7}-#JsruhnQH!d-qHffy z`sw`rG_@{^(KcTkf%@fwm;%%gfGN1~n*vh$=4%EM*HpG{)>R~cHI!)!{!GZ}#Q9Bw zRHTX7+Y*2|{`kPY)r;w40KT>V=yN3CmLRo_=cYlUj=8pr#oQZQ_a}`<4K(HPP0md*W(|#f1S<0=Rjtt+jKX#o~$2OdL^T*Vvx0+EU<; zW8HO!MW_EW;Ga0=bSt?bOn*KBz!y`-2mxF82CO!TlNu-z0<_(&?UKd`HiEg%1b*Dy z#L!w2FvmBN;&QG{@V_a5)Bsq6aYRC`&l-~>18i;O7wS&Htnw509*e#K$e$Kh_gnu` z{jvORaxCeb*HSN10NxS+c8+$q9DgbfoEt)xNWs*L*KU2d&2^S1iI}yzzXH&zoM-n` zO|!T&(&6+vA6j3h{3ZZrF&4nqXtgLSiUhhiCnY8ZTm5TbW;3qKo^sh*Y%coC=Ce)R z5RFzaSy-VbAVctXUm@!3Kww~!n&gHAobmG=uPeQ3KU`D2X&%XbCQTfjes;|@AZ3FB zarA`|C}mqB11$lVcFlV%H*WiqI%c|+zwCG;EMFQ>WnWdU=BAosxzI|=iL~Zs&lH|~ z34Ca6O8%{$C`B_2MmN#<7(|q`P4Ld)R(}fvH#X~MOsu{ zC8DF6LYp7TZq@kHIm;WjKIw$x}dZIl_p6TX*{8K(rxtiFh}?3We1cYrqL?rw&!GxUQ24E)4kJ z0DlJFZ4aH-H}ki|jji)dKC#*ozD;doUL3%mzbuY+;Jo=h@6UVgKhmCYzgW{y0mLL! z3{3^E*`y}m%<c2AxHjnkuY6~{1Li8v`~ zas!ElYC@aXY;1f}8iJUby321yZjcac0Hh%$t*G2k;Ln;k+KxI?{<%7vuLJ%JAXF^c z^_o3MvELBpmH?nW1paTcR=UshPhtHY!5{Z$+J|}^Sz|!2$ljm-28iIV0>Cz|VQBci z1y7|r1w%<&C|gp1#f!mKMRsJvIvW624b$n__=@XbGiGCxt~7`3eXExd*l9oWg#nnF z!`5m5M(J<;e1i-qzvn$CZoeDyb>^|Aw;`Ks9_v|7Z?k4g3RMqay@1sak_GqiaI)dd zXXnh;Da^-|7dIr}CkX(X_dn#j`jziDB!J}y1L!$M{l~`y{As@#5upDIKn3vxOeL@j zTw+nK={CX|&<&`$qHOfB`L5uTq%~8M#zut&tZdoFZt76)8yz>2NYK3I#w9RJ0E6o^ z4To>hz z2_ptB=4>ur+w|K8{M9=DBmQBJ>ys^^o^fC`GRekg4Er;2>=&u-=RG6=>c;_peFDHF z1+gUo>KqMsP1nt1@6MJ4maG}LMyTfFS}l1}l6-LU7>F^w^V$YH*giD5weh8Cnrt1G zOW0(Zd}g+%F-k z`x@B^5Y4}mWp>Q-O|RTOXyClIR?gVjcM=95(6jt!b0^q;d z9H#yxDz7C0z)9%z1_(Cd0KeG;2CT{s#l#S}R5M_@n$2Ngh=s4-lejp!!fpY99W(z} z4Dp!hc6Kz4Nr55L(~=&x-mQ){hRlbCaJu!-_Kl=@DIQPB9NNr&Jd+7LzvKsb*dMHhHerWN}rbc&>%{F>^+d@N?~diXQ=N zrKwxC)@Qu?t2iZXPpk+4PTUmWhK0BDn`WfIwOyC#nhgMd?shiHfv*C>4GEY!m28{5 z0%g>R>oOadt%(4z0%m&IIa|xkb89@2R_6_9&3lP>x%DuYSZ=Ga={mEaA@IuXdA-Ta zmQ1N!JJ-~|XhrZvAaP*C>f620=VLef_D}>o1#mb0w|%g2_3QR_Q-|&$2(K&PM}6-2 zdp!LsVBQMrP17&77Uo;)i;Hi{zdr=v$)-5R*?k6h&D^ve0?t++Hbw^W$6oizfU}Ri z&$*8IHa<1Yy3sTBF;T#f2mspY8rSxGUV9t(H>9i?;~H%u0uKBc2{^|rx6dn)&>O&g zx}UE3ZERBd&o$PF&vp0Bps9DI=F9rQ(Z@cXp6}@!OWzdMi&PHYk`L6;)|&%LMUamC z4|)2>-velJ$X!>WK6RLTd)9XSfaM?SyDIno{1PDEK%*Eqp;0M8l&E`^wVrt3hq0obkJQwCmR!Wxm*U2pX^{ZsYZDAV*;Df!$^Q5T54{xee2UxU{hd?Ig2EzApvN!s`~`~Xsi0ze?GCI zJdX+YDAzO;cw7IXFPr>1N*rf7Vm9xxW=hShuhfXAD%1rJny;Y zqJaOlP?UbW?xZOYHIL=;6To)`$m(Y!H$bUK!aQcz<}a|hhk%9zD6)&^>D5STlINX~ z0Y(B6@T2~g)i%!Ti9te(i3KWM-6 zWnO1`T&^*DSs!=gli4D_2#7h~2nK@{hzf`^xJeGQSJ{n$pL3?D-@H}&lCR2kmr(PX zBLU~#+2XD-B9Ope0kUf}j6nVr0dV9-UArLxP2q3F#l~H4L2*N1j*luoR3l=|@rLYd z$xy3D^`mKP0?X8Ew!(VT5%=5bXrBTYRijU3Q^)E0ZC`Il0P0d}{tx+`5VyyQ2%wKM zudo-k#EjXj$jQ`=dp_;`)qr@LBB(b-vYasuuV!NX8OAY zxtsVU@-b~|3I~&I9y9Q7?Nvlj>1qj$o1?}iFl&lG6<~YqmH@c?b>i~qyc+Ot#Km3t zuY>^)vmbp?dD1lCxo+!Yn&Wyy1KPig@@eKn^2dqWGxW>Z;lN+*<+s5M_F^QU ziGlLh&2N0b{lss0RC<0B02f$Q+!!boR3_W&Hwonet2Tmx3G+YMg~tg<=UV65wm8|| zZ06y4B4X@40d~_QH_hl4#9C2cc2n$1w*n#8%ou55Ivnvzb~(S*S`I|hTJE~@vGuFf zm1-5oZivoPVQZzjbKw6J_|LH-g$+SLz05}ifOtQFKhyDTKKWAMpGbi6kJ-N=kt`k! zL3BF)`fq?}lw&t+vo?i^iL+sBO;b@MPO&7eFXmvFpg(yBpcX0x8G@>5}v4@$@UQ6XS|$k-((FIM@IsjzX>C5`j)bl zkpR?BKLKEc&w}Hc5sp&@od|#fhSXpj^GrpHHaeS`es^ulG@XA5ER_v24$2Q^C!5bC z0e~WzPH>#_zm;a)H^qVb9HW|=syXL_ia*N%R@VsnO|5R?FyB)Hb$tfst9gz9Y=D0& ztR5YxH8!!jKG)cj|NKi1W?R!2Tj0eW<^Ym`SxW}k+6m~DFDR@Jvum#3wKcIF>u{u* z_j&*22H`ROW;|{b3p@sG>THu79d*E#qyZb&j2p6oqQKLQ#}^hXYt9N7Rdc{*$&y&v z@5oF`BwD#?#H>*`vX)}@UxB~6=KN^t?%Y>?H6#FiP|bc`W4YJZ-ZbfLb8r2)m8NgC z_9dHC?^ED}PyX(KV!pe6Gmjk*-^8TN6K6m3g{}1+de@mdW6AzXfW;L3NcFc7R;P;O zI~||gxrdJd!-@OQr$cJ!yZP4(!A3nPj1I$OIT0IUwu9w=YGNght{Pya8Vt-Y2Q z9)0YuLDGJqZnl>EcffNI#d>fKYeY`316F7BmQAHVq{3tVu|~vfNdS*y)bX6l8X!An zwm6;m-pXby16fzVPYA$98j_~g+7zU-k;TNV%if($Psw!J6IZJZFtc+8R8ya7?(W>J z&-Ta>f>wWuJ?lwNz`r2?n6E9Ev^}Ef<<6(3AE=sA`!(b9ebM6>L1=tr{rDWaIcL53 zXXqPJk7IEBpMM9OifB&8@lDX#H!L*-fUT_vMk6YDojt9MXp;h4HohSNZNjpoQl!Jh z1Vs@ix9>@xdf$Ly(wp>P!)Xa8>UPEKeA@t4yGPj6+eX#sb&b7gfDP!goMf?MggE)} zL|Rgy>~T4`%?noF>gWDQ0Op!$UpGWy+Nt`i%M}&}JXUc@M8N^)H}OaHIybKc|C@SL zSdjKb;lW4bj{v4(1mBhb%yIbr%2-I}-yotMXZ*bWq)EGk!(w-!=K$YxNKHn-Y<7cDTH1*2i~@Kjw1lE34_vx236dQk+yC zDw~+SDUY_&06x3lJhb4Rv09jdF?WEk2!PYC>}R6xr~P&M=KR!uUH`cC38m>fNgP%+ zIkM@~^P1E7&ldr;f!h#(lYBU40zU$+bILiCY)6~td7D5j;G&&wV_VyAZF6=t`rMu~ z;9=)%t<ngcI(VJ^BR(f zzy23M0!g=+D$bLD>_04>jY1Nj9LFCcuqRm+hz_Z<-hK#Ct%18E; z>qtNX1P1=>#{sSH%=Rt#JKLEr4N*#Kx*TbG(pQNM8w_EXsB(MbZ}Z@e7==Fn`5u6& zP$_cEnt{Mvn6b~zj{w+jVK&X?rU_$Vq3telS`wfH0aP`Hn}nHbr2Fld4frc?R3NSF zV0zNi`UX6h51Xc$Y-0M}APw7mPfdf3abU#!ndfahU5~+WN{_0S#&%~D%lkC1Y1FvJ ze18J|>gm79Q@20O9;IAyae4BPmil80{9gjQRBk)+^2Xk?YYk?rx&IJ=%@eO`)(i|9 z0-%cO=)r%5dE00)Fv?Fy0NB3&sA}w1Ryn5mdFgiCwR|b zDo{&K&i_WH{wDJYb3`3jSnOW{PkR%KbaR{JPR*#9cg|VURQt$uJtZSdAKIesC$L4` zZk^N&a80Gp*~0Sv4Qap)k6AO=0%#%|iI6KDEyy_A)EJdNji5cLXEOVca5a9g7$wkm zuT~UUeS-`+!8O%BG5Z__;XC%^?@+slUGtvv1I2xZj+(oz2_CT~2FQ}15hmvY#xOMwD!#PTh^PBzKyxc zDYd==|D=B-YA;wE-F2pyU26#v?u}cM`JigYt*Ndz&5jWVC5;X?t_5lb9<7cf3e?>{ z`q}Qk>OVLq?R^4oHE)h@$npe#rf1VLKcZr%uu%1BM=(`?X>zaWwP`)XlYjmUkU8R% zJJ4v1ZO5}FK0gIuKN?WJO-Y-}k^nyqV4Kwzp4xa;2<_%8lEwVRcYHbfHwmd&uk*hF z0wu~uM*>nKxRG0Om>TbGL)$#HxIJ=BpV^|s`E|SJP7vsx=xo5u5prh(>pSPXeJR_X z#JCZ+=gh7|#5Ogw1=OVb?b8Vstpb^3|GAeEimk<(Ca$$9iWJ8LM#|5We6IO9osJBo_3)U13En?4 z_W}RfB>8YdINP|p{d2i_j?)v(_6aN1Mw&N$aCvsy+*psf*&nt4Dc;E*H+EiEz&}A> z5x#~1Ca^-VJTDbbHDARi^-zW#il{cd2z7iHzr35)O(ccYOR_QDV^}){hAudf1j zEK)Tg2Nn#x8sJm`U;)J6u4IxLmD}8A<67I<8jb`^8*qHHW=fPf?j!0;nHm6hWG~st z&J`%KqGfxFa}xMZQBMghpQldOSe#$!#e85kDBx%QIe^&ayQ(eM{8T)YJ?vQ7lI*$_ zw)d{ds=UPUX^$A_J5sla7h9L*RYM}NW*kvab8mVE-g7m??D#fBAoYKRYf6X8hsHmL zwK&ejeZK!fCh3g9K(gkfnp9$1Pd1+YQs8vjZSbuyU`UCn&K=8b^*E53*VtSfKZ$@^ zm)V7O<6nPld}<0gfpcmAE?@ANou@dw3H~l-DQ8mSp6|@xO^##pPy6Hia=`XqiSmKj zwXrV&qXPQowe>C8G3#Njq4I;}J<~hyo#SmEo!7SWQoY`iB`1kUw8F>$+Aw{a@AFmv zGa%NG6u7Vrp9Vbvdv)&s7yIb;ZSY4)46%vr)WycMg`r~85P%8%=a(E?67V#xxDFB$ zw!Q-=%-06|(Vr$?7#Ts^m|yJE76<-KlWT<4J8Sq3^i@u~7_eAiOj;k2F#nXOSU6&$ z;)2I2uTsNC0Dj9ycOTb04_hiW>>m23B*EmkaLd`)tRPRW{7~_$AVr#h^*a9WnTh_B!?KZuoy{|~X zJZAT9MfJRSpZUq^gOLEY-Ue8mG}xhP`CII7$}QEyZzc709II+a#helV^&g^?ajsD* zBV3bC>$YRu2DOFQCLR`pN(yDOJ5Qj-M1fulDhRarBLLSJl09l(lVj~aio!T4uub+T zf|~Ys+T!A{u}j@&*Ht5%_cvgqYADsyCU4zdDcf#oLGd#Bch_Kom*9xu<-%#R3O_$#&H97519-hLlImeqi5D_gx=Ug7N;m=8yeVnK3LOTvKR_Pr+e~lWitrKzJV;o@#t5DbmK@H2#L5HT#A7@ufbNW};%1>{E@Q zwTbCcd9?+97E8A0P8|5Ly+&Sk(rrGpv1e<$J>S*^o~u5h>PO|94OwT+~FB-L6=vQn?Y<5q4! zJI8ThMORYYOa8yX;IYpd&T*_4RDTqDh6HO%@T z+ppB8RF6mxms8YOc0CdNmV{5A6j^O>ciP9sAA5eaA7gJAUelNrP|d_wEDXmdX~{yn zNuM$2sUPi~dK?>M33^MU<~v?%UfbXNS2nZ8s2ahX!|Xu<;MQe!sD{5GF=}1c2p!Ng z^O>$hEL(uTAqC1_R;Oys4QWq3M%8Mw53Hf8K6ESUfn^?ViCyZ0^FE7-0x;E!T+OC^ z#%pOD%Pof=tWTwyP`USINK(pe7Y8OPj;vSDa~$K=V74%wbN=uD2G{~SI)cjap3=t* zQ=otDrD9hNb<^maI2OnnoJ!&?_&2gmkcJd6zf;p~NIqp0N%n+}uWR-&+FUY*6dCy~euQk{<^kw`8EPDFN*qm-aFV?#+Bho7MXq zH|=+8;(awo_3Al~jdQxxeO5zCzDa*ed(hUDimMvy@)Or@@>TWs-=6@WBT5uIOoh`F z-_hp^K&?MF`Dvp|ezRhkBtXqUdl*ovglpKQ0zEZ{?E@yqv{$X)kbs6nPQX{60N`A< z|0))aaH%~}xrE~@?Y9j(4JhV`Rw(MQz$R+#zEH-oRa~0|MghQ$I6|U8@JSPQBt!YMZFuv&X}HW!7Y7CYDp46J zQTm=pA}vkBep}k6F>~k!jMly^YGIGzq-cWnO#ps`E^r+ z+jTc%SZ`+_`BYTwRgKMSjs)Du+>6g^{@b^0TKxuMD^)zI5w!eFyxY&X~yX=>C8 zP!m~s(&$~IP=&6VvWf|_O*Pk~YfAzgfNV$r8`CzeZ3Ak@&SE;}TiaS`pUzx^X^2C+ zK9W*J08}g$nNnlZd_|BHSkG&Y1T@DkP7K($``EnZ*p09r_?EC9!NxE<-U8sMlj%*h z`wb70N*Xf9&RK4#b9Jo|+=KIeegyD2|K~>lm|QLR3plo8aPg2SANY zX{~clU3cKG^fC~h_BwxJ4by({+-*h6sFw<;0W? zz&l|zQYz*?a^Ic7FyE0mC(S47xg2#ozdF!^~BY zJJ!A_q%9F($=MXR0{sp6D_hz^Q29LbmgPpXb|NNEqyhi7R@FHkt2$R;=H}pWBP<8z z^^{L+f2O{+AF&*4auQ>yVx;2rlmIBuK)V~zPr0Y$%$s|1+ozPM9CKu#!JTiDBWJB? zgR^fQyIzhyuydsw&r?rU@%i%!K(in^rixP)7z^@T=W{-P@y|8ol%T2rls>%Yz+c5g zt+8c?cLwo0@a7=}$Q3CLRf}owVTJg8sSZKt<(d3q(f_=uhyUHoJYtb2XWoNVW)e(7o)`^*$YyyzzYsS%{2OyHjii~)a2;*B^)iiv~Qrzks8!@seA z(-a*@DRE4F+k_SPGot0<{FL-KNlhcoW9RL>)QH=)-TY0X*__X7j;Jz{r({F8TTpg$ zQ%_RlP5D3H@%X0kCj!yvVtq~3gy6U`kcP!UpXau47cJ$egikBQ{K%F{meG!huuE*wkFKnaQT9o^w?Y zxOTIy12NTX8k?A;eQleRo6G!DK4D`}dp*~1$4Lj9%Y17I00XC{hLUc#*XWBopV!_Z zfJz?&24xqEZxcft`=sx%`5U5@!2gDrqTu(p%&(hIIKb9+eX7-~Fz73g04J$l6bWQ| zx50&WZ<{*%=f456396u3;x&PD4lvzM3+BiTlV)R^X6bCLmGk<=9i|eGml7x|E1$u7p(U&;!$`(9VxwI9Q zSB-D3QQ&#Azf2tXW3DMsSNWXC277{aHYxj+eFeKiV#wA=;`Zo<;JI<~695bXQ&^}!5x9BX zI_fvn3h*5q(3#=#N!2E-hRX|5?zM6UrRN6gZCZO$t~#@^YS4Jfl~4Tv?udiObQ zK}emaT%FgD1SlJsv|x8j0=|leK~;y1UblwIU)3+$*tC89*z(sW0BR8hOicq%pvMfG z2BtX)_)%B0=`02tVR3019MhrVn)ZZ&ztYzvO9ICxX_>uvoaS!<-|1`YWZ>?K_=fnZ z=E}g|t=9n5yyl32vvmu)n0sdo&SAa6fuM zXX_*YJTI5mu1~2wWHGzNgs;mV;k5cxjw&CR-x!;O#klrAp8%-FrkXVa7^RDex`zd) zHUt|010xK2+GIswnCYnBM7|T954s1O8hAphP8F0tuH-jj;UK+Kjr?eU?-6 zI@8}?Z_X77z;P}n&S%!FH?qwmMFh~droM1Kvq$AVt0}b~DbG}|NpWDu=iC7r^P79`9nuJTj8k+;21lnnA^J;9~3M)E1Mn4iEagB?; zJ6+7D&Hk}GX2-5UC~*x%#TDnN8ezII_uZVET$rMA4)T+xM`$I%G8XNex=(Ln%gLKUjdu)VaAh*A_8e&*w6pbt|U#e zotVsPES}CEw-#$$4)8Vwt@k>16NcSb>Nd zuj(0(69_0t<5Mj%!nv_yGZz7&wSUud=Q@gb99{DrkKHv~ugUqZfHSW4mG6n{u>HXo z^8x!gBTJi_I&1v(BLJ3&b5XoD=Q!|JfWQcX3S3IkL@Zc=GkclsY%Mpx5eEMAo_nz| zY2Lig{BR^t>2`jpbF{+&0xL*%JjXP9@MNF1jW4aA2q@}Qjg2)hXA5gew}i#n+|F$Y zb(+uSsGN1p8}B!Ss_988?=dG>|8aS-k(SuFp4O0nX}!+T%h6N==w z^4(!rLjsuJ6N8g|I2Y}j=f9Lw=lv|KfTj`ExY#wv#?t0AcDQ0?$BjSEzf?qc%z#$K zUyXO;lz;A8WB&&HTm23Av*LRKMob#Nk|aW)$mW?l2?&xA1yrdCvU++_TW#FgzAK`| z#wnc&IGTEP?@xV208ivsfqzRXTX8zsbw`4mo{|9Z))!Yh&bP1fN6b&e>1}!!=E(e3 z#Yv)2hwBq_-9MiIVBxsrOkmPT!^Yben~!=K5V3}_AvMa6=?Jx4-7e_xTc48rl#i_ow}at$dQ1j(vZM3@0>fJZeu&&)inm3YMv7$eB1nK z0k?^P8msDRQx|Gom2+typ8`L-{}p<5OA?Ya#4(Ybgnvq0T>lUNRgjKV&*Ris70{uf zH{jop0v4!^ZBtij+IUTE&>Mj72paP>ou@oV1W7T|MsQvN|BYUjpRTzyq_J&=Y%T)< zw!W$>CNA#mydez@NqHh$@M3msb+v%6VwgZ85x+D>+3MCz@p|*VBQFiHm?QwdHdxJJ z>$0At*4oIo`B5ALUCcpzC6a-9*!bz|`EsQhN2VwFNC`j!{8ll$h8vZNq5U(BmINp} z5j;}@I>BU6jgqa&fcysh-F~?OO}Vhi1IJ8VJQd(K4VtZ^atzm==W73zI9+LuYLK|@ z4InDp9nor~=^I-~I&Kb z*R8E=uG?=mcME*(yxC`$e@xd#&f4=yJQ0D`|Fn-Y-?8Sgei58;ou@?f+teNFOSd`6 z>fDh~95;b~`a1tYAd#bqGk^UOU|V?D?6zdUi3;{{dJFmpK=T>{f5jqinz0fF_ze78 zRyTc7=jqxTPjap}8enFPZrgC%wYIuYPt%B>fcJJj^EEw9ZEa?`;*PgDJaykl>O~D0 zH`f}no9t|A7xPV#hgRI!`3V4iB>z_5Lpf_OJi#74?@fQwgXLSce_ix|M&D))1poQ2 zdblIjANlhM00s{m=vx4>Apz$Y&+(g;82GycR*kHQ0ZYtAwuQ=)*)>rYTbBeWL5}V@ zMzM69gYi!Q*X$j$Nl9wlXiM@f<&)F>HW5&H>2$Sg;u;&!$Kxc^{`sEfY~%*O+XlU{ z$K~G?>_KUro1WZ0ATT2Ba!TO_^Wo^3uhtg^{K}{QUm^it4}V7rU-OfI69cygc>L#k z02~W-35o`)`Hbl)$1qUrK8{g#Bw^ss>|{RNJk_Wqy#akDX~Ufjt^PB(2dQGc%p8VyUy2nUHRci^E`LQ=%eZ}Y#&liI9)2P>e`l+Zpo;d zuW|*~c3_ukdG1Y1yl(RNiMU><@@&5UZ`sYH$-6|T=KBGD{PbqVr^W4m{vEJmdkR>b zgQ~Hrrf}Zd7aQ*a>VlDcyh$VlzzK|7(sgXZaf%2eGQxDW;`V2*>2yBVAPzWbe;EUKk7Q*&wdYpk%t-2e+WSH)UgdnYKYqcYea20 zOD>gs7_8d{t?u8Y`g*{H$UlfbK5;n1V`X~0tv-)%2(d&3&IBV25*^TYMrO^&D>XmSDL_%v4=TirttBcFR(-RV-)Z!0+2w4)dsGEPi@Io6>mjK6iH&z^yat8z*(2&4F~f%jJU`%%AOFu!0?sj8|IfbzZh$<2k(EGCp?lGIxy%zl4JvO%? z0chX$VFHg`f5APO^`VILjX2O#jhX3Sb%DBZ|K>aM+4;vn@Wyv$r`wZ6{FHBO{LC>$ zHdQ`O9Sk4P_S>GYB?7L8ZFBUD$B+N#HI`Fn?y-UXJ7AS~Z7`>vr{KjBVg5sexwdM8 zEZ8m)lTFTkmpHeD=;EjBQ9e7ucS8c$T-GSjwx-duYbvRoAlXd5IvY~qZitiflljm{ zYI;)_)&YyFd-B=>e?ugh-EJ=w8Axk78yx6uYeRv*(#v|L@;$Au9+I{_jMMs-#@}^NxAlv0H{wr&?Xm`#>RC07Vw*=c;|asL-iYV{sqTre_Y>EHgD#) zeWAr2<;%JDhX5Rf?Pj>5ZGb5OAZt1k{2dXPy62|`4t$hv?i#yy^iKwsV#3C?v1*6} z`n&~y)TJ84)RBrJ#ke5>=i2l!*{#-fO$}|@#thfmh}-vc+|Ez=;|PEn*EF#6enW;_ zouu4iK3Pp4XZ8Oa1A9GTb#D#&+;{(=%raV)xB!!r`NcAnzp7M(e}M0Ecewl*Z115U2nnv)9(=E9SOM2A+_h} z0k0j?;(_aZn};)211*WeHC=z3=YRbO;3!U;@{|Oip-kA=`LvIOqG_J4QMiq6LExO9 zB4GdOQu((Lj42~m&ejIN9Qe5x#sSx1x|?G+U+t@FaESmkV8Z6HI3|D6y)@@7K>0}X zAkgIQZG&}<)B##s(``<>>y7W|3#*SL3IOn)^1soa{AIRdZY$t#Nx;;_d}MKGazJ6x ztkJI`N80|8dWrH;?alO4#fIroc{h*Gga2;=aFjJq3<^BbT-87vgF^?I4jik1*0E$4 zL=+Wh6a;?+h7_}{B=BeU-t47o7_BZ>bZowBa4k@`|38B1R1IDE;zaq0V^p5Fz1vF4 zKS$5l%5}+O3=e>Qo@mpWpzQA-l3^>xp zCG|kH<{9Vq21WIPsdHYRcFlhrH2+HA_m2RMg308R@wk98Zt7%4ZX~h4w?I`TR01rw zVGd*(n;96ie&SqJ{AsT8TitVd(59r90l+z)_tm~_3YP7^YMLp2PEw(}I6aXjXH>&% z^`Nb(*(Ts*x#Iw!sa?F^0TFafY z4M*GNOZkHHQBre3UF<$KQ^n+xR>ds&sOB=jx=B>jrPfvR9KqV?Ynk+nyZaHqMpO*9I@Gyp25h`r`w8f? zF&iSVA?<7o%ADu)U|iDPY??f?r#V(V1MPc4^pu}X9y8L z)p)eq=|xd7V!tSP+Y?mHI7vN|{TLcQ1^9~qoY(vXwzD7cReSKQ8|UzCXP?}=SKj?Xa@f5e+(;wog<8av7(v!H%|;r;kY81 zzT%k1j=AZWK<9nN`L@`VPs%1|3p;kcx$6!nS&^YVPG2fmXIpyuSGI3-yBJMdnGPlD zN$0)j4)gvY6`|0824Rwa5U(f!y?u34;GTPA{jI*+17* zabj3A-&MT73S#M=_}hMnI+Y{>t#W+&ho1nj-}n3T3BZwgo;C~{Gjq-*j@npup4c|i z&w_U5sh}_)Swm)l#|>cX-8ObrFo^`N1m?=G}*{U_$hR&K?0_~&+b+D#$ z6rFb<)c+sH@6JvLg`81l$STermF#nhsLYUggY13eTal5Gbykt>?7dDIDMwjn6CryX zXPxuA-`}40o+B$MGU!q`XJSfqkHNNIab4lpGg4jxI$x=c7*wAKseRNMr zb%iK^1t)^R6b2fBAc0D&1ODpBzYwL>5dWObslP9&JudZ4ChTvez*ndG4?rm)4p9kesnce1oZ28&5imFpsc_Sxi7Y@Fbv*r=A6hyA zo=kczar-y;{+%7Q8;F=|^BCQN%;i*bh2SlR1$ZcE!i2!Wh@jFO^NKp^mS;5?N<)pl zUcHco-gh*uy*gu!YL?CZty*!#9m|Z`Qz&ju21-0_-mN@D5O^wR+WXo)?@)$Nr-g(7 zGC>x2ha>MP2K4R3h+$Ls?OL99Q=<8eiGU{&^11xV%Ap7v?a;VzR_!bsZu ztUZz5z4PL1i{!X;6sLju(V&MsxYgzm#n%(j@q_*0u34wy+3z;b!DlE6lU7;WT~ehW zkk4hw*$9L)0Is37!e1{8B0lumUQL(!J>KAiH}3>2)(SVc@r%ob%vS&h_1lQ4E@5avTo^2le9JjPkD1& zr5!In72Ai@NbM|n260L^cp%nrYK=#{yS~9-IK-8Xnb8U|o2%mW8Rp<0jjr!@|FPWi zLs~TYSTV3`93I@Tj=3~5(!_FHbgaDVuQ>C;@}aI6yS^X(q|}8%e1db?T^);rsD7g` zvC=4y-JX=EcV2%1G;P4kBfi-KQaO|p#p|R*Z($R0^T0S^;D{JsLf^k{x+yuSvwQnv}#C z%FGk5UZ=9?ZoFLo!>hq!ycE0N&NwVc6WuW(PFdm4^*#oEo9^ zhLeLG(qlub3j0840`GW!mOp9nd0fH|20m~7t7?8a3ry*yLf-<4y(HbnLb%0UP@7c6%t|DfL*#IO(cE!7c}qMX3qsYk5;p2@lg8G@zC z#mCrESnF+tGH=5MeV22Ef$la9h3lUx1hSClq<1neOT}CJBP|Ym-GX zRpjCQ461s=BFg@>_9n2nZ_T1+^1&0LQvGZapg|_M8V2x$iVwb>r(L-0K)0PVD$nYN zQn>{bl*$FIE-5oz~iO_pXxI4-WpegN>%iyUr%63q;{O)*K3QQ@9^iS$a2tnK?!NSR8ISmv{8|LD{ z0ofU%9?iUn+{Zcep#p08Hu*2(#dj?SRcGTAZlqnJSuJotF0QXAS3T;-4`xW?pI>x} zA?}*?>FKqOk3;I14(sBo5eLMHHKcn{#h$bJl$TXmsWa+3>o&}h0FJ^{EC~R3ADzEj z)?N3wU3ls8*n{lMA`uP!TNk{KFAxGX>7h#PDTlm-x9OcM>}H2&)Qx-K7I&VXcc#4p zt^K!Oa*0!g-qWsw!B#g$QVZ9XCX{;QA0J_RVeN*Q?c}w>Onsb67|6Quvpz<(QG#-5zY4N%vAuWYuIBQ4;+Hr(P=v~xLo5{b$WK{A`nubeX>(CnnuNweUbd^z*^N&C zB^$@!;gu=>lO+}r=vG*3&gX6USdZ~X@)y)%Ptpr-xcj~|XV86ssVr8$-`G&KZ+)~S z{r2`o@+@b!nI{bYIu2&PJ!f^lD)jyf`5+-_Pc^^;JJffUx#{k&{H^sfd;|RjBR#_G z#f214N?ZayT}({+SZbl~Z1bEefX_6K4W=(89IeEr(*Kr4rRC-vPjFgVi3~587ZgbV zxAl;_(py7Sf}YJ%sJ#UTdC8NE+S2Hvul`+bPcf3+(#N&wO}&4gdV%b8lZNHjA3LVQ z++^h}piBNDk51Oeo|J}Rt{kb3LVX+c>b)u+g|MP5(;J5tIXyVR3b zXsosMwcVj=S4$cSF@U*H$&uq;WZO06H2G~BULA?0XJX%1t7i#-`E#t(q3vLR{#FpZ8ml*@(1}s+&gxp3qHB2^mAm3 z?<08AhF>jwLIAbFK$TxuDi#8+5ch>Ae(o`EQKJE0$C2GzZV%e{u;G7?!~s}#$gju;%|H;>xD*%@VAc>-IGaqHmS?|Al z<8}-d6BCdd?;tAn#;Qd~zinQ_5my^N{_E65!18qf{jzptOH;%&^e!9N^dSxvJVnhO za$e6l84B92V_L<_2b}Xs?dC?xn+E@wODI--RR8+}s9rBXU3BbUE(|dG_e3@PPI`w> z6Lc}xzS*rq_{wI;R4H#V=z?VgilMp!yw#<&A$_8MVeIINaE$Nc#j@bi7jS}})zuGq za7_6WilNZ3j}w%~m?C@$j!(rk98O<}j*|nJRU?C;G!&+VESn!(2X0`gj~+6sSD!lU zIOQdr4ta#RJP_h$3g~<-tEAzI)u?R5X?=;_h^^f47d%;hI;QX{Ou9-4RI;XktK?Z! znA2dX+AF`O@*>RM{@O&Nu>fJ}s-TpRvBk#;!+A~+x{pVEjBzrDdP-ng7~$b!Q$z~i zPAjaV?OM|cnhJFxHhLM$o7AtqI@W#6FE%R?#yUF2nQwKm^6B28-Z;&Sb-5Wt=48m- zW=wsgi-bABk$fXB$W0U`pmI`(mpvApi#J+IQNHY>_{N(`NiLwnCAAm&F0Oru;Z6>< z_0J=5E?&Bx9Dj|__Wy1txZ^#W5vcdG5}F{g-!@M5+B}{8ZuM{m($GDcJfrR_0(cVW zSG!6jAwxWy-`5xO`Ywr=O*)4OW3jyF@qh*1rkq-!F9O$tf7u|)ztzEi#?LySoSi!o z?!*yseMCj+u(?v(eA1fX-@5wbNca0?S}SfsH~Dmf6Tl}I5e_;R0j|kHqbF z^e+cPFCOn^AW*NL3^z&$`3+`XrD?`Eb^FdHZ@0~@{21*7CRNzdk*P_-6F(XmEk(*e zb115>#rNYmTyU4?%gxmQvN=bzi?969M?72>oM-8=bdsEz@fFZm{okx*=GnzIX#fw! zHTxDNZOG5%qOzFQvxFdANQvqH4j~x7Fl=}>ZRHBSq@8@o;&TmT-Abt_&kdg zQ}6*571;*|*$z!~4k0(py+>3_QF6Z=+dbx7XkUQe%8oFw{x($^I;97Xl&|)-&c!Uc z|FLX!yIagko<{;;Nu!9^DRsr81~&zgl9RsT%R?Wzz88B6Verk;X`hAWPC$gp7Wqj zcHiJ#X@d7H*)kAzXCR#AMSR*HvrbLV8MgP z{730)&{-hQOWK7jShyKWc!qd5;q90gZ_(fguXLAT@Gij;SX87>rUh`n_%Ac5?7GrU zQDHi7nuoJHTuLb}EZ++FWa?kx-lA&%N<&`e;qZ}Ro_e&`zQOV?3(zxnP=Up23j>ll zy!s~SBPyP@_~{1b+<)%2dYg&SW){+ER1GGN9lybD$E`>(Wn_)b~m!87p64FCb@$yhTB%cgY?; z)2arFS`jaVjoD6j$UgF~ma3c@n9uMuqh_;;|Ef(L)59m!1vF zKw?tdA?}E_BF!y*rcz$z*4|!KZTJ`IrnnAzbd`kSWt*PwYcF?t^pm?2f5w@{jYuWNNT~hQNbLJV#qub*poo!=*}syhX^5X;Sa>T%0X&}KEc9Ru1vn3y)D^y zim*2_T+;0Kt%fygH_EXP5%gq4pR&c+L|sJ1(uzEoN*Nk?nFsM|J9^uo9^3z0JQjTw zWb4?6#DYQvqd0bCe&G`}+ym(;YTqV_N5?GH?MN*=r6FxT zKTFv;$gfZiJAKzDBPIosqbW|J<-cG&iyL5~Y;Opg{rVM1?~Ht4^MTV;l}eDiA=OfOMPv9?l*g$kHOb9l#w))er{``l={a;T7X>) zZB?@bD{}s0Li_~fAzQY%#0({9g z5BPWer^Da@DswgY4FT|*zI>u>)YY8dSi=g9WYn&)!H05^ZDOZF<~DC)N0K5e(xK1` zoQG0}9N1KxC=Rp~#y@4qan1c{R_G$k8lHJ>64(N~kR=v`pS1&7SS31#d1BKx4i72; zSAUr-6*;UZQ>GVA94-v5D5)YqV(+AC3hf*6hPGAOp)`S)PQ9&-Kvnuzcf|oA56s~x zUd9n>e>GplF3y7yO)a$A%z78xP5jM;GZQFzQ*+b)GMNMT;B3ZA$}0H!6=e;q?CdB9 zb`pIrpBeE4r&Xya^6@a!N$S1S&n+>eyD{_1MG?UB?UQL)*2jdWvZw;l&CbV(wUOE2 zAGmNDvIIZ|&9X6b^}Rb1)(upvePZMBPR}7^yF_nNalY~4_bA1LhK!+hpN2YgH*Wr< z1a5XClwu@CoK8gh%qD{9l*^fFt8c$y*=_aBxOKx=q1X$Auh*JHla#HjvM&Ly6<_26XYhT;R5jB^V|Aw2lfYGnE)w0)1zAb^i}|$@nu~+q zo#3C4|E|v`F_7kHB_oo87aun?;B`*M0bd+vptW96Ze}5ataQj9V*YJ6?+td)AFL`` z3*MWLxJ%>X-x+sp&_Ar1M}8OP^%(DA#PM-QOB9NoL0_bZ_1V-q6Kt{zeW@{;U*^GG z?vZ$eKG&)GR@d|U)5!&4WKlrd$$uTvKz$N)emMQ=>y+Tm=c2}muYR3p*~L8x+Uq%x zm|js65bT`r<)(@4c@t+c{~WxkTFWy!#qz!EzW>9|yqz^(>kbd!?cGxnxX zoNVRBsV6Eh=MN0pKj<2##<(#;>Z;Zp)#tBh=@O>XtFlbd-+ja}DV@;QK{b`d-pqqk z0{nP9M}q0;Rk;`C4S7dbM>%imJc`=xOwtLM4Q0q2e9L7$k!d2nLMto}N11oj6=+Xp zZ`Yr!q3sd@BDC(YH0RO=9&+d{rjcX^BDIO!OK0}YFM9o{fCr@UziAPkR%}j8x}ZUh z*r%G{DdpT-A8pnoOe#ju6*NA4-{yuw)h(KP9Fln*S}om?`L9pZ?T#JMDsR?2DP@X$ zUHU;`mw3P&ZyZ;fg^dhnUhBKZ$Jw9^j2uP}>0M{0kONTiMCf=YC;P;~%eV5Zm=*cp z3nALP^F|!?CCX3yJ)t^+Gz4pQ1(wQFb>DK78{+^eryQXdtQ?^`8Ovo z1s0b*0x3k%RF&s(W|;dS7kf6H`jC~g8mSx7%C)?&w43pI@GI-bOusFB$CJdD6NG33 zDsR?~2ozqt(W&9}tUsVnbiL=i{ocyogEMJSJWNy$9+W=y;4Q!OZg}_MngJ~@hukWa zNwVjfetQ7;u>Ua(w7{GCyuiz8&V}hWQZ7d=`v*M(?QP&ReVhS$i8H+DIPXxMOHP@({oec)9=NlyB_`LK zlDb)Z5^ddnv-IsP?M%`Oa_%;ZFc8_Xk-W~o^nE;g$%_?RarzK+k)TY4!~p9mU-$F7 zjJentPH&lXlKBCI^7O}jO_N#lh*ws!7x>K zyps!W0oH(4O>U0iMQWTfVuP}b2^U!|3IQ)#tqMBeoW8VHozHq1a7 zS8&P>FnibK60hPF{Zj|{p$2xX+pG$9IaZX>NOC3I>_a2L=2c69Z!B5bfYf^KmWguT zmt=TW>mOupE4S;9Pw%lij|Z;1c-hAVW0&43s<|wZU-ZR)DCWTj$|zNhqrk;2e&a7U zngsp^b%Dd3A)zjPBS_IWt5fW={L|K-?euJrh1`__+o^-&z{=U==`8pG`OpPT z1=OCpLy#1{got~ulPM{;SsrzPGIws@d*bLz7rP;n;a69!t2QBTJKen)U7*_)GZ^Q_ z3p0|*7%~uFi4X#9YD|{k<|9O>%gkxY#W;?|1+Pv{aqOs2N#4SpIKkV6rG%0e*RFpO zyRB2%npL)+PTzv&0>g-va~ZeD%6^IqmG5v|WUa5gb?1s}E*&b10|i^Hm(?auc2J<1 z8kD&LKMeDArNmLJ(YW@Ru%a%!ExxYFVSYmEu+_ z--Sf&Y<3!m%$mi*&^vm=(w{zaQ|}v!Y(F!;-H*r?20*%-M17P~d-{Pqwdbj{#aj0q zs|_?;=gPke(S(xtWx)4l^sCL6_?|rqCG>0>9o)D087J_O(`wg$_&~B@Fc$@K+G#?! ztR$5+uke~L>CkXd`K}LMwAww!qItw`(USD5Z)^VJzuP)$tAMWE6W2f}c+?Tiu+(e` zK)%PAU&;o*VZm1nbz@p>oBxnqICrhf(RYlLtNR@E8!*AO3*NO16$K^}mGDL|N@MfH z=2BY@VCb+JGhzIwU`ZHRQRuIbbXsr4;IU6F*_7NY0r}u-ypke#nBmi&5Zn?3ww7D{ zwMd@sJBHRLtSQR9d$Mb>f^^`vFFBpg@7AV+E2?ECc2t;J=IQVBkI3tkO- ze%1W(3$1>6zOqZPif_37n!gU;SgS5cE~3q&^L5$bYb?1a@TW;vW=mKN7OEH=YViNU z=S3SmnWYb)2`3XfFCNdGUR@}p$Tv(RD`}GxN8j@NagC~oJ}FsGK14MltyWV2RS$+risNS_Gx--eD51`XOh zG8hBdu;Zt#oSkGi)tZXyRB<_sT6$J_`~Z+sRbNri`(@nd#_$I~+EpuiA8P%gK>Hgz zL!XHajpxJEn9oo{YF_fIcMdnxj6M9g4^MT>h z&8L?=S!>PW%t%t_CLFtW1IqrLi$n)0@N<5ET+RgI`jt7zi-^w zbDd{zMBUXIGn)MR*ROMKYAJ)T9}a91nZI2Z2wh5-Ubp4+yj@j7i1^;zn|bxD@~xZq zHRY;qXLRAE(&z~AG&&P>mmvpJ6fqvw)*#VjzJQFaeVZuakKI&rM7itsv{u+W6iSaX z>8rSqER11X$ZGbB$*7s;uT_aJC?Kv~1nT9XF0{_g9x3LX{>myMzxcX|T7NoL_`axh zCup4I=F~yE2vpXDFwJ`m1t4Q*YZ6!HzbqctO~%c753GPONrOz}SVjfy<~@D_Y6~VG z{Qk+E?AYpB-|Vd)9f(0Oo9WN9AcLB>lSMd+IO#IsCCNV7y!+|0@6uiDeKeoa3q#|E2Dq2)HmEF2ws`3Ln>>{}akAIgUSfo>KP>z; z4r6t~!rOlzOen*jUOxW#Li$(Zf<|&|BB1Dj{7}A;#(7y`K)lTZe_RF`H6eTI%X}-k z3C>~D->cb>V-!aCo{m%$?5opWT3HM^_*C6z(?`w?vJto&I>1$@1#Gh7TvfHvdI)%) z=bQIk+b>d|CPms+Rg1@zTGJv%^$4G!DFwd2xYdzCEyEY}$2?kVFRD#H?@we8c|Zq^ zKnqz2DuV~5qa6Dd@?3rh;wx@mH2rT`M6tA4m)Wl*T8sT)`lKjEQZ#m0ex9Gn{wwct z8+Jtm;ICP*GPW5f^&_xNlxBqFtOcoG#G?M&s`!DT|Az^koQTq{#I}nv={yxEXt58y z9Ly!qSl^4*Oa!RIqL5S5NnqzPwNAQ=@r#6Fjey4wWSp(q0zeeOlQCXWf&En`(>H(U zOF-#MlM^SJeKj74nv%u2?!lf{$*$OiXY#i^-GQCg^gyFwLtao}Ov?;BSmJ5=bDW;I!^Ic;SMFAr#T3Vp=9SI3caR0;! z@xqMj+)ku>-#a+o61El zimRxAMU4$}W=mRZ2GsU3<$1PSRDxG=Ie6a_wIS5^2U4+4Qjs4*EdMh576-fJfP(4X z#n;Ktk4{Yp=l&&dpYUHkB=8{4Zw-$wR%}R@R@R;;h12D6SsdBysl7Jk3lQ)9^8LP~ zvvXzbPJ@@k0!lwYSoSj6=6MhqoSEa&f#BA}-&IWTzo2AQmr%nszxMZsO7!azMdnyJjMCom z_vA_^lSRfcc0wml$D+#5TDOf#=%+TO%ij$lpE+bj|5~RhvT<*U-SpZ@U6?ucIw*_u z0!)d-5dRc)N9cGMxtEqdy6d&@%4`$*NNoW{aPd7%&dYr#77T+~|L_~X^+cn2LgiIb z2=p_+VC$ig!ekp}W>!DOV7h29eI=rhJylPp!AfYCZXq{^vR8R0r7|{U{>lP^&42&w zJ=*#nWZ96tjm&BaCRm2616kE=o?lSg_|6g>uHRhFZ?4PJUfH86{~vGp!UJh zaZIJwXzSrd5ocO$+|IP`px4x0m$&X{6bDZO3?AE}wB-`ii2invLnuVuYPAkU?U#m{ zW^`!L1zY;dpgcQ*t4vk8v<0ZT9<;CZfpyx@<64Vvkp>p8r{f!?S))6qZZ z3*pkrEP2+~NFDt7hCcuj_axdeiP#ge@R_HnPjWaU)}GmS+Eq^cnCD&$rgN*?o7aSqr5cz7NKEd zmubOYy*_rpo8*dp zO>pbGv!HiTZNbM_DyFg{{kTg!q7bd=39zVWi!*mnedQE1sZv2bz!Q946r$^NC8Wy?R3AO2&b}a)n|l`@ye$zmIs48bYd~x{ z&3(mfj&EMBKTZ(pXsm;axPu+*9hjsN-9awH02OdTAG~cij#M?#L_Y?(5GjdJ5sD%{ z$w6lN3QLlZw{4A1RJUIC!3x$8u2P_IBM?SaUvc z8aJeJtNgA3sk;W;Mr?#&2L-Cw;~X0!5XnviEhOBinVfY8|0!R5hu3d zKxbU8*KseF<{+LgT{1V|kCS48AN)(tl_YR#--*O!re|)1Jg#w@k+Eou`f{D{cX+ht zRiNX@I7Ho~fEh>y8wT{28%teQ&3?R2X?0KFadU2vvuf0^9lKO}Ru-`6@6=umJDfOh zUz*-jBCh5N0-Fl-wZC5WQnksq_a2-W+6WPn>+9&5nar%H`uiC6-sEjiivui%x*^y^ zPc^3M7+YOsa&_`(%uFEUTcvePsb?8AqKX+_vwpd!i&&-F@ ze-{(gji4{uAU3(V8_Rl#SfK+?oj$EBggAXPPQUPObI7CiN1irD<&C{xh3Nx0Nj@3- zD~S8Yy23hwb23Q3>}B@aKKz-)qxMJ}@54eL(Q~t;Fh$mRc6I9k{(rgEGAPnN?=;lMB z)A~5P-_o&jL(R;3_SU%}Ar3Do6JWz0_J2g|#&y@l?Zpiew!)~;vf}spBnOXfQ#z6k z=RTKqGNPsXW7mHqJ#J9&zr;oznYBmFMCMLhm&7Z3T-IY_9Z{E8w!p3CG;J|a)v1UE zcn;v%eHFV=UbDW2HN`&T+?abU%ek%yg3n2BOd($0)Y^d6rj6)(z=jmVG6J>gOn1j z#E1f^lL!%$xNtLwN;RH_0KU4LyHvUVJ~W>7(0(x}v8+N&rox^gRTB|*E(VYWH2&4T z{s%2lvDIrIQ6u8QGM)(dXeybpt19+b2xyk}{=fIDEH_)-WCFur1Rwu9(HHVfq9Uwn zki{UmGAZ?o=N#EAi7)&_jNycD@=ULX6uHvu{y#-&zUiHJ9gwD12bly^YN88Y)mvI6 zmDtUik-n^a-h72gSo#<*BBOFGP+#7R(n0rOdrcvx@1j%cIW{(LXy=X`6H2?_3gI6bC?YobT>FxW?{k0PyJOzbCq8z(;T!90~Ha zInZOO?MGl^s)tKBN~`Gzv-(1e7@m@5L{C{o>~%OWbO*kTGpWSHKslzr$PZX z0-ZylB_@X_0%)F=7eOEIzNtVBc<7Ick>b_i!O}m?6w|0ce|63jgDxnYmEi}XJW1xw zH$xW@0QAb$yr#b||HCi>yyr9yrFK2NQ&_glT3Y5nY~UeC3Ib^;xhg(V3HS1l3YUYF z-nzCc{>wn;ljzmb=mK9TooFZ( z9dCHb_$CKbUeQo;s~O1{7@!JOkE=YD!xoKGHi7}Wvl@?3T#p7N-cF>u4UuyJ#QfCj zA(qyLDRFT*g>J~`9={QrtmLq)odcz4B~Gp7WJ!zOA1H^n1l~78-~Zr8Y(|`r)YDWA zzCL$!c62xyHq4jTtXmr5mD~GApk4$Jt!=aoX;vk)mz;~S5(P$z1}{9kXiehIIs*P_ z5H-EUNLdVz(~MYFW*yK%AUN61j`lC0fXVn3l1A*3`HG&@u~so`*UQ9_DE)dyz2wVR zHwVQ|$m0lqN2BLb*{$bO5K)G(Ql>e`^_NApar{cB*@KNZN{Ml>R{h<3rHBG5KQj-jZ0v?oG_dLnq78@ml;k2Izti5 z1E2oyLN?1c2Y&0V79ob44kg&^ngO2FG37x7HJTGh24Z6o@03d!wn zKj&`p8|vj}?Gfw(9Go?GabK=)j5C3{d;HvA?X6^qo`Vb$2lpG?{wmXZx725Z^8O0D zv@qU+KNr(;X$!k!N0mavYpjjbIZH$QFa2aM!W>GIi+s!3S~SlKF7Ad(^j?tF779Ml z(euAjO?iFnjS%ZgWFl=lLPUTiHy!UNE_CN4rO%ph0C7dsKR+U(w%*I8@r_YaN$bg| zHhSZQ`Vv`%V!>0ty};ZsKj8yNvo5R4!*wy>}NX)JE>AxX`m zw2kMV0!L@d)OqJ> zJs^U$;bBaqVC}*l*#>wx`l|QtbULegZ@S#M@#u=g2TRNjX>BEa@;5elaS7LO)5UPA zH_b;O_bD7Vzx>KbGOPVZPGi?9)D2gVu@ltO&{(GeAqp?^tt6dY)ee>XpegNnm=a<@ zcrxUyh+S`fR$DE5(!Z~fD92s9!ys594*IqJquac9XeDUH+b2KLo@Nw8zI z+Hrz(#Rc~t2~v|yDK`jYOmBJt)feS;}K`9y%VZI1>8}|ew9}%7- zPKv#wgrvCLJXJKnWxZ|TTafhh6+BYH&Dfgt8L-8>#APYon$e1dxx2b|&e+~LH$Y;7 zN7OOm&8dKT*neQT=0r@Bm>qb(kuH3Ez5y`1$!y4M3f8a+h8$r8$Zr-hOSDupCN{FO zw} z`glWjpyK@6OL=qomo4U-=1a&N_dOopfzTI=IgztuM8vH}BovrO_+cE%!0t=|(pNm8 z;YNmX>a?Or#cS-6vJf46W=d0G%rdQf$gDmmgKvA>m$J3EP~*^uX8`xrz&SdGS zb?Jj^cmHE%vco7Ak=^;snZ>Sn_i7|UUln<`jf=2yyjJ1%tvSSS3j@40v7c~Scr!13 z$I+3Hk;_%T9ls*=LA{z0c1D+;=gkhBxVagmCPCX)v+nB7dJy!^Fx5W_q2J6&`6j+L zz|&rCf0nL->J4Y(2Y{zx)ou5z%qSwe{u_6G5E zy>!(xHN*Z^YSH)%O1xzHUYH-l|EUi8o^C`Gns0Xg@!r1tWQOf}^nB=sU~A!~ijF`d9J~eCI#nm?D?IH32x{7s9EQ>MiZ5EI7y2Kw?RB zM|V|3qkDVnH&A`m9iLa^eob-I90 zyB!jR18-Pdzw9xEfTulkXwK<(b5Gko=8n(n3n|Ljv5TZ^AEa!P7!U?=*YA1QTEfcy z`3GyP8H7GO!7Wx-Xw(JMWR+IlI4%=7=U*(|bkOjXijETDz^y2ps4Z7s(EWewe`)i` zQEqI;IP~wEM({wE5KD-yuV*qc1HA_TGz@OS#+j;QFk#lX4~>oqO$ImF1}(xM3-W4~ z-ew7&_R8l58_jKAVIrZd%wor_FklDGFI9CXiZM8ejb*$>MbhQ^v&T)xL$W~Xl((jX zYyVBOC2_*DS5#>()%!6U@>H#fpS7chP7H2`d^VNWnRWkW#ewq~^yYf|hOsH&MLGYC zB~6J%w3owDN+`?j-7~9Qdq>p0w38sLW9-BXg7~uK8FcQ0P+JG^K|`TCe4x*7$K1Nw zHdza0D8yebSmHG>IBGQ;JObvcl2fzZx|?U#GG%+GufBcD>WbjC*9B%<{S65s8q$H? zPtOhWvTD6z~ZX((^ zj?293sK#+c7v|HFO63baQ}Jx%P?kM^)g0-e!2X2F`V((t)N0=3#=@kmyf1}~K zr#Z);$Nj5as-3(n^4^|}KnPoAZ}y?tj_p;aU2W6l82v<0_($SkrB8dJbMDn!@Wqt% z`B~KZHw{x2u=(vmJ1l7;vs|o$+4zhJw4hJ6fYkTPZBmio^rgmyd9GOIx|aEWrW@@& z-)Aq-&ft;W#2%jqw4Tzj3BzTiFf*Un8&w-9{j-#SO?~3()1$g!^=F!ggo_*=^RK1tReZyVT<)%Ezhg{zj#_rVJ;x)Lk?+ts>dmr>x1mQ?tY&mm7P zqTY#Kh+!q!%|0nbuPvVIzIMQ;Rn)pAMVr=y<{nX3^7(`CtXp5@63m44FsPMQjf|Gx zMUA}7MA2-m`qvd1``VMEA5`_ykqDFEO;VWAyKCPiv`w5|V5bl08@b-o{hU`4km)ee znhemp3fUij{oC(625_s&5|~+oNZ;f|R?!6cYW0&!-inG+%!FP}ClhoznCw?k-=W0& zZmYG0TiU2ri)15~o8GWQ6EXAEFA|YYF`%(S>@SSdXq^$M35ZZ4vu(sj2Up8kY?< z@ZCxT@%?1&?;amzvXkbyJLP1c_tBF!{75D<$a_2vA*%7GbVF$&dcZ7f;(_ zcmakt0FOu|Ws~BC7)5rtSX{9#Ddol|ME{#q+Pttry^|yJD+hb0>U~Wu-r|yBuzjvz z!Kuo{3F}}rh_{BW)G(*}EtEoI|E`dDaLAL1Rnt5`$g9XeX8aj~Mn(ar~~F zbB+s@a-Y~6oWF9@hI8F5f7U2tdjXLp>tyxBL<`?n8vfg`e)2=QNqZNu!`hLwc|J#eoK%~ePQ)=MzZB>fpu^(_^=1Y=Y~V?+*e+orIOw3` zGJ-}&KG!fD*2&N5gF<{S-k6s(AJZ1S5?M9?-BY)`QFT+<9rmOH>E3vP-!%wr3=U%kXh)$t9F;yL+#dZ;vL{;s79i;Q)U!;5WVbndmH!i!3 z*8{k(7IGgO0G3m;)iivK$;o^uW8`YysK$Qb?vC;3i-5wAf{fj+PY7Xvy&FEs-x z1sf6>rt+pYYUu>WXu$^f)HoO(y{5T{(~5{4-r4jejPV@wjs1e!7CWzPBCzrwj789E zrV9tDkdP;#sLCWhM+FGGo!RxP^#4kAxiv#r7D4J+tknw6_^Y#j7 z0)1WX=IR^Ts-TzElw*I}Tw<9l=>BrKHq{D*Fyw4f0=*-_d!?q$_O*cSmbcw^AVCQg z0q0!DkE%W?b-ySJPlj2Ymv`ZVjs1my$lTGaRJr1nvI2fB$C-&BHON$BuwQJIYf5WsX-)umAHNOi6i-y!fcz zTq#Fx3xh&_C84b*Y4xsYKDa{~5Ri)23=ru*6zd)^6 z4zF6WUz>YD~vmTJmjDj+OR9@T7dNBZXK_<+c?`QCE$vo+a3y z&29&C`V)>;T5GI;f+R))5hETxy0O^3X_1$rGZ``8{*~l8=-;B1MD|rRADsOwwBC`e zbo8o&5(28C)~O}>o+s0kJ{s_+)m(A%##ihlrJW0|(UU(y2XlKZ>$Q}7*9<<#1ka(m z{MG{{%Ri}dByGB0;xw3fw<>09RK$AvPJV;H5*Ms2A%rN4=nZ=K&loFl+;8_d{d zrG~N`Wx}h4MG?HwdG;UeQ(E-NpT&aB7bK}9CBzxy1|EFpPS4~L8Tpqwgx5wgH?#fx z|MJj3Rt(k;{OG2xhSf4PM$sj&WG(d>#|;@b)(im~rkO1V5an4~BVp zgSj={jn34km6^GW?}V?yq1IZd^VPdN9Q+} z0UPoqt7_J`LwP^&Zl7b}UHnVf1p?-I?kAw6oPNsi?Mhl7tU6&beDRQK1jG*oPlze9 zSc*oCEb|K3&O;Ck6qf4~>lS{6QQwvV=Q>jGg9%ERo3KGM+C8RYYU5lJ@jbF8S#fvt zau%Dr49G|br@Uiv1wb7?06eI7i$`4bzP+8YsCr{^S!SSS{T9=$!tXr@jyKFQ4>v6}QFdcqet*nbiHR`2;6?LT2lR%-UD@oGh85t?e2lsmg-8v}c&CYq3j{Rg-uuuNhMKEQrw$?h77KF@lICS>oRqI#Z3yqY6NBZ+&)Roa{BnD0Q|-+ zxWH6C3Yqq&H6$#g z*QTyByK;|w1NhTiVF9{^yx3T3dP5fF%k<9Z7@h9069NoCEV)@MS`IV#@f+OWO-;J= z$ewRu9e?<|jmv$N=_0AdbM>jh#oZ`ZI1_H=3*2OQ6S@zLXa?O>BnhuZvm@Y)+74qQ zZw|tQ!3$1=ELlmVXX@7{8armM(8B$Q$`M+%s}0<8n1%%{gtprBxmSz%+)yM*7#liWZX# zIH7ADwXfMZ9-3S8j26WS$4z)Y>s^lcXJk}Bx$iDmhUuYy-J@n&z*6NMv zg~W^VNZwp&H?@({Q^}l-m-G~^pzMgXLjX^dT5pCyQGCzoQ80GVKJR8>=sRM#a|M9F z7_GCKT1sC{(>BxGN1otJRycQarM(6RpX)bO+H#5(B^efWnobg@rU@22n5ir1C>W{p zgWKRyP)@3F@jY_c&Uee^bryDtd8z&B2mkDVr>L4`!Exld@+xxpv8tJ~#6l5xbgcU^ z6Tuw1^?%wT#=kHe(1cwu9@97$LYV+%*C-dr0gt{r&pB~dg9-tIBD@1%%&xEK>vZ*w zrs@s2Z+5`G)QI;AUZ?kNj{mjs!uFjK-)O_ttDx%5d;{^*(S7ti9vDf5mk9H4O>?sA z4Px?>;>egRij^>M(^I{@>?gFyYoG}zkykxK=ThwvUPbQ|aQs2yXAV=oFvXKcze$MZ zR<+@#g`+vpZu4ln75qMZz(P%e2E)soaj>}q&fgU14t|FDEDh!Rpd~mWGe(0Z?z(=! z1UhTqzzqBnW^pdA*#rfmdoW>Tf4`)1ZbaPb_;T2Gnc zeI7D_**fLpF#Bel1!+%T_i2VS7D$j({>CUWksff7dvSB&&TZ^P((T3{-l)xtMd(+B z=A}oGcrv=p$Y~4>l3_kp%RYsdvjjR0tk&aKKYDEVQdyfr7xy6T@E{u8BGBA%ON0w$ z&nf!bZ-OT`S7%DGS0sooNcS=G340ot_P81aTG8p5urzFa*P9fRvo!5@U+FZu_3Hs` z#k@!hPtLW-lO#a(SEaj;pTG>GI`6&tR8M8v60|J7r$EC1u$PWgbZN3_jI)cbvnhT5 zf7eG9nNRu8B49^gbr!RyFa4$KiHv0y^3oUqzwptUnqA_U^3_+ zmsdgACH)C0%@V`(N1MK&vkAP6+3$0U{N(pT$E$P(cKUI*_n7VHBQnmb+qRk+iG}pE ztb6YoEpNzYOs|zhkwC{ZGTg{K98m(ZMjOvZ(*x*!y8<&o;961qExiYRmTJ z34lSx-y&a{=!hbg!27{360UQz7ugyQOrm#UTWqfdPtyjV+0Jao@YJ6mhCT2(o7)>O z#4n*op*`f~?#&GCYh&+yKeiWajA3(!;foX6n0AS0Qf7s#1*BZ>9A@W^kn{rO#oZi} zrpNp>yjs&neK0#ze4=9CkiW);!TAXT8DVhqn44BNJwS4Y$@hVRheX9xmzRkmHoKa` za*xGeIzK?dIk$jL4XJ&+&-!PFVQc47u8uqMs{d*&Jnhh&m7sCjLArgNyS9_YM)cPb zd>QBv)1?D`h4$q-jLn)Fd=JHV3_$qRaU}a`u6P^L;;|O^a?3!ICZIHI@Mn&YCXQr< z15D6e@?c1xet%2p=R$d?)4uG%UUI4M)cuEPn^`wa^g4+pYZf@@~+%>wf+H|Lt#a*(wm_|Ws^x*u}ag5$XHN)!)7^}q#m)c{O zZbPE6|I)!SehSN44s?Ke>IXS!sDlRS4 zEcLB}IdqF8sbBmozO`;w8Jd36v|>(HCL+ z*$f05Yh%Pdg~?3-YRXMz^eU&X>N#cL;X9~favRw#(wqJq=3aJ7XypVEy&w}c&c^b( z{q3xtOXgI*ISak@^*dTN`h5+>C*OoF6%=prcF8cS>z&_k{>;DrA&tva$o&4dHPdZ( z27sqr=FED)7)K)l?-CK8U`YX9mA$fsnB%a;dayjkbj`iVC;=28MJ|tcVyeS7|Iso1N+ldyq3SpY5VUN+2*ru|1PZHlfY``_F@FMI8uym=>h7HuYcI?z~YC3RHT z=)C*>oL$}KS;y9bfKv9W>TnFVUOlRL+(D!#P?$6>{cbnJ`bC?{t>s= zcmj=QQ*HM=1(0XD9(=?MwkIWY&27b_f)g)cb{Z)DZ$@ghjT#xIZYH(8Q|aP=V1T{3 z2dEJL0a-B7E0I3Z2QJ9oPuWCqvuYy)ojglljV{?S4C5a)vzl4jwz#5IZnzFyfqaZ! z&C4`_F}bXR|FA^k5`e~koS;*Fw{89UwkxB`{*M0Ub9{jNe9!6LXZ*N7h^#d_66S}Q zr~&d?I0%7UeG;7i!wvFQC-6!uw;v+TtH61w2D@C^c_;3G-vN3D(0nN2KqXkH+Hxx3 z6HGY{Rn>&`@)I6Az=NY*kZXgb+LBD;vwvXSV1y z7wj`$M^q$!o9Wv#&%bd+J@k=uTqKWbhAU@3u=8_i8Ehp@Nnmld&k`iNR(Xs&)iJnvb`k}*RmrHk8cw<$D$OXJY`Kr zTY5py2aD!UZ2j789Uv7yhobuxBT#-QZ32SPwu#nbYzn^DChtGni)nmUrYB5ZV;lGu zPJ}koOQUtkjc%#@P{sq3E30ohhQLh5`iS?LSP7`58AyKwg~#;Ko$Pa;F(m z63P*F<~e&tM$aLwQ}??;(vBZna~%iqfm(~SrTl?Q>gLx^B~n|nC*1;r-{JLvHrh8U z&^=-5E~|Y*8Fi4e*94j1D=j93{L@~XC=MBJa*@(O5Ty={h*eGPi8A@P#4Zj{f!JM^ zSM*tV6@kRmMHULAKMuF~zBD_~c4To&np1nCxk4=QLieez;Lik_OX`|jl>tO=PvWhp zs-9u%<0_qgi00^IOrUad(O0nF)tkb9tc6h`jZQoN;jU~u>!^1+!dav9D4}rFg}xWv z#)i+gQx17c1D~W|#@-&Vfg1d!M*!5vyf+<%TlF}Ljc~>GqDQ#X$V+gFD~TVNa`+Wl z94unqtl9UYHXkCsHNAH#d?e)YW&G0g@;#sngthO*LlLj<8}bQQ%@Ln3M)+nAM0;tX z6%tVGJmS&gg=g|{`>!6y^&Gzp_tsOB_rCriOkeyoT@A7z^&r@o+nqeE{WNcoZ%8%; z`YgEM)^&g8laArs&{m+1MzWEbOGIamsn8LOhI&R1=lDjSy+Aiw?g}BGcI_*vV~b+H zO3ME3t3L%8d8G7TyC?g0K-ZwznTf^crT>i^Z_kWQj56hftqTv?S`=JR*Y_uAs^;&% z8O$-3rmf_IoAZ{2za4R@IVB5aXZrH<7l@TVhh_)P0uwHx1L5i8NB1pvC*s?gcqRhv zD^_usSLC+)fsw+u44kxGR$A{4E5wWvFZl2H_MWo(eMTbICRK)>21nl1q|3;%{tbVI z;ebc?e@8X_jZ)uyg-1pt)$>$#jE(*1<@PtWr=@9@oazNis2sT&?eUb3kl@EQ5ZKEP)IW`f z*S35e4CbBLHy+vW%O5yfsPdZBVo;aL0$U*dBnKjJlq=$Sp(B9v#qUzgmk`!qojp;q zvTuSyXSGKOniC6QC@lMg&qf7TVCj?eZdKbU%J+J0q{T9*H!kp0sZ}iMy*3s`Xk40N z3e27*Y+N~#J^dkXu0660?mjjum!q z4QS=jr;|pKZ59>EpmOg;9(E;+8d}g%jrQ(-9lXQxG))RvCqgHw(^nf=JwUqBX@O}~ zxr!7jb~Uzj-!&3KVzMQRFbega1j7CCD|uq%=@F%A912gTv!{W^d1ZM{-DFtb*R(O4 zu#~Emft*>>pBFTx)=E?xl@Pt>D+)Y6g7jeLk~-Z3slf?IbDNbDz?m#jsD>qi@frP# zp9ZA#@0hvtU8Co1@SJi;;(D~5Y@}0@YCvm7?HQ?)>25YHgt_~LNl&=oc{ij@97@o_Oe4YL1FLT4Wn#SJc|o9vox+YgX#!cpN}}VOb!!ExqI@jjH@Di9xN`r25%T zNqYO*69(eGn8T;?(U7z8D$@H*IzZCq5q4#0UxGxR>I@T!M-3BXks24&c0?td7LdDA zt*!e~U%mvN$!6qJSZb+=g%d~ueX`0?{>;8C#EZGaSQJfu`=B3x15*&%1Ji>ETA;u6kHk@)~`Sz?p7r^vr zWqh)9x6B%UiS@REW&Y!_z^~-Kb7QZa_T#~m^W+H*6ykF2Ez9a3?lVjjXo7&kpu(`f z8Pv`0EKDKIHo9uXw0oN2{4TrCx_xu=!~Y<(sWKv5rhegowEUJx6{1ou5M@dbbn+}R zz$`b{F$mC-6CljIW(WY6Y(J96~3JOB;m&a4bO5~-JgZ;66zrmyot z(Ke{&1wj4AIVHB0c&yqEndQ0loQ1q$%l9>}raa4uWVkARpSU&&Xcl4^&!O=I zX|+BV)N`Pj@?|PLDC!Gc1Jj!Mw83-y9exQy!L0(nv_l-KvD27Dy5j3gckYCSypw6E zDQmRd|5Te#Oz(k(l5=S+j5zFE^2*^8v-BEYNtpRp#j`sh%}>;s1(r}Y_w(*4$}diF z0U*hNEFAPl3=}wDTgY)1#T&FiY%O~_q@LRp)+34=48qwyZZNv5#+eih0gTX%C#RLA z{0n}q-0sjFomAd+Jd%Ji;SC;8+O6w93@vAY@??MeS}DDQAEJ}tIV;T&ooRA4-J z!3vcjwdsO~qXy{1BAHhW?pl4VQOnE{xf1!==A3qBND1+g3W4%Q^_a@Q<~c4h(A5h8 zxGC`rZQgX{22(@YH&i|U} z)KuTGp03_yK4SOgGB6CPqd{ZC4C9x*V~|Jh5%c7 zI_SABHQIoI82@otH0h?(0H5_+O8XJS!|c5bQ^0^(n&6Y^3FtL%=JPxu^!P~c6wQkj z9*c;Srujjmjz4?ubze_dnTw^jnEzD1{TQ4z=UAcPFJ}E@W+fF*%h=zdwIs2>0)-@} z7L|f{ht7F2)~g~EstLL0Dy>8ME7l%s4b8Wx`C5Iuco(tYAcj(dg;W%)9dAi5|F|t# zO(#T%HRe)=6wjW$(3nDL?&m=}2O+~*z6=@MACraH+t$cPdMaCp-679|7H>u}i}dJ` z<0sH6JS|hr-MIGmuYHc2w~{R zl+qTCM z19M5LQb7G#UOLn{m?h#6bW=-^Um6*LX?=!|or!!?T=<(Pr8p87CHl$2x>trBaMeuG z5@VaIH7^7H^Vy-ZPm1NW;@cD%5A`~{xj z%k-WH^TuzhJ1rNe76YRj(i)tX(hfxqwCBq^afIR|L8%DnO}4jjeg0hR z=JCA?#I)RD`^`o9;Ef^hychioSsR&i79ECL>$gC=p&d$~>g}mgcgAEX*K+@eImH&f zx6Zk@0PJ~~GvkcHclZ6LxERk@COE^6r?@_WtTQ4GEi7dW2uNFe_+TX?6!_ZfPIF3n zL_>yU?{wv%xFlBMp!j?!uv%cny3F{*Vl9XpMY9ovupZq|cE`94{N@Ad3lq zh2fxXTy1;tH~9a_DsW1{Eg29mm#YvaY&=?7$#6rIf4sI}YH*e5XryWCLRZTiiEXQ( zW)cOd*uAsVvVY^6MD%k%3w+(HA-d+(%MQIXf2hyJVZG<~Kcw~g_6|3T09O0~SPs-^ z=@w|5eM>Nb7&*s811|uhte=Xfu%T6-1*Kl7dAz6|gq>|;w7#!+j=O*y!9%`#@`S-wQ#&}vWam)KmV7(sQ*zY-D$p2Q>Jn>BO zFy$E`xswzIuYMTJkdgMhr;(8(N%-zx{;xh!fYR;mgHcy#$?>~bGBjjbzQ49(Wz!*- z{cs*cTU2;R6C}=QXVY5e=SW(e0vCdKl~Rg z6ncMKJ)-ap*sHi>&CwlpenA~5U%voIQ}iyh9or8D=x%ujD}}qYuvur65OMMf4Bimr zm}^kC`#WKPSEdb?*UCxTTscJ*sV#ZaYmHa}41Pp6h_#WK20&vFsh=uhk8yqsyBe*F zQgmcMn*3wNyGvU1lwFH`G?T>;Z&6s9<#>ujrt5cEe8}O{O&~I)d7w2wd-XPZUa)`P zutAmDbT0q6pB624m%*pj&MIy(;2lwa3&?8MZv_;mBen^i)f{gGaB&Um@|($(91Ah- zS66Fa;B_QI*MS?^FNG|<(Jtn5tad{R%-t($^&7;Y(VwZ~gk!K?de2v|(iW`8z8G!G z_f7w>UumhLJ}1C5y&Tj!ggx$~(iJPXh6ozv!P?}#>9TQ?0@{CCP-9^jH+`9rU zSy#(5t3Dl$6k?AFubi=2Z^O+FD!9Uu1U!3a;7Q68n5+wymWk?}~CYPWD`~EI~nDRH$(PFJdF+1IG$<3)(5Pk2R-6Vbz}Qee_Eh}{N0kQNfS#hxsl}%9?nW<;v+{qTewxT z4=eyTG}y`<OA!q4pkrhg`32YO_+A;Ut?!v)Fe>Z< zYBzp;QZI-+RWQr;ws#k#-*QG=g)k8{hSyjjgt)z4TT);s*H z_NXpLYN@&F%>FXu{kCbH;VbFe|AviZ9K+EAI6u|mZ^MR)-V4%O2)}f$|2JIegCA>; z2!KyJ-Crk~QYrUPv|J>cVJ2(-u>zJ~S)Yc`W3nk4`6HMdXZBw-LygRuA?o;ISVzhC z!!SUe$AYb2Sz}wHoqyr5VJodX{;PC+`x%!41Q7KRA2WUBQBc1R)1?9lp|9@k66{($ zW_SE=_((t#+-E`2 zxq9g-W)VM}{nrG9FwfCsW2mc4mW*C5VYW{B+6(RH-G>WR@nH;b7Qo7zcD*J9?~{uY zJq;ewvwy%QsC-Tl4DXgh&*fJ6EAkLkYdLVm{z8z#j3-U-TX^<*+c*OfXy{xyqyGKs zmy5e6Z23RsrvSnBC9C}1ge!uTiH%^@`NE@Zy?TeEFTl0Hm4}x`$joDHYiCQPRQ8dO z5_;aW*@Z&nj6XM}k8)5=bx4p4{<%Dp%PZi*%6jbueYrx8BS*(M1SRN6U~}j-fIaqs zg#?>RKl`%dFqXOIL`qXyRAu_rbM6wZ?dSAFEyI##Qf-D1p{jos$8<~u zUAht{%i+bKj?Zpd5*z&zG`iKTifKH){|PV0=-!t7&;YbH{KS9r;Ne+K&d?e64g=*X zN1fQL?rHocqdj4r|F~G69&KPXKY0 zw%+`~5Cwcrvn}X*+dTk|=%%e#E9vng!<|lc&sEe)7Fhm@xQ#go!(p29R=aZ%?MM}4 zmtVj(zFBPPVt^P++)L|i$DQn<$)nJoHTx0*?9gdLYl4E0U6z=dbaoyZCCy!kI<&t` zuv72R*^IbArp3jE2AYKm#K&K1`+g+lSgrWezg%nak>GNLpJu=Rrv5Qx;o4_R^JF_w zPMC;B!0lQzw;Y0>&wy>)-Z3=~#e*^Y6>br&%4d3%1O*O#uw2hY+klJbW~FSmtQnI~ zO)qAfldC~wn2z%&zev;L&+a43PrYh#$@$g$c>J;A0vlJ_OnVzaKBS$M!p-T514u)g zx?nosey(Uo<#Ta$AoScveHp%u4x)wRg6T{cxt3?Cwf3`d9`9%ciUk(6(^j-7%ZLok zGm8`bMt4AP-98C`LER00L`H`!4SR!;r*a*sN@GQ9Z|cC#1!DcxtszO~$lc}lo%Oy= zw;mOQC8E(ic&i9a_`$spYV@N!E=8-$2&m2B4KwJ~tMo%%+XB6w88M`$UzmE$`p7Hz@WdKh3~?Eh(*sQ{&K6nij&)hfcNbNu!r<-Vs^H4J~> zJy`Ba1cP&7`bL}xAlp9JS@HIk(eoYzHf4*abv{w(4MyH*U0@P{od879%`()W1$7*D zJ}r9_;Ah@$reGI@%dq#MWdk2jVN(kDc8zHJ8AH)%`4piyB?AKE8fuz)=4j;bw1-Ay zz=N|FR`lo93`9k0e9z{^Ji14iD~Y-=W1qC&KS8adDu_IWr!sAxpLK(=31Pwrw|ddd z;@Y-p0Lr`z?@izR#0BbRK;m;H^fVu5&Fi^2i_egW@EB|7e6{+ri|ekW;2G|0M=Sa# zpgSb&yzR;fDOB~p zcywcnVD~|O4P-t=mk3#VXkgcT1G*^v+N<&I@~=}}YVfVzWXy3vY=wW#LNWv8vz+j& zeTTyip;dmia|uvzZR=H@-0MPg8)hNCt7)mZsZZ$N`{3k*j9oV-LK^AVng4?~8~t=c zt61Wk#jVv-0=%4QwKm)BnvM`!%5K7$QQ!&ksdnl@RRnA|bmMmmNZsG=M(Sd27Noe~%K0-CEwr6;PELc4zN4f%-fsQW`5||$-lJEb z;QW{&1IWbuYdsgRQQZOQh$G^93`5|L81`Cr3`HEZ3 zK)mBmy%rbRh>=jCAYLBlesqnmpSK`r>NbE~+U1=K;6k8>()bCo7z%t^9Yp^hE62iF z)`w*k50`@?piJ@yN2`gNk}V-uk(K4ZRj7Zd(aefa>$B05#V-IPi1}^~>bzqBs?;%T zx7*KD*W;k37QtywJ@8!ku2y+{QAOq}`K=0#z zzr4*3`H_k+w1u&#+nQeNA6Xjof7!R}xjKzHprNq#38y7HJwqw|AKK0Jjc&HZBwdE= zi1>w=Ys^0!Xdh7#>G_!+Wu6cm#PX+XuNW!lj3F7Dt(-ZnXMKODT64K$v|fN~?r zwCsd@n$8ocLGHwdv?}%qIDs^)?FlZeNxlm8N7MI6sXXksqip-8`gc&d_>D}&L z5F`5bTV1{NRQM+T&@v!Wp`>bmhk zFbqYMFJC*F^s~BXG5R`7D4;hdM=dE(ym&LXn7VHY@c2Q?&a`qXC_4%^REw}odk*vvIC!^hTl^yG~!Xh$Tg{5mNR|Zar+-T!A;0;uibY)7Ak8K-ib-O zguH0|9xv6=PI%9;_TcH)$`~qKmW;`uS(?f6HB0^36|FMOt~$B z34~*n^r11Wr%NO$Thr|caN@PS$>Yz1?@~o8MBtK4Yw76V+vus|ZRLZSxdFp1TiS;G zS#8SNGq=})HuOi)x(YKMLV6_z&RJupTsU3insK1PerN47A=uW&5tW1k-alw%1;$P@ zP?JYhZgDK1gAFjD{u)=p72LL`)ozt3?()VM93tU?ptT8s=`~!J5VO+C zym|+v&8D!A6COWv@<<|N0zT!XmF$-NEPIw^&Tjl(0GJKa*ev#_kOZw8KG`g(=(SwG5D39h9`C z1<-ZD_K&ETe$Wmd;jw6+=Qan$j#lNL;-~P#2DXMcr+uZH!4LB$>Gd_}US>x4$E`Hp znrfavc}z8>$(Z|Q(kF)TBlrK6(JL{Xuqo7qDfA_kpanN1p30|d@>u}kb{eexBYLxM zi|?4HZcF5>zg#{iFmZE^a+MCg9+LM??=ZB+y^SQ5kO{xfVfhwluBGWn}FR2^=qdfKVHZpobc#myMtkju! z36gNt=wjqX{$COI@w6+yWkG2%U?!glx>3We(UFY}RYr^uTbaAZKz$u)j8Kc^%@@K; z-ST zA?6eGR24Aty1!6U2#& z0CB0-7Z*|zJ=PGHcwRW1fKQ7S(tMvCT1*dJso(fb)Jt;-A#vImJI7p-2j>eEt&OQg zmo?V-syk+jc!9W39tqFvV}&kT{Y&ii=stn0ZZ3W#2FK)XXl9z>9A!vD-_AtU_e1}p zn(Gh3q&fWW_`0PhF6Z~Jk^F)LfZ75kbCpxJDacWt}Fke;w_rF zjae5T*dt%riXIPNNsQ4B`Xm@qhs`*F(ot0$oWIjp=nG1htVzA6{~w@Y(qAb+Zjj2a zdiTksdI{JX!LeiHceHr}>)=qCU%L#XzuUMh_~ozSuZ<$?r5w7v1Vi ziw>`0W_bUhgGBCbz}L_?IF;+4%ifgQUMZGX44FB(D75bM%K3B{E9auY_i`uAZ=>hK zv+>m%TVi~!S-WYnX>aQE^*1*Kfzl7uz0uj$KgKilXCgl9xf}BI|d8Y`8N5tU_PkY6MVX8eIsA#C$ACPKXj_0W=2x7E_go^RVxy_+t6`ORAN}K7@Cxvl}o)-S|v8L z3=+P%e}#krI|oFFEnXQXELJyfK}c}H-+fjES&hbJ3Ldnv?XQ3H1)na%j#qylC8pf> z=X2COP&U}#3591fslDNDmOEg2E0d#lSHg0okH>(Q!*~^PnF*p<_qU_C%5EvAkMF@a zMqoXLrh^0q@9#ku3xc5YBs(sTTZEc=s?n8-2H%;KT?38@@Xh(X@>SL%{y%+CkQ<36 z4~ie;8T9+F#g#hMi`l!97oKi=Qv9Ov>a5uLE#JwwT`&HITy1sPAfbY9qRNv^)fwNh zD~oPU-MuAkVT2O$m`}3^5I^yLKUr>iA#Py#WYC)kJOF(53!(ELzQ}ze#Ve0u=et51 zJy8Sb{>WW|Y)IrOk>4&19G){-_2yUh?Y;ejq$_60tV9 zGR1|$m^D1mObL(*%I$8f)xPR@1e9u^%cO5ZF}8jBM;_1!Fn5VnjD*yhNZYwj6r`+ks zoeYPC&%89eJVn(2^xwNu+V)ot$(X_uUX2oL7p3uKvTO`>T2QahqwlHfT2#qn&{puB zW9|>FyO`_6vo9i+gJA!njVcci<|W?=`(;)%Kih#!x}J;6u(lsf=D-#;axFxNo|;`6 z>OK3pM%3f!1rEwU`#GDVwVDXOc_Yga-$;e?c-uHY>e?=ot6`#gc}4(kcPk}b7GpM% z(xX@L&lgTMa-+iKU*aI|J+?$qUjEr?{_HczYr`nU1D}0dMKMswzvW74uR<=~|hxXrb=o6w2%N-Lw!}p@QCYwoZF3 zrkBbio>eeRTD=I9E`iv$P#?CjAV*GeH}3Il6vAVO{u%;f@_)XR=#!)jtTTt*Nk)i# z><_=P3J2QEjTL(*rW#f67zT%TO^t}w*A_oeHqbyNLgGbEngqmb}H}nlu;Eqk=UhPP=j3;XIcYGKBLR3HIUCz{j zNwWv>mnQII1seZ-C!|$(-l5-nyf9%3Ed@G$^}Z=_5SgmvcJ={{D05#3r!Zt#%N0jJ z!YJ3vbxM7USq)_rR2+VFOnQU@+AEtRa6I2!&-tu-_AG+w_eRa+ad??&OFrzvw>xul z&?tvH>^@29yXpLQp#9t*>B#1e+AlmT<;Q&SJ%u^VjgtePsq9C_K@w+cfyL|EM3a&g z+5C3#Gne8-+T13a6AZ==afRri>yJ}Thr|F823cYFjkD|h(lGEV{=%I1&Q-*zk-3HG z`x9PwMO~dPR5RQ}n|f@YRNgw4pCzY`UrQTO>Ud`HsnxIdsW3TW5`E7pI*MAhyqkY5 zvQxGyr|XkcyteP?Tz*uSlzzU4cM;s}vcd?=0xza+doper@uG;9X`to zAkNml8sLdNqltCjK2ICs-fi0utu~Vqe(is8VyMcfPvQJ%chp;$4n%I-PZV5stXntE zZ%1TxNWF0==Woxrxl}KZPi)S=7!W+$^5zb!S$e=lsO4n?lA#*pSLeE|vUf3bdQk%^ z0?^x*BW|=6h<5O``L9S&8gki;4zGF`J!qF7=)!+kRn&p$u)Ka=;n~qTY~!!m2fd$o zcm9%clVe+cI5u2}qr(L}J-<8*eS;J#*hsX(tJ1KCZjN6Zw&mv3{~mH56~GgT*p84#^m_AH;2 zZWtQ>Q2)H_Vg=JY&gQ))bg#!kKI|%yHc+^SwK;$s4jq?GPCBPXOU`B4juXwWrTe|U zPs6vdT#T7;L9@vMo_tT~Tr(Gm)6W78xRtSa>lFiJNK*lb?u-)@$HHHN9)(c#kYgilF+-{!8O@csZyA$t zTrX%Hv1TYxKdQjKFICZyW_ndLK|JdMgOa-zWLiWEdHy{fG!z&sY0J1<{jc}(@NcfI zGUwuuf-{$vW&{QC({2%Gc7Lo`Z9UmDxA;r;& zQRfqj{E3b%--P5L1mT%)L~5=MU~Jbij_%EL+whI3YkTHJj@rbA16oP)$qkHL3r=LZh)Md7(LtGfl2uGYp8wmYiP0%U-Rm)0=9tESC_tVJ1l5cX zy(Il2&9EWe#cOh2cx-+{te@sO`8~yS`=&VD`XUPuw0Qg+nPyD{hx zG|f_nWJs6sPI!x2@_PVM_BTEW{miRa8};+S$J_#ealfPN=+{vdjOO%ae=FSBg>f8! zOK)ug3T_hAGk=9N*R{cGs~pW=$8d<+w5Jw1O-O7GQU;5VhiJ}bbaC>U9GmTmTh9*R z-}iL%x;fV;uj*6M()5Omxrc^Jj0&*8=u=JeCGo#ud!-?(%7Qe?l?H(4w@mQ#HPbuO zGlmtKzXz3yN}A(AFz216Gn$F4{c*4Vi401XDHg}j%Zc)}x_aT2N56|t{a4nd$&2e} z67OdY7}~|ROc^p9OWERb!Jy{8?7+RYp_d>YrGF6`T#}82Yaj+;VNV87x7KW}{jV-eI!2NB;{r@so z<$$J-PBdTIjQ!4O&-qk~R}+CP(!Q`%2S3H{sWTcxTlJJFbJ1m1->zubfOe31atZ)V~;qWNkp4 zM;N*6A}+f7OpR<(CgMLvx%&|Dj1OZU3glMv*1I5x|~FGg1+RAE5UrfwSnha zM*sPd&F7BWu;WK6j8skcKUlk;V6mJjD@d#R_$A&gQ!4uxnLLIWR#(f1$dS)G2Jvy+ zxmi{~R#A?`wgb`Q2@A-p`5$4@nU|$*jW)S~{zByfLf@JMHHhY%EOpxtON6zdvG6Z;C_V__t z^^YEH%w%mn+W&gwfGVgl+9qUT%|wr6Z=*a$6~|`Vl!w>h)7Pvn*0EAX6>a-KH@!Vx z{&|xjy;#5ctmI|kP)l1`&OfX+xF4nAjI5hJY5u!OR83c&WUn+j&#!xu zl7oj~#q0i!%Q2>4d10CdNfzgNuHqfq1q`|kD0q-5#s0^#o1i{8_@?~pXcgmcTwclG zhg4<>zS!NZy7ERK0T-va#NT1il;1?&2?Ykqo z^!SS%YxX~0_S!mt-C?)U6^HRBdqz0~Nj_#g)H>`x){=5Ei3z3zlt zKm}ZS^)G%PZ8qS9X41CQ)z482@b+-AYh(A!I!_ozc4VejUtPnn(MW4+Blz|;tjwEJ zpH*{>gCY9y5!y(e5a&g3BL_pwHJZ*|P6bpE6EwRwQlwP7qXU}2r(~a$? zi0eQ`dN90VY{vFrnv0W^F;Q2YfMO0$kq{fyE1dc_iSS(0J7H2j@Y(nfuQZ69yY|AHD;v+bnX) z9fv2d+82j_OSh5le^t79q))<>a@M<%%%w=Wuc^WG!k36wmu4GrfWS96`Vpam8%mZ= zjV4zV>+zG2lm7}I1=PMu4M05TUruQsvEr!wTT+=!BTV4u1QQu380?^ms|cU<$!Aq3 zyvI7py!&&AQoVO#=^{_MZbVg?;^bJl)+2J|0Q8H%Y#_|BZU5x z*<|t=v+-tFew@Nqoj{z92M&h(NZqUrnnK$CIfz>ri}!*W2>O@XIV*7yX9m>l2x_93~4Fb!! zdzo)bY-gm1^n`8;{3?P2u3nb(xxUzXx~*fKzrI%hoD!%DOH>{Y3!)iNkG2EqOJD~g zqd(e8bE&&A!T`@e(8H`1*uN+tU`IQ_d-g~wMAN@u8t9r3c?-BMYCZZmpG{vcVzdvg z!M`F=4-8r?6r%IMR?sFjzoN%Khb@su7y_uEAE-h`^+$K1nL8PQ<);!{u^|b@LM|m_*khuopU#pe$zJ^LnOupQi9i>F5=}n z8i6Cp3|aS7pKGFy(-U%~EQ~+vB6%^XBQS0TIN)xx@!i^4bK=`Itj87oNdVOsY4wDb z4w~gTstHqD>|RC8dO2qW8u=OB&FFz!J`zvVlbG(U_b(GvR)S6*qf48*ZTRT=uqiyAgmc+Uebwni6>4@tO)Dm-U*ew-#@{Iz|KUdB4K@$N!%+34!sXjckOlvD4u8DBE3qHXGngSJwJ1b3_R zWtb@Jlxiho7eYCZ>Z>=zO_O#SuRmImflstGi}mke=e(7IoW>6yZBoO9_?EwEaB|sO z)wS(On*YH4ewxy((zDR47wg9x%G@(vctG;;4DV@=~fIa7uZ_l zM(u_EN^@;KW6zcm2eh)DD$cC*ua62;{!Dv;dkMp|bcMz8>k)zRWCy<^1=V>+xgUmIXx)&vH z&T^~P_5OIz`4^LZS&il$B&Ca~oPv~CqM=H2IZbq9mdmTY-nF`rBqN(}xwuC5mA;CM%;Z`jd++TkJ1hH|H_FYX zYme)9e*eLJocnlu&iTCGuh;WAmAz%G;xL%>+Pw>a?(&p0j4zm8K~RjZF7CVu+Frt0 z&WIF-`jH$sAzpN@k}K~YLOvu09wfu`vJ+Wmh=A4Tu$byW*}i(%wK2Z8WMMEqUI`2a z)`c6`Y3}w8Euui-v+eTG%i-tcQdS$qQp{ht=*nKR+YE0aIDA7VJjWUDKz6A|0jRIw zxfmUrtUhP2_si@`qPra;qm{VF9#`8Co$V*FS1GhFc+4_Q^c(J*SL zX{t}DLpWd9%LqFu5lc&4971Z?-8j*CPTpYAYDqNmTI@DIU6L4vWR}Y#`z?6z)TJpf zH46(SaF|in&UVsa15YSs6rveWyk&~-OyEBsT)TAr3<%`;9c=6!&g=Rr^v-{N!hCaR z5WKC69%aaRKo%vv5(>SR6Z~8+I9B1PTWVKq-jmaUbAi;j@-Jv}b#k38k{e7ox!@my zk)kPp8o+T{_6R5QJ4{v;uL*LRc))9Abd<3w9*@h_s4Gq3zl069J}}?TNZk^(NXmk& zgeha$Aea!tc57UoxA|%NdYJldC9%-jHPQZ$u$g?yv{zf)?s`--vqBd4-8OaubZQXd_o79Q8o_x&AhB@gi%b-!g%nxI0){Gn<|veQGnj1A31l%NHv~w?6lk_~|ovw#XSOmARUiqqY{hQA>UH zlfxG}UbZ2s<*}@uL2RT&_cU633`fhv6qsycm1F{BF&CQF}tKI%Rd3fMDZPN{&$*Mj>S4H>8`NRJ% zJpV8LNGDT}GN%8F)hmBx8`iPHPEN zFQA@~!^8WYERyi$iuW2h6jJE)@0~xKi#nG_w{q54 zsV-1D{|3T4$#()WW1ka&v^b`D@G78O7(oz%Y`!c}V*Jv2C{CXj+W|YaLYS`n7{7)2yu3Tnu|=%ya^H0oPnDVq!@R{TxzaPi0M#?9L0`PX+iN{$lH7Owo(2)Vk9e zs;rex0w@fBmW)1n{PVnVUys@%J^puj0NB zU!JvlP&*Y$uWuwrT$0mO0WC}39D_cQoDqW(l*uGuqv7(h&MkXv7FU}4c@mlm@wnMM z<%ITA*Yx#*eORpT#Osj^Ont*Qwuhyi;sw0mGAE|4XYyvdI*M_6f#YV(5I+%cTZ>;( z5b`$K&&sjSiNi0LJ1VB>9UTyE&H%s?o^7S1W|R59@8Z#rx3=xP46E=*hz*Op45V+EJnZ_?y)KK6^N!O?$L5zLa9 zV!<}Pt$rZu5ppfCq8Z=p$}_-1Wm)p#;TU2CfGra_KI@%VYEZviwlq3U5us_ykP@B! z9Xgi{v6XI~B5WC+k9dzH5LJwmtggT9q-b{EG(OFsE*$K0fe@*-5gSr8yr8tdv=(kV zVz+gOY7}`#{A;$2VX?apA4x7srG66Nt9Tc@bEyn6$2$*+WQteKe?+5(rajUl7%9hT zPsBu*JI2|>Dbvh~-(x6H*j*VO0{&p_x3GIGUK0K}G+kOoxx!j{M?5@&tkP2!Fz*uTu?zw>n zJ!q-U5bWHyEBc`FUvLwh)YyioMti2auMXMgE%WR!snHoY99mq~PiF11t|OX;T!Db( zragVuIoagb{uv)bUFFYTW;UBPEIl_AhiR57xOUJBIBv)t5EqdY3lcr1>1(CaKw;Ng zkUoXwg5ayVWd20jw+(E!bB`N`vBWe;wBUk$Lo1{i#@3>V`=uvu`xDgtg$IOBMo$5V zt6ymEg(_&$PH(n=amMnee%lVhJv?l~^#0D4kIiyhdRkHM0?+&L*5<6GpN|_!6rrkQ z$?)RahUBxqDdzS}Nwpas=+f#Wa4T843d49H5_DOYiSyQ`F1$BwG`A+>?!_~BT3t@h zKYyQB`h<0DC%F6?{oH{rXUZ-PGNYqI#!ZR_`}E+JJDgd5_>hGVPv{3m#lMQHDf zVk{>d?31%3-$G6NJAl{|En244q|H!gLb+GYkH@9e8E6yFz(e_-KzD}^mu69GKb*>< zYq{ti>K(5N$@#X%z$2!FpaA3f0wm*kx!IpMq{-VlwB=hkHxqm9gcz$B`7dobD){)g>~pNhw0uuM>Re z&`2g{V!tz|X34?tMNu}je%4&l!>^Z()F!VfLGt}eT6%Glj|LdW>MXj-520r4*?x0(|xfwV~Jowsn#>6Fr$3&yCxAPuwo)a34uxca6GI~ z$nN*H=I}fHYJpP*LIXQ(T2L7q{^Lg;)wchPg%@#>R0Cz!kWg6KQr0#^hA;OxZ0Oj$ zgSDxGkSYmczE%n7`TEXbWoGhopN@TT-0xrqd4AhJyI0rtPW!}+o-$%#{kIk%HpVO0 zbKN~H6n{=roW#ZpubK|TH`HtP1iJ+L*YLhLiR-&Qw0L%%+VXqwoP<8C#n6Mt%0q{Y z#8&}~c-xpzj#L#Bz5h}vq&UarojPm%m8&uqV=rSgnhqOPFu2RfV~V=hVRI)=m!a3l z-ZxaABg-)PUAi!*xr=i~v@+R8N2xy*w(j9Tj4=cH)5$NiG>qJg`UF#N^aX}sAQVUo z?jTtvCfDENVl2+6Ok#^DeaD~ae=hrb@_%_-npFa#nh+RlOs%D$(JrgHIoOaF{ zeIgUHJTPVTRG8Xi;}v>?sh^}2Rb+TD%ko9*;W1AR>!lwO;`>c0gK^Eql&GAFwXPNRqKSifiY zL8i0Q@62t#bd5bF@o2Ky1m)2*9Ay%JLuxg_Qmuvk@SugWKN{DFRPh$gU=7C~VGk^B zqrAoa7_-Gm>k|SBDhuyR|CH-q>qac_9V1uru-5e8y08dt$T=%u6X}e;0&aY(j~{D3 zk9g|(=x99P63{ApWU(-R9EJO94k#^yf!BZVqam1$%nZ6ykYVAi1wQN_ldpImvt%#% zA#BFn;J>BWI+y!e)?Bv{aHk|9lKi4%FYQt@i_Db!5vjjT$k?0x~ZpP%DsBhw4(DS{NCcpuq(aWq4pNILwv7qeK{Ywf_gp3RH|x8rJE3jI45&Nk9CeW|?DVxVg(3V)=KR?!jaW9!8sGS-ns949;oRa(-%!zpCkOmv(whCY;-O0^ z!M!WXYtxN*g`pku@#GMb6C!=0Berw&a&!*KE4>O<|aeta9=HOIGKC6QEycGU2 zi?%^s(JFaR*AF%tCqc45TkXuA<{;eNx;15Y+-;$y_Z}z4#)yuh3P$NQ_(}>4kv-LM zH(}k56N(MhEzFTd%fGwIUJxTKYnwc(qNkWk#b!9ioX$Hz8M(CmIgHdKLVF0q=eMA% zxBCq)zyYk99Sql8L~~*C;<6>wrhL}qoo4Ek+tbPsq1=!C=AX4SA9p^eSTp`v+B=H= z5o^>Z_&hO0olSl)Y_h9Y5qu-HcEd6OUOe%l0g>T{rzD z?mhW{$jYb9=gv!QGanUAQ7&=+zRQd1Exs=M{rvjtJO|}lWAtnKcDcm4U_<}ThwnpI zpsBZsfvqn}u(MEA#O&+TGzcDbRM3l?o%$H=ApRkY>o#!bTb6>+Or)&LXUR5HZlTaq z=rjyr%VF1JxOZosGF^pUJ2mXqqwKPt_jfVxW^N}N@9Nfn!gf}7S_t*JQJY>5hCCD9mF26z>N6i% z^lWUoYAUTJ$@EIUF7wID9A1c*(DU@LVmZg~Q~AaCO7vBwJ4k==UM(B{Z?^6&hx;4p0riHH!MMz9 zO;Ru&IJJ3I@m9h7tBbb>S#7Ri@MN!oGe!ckoRtk#ovF>@+GXo{?+YD`eV>dXR z|EpEqvk-c{Ey|2%%&?u#wnABPX_8U5TykvCPhwun|y21D=;=9`^ z4O&ZQT7^~Iiw9&)WQ>r$HDkI7{5Vd7Fb1g*!(28Ha5Q3ie~J!HQin2ZeFpLlcQCTP zh_!|W_~mPk`1rcvx9bKE}fe{o{d_&Vt&4p}D z=B)!>4IMQ!@$tfiyYNh6*TbHIfLB1T`Y>bd!3Y2uw|wb=T9^G8pdM|>&Qv9-bfs2) zb*POdrZG3$i{6}Mjxbm?>*8nlrT~`W{148BXZ;=QtaFP-!`1XJN~l5~f}dlG6H>G6 zgT6iOuFN7C<(+Hp^|=k+VvbPjtWAQx=^n%!`z%~1p94760$>jr(@kh~&z#ybu|BbH zv$yX}%FeVr01Z+({kS;EzSK=-q*RN++dN7f))JBRS15o^`l@E6#B$owrA4Vcd?s}` z9H4{bB4W~gf(DLuKixPgFwnKh7WsqlW{v2_auV40oR?|G_)-LqDEnvYB{h^Lm>8?- zRO5(~@`&pMXxA0Hqzsdbel9f~xwE74jo?S5o2+9E6eq9xEbz-`xk?i2o=1|Kk`7R) z(^^EUqVJQBy}*Z*P;Ryokp}##7Y+=OC*xHc^Yw70XOTYy?))$=)%BqEB>#&VVn62T z{Jiuw6H$ITvcqffJ01kPWyAQ4Kbbq~Yq5%*3_I!@9LID&F>+K4-<4&i8Yqr%`xj_>Jq|5Kqy zg@)D$?2{=Cw>y~cp;S4if3tBr*n(<{!$B3f3?%$k$-SQ=m)EmfU@M}6+t3eDGNEty z5u9O*eDCapQ3x{3p$?R7_dKlOl| z;+y5LCs*ue{0?&!Sk1%LwFlRH;h9?}G<#tb?%tui20;a9^xwa8um5sRB9_EoeZw}x z?Q?eDgT&GFH$L4Q7-$e!hPgF9ja^))aaN{;-Au$i>67v%qZ%9QWiCc0v8Gg}31wcQKX@`}!fve+1Y5i4<;8*einy;(FJU zH3sRAcixDSCrrO$NQp(}KX^PqrQR?*VIKI;g;kn7xm+TjpMHR4Hd%qvuxZ3-ZyAouicgT=AuP zWJC`LDS1E_k;q6lEek!}RKU zOA_oUQb})p_RWpn2wWMffBH<>J6NNiIM?`skzY}WGi|Gj-*$^fhm2EfVTknN4jVB7 z{9@H;N1}%J zb}@gm@9ep>DYP~P`OhAhCo zTkJq&QxRniz4L1C%c+Ira+jCi(TuMUcHgjJr4fI6!vrd%f1Nfwb$@z7>k`#o^WP!4 zqxcWahL<-;1oI8aXo)Iw9qIq1=Ry6JMmQBM|8N7T0N~mdS-CxRvu1MyL5St6jrs;#t7BE4NDCgT2L9*OK$ie3)ydyyR z1>Lb-zmZ-7UdUFYKnvIs1o1HgtxD1iIoDk}W@knO-+UySqjuvdSC~Q+SnG0XGfd@V zGl4C#dywP}L;k5kTO5SCebZP)=o5-!fs*ywePK_&%&U~93j^P*11Vgh!nKpFxkx2D z4@vCqg>9d!@EZ5hbM)xhoU;Py;)+_qq{RHYo(<&w^ZJqBv6fc@fu{`FLv;bajy8KK zGYajtk5vEg@q%`Y(0Ru491rmzAEn!)Zr5XFW_)It4dz)! zDK8kwP4?ZMiYAaz;kOx8Cx+NH^=A^Tw-;bXu@4RX;^z5zN=Y(*i56l#UakpYu+LVQQe;7nnEg*B%2Lzp_aw zHS}J~_?Ky4xa^|6f@C777nhwLBSdkBlb81WaDBu5HH30|$x~@rb1u=o5&Id9s=LJ( zTAPYdcYCqm9|e?FjELktie0nC9?!RTZMutS1}@0X zEp;E$j$H{V%DdWOz0;=z?n!p!?_UKod(P_^%=NBEdv6%5-aeU_2;(AqgfI45Ci}1E zt7c-uFW_6TIbH_#R$$S2pcK8lE<-rsE;+ClTH|-)dW8`jbhno!!j6rz6kbR|m1C~` z;ASs$m}}Xu{8{Q#-J1>ayT^}qY~WN}J|X}w+RAGRoA|;~^*f}Yu!@OEm(9(e&Gf1; z%d2m`W^l#rRdwUgeEGSGAMd+jhs)jp>?o%Bv>W20nm1$@1*BoBT>wfWZ%2Hs8@pxN zaP14i8?D&K_r&u+R;@&XUQ2MuY02_@s3+UKfd?z!T-BQx>`YqN;y)*SfXF|D%ri7K z;&<=eGE{C$a1xmfQU>YAn_I^kgrMfd8BS_LWtMT)&xh|2nWJLAhx^bT*32$+UzGXNc|DA49S|V_g4M*z(-gR}Fl5@*lqjSr+WkWY@~~m6eL-wvGo3dYb)w z&My@5oqU&YHx8>P2M!jOv_a{CDz!*~9rtVvbnbM<*(gv00h{TW?rK_&`ePMUry#_i z!$?Bt2W$P%XVhsNT7b~`-lTqGpyxAdum878KZg4xH3`o`RnSpxTrRg{O;>T%|Lo&J z$hQo3hvle_9wjscz1L%_@Y3;$%|6BUN6qV9g#E=x*>x?fJtQ(#ia%?<=Cu$pydb`e ztMtZj$cCA;!+zhK>ExBUzQqVm>l2`2aG8~};T`kuXl1iU0Tc&yKsonFS8%j}POxl5 zVRxhNo=#RGxQ;PszmLT)#y&H2>id+!h*($%V~!Seqm&{yLG$rM@$1o=FTd;>l zX6uM+Wk7$L8&WB|ycK+fk+E@;3^cP`zVAn!jtDU=#k^krU70@5@EF)^1)udR(4Vr< zu>s4x|G?KT`1fj;%GZSO>K2wFq#lwHP>SSjut3NxMU%aOSkYWy3Vds`xX1)6oNo%< zFZ>kiMMS<$c;>hpN%7E1po?HeMfyJp_CwuOBTtT%I*^bDv{d9vT6Dc0c^|+?vKPlb zZ~U6t4aAhHoR6wcAI`VwU%u_xj}6xLd+05?jA1*mM0QlL>`FF$>&DeQuz$E7Hrf_W zb8{Ymdolp|xO-8@=B)|}fDt(@rZ{2wy%tky&UK2EEWgVF{PN@82Cs)#Cg4%G82Y8T zxfp3LMCkd;^}Y=6DES$atL-hz?-wRVdS0v9WKX!|A$D(IFt-+Im-xGXbuZxxNc-tE z;Z1Fi5K7YeQ_!~I`8ybb`px_uwWj3IDaQ1RZ|@Re71`&Ea?BrV>LCm{b{l{N^at+0 zZ~q*Zv{#O ztkrKWef(_vs_$ZZg%UAwh4G|7uS*@LEWvMmT`NSGI-JH@o>l%cC?BQ{)K)Rhgfc!Zo`PYksUGWf(M;_;Z3BuSXiF1kQD{d0; zS2vA1SBh0M9*VEL7YYO9*3yL8JcWOB;Ivy@cReBB*6?jC{T^T)=tV#UY?mM7zDeit zQma5soh)BPDe-d4Iu5oe=4lXBuEBkNMQ{yqWOTcK1^IV0R~+8t`yx+Y&rzq(BIm=A z)V%q)ubah<6Jak+Q281@@U_TP?dFG_QNz#Ww6G#C7f#DXNrxh=F^0#5w6=v4|+25 zyt>b8BY5K#!b9WzV5hOON4ym`vb$%G^5I1G-5TF59XDvje#U0Y;1sfF zC_;|8L2E)ss%oG~_J%7!bSW9GmlJk_6bn?Z;l8UcAm z#%Ler=lh}3!|WGlC($t2iFiEtHsr^&gLDvMU)_@5GTTQ~jms?NBwyZ!R75Q3_^cOm z@TI-Nbb%z{yGC6B**w|-fF$iDcM4aXwzoS3PM>7|&3|2YIvfb$29@jzd~13D(wYID z)XCeQoc}>I@yoZVOne?G5x!AHG3-Pe9kGuA(%zl?j`_i>3!wv2j&4bD|KtlYU)4@joSV70pi`#HM6mMzQ?Qt@TW zfc8Uq@rAEmP=$x5Zs0SceB~Zbq4^NiPQ!?&i`yMfF1Hzv)@WL;m9qq6q&)1!d`CA& z){?CsSr_Us;Qxhb_c!o#o&MTTz>Q4~K7S2Ty}W)v5IbMtJlG0` zU1zfet>4UUVaB&Q`M@Z{+P;t;L~>C)Hn0Rk9k3iiqEB1*zWNk3zt*D}c^;F_%nrMn zWHoPvb%KhvT(WPnd36W{HRLG%w;l0AyzOYxnh#zsF)mjuBZ@Vv3h_gyb%R$JIbJU( zj7n2h)&^O8%j=bTHa%B)&~ymP7uYTyKeSR{<=gY`yM9tF0!*B}Ae_Ee*;kZJ%sS`- zDMLK{1;GtSMSQgg`Sl$fYuq;6`_L=K1UIxJ&4sp~@@?z0n__rdx4Y%CExf0g+d+>1 zic~8iDgD;fn13C?&)`pJ9I`(+TX&G*!8hUcH;YLuLMmDveaw&?(;+ zekJFBTDoP^H=j0qXe{FT?{dg1BtEu}wd#C1*T2Nl@UR9P#sjt>!y;Vzs-^VFasX(3r$DGFNB}o07wjK3!}FQ^G}7$rD$jN& zxzmw^-t~#d%AB z0WAB5>Ww0wrPkj_+tz%oT&yB*l9oo{cN{)zW4`bTv*zj397eT;Rg%9fY*$eYr3qoSI!L1mkxH;zL;ksFVFsxfEJL|H|Py0JRABG(5TfHglsMS z-&B7uz|jrzm!&@~|BYkIXDbVaDuD`0rl}_PGRvn`VN@{z|0oci!@?)_Xzs`lR3iifT1@w4%J zY$L5!g$V_KZDDb4)pb{ma}Y_Eqk&Fb8_01{n~+wOGRG8*oW>nb7J@e1+?nq;-2bcs z4I+FZ_c*dUYQXNYJ*tIlBZ=6|Qa4Tm3Xl2xWtMAWMrKAPO1&#h!zp}i4H`o7 z?P%T(=RBwE{eYW|o`{f{zpy*+^{KL-!I*FeVju>G^B-S2YTLhy7L(Q8gw3#>-pcmL zEq4hiOX}^QUE{T_C%yxT-uHm*rP9kBeX!Z31`3r8d?aR&tR6gHpp14;s7eQmgZZ22 zY?&u@?+7q4KHB!56Y;(JO-5q1(Pn9c>zo!*_b2d~mN!NIPX7y>n2Lln$_t$3A4V-@ zGBe8;Hz#Etsb^2tGdhVgQl4$KRg3)zXHZu&SNtqLaYbt;7?cFpJKlP~c{RexURr^C zdf_KO#Crx{o4;GpE4nvqXuih*g+$GFn8+|#YG}eEcL{Cpud554E^@bKG0*(5%RDLM zXOXNtUOk7u?|}F{5gN~wzs23EyEe*Uws<>S%{T&%9$pi*Tlh%BFR)9HKSZwtCUV`b z7=ht8rs1&apux`Hs11L@C8fhygCFSy%HsTSVHzAD%Hjr}S04j|g44wVQ&{H%BhAWx z+cSll!~BAjEybedA3Z1-+XPPdetI(QEf2t~1%4b9@y(wN_9BA~R1>57IT{;~8rXxG z)3+pmI`(fG*g%v?G}K+oj`rc6efZ)YzKMlhQ~^j?!bYM+hwYEBHH$`#bd3RC2)DB$ zfLUt~!7KJvI7YYkzVjDh}=3{0of7JvP?@0og)gUrI zbsnPx3H(96m>y6Gc&61*mD_uvxN4}ZH$KvSjlAGI5+wJ)OpiKq^Dd^|pwr^2cZY&h zEFz>oHyQYkP$uhJ9Y(ixZ#M0cnzWVu26|?pn8x#OCJ|KVCUhM#*YU&W@&Rav5#*@y zB=PQQuRufX1Ev`2BBu-uP(?ZDb9-%lQqjKVXn}2Ehqo%{OoP~`$;?=f2%G^fRt(afML?h}x;`l%L;wLWs z4>#JFDM`dE@~}pA9p_s03BH4njaPk=XH#vd)DqtUmyW<1xKdDEy-@hg&>w7KDlu2}^3ku^2JjebEp;t^1$1HUDcaJ7ldh7yqe)2c0V}(jn67zn;l57~Ztk@J>s7UpV&odt0pZ%Cjrl1+FWx+bomq`eg;N1-ZpGPiM5gz|d2(bQ<|X z&W(3%T1-gn^5tz$gXa}~^a^0@(ck-V)4D8@{+YPUGf-^`y-r5_$xl#ri7k`5Yxa@U zI~Sk=V=f#H(2vOjcL-OQ|D1CslgMT2A8_jGuC_5`f-7;QKP}v z)+%J_Z|W=z@V(jwE88 zPhovLE7OUp>=;*lTvc=BT0Y<3BRT@~upX>$+IO=EvIxY*UN+r(>Gm&x^pJQG$f&MP z!ETF{j+4Th@z4&cO?@z=)BAnqrul6_p(s?YQXq3DbJ)LHrlIH%5Q4xyRH*`0#Xlfe zB9lf6yE{3Gb{S|Z64Hrv+X6H;i42wSz-u-L_KI=<;x&N^O$V(5`{Y&`@F{O#9rA*iR38SLtJSGhOD@ z(P(%G7ul^7!to8!d6M=iR|r+5c-P!>>Fs}2U3s)%sa1f-&eK;Iu|1*A*v;ZI3#F8E z`3~zY#8Ehgxh`#FT*j7tXynqoo^QofP395)Ac2KSW-H6JqP#As{=to*my!)G_Z~}n z#$YA};SrAL@Y9|(#F6`&HCaRN^yEys!Y@$f`r>W03Y&2pG{htUG(%^0Ddd@bKDp2r z;%NOyf;YT~DBr^!_5EG)yJ&PZyX|p`uq*pcS4<}lsJs^Ov-$qxLD(>Jr+uy<*et@B zBc7olK^9^Q7a5XslvsC1myMz7 zC$arR=y3|S6p8Wz!`dV^Tk8dQ%E?|Oi$yHHiK~|!skT|EFNEtbp9cV~uc4j0pHSPh zTpw3FZ|Kfa8^5W2<3y0;LDQ#A>P$3Ehy7VS_yE;{h9z+mE(Y8-^g|ckyTuVnl#>|` z$)aVDAzraKEA7+&P%mozZEA*p6~xyD6t7L~Q{pG8Yv^{1WwgX_-K(WlNw(kBj2~BR z8X511TQHDFL>e@T4}9O^wkCSwovt9rvj)ueCyzL~=&y;j!?MB7Nt=;3d9sbUr1b4j zW(G~?Qo1-oh4gKg!+LudtYr49TG0w1)9|{OtbDKZubJL-X8mOebKMPM*#pComyXIM z&d`_gD%Gn6omKnS?~A^yn6{GBe zzsCSOcLgNK)BTSSDS(@srvixxocuH^TNmH+ZP)|`$3Azk*=a0sDNvkRw9W?M?-$I5 zhmnhpq&0>w^UgPXX(j7EFI&&tjK%}xYixFUbM0l>>Q?%(nWZ893Y7U%dHy$TJto(z zTKA+@08TxTph$J5)yV}eYZl;eZ6i-t`RGBkZKW&w+#-geO__<2$Y*1vLZoWc873pVmS}W(&U!W12(aJXJ-d?T zS6~!gkP{iGIqsF`7T34RP4ids{J-Dv315cK!a~TZZ2v@kUZ8YW24#5krZ&p3(_nL2 z^L;cgDJ(7Rb$*w*yhK=w{q)>5BsUU+6BC;viKcnM+R0Xeqa#6q(u19vdjr~!IuloRh((4yQ*cs-+>lKtr2SR_Us zu@dDc-f38Aw4#x3ZvPUqBDyzI;8ODG2_=X3aP+lWcNv%O77SQe6|ZXCH5s)JN6^h; zJ9pgg0;Ne-7EIy&ifN-9src%#v@lF?3%zAFbhunT22^Ctq?%dr^ztp@vP&8+zNEng zes#xfZeFJQd?TeJJOwv=*3HKdcb8C}fA^0L!qFs;dhyO6VquOeQ7Z!cRx;7LxJ#RL=c zFRzXoXu66Csg%snPpV+<@9bX$&CVpWvibs*6L**lOz!PdB{%*%8nyQgvv>}{ni9?D z%{B2+_N-!VVDF9D?gX&n`JF8tPHq}t#%AMJPgq%9B9s<47dGd-cUPtyKiSQ#V0>x2 z>>2b|Y5CVOCFZjt@GtHeC8kfv`GEqz2akve)u}m8u$yyXuhY4kLPob?IgCiA8}z3gd6Pv=f1ERDut^1eGqCb7y^ z?uyTBRPfJzOMT?Pmwq1Oc}tRs-d%snCi*UHLk7yPnEOL(cYfyeL%0iE;VGyRz2Xc< zR9F}pUgg>!HFkT$)xAK$Wa&tE^o^#LcpA}o2t!^4y!A852Obq|G~w`~j7jP~=u3NS ztZQ2hcaQ!2*yk%mwuRkK*moB6NbC6t=OLg)u6Yq220=}E&bN5MezCc3tG#p0oz-K| zs5<1JNwe-=3kG%)b@j1NYkpB)cx6`~k8UD@8K9qVQ`2kyAY;eE&5-ClF?~PSS5nEe z)3HJe5BSyHiX_wbdy&OWo2w8Cbx{hHD^AbX<`iDA)W zIkY1RCQNDSkg-rBJ}GxWOiIG{n?#I;nJde$rofx6GMY*!Nx5X|k6Kaw$ zdN6-u&X-Z>!Tdw4A%)0L<20>O4q-oZ!7frVDU#n zpzCF%iDCjV^N)LybBE1hQ#{FpJ-x-dI}z&A;4w#KN-ij3)sfXSa>Xzs@v@E=heyXn z4RT*Q+vs38=n0>#3p)Gy9&Ismctc&nvW!hmfBcAU>kBG$e0e8!gO`vM{Z#wROc5&u zZQ42rdzlQ917G2aM#M-cjEjNgoDVz)E`0;Uun8QsW?4TePkjH z>BR8u=Rfa%e2T}v{H3r$TG{`(&oje%qBp+=HCZiwC!rK?pTH`X_%yz&Cp*~2ORvSb{es91`ihRVzxE#hLUDjcd_$$miT^#ZlVQdSV&;B3zzVC2 z??b%29EVE;qJ3y*)LNONu+6h$ljbhV3o@E%98It|R5pcZy9>~;!=CPh2ua>2r52ri zH)}eL!6WT*`>)OY4qcJitD_mv&sH1{t3?2vuEUll`n%TnWflkmuYVs4$fk zrSi6i&>1c4=)v&p)hUHdp2}TG0P1qN{1MIfnO&)FW}ctZkuX`B#SS#pp9;xBf;_S; zjScxeI(>_7zczBZG`BEY^*L-!kZdFR^|T7n;_9#L1d8!a!Bym2ME+ttnTEztSFVZ#MXL0!an! z0Yx?HjLtHJDg)U4kJM;xE*)nfgnZfdse-EfA1L}srL1=WVqvFvK3n3>jBQ^GdyPFb zGP5@H&q_Trg=q;Vp>V=Xtd!XH76P3?ZzX1q#h3%J)w1xk^Mu*@@z zDn-}3pTHwLWsJp|d^3g-@JEQ4D6VJoy~_)LVu!q`gxXlnQ^$3M*d%;A*crc@zBz5J zDmZCA)EQv4eMJlQRO;LEjCDx*Ksy#n0dY4vrhVbQvbxr?AWNWqUo406jgT&N)i`|r zd)bmI{>39$V9IXv&8mR%_Bf^nV5tlSc32*qi~foa&*$zHp3zNw83CB!w1%|W(6jC# z!rTDNH@1B0HrPpzd~lF5rKs4`3m!u!>%U_5*uE|9OG`TyT0TpG#lK*J^&*JO0e zbgNIQgY7OU`Yw{|n)W%oKOPR%#c|2qykNd-=;hdf7n@GqxIwr8R9s4@8Az;7l=c=+ zv^zYAxzrSfnD)aiqOa;u6MK!6KZF&xIZP@e{Pk9+9SPiyzyFa#>{V8qV*JA zoI&&0bW!>zYMMD>{?q0F1kzABbJdv9akIc}umED^kVo0H6@Wk@6q+hejRSr4){Xrn#w9kl(_=X*(2cQIkrE~C z&!LSEKus(Y;l~B{_iEW}VWhWpv3DT?wI8M`H;j;p=<=eAS6$1eL;VG{${ejIN9VwH z0osIDcIT?pgVI?u6Z;uq+MvCY3?R|=ZR7IqDRCKOW$%u;^>`)Uz2mX1A-7T7Y5=(w z(ULiWJlOe;ED~M8O9t7~qrmflGn?d_To__IXuE8FONA$o&r1}GPbZ+a7uMf#*WEg? zil5zKbgQ9ZwOq;_aj|Gg4OH5UX!*2tN@M%heaT+>=NmokA}2bZcE_Uup5ZyoNpov1 z{+}M>beiP$zGRSgkMcQcrI) zC-37K3j+*0hbF<}C-Z9l=3Ko(X{`BED@ys-=g zrvCOGUD>z~hb;r@2fz5@7nQqc1wIEFBYWH)D^9jer@D7~QTWb%2=#DF%?uMpjUOiE zmGXkb>6qwk_HActqq;<#u*Sgd(Usm`1RAHpG|O%}DWxk`F0RPCBKN%K!|u&T?AX#b zCVc`+#jIj+Wl}$n_|yjPH7E`%l%6+Ychg6coW2BWsNiV;$&*dRIUjwfvYAJgJ-cMz z!<8P_2is5c_zxfPVqPQr)3`LEJjF4h$OW&>JDVZIO^mZ#_5s7%vb29-$G1WT!8;Ss zgBf~IU2E|v5HkA6r?)BsUU{RF{!Jf~hwWMS^?MR>6aQI$zPG5KVJ>d-Ntki1-zKjC zeo2BUbAWUzBU~`s<1m0Ksla+r;JQtqd|FwPzVC~)VD?HnurP%ABICT^YqjL07y&$$yuu-k|G~~_%kMw+UfkwV8NflS^NXrH0lfpAQwuX?8ae+>9vg$xEb7~2!CW2_iiE;{^qQcv88pXEh%(t^i2tyabLg*^3cO^o5;m92 zof&krT=F|mUG3kECs)R1$4`!@JD&1SC4067&j>QnZyCdxK# zHkF$O=@LwVo!SrAtemebhIp-!L^_nre3#opMYa!s8(`?m75v2J6S^t?6R z<@vdW`^oB0fu!qK>4pCq+T|7c+k%D4EjP~jdn>L#Dt{EoY2wt_qw-;6lZqXRn&*hd z88iJoPyA;+78ktFq#^r%eh0vToC5;Y3|NzV1Gt;@Q)8cFSfZ&yR%Aed6&6~vdDE6Q zhAH7NKFn5(MM^q0f7=k;JY^$`$(tmA5yPjzxoN`l9-HgtZRV(anfDq$-WJzjlPLQW zNo|0T^@#J1eFEprkGauS=Su?~%>N{cFl=Kmh4s(I4n+dkn(g{&Ob>onxWFgNJDD_i!VD@^V0e_4&&Am0M)!$9F%MTa8uXH&ZgGp zTF!p7=hGKHAc267GRM4;v&omO9wjVqwnW%riUPrTyd}GDf&Z83qs)$Jr>m)SzxDSP zcbWV{0GN}m0d7FX1uqp}JGLz{1h{EqLjZ0PQZ;*KD@zCkE-KO3`t3DF0+dgUeg{OY zE=G86h3QavKkwtdp#APX)6K>=^WTs+z`tqEswOwRL*+F3!fa#vINwoETiXo@K-(MO zcTf6lyj2cuaw^qq^8l54CIdlMldM<&e|x(Y9mjDTcmd}B|9H`d9zX+5ZM`VFmp~B5 z)`O&|s){?E{O<+(kh`Q#04Pjo=h)!1T5GjOv98m4eLqtHFbBa&+r-bw=7dfazO|_6 z2lJm586*Dmd+b{=kP1c#0bDqxEay3lPrX*>TEAHiRIVBTY6M;a)SsxOGzSxH2wVuL z3e=MDYK{3c{b&RLo>!ol_Ar~Hi7%0u8&CO^_Jr%#=ImU%5deqy6@Ia?SpBFux8e)O zXX7Ofapz~ASd2_8-w01v-=Fa2YWm^kbdScDITwou@g%K_5*bzi;2=%nRROlDTNN5p zH%aB-IIaJT{HF*|u_!=M$#=zxi>o#TBYm~tDL<^8Gc`sF0&2_~B60C-Nz@6aWSCn= z;x5*0zvfuQsesAVn*y*Tey~pqgsg2->(i%nz1pJ~lYpr6#zKk%~ zT*;b!fO~%W-*C(g4x};IvGV<^^smV=&h0R|sXG_jHea|de+d8<*w#7L$0b(*T03_G zo(h=QSd;Wo0L-yee-=B&#LNNfY@_-q0ifbdLg|d{)@=4^H}{Ogtx<|yZ8j$Y66iev z9Gt5$MYQc0j_jM)`0PM)^P0`W=4A7`T6VtT{!#VGux=hRfLD9xJk*~7zT2G-lc~7p z9BSV(Vaevgu^sljQR~bHNBjzBm@yHlus)5$j;9|VB4jb|%0C=?Q==_TsCls_6sFI( z(Ej`zpd*Mk)ggPWz}b zn9(mM8r}G}VnD5RYO8!zZRWU17@cQTXNvT(h7uX7`5AC35Zz#st8qvEQv|3OnwW9T z=BM$^BC)lU3WI)$N3$`Gf_0hYwU~6i2^K{YL`SJ^Tm<2 zn%8ll5;eaC0K2`70E~k=p`aYu068uy1C$$;hzXef&ui)7{8e+g0>Re~TsP#u&Basl zS7SQhM{7yH5eU;i1r*Md5NU~0ty?pv8qclAx3Tq6{WpG{c8bil`C)Aop|ko{gxXpn zwx+h4y{E2YEI964)`(q)UoHz zuKO(ja3U3i+XxAiFxaQY)S&Yj&ux`$+a`&Pme|>O);|dV)|dIJ1fPpFwNZjaT~A}S zb@N7jwcyeS0QStrWwoQmO?}@mMmUOiHz28At3K+1J~+F56M~>Q&ncg#5NODMiiY#& z1f18^jq+O&={EN1ha#2fPkIxC{>^JjIHnvhG4Y0dnY)Ku+)lOEyuo7QtM+N#{t^JJ zW1}kA!ZIgs!TgvbLw0>@N|-oeoPFo>4WTrqNFUEJ-*A0qZ0hxCzg47ZW88w!e`!o$ zGD1|>*?D$dk-i#(wfPB4ZwOp0Nf2z;-;Lu4kiDZi*V?(=Sd+9^J+SMH{O4x~*j)g8 zw8X=5sDy!vogF9gS2(2VLJ6egJIetZllh7QiJf2}FsZUg`ucLRV}nDd(QO>M6Ksvb6TxpC7b-wJ>? ztQm{dPqQB#>9;Z9`nz{6ZvM@jPfX>-0rKoO2@lCU?bF$Q68||j?*=3xkjUTFxf4na zAx%r-PxYC(XfO=t!|~=b_EjR~Q|uX+ch+`3J3H&Hh`I1;AFFG<}pp*(OM} z*{Ui$*Yv*?0%=UW(@J67#6hK(qD zlXxBBvifUsg@ogXN%hC$CnBP0r}{KNI(<4{4SbHB0AS=l3Ezo;>0cw%koY!YpdtM; z?`EB5oJju7Z!EZRTOYm+*C^HN++Pj(->Cji7a7#Jx5(0~#1$K{9RRK0fd zvs^j>kX|=J#9iBfnFA_y{AA7>bAUMa7CWC}Jmphb5sA!&$Op! zz9!kv=M(1~4{HhU3^byzv9#CQ{f|D+a}FC(2(X`3Zsun5yW0O0@BU{$CPthX#kE2l zoZ~mYL8IbO>y+yDR{}V65RaWXlDrWUioDXhf!RL+7{-=-RB<@aZF9%gMxDpw#@PC( z93%ixb(A(369Foo1lB4?Y?C_jSM$4=8^17)M1~4%)VQu)OZuBSy{Tz-%=~0+Q3SBQ zEf^*YbHvALq{T2c{&{vnU>={nblR|-Y>ma}^5rl9u{ast5n*=#aPOk8j`W>RpKwPN zV{P7CG=_aP0zl2x*7}U$SK}xn157|O4m!z#Xve^F-=;u-jgkCjK5hU+joFe~Lq<-p z&GRY;^R*(NO^itdxLBCExiR*eccX4>w=QZ91%QrxSuLxYX2&j#I` z)7anE=k0SQ7+MjK=6;G-Nz~vod*|!+)OL@inn}4yc;(*oxOv&1Mberv-;XcK$EMx1 zhTIiaE3UzMU;==N036@Ffo*dp1%M)nM6jxV8lzndH#b`>=100?z>>Em{RZ$iW3)Ni zTDyJ>6qG1oQI$3;ksP!*Yh#o3_qx20G3*Sg<{> z{sf@jfhFQ^2wmaLrXJZkvUO#j319-?yz^(GdY)_7&b?`B@XqbwZLaXoA?|@fN$msl z4fF9?egM$r!x6vgJ9Eu`fDUWb?PrG3&c6Fn0pP%jO@!AlnXI5TgyISm`zGJrqBzob z=b6v*S=m+$w413RI#rA;CRSAG9X;Dr8xnD3)XaZF^qW3z-5L2ZOZ!TZe(U2IqXD*g zPLZlI2W&UzRessuQl|*Ov04$3Vq{_=tp(OTNx%&ML8#|de)W=$J{S8r%sohAW*{jfjg;`e_6xWZrr>5_uM zp)<-+rO;f$m>(<#4Bj2j@g4cIIX3SKT4O-&ia?F;&as%>dFCGjWp*CNcSO|?-kc|a z7VEne3U004dbpZyh#!4Wc~O2cLT?4C0xs7lU324Y_J_+k?%VAzdv1L}pBw==k%Qzm zNmA{vbBtqe#GS)DW=dqRdUv&kVn^KpIM?v*$OeD0&8PU*K#bp!dctcQbH;ZBK>7gS z^jVFq`r=yH?*UK)F+x|9G~~Y#0_@p=URspKH}t{f%-Ob2^R6!u($;5HIOx+k-aJ@L z3XE_rHupR~ue+Gly8$=n9H$}SId>}xPy3s?LSNC(^Ev@#OH!(@d#$diV>kEl^E?>g zFo4|>95a`@4KXULn#YqEPFr^Se}3&SK5xj8eiA14A%ADo+wsTNrIw#~!r zo37z8j_C-Kjk%4-ZFUu>D#S(rV7`*zQ+b$Usa&Y~alptnu*ya9VGDrFj}$WlL2T>x z2WzXqyaB`;;#aSezp4KlcL0uIP8i5DUtA2S_ED_hzW`#qOl;42++K3{?0jWDw5Dn* zeG!ln0BU|GSUwkb0Gy>eM8L+^6n7&8RDV`@=-WI##pp;MfoN;0UjsG@jGvNr3m|TN8=sqT zw?d-@$z&Tsch^!|hfi*NtY!9SOcAC6_cTt50p~%oVApUTo^f1lB#dU5=f+UN(apcz zhiq(xFUkLp5z>mIoUgdH?u`T^|9}4Zhm!vs_l$|3huPHOy#D9sfR(ciF*3p6f}Y2$ zAKM&maTCb53xYnfm>beZLtj(Tw{4VP?OHfL)x7ide0+nC1E3`6()>SRibNBkVtfq& zyV~3MyCwb`l52@}GiM_{H{(0sZUjIhcAW@Ndlt_-kah&BL;!2=#JIDBfjCAYovkoc zeHGRno1Ym#-&Cz6T)L@KhTm#0pa3}J@BaRe>zls+p}~CmbHE!@N!|dv+l(raZZ7q% z%lYEQR~r+7GAU5yqa*%i11k~M2!Y0wPv=n!Z^3cr|>u=IQJqWh*mf;|Cp~V z7dVF!O^nRb*f<`Gtsy=Z--r0oVmK2is^58LxBk_q|CzbF0Bk-h0FoupQL3jUe=NSV zDcn4+k6N_G=bwC+v;hhnCy6gsQ2364jl+l^pB?e1Vr1*i;#0-z)|ti7h$}|;E>>*Q z)ItjuxAvqS-9B)l*NFmWN&K?sCI(e6ZQZe$)qKfkwinnq?f%Ap2b|=8p4oToGe29h zWNUM~-l<+1oW*^bh#$vdF&$ImIKNds)i@{s&TEC&IF7<0Cjyv%Q2?Y(sU~X7B^m_< zTX5EoT~G7B0l>|Ma0QIdj9}P!YQ82m1?Z~n^bP%FZIwS*+{#xM*M|6KT#3XnR@LXc z)0%eyPTRCD%(qm7YCo{?e$qA~U_%a0gt_a^5}0nuuNi~wlg7_UVh+G(dnE#o{E4d0H)0e&TCeviTIK^ zfNX+lH#A!yRp`weEFUa4^O>Dv3#xo_q9~aoawLC8^2tB+t?^IQOadZBJk948Qch++ z)?WejE%-8fB8=WPwiZkTY{$4If0b|MV>(uGHT@LvGqIxx1!GAf0KXsy$#C8YXz@6a zKikV&5wOLo(+BgX*4@SW6|lOEWzLcKBZ3?5S=U`MWmBe?x;y%a)jeK;iTSBoO8`L z>P4bF7`a#`2?b z%s$P};zsgUJ~+}&@v8dVh-8;9*9X^1`Q6lk5)KaCly0&&f6fJ zdIx~LZpld%aaydj5t^jT^K5?Rp98q0!e$JPkZ|1jIxSYpg%cu;57X4GO6Q9LL$<+G zkvpQ8<6$4>mjVa1Nj859*zAL3%s&U5j+h;>V1C$ZHZG2t;z*>n5irTre%lDGIaYJb zNFCeF_VboI1M3u@^BdQQ{e&+2O#%EsU|M4*^wLAcN}b!#20p@(uvnjQ~(ZoWLIYx^^rns=rfhjWvbqKo{Ge=k%EES|1bOGv6_P ziU8+aX`?&vYHZG}@~dK1z;=`Kv|cK<2B^}tId;>|t$nj+-F{TpRGfIM##QD-jWS1C zvyKw+Z%B-Zrn5hiFk;tQJN??9&}DlziN}pEY>t$-o4S~OH+|K3U*b)`wX6{5<4#d;(eBF>rOZ?8*qM!9i1m*lV zg?Nscxs9ymYDJ6^05iT6Pa^^_{4W=P5j*;m+RX7BxU=}$`bqP(0szg4#LYfed1}N{6FVcu&DuQ0)vxlM{f47lj+*tH zxHLc4HUIR3nDDD5Ee3#SN=T^9$mUXsQs)}wq^_};n_^bhkUc*C6!E)xSv%Ymiu_Iv zW^rNe6!Efa%IDKo`NW=a!yss>8om_(DvwVr8GGv?Pgsq~kfymduoYGj1gU z7`|-c+6VwVR(ovrV?Hat)OnnzA%6Cg!l{-M4q+Y=8RNCIK1{SU*PON3`YHZ=srcG#Z2n zV(QG(6mdglI0lT)GQU+0RlHdEGgiz8n+xl}iH=0Nb8bgAX{=jg@ofB8 zqK*L-p5I#PGxq?Jf1Ag)MjLEVWUcVT^uP+7B&-0RKP1!FBRJ2gY^03`BPg@S)~IOk>oZcPrkM z0Biv}k-rjC%+kCFjL9!`&H>g0P!$J)Q<~R-X_GTWu1x)^IoNsUGaJjrtt@Tg?Rd(! zdE5wq>0k2U*0+Xe6;Uz~;0W`nZ^@L&fU~I z!w6OzP6#-En7EqfKgE7P-beywj^{+D5=QMq`Py=d{1X5chx&IZbb1CniJ$#sBXgXc zb3vy?V0~P2RI!|EZE_v4I5JaWqb2vv;-hcq<2>)$F%a2+g91q$`$hnqbMqPF_(?+$ zXn_L>?Z(d}e%$M~#E0{ZJ>QTa+Z&Fs-Vh2+%{IbS;lyTM%prdGYwk(4r_R{;rC>NO z!{Hl|gg&<-&h4MHU)b26Al97ktu^(wCH|Se#4|3IUk&i8uRlfSS>a9M$1kS9gkqu` zC^Y9bh8uw;R)MlplI@qzB$e215CsrVMZp;duOs=hA0J8P1X{`)_GtwGi*WoK#kQJ68|~I+Gg|P z-bKt#^D^PRsbRKeO>UeBQ0=$-f10}H##GmD$iDU8i2-c?ZGU|afC5_-0&_szJnVUj z10*^ukVJ;cHf>cBF+mRKSbLR6msd7Evj$)th%w)v*!1O$$^I8Pt&8$0gtT& zh)O0Zwzd(h&9!ZPCop4V+v!6MdIFd+k#&VhLmNeOVw*GE`Qq_!l#$z^K ziqZM+z!9${-`MNc6p1}GwHFe}H*Ii^h9J~K4Cdz?Y1@C@WICjfXQ#|-E52t_jG!wb}0t*zkU32#GlS#l4hdV ztt1QzKbeXm;Phccc>~C<0Gij#zb47*cHkVJ#oU5^D;%~PsM&N3tk}lb#52cL#f|YW z5pZHq&nmVJ__tu(0>usSC$Wre&TG~_`Q!E_wnOmS0y2AUO!=PtZ01Ncqi{2QhDDvn zm{hFu^_=(H@Gby;<@c#g#Bx^~4f!{w@OFz;PjKejSpKIW`Kw=7ROi}!-Q;kZtE-g& zn1zdX0WeW2F-}?H2UJi5oc;>HSmNB`n-1JeT%h%C(Bo0W*%va@vXx z6_aYG{A@7*`^@ngd`LBne-4=E-7SArxA>e0ZN}JQ(^CO3=bOiebKHC%^2Qc%O?~Y$ zu8*rZg-LIjtEWl;Fd}n*0{DK$|2bvHiTtOd3}~Jh68C0ICIs-h0|7^3(?2#JizCH} zzRmNDAQHGLKiPP1SOb=94b|GrIB>(dTr*SoN?`01zXFZ~BHML)W35zltej0k{Sezwyrjj_?tuaXuwtnb<_Gz}#V*PT%MTwPV|a;#kdQN1}EB zz|Jut@J1YGu5`_{oqe2tGhTI`6?~iPlrQH?A`A6yMaAiIs?Mu-N9uun->@41ZRck7 ztH{3T%zNW{qc&xPJBe4Dfe z*xv?WwTa!F$zL~5vkA83fqgNSjriCICkLc#Tt)gDVp5jKUtOE$=Qu1U&eFbV$7sZ! zB2H(FfZM*ze-#gkfO!s&(T7GPG?wZW+hB~FIawXe_z~X~@vA$x&3xyWmv_ST%71cvhe@Af^7A z5G0j(op`TT4rnKL;&m0#2@Cb z6-_e_ESG6r5oj41Zb?yztOS-UhMRfu*qOqwv=>_+72c^hoBCqyFb9o)Nd9=|0R5PA zDN)Dr>Gt$?trbqIdSmU5ZR-pBY&nAMv5hg}Kj+W?hLG*&ure7*c)@$fj+-Ew~Ber<4j=%8s5%7bD#RdySM5N z0M5l~7uOW$W9Qj1`pW!fmU6Kn-LDsM12MAYD}@hi9p-v|_r=dD%)0&Sn*foB8!%~u zJ(H^smfVs*7Vhn0DdpG@-;81Us00yPOtp}1p6QEvZY`}b=7sry*HzIbztcGnQ|&H3X+r%bbk0 zXB&-u01!W!>cDXZHdW4<>pYZC^L10$8$;r60LN|O1h9@M=XupP5eE9{0NDB50O6MW z6}deD_9tt9`m_CT)9;3;a8J3sx?|>_YwN^$+J`C*<@a`d)83nTRswg%$gnN-YwMvA zFinlQYj}HK&EM`Tw$~MIxtiYAIa?3Bw(0*dZ_*ssZG3j7zE}xOVJqWgCj?IWU#fuT zqBCG(j^X0ZvCcL1;gWNW5h4rhB+ZdNGX*GZ0kZbE2e} z{1qTLV%n0wvP~`*O zfo5&+{H%jk1SR5WYl^KC`)XVEpIr0JS-Z#5n-(^fT7O*2bp1wbeTwyH<5P1f!N}%8 z%y6$A7vtTXbI#rYVC2;}&4Ol0z#oe_Acc-8A%N!{c{vbq#N)t0jXV9B&(q%x@vEXy zek(F{Z4;?(3;ovIS}6b7>$gO}#_uP<%#0DohWwo%#A7xF&g%&7JS#xL+Gk@q;@<3Q zHpVSEBt!Cd;-wXTYTvPZZsNfH2Wyib-{w9DnC8McagJ6{B%!snP5pnH%5@`h=CKn# zX)h(hXTBi^A-|s2zZx}xL8W9UKtAneQQ@tLBmnSlQ;Osn05IR2NMM#gl`XacxY=J7 zyy`RKO#ZpJ7*V{j(dPIpmW|)+e0qKBXVY#20Ne4;*bFdQ5Nrs)8AB1=^yfS~A%MOx zP*gN+3c#z1f>dFsN~6 zOzj(#rp@$=?GuMb%GZf!H`WLP=KqXizGi;{nCJ0sZStMv@JX#H?0=HK=KPl+?RL*^ zO_4KOL*!+*x90fAH|+nfI{-?lw#G;qfffll-37pa6X7>PVe_t{AxcIjPQBv%3`AP; z#JF5;+JeDj2I`Htao1T)=*OGF1m|%CHGOEtV(oG7IP!2~o<2!XwdU&b)>0gbM3oR| z_NmI_4G23^`%lGiGiLin!HuWRF_ERd)83Iy6Vscf$RFovh#CEJHU9~BzZkbXA%1-A z1VFL|%j!D-O#;}&ikNV0RG{b}yPinn%;`uIkI_FCubPjwy8+auPa-TOx|n}XNa1`8 z2$-N@ie2?_0PhG8UpMMpop&Uda_zv#`J8-s8${nAAl1Jm300SgL|siJ;$`)!u064~ z29>G2tJo6Zu((}Y_Be4g`<~7Fws22^x7pwGHEYw1wG|#{%{r*pZVZL_4lj?{gUzi- zza{jvFPte+gMPAex90Hsi{zGxcqfXsZx)y!I3mD}hsQ~1&*M4f?;itF<^a1PLPajv zmwnje+Z_3)F_kZA9QSU>Z2$#oTkv=S_RZpN=5g=LoGO1K2AqdIJAWEK()E`7n;2Sq z3UIf!d1sY~V6IaVs+;-KyHV$B1L97IV0|ZFa12JYvrkLxN=)J$ipU$WJKIm}O`>-b zM;liXhbm?zuAJ|mlI+>sF0Le8lK*Nv+($}WyITI&R{>)uNB9Z!UD2I$_~jGdrrzx{ zVq>pyUIjc%0kQ>CnyHl`3lGyXw2Z5vL;2Mxn7jxX>FyWBw)Zb7YjP z0ii}HseGWhMOXPbcTrfrwncZI6Y;s6yEVTpXt!=@v!uFBb)mq)&2vi_sfLt)PeI~V zK+JzDn|&OfG~+kx@fI+)KdVxW|0y2Kui9UYfZo_GE;K|u zeg86;b~7Kob~#Og&iSF%rLnmW|J(tX7qRi|gy-1c%vt9gE=EI)%omJh&g;a>Y|rMJ zekrBsa^dDv*HYgklIC&pf6mhYPl^EnLam9)35$CpUeM5=2E1@=C4|^q*oK{Jd}hzi ze&QX1@?{diBp>W^j?+GD<5RxjIYx|4jEWqbKN}(7+NrPIWkl~D>Nizz{g8YZ3AsIU z6aS6f*2cWX1k5pqZOWVo`)MJ%snIhQJFe~kr~x)2U;|32nB2RH^I*lgC4W`m+hT8o zk2>eX%#6#Czw+6=MnGU7(TF-F4zNEXOEq4K0m;eDtNe17YV#+6P~}k7hkF;&>_xTT zvBoe~{9+l^|5gCZSf>vTEYtW+eX_anoD;c?;AP`ElEG{03)I;7>`Q=}*i>9BS88v5 zKh)2`Gi%m%wo|6?=4**-s!uB-(%Lw#VL5hjyLGGY0I>57`M2ZFV9v8jOhf)k1gQD& zn8l(LqyuJ^aFz>2?5r(oryeR!x8Ye^B>-9xpkm&TQbY3WI=hB`H2!Z)fngdC{dQ}w zjK#kZTZ#Pfx*{E%=M4dX^}<9BTl*yXZV1i!m~a5w;vSuOZ3Tc60W&}Br`OqkTOo!_ zTxGdZv9NsNJSyjE+*>i=&V3b_$c*6+Yvb0B`QOZ|!~*UQ$LYWC0H}?bh-*V)tZ$oa z3=SVh%`%W;J|-V-2#NJ|o1YcC0!o$x*GKicCI5Mz^*=dKKlAKFKx>ZBZ-m5b&(>uF z1c@vdSgN_6vkqzP-TqF#Ibrg@;PXwbs~FC)!d5n?$_>lQZ0qnA>+%0J Ws-nQCDFE0200001 z0dCwd3`;Iv*Zu#oYnN+1Y&Z=ZOG?V!wdgPdMzSn*Op1~%xn1X+%m3U?%YWX+2YqKX z_pYHV=f6t83fJiToZA%FDV~370@h}CZ8rO-BVfJsleu1EHbAO@xl}MN$MZhY6Z*Gm2Ji^@!Q49%c+wpG9r!`OGw>}EMgu$ty+L5P<8vlKxj{4~e+2)l|7a*F z#%gV&M}q(aj@C!eU+MZ#kkBEZ0$=SWdECPUtWE2GHJwRtZom^&*8d?0>{vc_jgwZJ zHUTH&`wae(u1o@}0DU+bE`Mx&ryfUu${FSn0#;%9O$0tB0Xw<{{+#k(6#(~y0Jnwa zt@AKMvSBmuG?JFBJ?!fN}x13OE&fGb%tpOaKPJUHCn=Y}Xj`Bao;G|H_VLi5dwNrFSm@CM0f{X!c(Va`Ea#&U?Km{lQu&TpWXRr!*Ci&6@DmPG_)iDw`;X(<=0Oj*+ zW!@O6K<5HqYZc&Yr3gociysIA6@cKKA6KBW`7*BGt&!`f$?-9kI^e()9sJG$+hBgTGrl9psVPl|X9K94l*`P{ zW{0Qt0a5#pD|S8_%gD%xkqVeii?fAzQmU5EIejd3iU6zyVrL@wyGk_`rX^EAW@QH! zt;gVbhnM97Q3{LBL}a97>PQt($-g?Q!->24KUzA=24kFOxIQO@I3h|n7%+jZ8LNCXm)xk0Ib1tt(2mB7pSAkdZ=OF0Z zK;;|i{*d*%C9#;4`#-gP=1r42vFWTHii9KSa%%mfH9;mJOPu0pIyja^lv@L4P1-HQOWKDxjrDn8wFx~v+-WGIUt27e0y z3w)55v9NID5z6xFbVtP$M+82<3V5{jj( Date: Sun, 29 Mar 2026 02:37:45 +0000 Subject: [PATCH 012/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index f56657827b..84a758166f 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: PJB3005 - changes: - - message: The patrons list in the in-game credits works again. - type: Fix - id: 9083 - time: '2025-10-12T11:02:33.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40840 - author: DrSmugleaf changes: - message: Fixed species not being ordered alphabetically in the character customization @@ -4035,3 +4028,10 @@ id: 9594 time: '2026-03-28T22:19:17.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/41878 +- author: insoPL, Quantum-cross, ArtisticRoomba + changes: + - message: Hot gasses now have a visible distortion effect. + type: Add + id: 9595 + time: '2026-03-29T02:36:35.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/42973 From b131c58501bd8d052e5214d83834191dca38ac82 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 00:09:08 -0400 Subject: [PATCH 013/126] Update Credits (#43376) Co-authored-by: PJBot --- Resources/Credits/GitHub.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Credits/GitHub.txt b/Resources/Credits/GitHub.txt index fd60cbee29..221a6124d5 100644 --- a/Resources/Credits/GitHub.txt +++ b/Resources/Credits/GitHub.txt @@ -1 +1 @@ -0-Anon, 0leshe, 0tito, 0x6273, 11BelowStudio, 12rabbits, 1337dakota, 13spacemen, 154942, 2013HORSEMEATSCANDAL, 20kdc, 21Melkuu, 27alaing, 2DSiggy, 3nderall, 4310v343k, 4dplanner, 5tickman, 612git, 778b, 96flo, aaron, abadaba695, Ablankmann, abregado, Absolute-Potato, Absotively, achookh, Acruid, ActiveMammmoth, actually-reb, ada-please, adamsong, Adeinitas, adm2play, Admiral-Obvious-001, adrian, Adrian16199, Ady4ik, Aearo-Deepwater, Aerocrux, Aeshus, Aexolott, Aexxie, AffleWaffle, africalimedrop, afrokada, AftrLite, AgentSmithRadio, Agoichi, ahandleman, Ahion, aiden, Aidenkrz, aidenkrz, Aisu9, ajcm, AJCM-git, AjexRose, Alekshhh, alexalexmax, alexkar598, AlexMorgan3817, alexum418, alexumandxgabriel08x, Alice4267, Alithsko, Alkheemist, alliephante, ALMv1, Alpaccalypse, AlphaQwerty, Altoids1, amatwiedle, amylizzle, Andre19926, Andrew-Fall, AndrewEyeke, AndrewFenriz, AndreyCamper, anri, Anzarot121, ApolloVector, Appiah, april-gras, ar4ill, Arcane-Waffle, archee1, ArchPigeon, ArchRBX, areitpog, Arendian, areyouconfused, arimah, Arkanic, ArkiveDev, armoks, Arteben, arthropodia, ArthurMousatov, ArtisticRoomba, artur, Artxmisery, ArZarLordOfMango, as334, AshBats, AsikKEsel, AsnDen, asperger-sind, aspiringLich, astriloqua, Atakku, Ataman, august-sun, AutoOtter, AverageNotDoingAnythingEnjoyer, avghdev, AwareFoxy, Awlod, Axionyxx, azloserbits, AzzyIsNotHere, azzyisnothere, B-Kirill, B3CKDOOR, baa14453, BackeTako, BadaBoomie, Bakke, BananaFlambe, Baptr0b0t, BarryNorfolk, BasedUser, bea, bebr3ght, beck-thompson, beesterman, bellwetherlogic, ben, benbryant0, benev0, benjamin-burges, BGare, bhespiritu, bibbly, BigfootBravo, BIGZi0348, bingojohnson, BismarckShuffle, Bixkitts, Blackern5000, Blazeror, blitzthesquishy, Blobadoodle, bloodrizer, Bloody2372, blueDev2, Boaz1111, BobdaBiscuit, BobTheSleder, boiled-water-tsar, Bokser815, bolantej, BombasterDS, Booblesnoot42, Boolean-Buckeye, botanySupremist, brainfood1183, BramvanZijp, Brandon-Huu, breeplayx3, BriBrooo, BRINGit34, brndd, bryce0110, BubblegumBlue, buletsponge, buntobaggins, buunie099, bvelliquette, BWTCK, byondfuckery, c0rigin, c4llv07e, CaasGit, Caconym27, Calecute, Callmore, Camdot, cammusubi, capnsockless, CaptainMaru, captainsqrbeard, Carbonhell, Carolyn3114, Carou02, carteblanche4me, catdotjs, catlord, Catofquestionableethics, CatTheSystem, CawsForConcern, CDWimmer, Centronias, Chaboricks, chairbender, chaisftw, Chaoticaa, Charlese2, charlie, chartman, ChaseFlorom, chavonadelal, Cheackraze, CheddaCheez, cheesePizza2, CheesePlated, Chief-Engineer, chillyconmor, christhirtle, chromiumboy, Chronophylos, Chubbicous, Chubbygummibear, Ciac32, ciaran, citrea, civilCornball, claustro305, Clement-O, cloudyias, clyf, Clyybber, CMDR-Piboy314, cnv41, coco, cohanna, Cohnway, Cojoke-dot, ColdAutumnRain, Colin-Tel, collinlunn, ComicIronic, Compilatron144, CookieMasterT, coolboy911, CoolioDudio, coolmankid12345, Coolsurf6, cooperwallace, corentt, CormosLemming, CrafterKolyan, CraftyRenter, crazybrain23, Crazydave91920, CrazyPhantom779, creadth, CrigCrag, CroilBird, Crotalus, CrudeWax, cryals, CrzyPotato, cubixthree, cutemoongod, Cyberboss, d34d10cc, DaCookieCakes, DadeKuma, Daemon, daerSeebaer, dahnte, dakamakat, DamianX, dan, dangerrevolution, daniel-cr, DanSAussieITS, Daracke, Darkie, DaturoDewitt, david, DawBla, Daxxi3, dch-GH, ddeegan, de0rix, Deahaka, dean, DEATHB4DEFEAT, Deatherd, deathride58, debugok, Decappi, Decortex, Deeeeja, deepdarkdepths, DeepwaterCreations, Deerstop, degradka, Delete69, deltanedas, DenisShvalov, DerbyX, derek, dersheppard, Deserty0, Detintinto, DevilishMilk, devinschubert14, dexlerxd, dffdff2423, DieselMohawk, DieselMohawkTheSequel, digitalic, Dimastra, dimmoon1, DinnerCalzone, DinoWattz, Disp-Dev, DisposableCrewmember42, dissidentbullet, DjfjdfofdjfjD, doc-michael, docnite, Doctor-Cpu, DogZeroX, dolgovmi, dontbetank, Doomsdrayk, Doru991, DoubleRiceEddiedd, DoutorWhite, DR-DOCTOR-EVIL-EVIL, Dragonjspider, dragonryan06, drakewill-CRL, Drayff, dreamlyjack, DrEnzyme, dribblydrone, DrMelon, drongood12, DrSingh, DrSmugleaf, drteaspoon420, DTanxxx, DubiousDoggo, DuckManZach, Duddino, dukevanity, duskyjay, Dutch-VanDerLinde, dvir001, dylanstrategie, dylanwhittingham, Dynexust, Easypoller, echo, EchoOfNothing, eclips_e, eden077, EEASAS, Efruit, efzapa, Ekkosangen, ElectroSR, elsie, elthundercloud, Elysium206, emberwinters, Emisse, emmafornash, EmoGarbage404, Endecc, EnrichedCaramel, Entvari, eoineoineoin, ephememory, eris, erohrs2, erorr404v1, Errant-4, ertanic, esguard, estacaoespacialpirata, eternally-confused, eugene, ewokswagger, exincore, exp111, f0x-n3rd, F1restar4, FacePluslll, Fahasor, FairlySadPanda, farrellka-dev, FATFSAAM2, Feluk6174, ficcialfaint, Fiftyllama, Fildrance, fillervk, FinnishPaladin, firenamefn, Firewars763, FirinMaLazors, Fishfish458, fl-oz, Flareguy, flashgnash, FlipBrooke, FluffiestFloof, FluffMe, FluidRock, flymo5678, foboscheshir, FoLoKe, fooberticus, ForestNoises, forgotmyotheraccount, forkeyboards, forthbridge, Fortune117, foxhorn, freeman2651, freeze2222, frobnic8, Froffy025, Fromoriss, froozigiusz, FrostMando, FrostRibbon, Fruitsalad, Funce, FungiFellow, FunkySphere, FunTust, Futuristic-OK, GalacticChimp, gamer3107, Gamewar360, gansulalan, GaussiArson, Gaxeer, gbasood, gcoremans, Geekyhobo, genderGeometries, GeneralGaws, Genkail, Gentleman-Bird, geraeumig, Ghagliiarghii, Git-Nivrak, githubuser508, GitHubUser53123, gituhabu, GlassEclipse, GnarpGnarp, GNF54, godisdeadLOL, goet, GoldenCan, Goldminermac, Golinth, golubgik, GoodWheatley, Gorox221, GR1231, gradientvera, graevy, GraniteSidewalk, GreaseMonk, greenrock64, GreyMario, GrownSamoyedDog, GTRsound, gusxyz, Gyrandola, h3half, hamurlik, Hanzdegloker, HappyRoach, happyrobot33, Hardly3D, harikattar, Hayden, he1acdvv, Hebi, Helix-ctrl, helm4142, Henry, HerCoyote23, Hi-Im-Shot, HighTechPuddle, Hitlinemoss, hiucko, hivehum, Hmeister-fake, Hmeister-real, Hobbitmax, hobnob, HoidC, Holinka4ever, holyssss, HoofedEar, Hoolny, hord-brayden, hoshizora-sayo, Hreno, Hrosts, htmlsystem, Huaqas, hubismal, Hugal31, Hyenh, hyperb1, hyperDelegate, hyphenationc, i-justuser-i, iaada, iacore, IamVelcroboy, Ian321, icekot8, icesickleone, iczero, iglov, IgorAnt028, igorsaux, ike709, illersaver, Illiux, Ilushkins33, Ilya246, IlyaElDunaev, imatsoup, IMCB, impubbi, imrenq, imweax, indeano, Injazz, Insineer, insoPL, IntegerTempest, Interrobang01, Intoxicating-Innocence, IProduceWidgets, itsmethom, Itzbenz, iztokbajcar, Jackal298, Jackrost, JackRyd3r, jacksonzck, JackspajfMain, Jacktastic09, Jackw2As, jacob, jamessimo, janekvap-havok, Jark255, Jarmer123, Jaskanbe, JasperJRoth, jbox144, JCGWE30, jerryimmouse, JerryImMouse, Jessetriesagain, jessicamaybe, JesterX666, Jewelots, Jezithyr, jicksaw, JiimBob, JimGamemaster, jimmy12or, JIPDawg, jjtParadox, jkwookee, jmcb, JohnGinnane, johnjjohn, JohnJJohn, johnku1, Jophire, Jopogrechkin, joshepvodka, JpegOfAFrog, jproads, JrInventor05, Jrpl, jukereise, juliangiebel, JustArt1m, JustCone14, justdie12, justin, justintether, JustinTrotter, JustinWinningham, justtne, K-Dynamic, k3yw, Kadeo64, Kaga-404, kaiserbirch, KaiShibaa, kalane15, kalanosh, KamTheSythe, Kanashi-Panda, katzenminer, kbailey-git, Keelin, Keer-Sar, KEEYNy, keikiru, Kelrak, kerisargit, keronshb, KeTuFaisPiKiNut, KIBORG04, KieueCaprie, Kimpes, kin98, KingFroozy, kipdotnet, kira-er, kiri-yoshikage, Kirillcas, Kirus59, Kistras, Kit, Kit0vras, KittenColony, Kittygyat, klaypexx, kleinerstation13, Kmc2000, Ko4ergaPunk, kognise, kokoc9n, komunre, KonstantinAngelov, kontakt, korczoczek, koteq, kotobdev, Kowlin, KrasnoshchekovPavel, Krosus777, Krunklehorn, Kryyto, Kupie, kxvvv, Kyoth25f, kyupolaris, kzhanik, LaCumbiaDelCoronavirus, lajolico, Lamrr, lanedon, LankLTE, laok233, lapatison, larryrussian, lawdog4817, Lazzi0706, Le-Arctic-Fox, leahcat, leander-0, leonardo-dabepis, leonidussaks, leonsfriedrich, LeoSantich, LetterN, lettern, Level10Cybermancer, LEVELcat, lever1209, LevitatingTree, Lgibb18, lgruthes, liem161, LightVillet, lilazero, liltenhead, linkbro1, linkuyx, Litraxx, little-meow-meow, LittleBuilderJane, LittleNorthStar, LittleNyanCat, lizelive, ljm862, lmsnoise, localcc, lokachop, lolman360, Lomcastar, Lordbrandon12, LordCarve, LordEclipse, lucas, LucasTheDrgn, luckyshotpictures, LudwigVonChesterfield, luegamer, luizwritescode, LukaSlade, luminight, lunarcomets, Lusatia, Luxeator, lvvova1, Lyndomen, lyroth001, lyxcaster, lzimann, lzk228, M1tht1c, M3739, M4rchy-S, M87S, mac6na6na, MACMAN2003, Macoron, magicalus, magmodius, magnnusson, magnuscrowe, maland1, malchanceux, MaloTV, manelnavola, ManelNavola, Mangohydra, marboww, Markek1, MarkerWicker, marlyn, mastermiller01, matt, Matz05, max, MaxNox7, maylokana, mdrkrg, MDuch369, meara1179, meganerobot, MehimoNemo, Mehnix, MeltedPixel, memeproof, MendaxxDev, Menshin, Mephisto72, MerrytheManokit, Mervill, metalgearsloth, MetalSage, MFMessage, mhamsterr, michaelcu, micheel665, mifia, mikeysaurus, MilenVolf, MilonPL, Minemoder5000, Minty642, minus1over12, Mirino97, mirrorcult, misandrie, MishaUnity, MissKay1994, MisterImp, MisterMecky, Mith-randalf, Mixelz, mjarduk, MjrLandWhale, mkanke-real, MLGTASTICa, mnva0, moderatelyaware, modern-nm, mohamedwidar, mokiros, momo, Moneyl, monotheonist, Moomoobeef, moony, Morb0, MossyGreySlope, mqole, mr-bo-jangles, Mr0maks, MrFippik, MrPersival, mrrobdemo, mtrs163, muburu, MureixloI, murolem, murphyneko, musicmanvr, MWKane, Myakot, Myctai, N3X15, nabegator, nails-n-tape, Nairodian, Naive817, NakataRin, namespace-Memory, Nannek, NazrinNya, neborsh, nekokiwa, neomoth, neutrino-laser, NickPowers43, nikitosych, nikthechampiongr, Nimfar11, ninruB, Nirnael, NIXC, nkokic, NkoKirkto, nmajask, noctyrnal, noelkathegod, noirogen, nok-ko, NonchalantNoob, NoobyLegion, Nopey, NoreUhh, not-gavnaed, notafet, notquitehadouken, notsodana, noudoit, noverd, Nox38, NuclearWinter, Nuggets219, nukashimika, nuke-haus, NULL882, nullarmo, nyeogmi, Nylux, Nyranu, Nyxilath, och-och, OctoRocket, Ohelig, OldDanceJacket, OliverOtter, onesch, OneZerooo0, OnsenCapy, OnyxTheBrave, opl-, Orange-Winds, OrangeMoronage9622, OrbitSystem07, Orsoniks, osjarw, Ostaf, othymer, OttoMaticode, Owai-Seek, packmore, PAFFhassoocks, paige404, paigemaeforrest, pali6, Palladinium, Pangogie, panzer-iv1, partyaddict, patrikturi, PaulRitter, pavlockblaine03, peccneck, Peptide90, peptron1, perryprog, PeterFuto, PetMudstone, pewter-wiz, pgraycs, PGrayCS, Pgriha, phantom-lily, Pharaz4, pheenty, philingham, Phill101, Phooooooooooooooooooooooooooooooosphate, phunnyguy, PicklOH, PilgrimViis, Pill-U, pinkbat5, Piras314, Pireax, Pissachu, pissdemon, Pixel8-dev, PixeltheAertistContrib, PixelTheKermit, PJB3005, Plasmaguy, plinyvic, Plykiya, poeMota, pofitlo, pointer-to-null, Pok27, poklj, PolterTzi, PoorMansDreams, PopGamer45, portfiend, potato1234x, PotentiallyTom, PotRoastPiggy, Princess-Cheeseballs, ProfanedBane, Prole0, ProPandaBear, ProPeperos, PrPleGoo, ps3moira, Pspritechologist, Psychpsyo, psykana, psykzz, PuceTint, pumkin69, PuroSlavKing, PursuitInAshes, Putnam3145, py01, Pyrovi, qrtDaniil, qrwas, Quantum-cross, quasr-9, quatre, QueerNB, QuietlyWhisper, qwerltaz, Radezolid, RadioMull, Radosvik, Radrark, Rainbeon, Rainfey, Raitononai, Ramlik, RamZ, randy10122, Rane, Ranger6012, Rapidgame7, ravage123321, rbertoche, RedBookcase, Redfire1331, Redict, RedlineTriad, redmushie, RednoWCirabrab, Redrover1760, redspyy, ReeZer2, RemberBM, RemieRichards, RemTim, rene-descartes2021, Renlou, retequizzle, rewafflution, rhailrake, rhsvenson, rich-dunne, RieBi, riggleprime, RIKELOLDABOSS, rinary1, Rinkashikachi, riolume, rlebell33, RobbyTheFish, robinthedragon, robinthegirlthing, Rockdtben, Rohesie, rok-povsic, rokudara-sen, rolfero, RomanNovo, roryflowers, rosieposieeee, Roudenn, router, ruddygreat, rumaks-xyz, RumiTiger, Ruzihm, rwrv, S1rFl0, S1ss3l, Saakra, SabreML, Sadie-silly, saga3152, saintmuntzer, salarua, Salex08, sam, samgithubaccount, Samuka-C, SaphireLattice, SapphicOverload, sarahon, sativaleanne, SaveliyM360, sBasalto, ScalyChimp, ScarKy0, ScholarNZL, schrodinger71, scrato, Scribbles0, scrivoy, scruq445, scuffedjays, ScumbagDog, SeamLesss, Segonist, semensponge, sephtasm, ser1-1y, Serkket, sewerpig, SG6732, sh18rw, Shaddap1, ShadeAware, ShadowCommander, shadowtheprotogen546, shaeone, shampunj, shariathotpatrol, SharkSnake98, Shegare, shepardtothestars, shibechef, Siginanto, signalsender, SignalWalker, siigiil, silicon14wastaken, Silverfur-underscore, Simyon264, sirdragooon, Sirionaut, SirWarock, Sk1tch, SkaldetSkaeg, Skarletto, skeeka-dev, skrybl, Skybailey-dev, skye, Skyedra, SlamBamActionman, slarticodefast, Slava0135, sleepyyapril, slimmslamm, Slyfox333, Smugman, SnappingOpossum, snebl, snicket, sniperchance, Snowni, snowsignal, SolidSyn, SolidusSnek, solstar2, SomegnihT, SonarZeBat, SonicHDC, SoulFN, SoulSloth, Soundwavesghost, soupkilove, southbridge-fur, sowelipililimute, Soydium, SpaceLizard24, SpaceLizardSky, SpaceManiac, SpaceRox1244, SpaceyLady, Spangs04, spanky-spanky, Sparlight, spartak, SpartanKadence, spderman3333, SpeltIncorrectyl, Spessmann, SphiraI, SplinterGP, spoogemonster, sporekto, sporkyz, ssdaniel24, stalengd, stanberytrask, Stanislav4ix, StanTheCarpenter, starbuckss14, Stealthbomber16, steel, Steffo99, stellar-novas, stewie523, stomf, Stop-Signs, stopbreaking, stopka-html, StrawberryMoses, Stray-Pyramid, strO0pwafel, Strol20, StStevens, Subversionary, sunbear-dev, SuperGDPWYL, superjj18, Supernorn, SurrealShibe, SweetAplle, SweptWasTaken, SyaoranFox, Sybil, SYNCHRONIC, Synthestra, Szunti, t, Tainakov, takemysoult, taonewt, tap, TaralGit, Taran, taserthefox, taurie, Tayrtahn, tday93, teamaki, TeenSarlacc, TekuNut, telavivgamers, telyonok, temm1ie, TemporalOroboros, tentekal, terezi4real, Terraspark4941, texcruize, Tezzaide, TGODiamond, TGRCdev, tgrkzus, thanosdegraf, ThatGuyUSA, ThatOneGoblin25, thatrandomcanadianguy, TheArturZh, TheBlueYowie, thecopbennet, TheCze, TheDarkElites, thedraccx, TheEmber, theexetron, TheFlyingSentry, thefoty, TheGrimbeeper, TheIntoxicatedCat, thekilk, themias, theomund, TheProNoob678, TherapyGoth, ThereDrD0, TheSecondLord, TheShuEd, thetolbean, thevinter, TheWaffleJesus, Thinbug0, ThunderBear2006, timothyteakettle, TimrodDX, timurjavid, tin-man-tim, TiniestShark, Titian3, tk-a369, tkdrg, tmtmtl30, ToastEnjoyer, Toby222, TokenStyle, Tollhouse, Toly65, tom-leys, tomasalves8, Tomeno, Tonydatguy, topy, tornado-technology, TornadoTechnology, tosatur, TotallyLemon, ToxicSonicFan04, Tr1bute, travis-g-reid, treytipton, TriviaSolari, trixxedbit, TrixxedHeart, tropicalhibi, truepaintgit, Truoizys, Tryded, TsjipTsjip, tuchila-adi-bogdan, Tunguso4ka, TurboTrackerss14, TVK-04, tyashley, Tyler-IN, TytosB, Tyzemol, UbaserB, Uberration, ubis1, UBlueberry, uhbg, UKNOWH, UltimateJester, Unbelievable-Salmon, underscorex5, UnicornOnLSD, Unisol, Unkn0wnGh0st333, unusualcrow, UpAndLeaves, Uriende, UristMcDorf, user424242420, Utmanarn, Vaaankas, valentfingerov, valquaint, Varen, Vasilis, VasilisThePikachu, veliebm, Velken, VelonacepsCalyxEggs, veprolet, VerinSenpai, veritable-calamity, Veritius, Vermidia, vero5123, verslebas, vexerot, vgskye, viceemargo, VigersRay, violet754, Visne, vitopigno, vitusveit, vlad, vlados1408, VMSolidus, vmzd, VoidMeticulous, voidnull000, volotomite, volundr-, Voomra, Vordenburg, vorkathbruh, Vortebo, vulppine, wachte1, wafehling, walksanatora, Warentan, WarMechanic, Watermelon914, weaversam8, wertanchik, whateverusername0, whatston3, widgetbeck, Will-Oliver-Br, Willhelm53, WilliamECrew, willicassi, Winkarst-cpu, wirdal, wixoaGit, WlarusFromDaSpace, Wolfkey-SomeoneElseTookMyUsername, Worldwaker, wrexbe, wtcwr68, xeri7, xkreksx, xprospero, xRiriq, xsainteer, YanehCheck, yathxyz, Ygg01, YotaXP, youarereadingthis, YoungThugSS14, Yousifb26, youtissoum, yunii, YuriyKiss, yuriykiss, zach-hill, Zadeon, Zalycon, zamp, Zandario, Zap527, Zealith-Gamer, ZelteHonor, zero, ZeroDiamond, ZeWaka, zHonys, zionnBE, ZNixian, Zokkie, ZoldorfTheWizard, zonespace27, Zylofan, Zymem, zzylex +0-Anon, 0leshe, 0tito, 0x6273, 11BelowStudio, 12rabbits, 1337dakota, 13spacemen, 154942, 2013HORSEMEATSCANDAL, 20kdc, 21Melkuu, 27alaing, 2DSiggy, 3nderall, 4310v343k, 4dplanner, 5tickman, 612git, 778b, 96flo, aaron, abadaba695, Ablankmann, abregado, Absolute-Potato, Absotively, achookh, Acruid, ActiveMammmoth, actually-reb, ada-please, adamsong, Adeinitas, adm2play, Admiral-Obvious-001, adrian, Adrian16199, Ady4ik, Aearo-Deepwater, Aerocrux, Aeshus, Aexolott, Aexxie, AffleWaffle, africalimedrop, afrokada, AftrLite, AgentSmithRadio, Agoichi, ahandleman, Ahion, aiden, Aidenkrz, aidenkrz, Aisu9, ajcm, AJCM-git, AjexRose, Alekshhh, alexalexmax, alexkar598, AlexMorgan3817, alexum418, alexumandxgabriel08x, Alice4267, Alithsko, Alkheemist, alliephante, ALMv1, Alpaccalypse, AlphaQwerty, Altoids1, amatwiedle, amylizzle, Andre19926, Andrew-Fall, AndrewEyeke, AndrewFenriz, AndreyCamper, anri, Anzarot121, ApolloVector, Appiah, april-gras, ar4ill, Arcane-Waffle, arcanevaliance, archee1, ArchPigeon, ArchRBX, areitpog, Arendian, areyouconfused, arimah, Arkanic, ArkiveDev, armoks, Arteben, arthropodia, ArthurMousatov, ArtisticRoomba, artur, Artxmisery, ArZarLordOfMango, as334, AshBats, AsikKEsel, AsnDen, asperger-sind, aspiringLich, astriloqua, Atakku, Ataman, august-sun, AutoOtter, AverageNotDoingAnythingEnjoyer, avghdev, AwareFoxy, Awlod, Axionyxx, azloserbits, AzzyIsNotHere, azzyisnothere, B-Kirill, B3CKDOOR, baa14453, BackeTako, BadaBoomie, Bakke, BananaFlambe, Baptr0b0t, BarryNorfolk, BasedUser, bea, bebr3ght, beck-thompson, beesterman, bellwetherlogic, ben, benbryant0, benev0, benjamin-burges, BGare, bhespiritu, bibbly, BigfootBravo, BIGZi0348, bingojohnson, BismarckShuffle, Bixkitts, Blackern5000, Blazeror, blitzthesquishy, Blobadoodle, bloodrizer, Bloody2372, blueDev2, Boaz1111, BobdaBiscuit, BobTheSleder, boiled-water-tsar, Bokser815, bolantej, BombasterDS, Booblesnoot42, Boolean-Buckeye, botanySupremist, brainfood1183, BramvanZijp, Brandon-Huu, breeplayx3, BriBrooo, BRINGit34, brndd, bryce0110, BubblegumBlue, buletsponge, buntobaggins, buunie099, bvelliquette, BWTCK, byondfuckery, c0rigin, c4llv07e, CaasGit, Caconym27, Calecute, Callmore, Camdot, cammusubi, capnsockless, CaptainMaru, captainsqrbeard, Carbonhell, Carolyn3114, Carou02, carteblanche4me, catdotjs, catlord, Catofquestionableethics, CatTheSystem, CawsForConcern, CDWimmer, Centronias, Chaboricks, chairbender, chaisftw, Chaoticaa, Charlese2, charlie, chartman, ChaseFlorom, chavonadelal, Cheackraze, CheddaCheez, cheesePizza2, CheesePlated, Chief-Engineer, chillyconmor, christhirtle, chromiumboy, Chronophylos, Chubbicous, Chubbygummibear, Ciac32, ciaran, citrea, civilCornball, claustro305, Clement-O, cloudyias, clyf, Clyybber, CMDR-Piboy314, cnv41, coco, cohanna, Cohnway, Cojoke-dot, ColdAutumnRain, Colin-Tel, collinlunn, ComicIronic, Compilatron144, CookieMasterT, coolboy911, CoolioDudio, coolmankid12345, Coolsurf6, cooperwallace, corentt, CormosLemming, CrafterKolyan, CraftyRenter, crazybrain23, Crazydave91920, CrazyPhantom779, creadth, CrigCrag, CroilBird, Crotalus, CrudeWax, cryals, CrzyPotato, cubixthree, cutemoongod, Cyberboss, d34d10cc, DaCookieCakes, DadeKuma, Daemon, daerSeebaer, dahnte, dakamakat, DamianX, dan, dangerrevolution, daniel-cr, DanSAussieITS, Daracke, Darkie, DaturoDewitt, david, DawBla, Daxxi3, dch-GH, ddeegan, de0rix, Deahaka, dean, DEATHB4DEFEAT, Deatherd, deathride58, debugok, Decappi, Decortex, Deeeeja, deepdarkdepths, DeepwaterCreations, Deerstop, degradka, Delete69, deltanedas, DenisShvalov, DerbyX, derek, dersheppard, Deserty0, Detintinto, DevilishMilk, devinschubert14, dexlerxd, dffdff2423, DieselMohawk, DieselMohawkTheSequel, digitalic, Dimastra, dimmoon1, DinnerCalzone, DinoWattz, Disp-Dev, DisposableCrewmember42, dissidentbullet, DjfjdfofdjfjD, doc-michael, docnite, Doctor-Cpu, DogZeroX, dolgovmi, dontbetank, Doomsdrayk, Doru991, DoubleRiceEddiedd, DoutorWhite, DR-DOCTOR-EVIL-EVIL, Dragonjspider, dragonryan06, drakewill-CRL, Drayff, dreamlyjack, DrEnzyme, dribblydrone, DrMelon, drongood12, DrSingh, DrSmugleaf, drteaspoon420, DTanxxx, DubiousDoggo, DuckManZach, Duddino, dukevanity, duskyjay, Dutch-VanDerLinde, dvir001, dylanstrategie, dylanwhittingham, Dynexust, Easypoller, echo, EchoOfNothing, eclips_e, eden077, EEASAS, Efruit, efzapa, Ekkosangen, ElectroSR, elsie, elthundercloud, Elysium206, emberwinters, Emisse, emmafornash, EmoGarbage404, Endecc, EnrichedCaramel, Entvari, eoineoineoin, ephememory, eris, erohrs2, erorr404v1, Errant-4, ertanic, esguard, estacaoespacialpirata, eternally-confused, eugene, ewokswagger, exincore, exp111, f0x-n3rd, F1restar4, FacePluslll, Fahasor, FairlySadPanda, farrellka-dev, FATFSAAM2, Feluk6174, ficcialfaint, Fiftyllama, Fildrance, fillervk, FinnishPaladin, firenamefn, Firewars763, FirinMaLazors, Fishfish458, fl-oz, Flareguy, flashgnash, FlipBrooke, FluffiestFloof, FluffMe, FluidRock, flymo5678, foboscheshir, FoLoKe, fooberticus, ForestNoises, forgotmyotheraccount, forkeyboards, forthbridge, Fortune117, foxhorn, freeman2651, freeze2222, frobnic8, Froffy025, Fromoriss, froozigiusz, FrostMando, FrostRibbon, Fruitsalad, Funce, FungiFellow, FunkySphere, FunTust, Futuristic-OK, GalacticChimp, gamer3107, Gamewar360, gansulalan, GaussiArson, Gaxeer, gbasood, gcoremans, Geekyhobo, genderGeometries, GeneralGaws, Genkail, Gentleman-Bird, geraeumig, Ghagliiarghii, Git-Nivrak, githubuser508, GitHubUser53123, gituhabu, GlassEclipse, GnarpGnarp, GNF54, godisdeadLOL, goet, GoldenCan, Goldminermac, Golinth, golubgik, GoodWheatley, Gorox221, GR1231, gradientvera, graevy, GraniteSidewalk, GreaseMonk, greenrock64, GreyMario, GrownSamoyedDog, GTRsound, gusxyz, Gyrandola, h3half, hamurlik, Hanzdegloker, HappyRoach, happyrobot33, Hardly3D, harikattar, Hayden, he1acdvv, Hebi, Helix-ctrl, helm4142, Henry, HerCoyote23, Hi-Im-Shot, HighTechPuddle, Hitlinemoss, hiucko, hivehum, Hmeister-fake, Hmeister-real, Hobbitmax, hobnob, HoidC, Holinka4ever, holyssss, HoofedEar, Hoolny, hord-brayden, hoshizora-sayo, Hreno, Hrosts, htmlsystem, Huaqas, hubismal, Hugal31, Hyenh, hyperb1, hyperDelegate, hyphenationc, i-justuser-i, iaada, iacore, IamVelcroboy, Ian321, icekot8, icesickleone, iczero, iglov, IgorAnt028, igorsaux, ike709, illersaver, Illiux, Ilushkins33, Ilya246, IlyaElDunaev, imatsoup, IMCB, impubbi, imrenq, imweax, indeano, Injazz, Insineer, insoPL, IntegerTempest, Interrobang01, Intoxicating-Innocence, IProduceWidgets, itsmethom, Itzbenz, iztokbajcar, Jackal298, Jackrost, JackRyd3r, jacksonzck, JackspajfMain, Jacktastic09, Jackw2As, jacob, jamessimo, janekvap-havok, Jark255, Jarmer123, Jaskanbe, JasperJRoth, jbox144, JCGWE30, jerryimmouse, JerryImMouse, Jessetriesagain, jessicamaybe, JesterX666, Jewelots, Jezithyr, jicksaw, JiimBob, JimGamemaster, jimmy12or, JIPDawg, jjtParadox, jkwookee, jmcb, JohnGinnane, JohnJJohn, johnjjohn, johnku1, Jophire, Jopogrechkin, joshepvodka, JpegOfAFrog, jproads, JrInventor05, Jrpl, jukereise, juliangiebel, JustArt1m, JustCone14, justdie12, justin, justintether, JustinTrotter, JustinWinningham, justtne, K-Dynamic, k3yw, Kadeo64, Kaga-404, kaiserbirch, KaiShibaa, kalane15, kalanosh, KamTheSythe, Kanashi-Panda, katzenminer, kbailey-git, Keelin, Keer-Sar, KEEYNy, keikiru, Kelrak, kerisargit, keronshb, KeTuFaisPiKiNut, KIBORG04, KieueCaprie, Kimpes, kin98, KingFroozy, kipdotnet, kira-er, kiri-yoshikage, Kirillcas, Kirus59, Kistras, Kit, Kit0vras, KittenColony, Kittygyat, klaypexx, kleinerstation13, Kmc2000, Ko4ergaPunk, kognise, kokoc9n, komunre, KonstantinAngelov, kontakt, korczoczek, koteq, kotobdev, Kowlin, KrasnoshchekovPavel, Krosus777, Krunklehorn, Kryyto, Kupie, kxvvv, Kyoth25f, kyupolaris, kzhanik, LaCumbiaDelCoronavirus, lajolico, Lamrr, lanedon, LankLTE, laok233, lapatison, larryrussian, lawdog4817, Lazzi0706, Le-Arctic-Fox, leahcat, leander-0, leonardo-dabepis, leonidussaks, leonsfriedrich, LeoSantich, lettern, LetterN, Level10Cybermancer, LEVELcat, lever1209, LevitatingTree, Lgibb18, lgruthes, liem161, LightVillet, lilazero, liltenhead, linkbro1, linkuyx, Litraxx, little-meow-meow, LittleBuilderJane, LittleNorthStar, LittleNyanCat, lizelive, ljm862, lmsnoise, localcc, lokachop, lolman360, Lomcastar, Lordbrandon12, LordCarve, LordEclipse, lucas, LucasTheDrgn, luckyshotpictures, LudwigVonChesterfield, luegamer, luizwritescode, LukaSlade, luminight, lunarcomets, Lusatia, Luxeator, lvvova1, Lyndomen, lyroth001, lyxcaster, lzimann, lzk228, M1tht1c, M3739, M4rchy-S, M87S, mac6na6na, MACMAN2003, Macoron, magicalus, magmodius, magnnusson, magnuscrowe, maland1, malchanceux, MaloTV, manelnavola, ManelNavola, Mangohydra, marboww, Markek1, MarkerWicker, marlyn, mastermiller01, matt, Matz05, max, MaxNox7, maylokana, mdrkrg, MDuch369, meara1179, meganerobot, MehimoNemo, Mehnix, MeltedPixel, memeproof, MendaxxDev, Menshin, Mephisto72, MerrytheManokit, Mervill, metalgearsloth, MetalSage, MFMessage, mhamsterr, michaelcu, micheel665, mifia, mikeysaurus, MilenVolf, MilonPL, Minemoder5000, Minty642, minus1over12, Mirino97, mirrorcult, misandrie, MishaUnity, MissKay1994, MisterImp, MisterMecky, Mith-randalf, Mixelz, mjarduk, MjrLandWhale, mkanke-real, MLGTASTICa, mnva0, moderatelyaware, modern-nm, mohamedwidar, mokiros, momo, Moneyl, monotheonist, Moomoobeef, moony, Morb0, MossyGreySlope, mqole, mr-bo-jangles, Mr0maks, MrFippik, MrPersival, mrrobdemo, mtrs163, muburu, MureixloI, murolem, murphyneko, musicmanvr, MWKane, Myakot, Myctai, N3X15, nabegator, nails-n-tape, Nairodian, Naive817, NakataRin, namespace-Memory, Nannek, NazrinNya, neborsh, nekokiwa, neomoth, neutrino-laser, NickPowers43, nikitosych, nikthechampiongr, Nimfar11, ninruB, Nirnael, NIXC, nkokic, NkoKirkto, nmajask, noctyrnal, noelkathegod, noirogen, nok-ko, NonchalantNoob, NoobyLegion, Nopey, NoreUhh, Not-A-Chair, not-gavnaed, notafet, notquitehadouken, notsodana, noudoit, noverd, Nox38, NuclearWinter, Nuggets219, nukashimika, nuke-haus, NULL882, nullarmo, nyeogmi, Nylux, Nyranu, Nyxilath, och-och, OctoRocket, Ohelig, OldDanceJacket, OliverOtter, onesch, OneZerooo0, OnsenCapy, OnyxTheBrave, opl-, Orange-Winds, OrangeMoronage9622, OrbitSystem07, Orsoniks, osjarw, Ostaf, othymer, OttoMaticode, Owai-Seek, packmore, PAFFhassoocks, paige404, paigemaeforrest, pali6, Palladinium, Pangogie, panzer-iv1, partyaddict, patrikturi, PaulRitter, pavlockblaine03, peccneck, Peptide90, peptron1, perryprog, PeterFuto, PetMudstone, pewter-wiz, PGrayCS, pgraycs, Pgriha, phantom-lily, Pharaz4, pheenty, philingham, Phill101, Phooooooooooooooooooooooooooooooosphate, phunnyguy, PicklOH, PilgrimViis, Pill-U, pinkbat5, Piras314, Pireax, Pissachu, pissdemon, Pixel8-dev, PixeltheAertistContrib, PixelTheKermit, PJB3005, Plasmaguy, plinyvic, Plykiya, poeMota, pofitlo, pointer-to-null, Pok27, poklj, PolterTzi, PoorMansDreams, PopGamer45, portfiend, potato1234x, PotentiallyTom, PotRoastPiggy, Princess-Cheeseballs, ProfanedBane, Prole0, ProPandaBear, ProPeperos, PrPleGoo, ps3moira, Pspritechologist, Psychpsyo, psykana, psykzz, PuceTint, pumkin69, PuroSlavKing, PursuitInAshes, Putnam3145, py01, Pyrovi, qrtDaniil, qrwas, Quantum-cross, quasr-9, quatre, QueerNB, QuietlyWhisper, qwerltaz, Radezolid, RadioMull, Radosvik, Radrark, Rainbeon, Rainfey, Raitononai, Ramlik, RamZ, randy10122, Rane, Ranger6012, Rapidgame7, ravage123321, rbertoche, RedBookcase, Redfire1331, Redict, RedlineTriad, redmushie, RednoWCirabrab, Redrover1760, redspyy, ReeZer2, RemberBM, RemieRichards, RemTim, rene-descartes2021, Renlou, retequizzle, rewafflution, rhailrake, rhsvenson, rich-dunne, RieBi, riggleprime, RIKELOLDABOSS, rinary1, Rinkashikachi, riolume, rlebell33, RobbyTheFish, robinthedragon, robinthegirlthing, Rockdtben, Rohesie, rok-povsic, rokudara-sen, rolfero, RomanNovo, roryflowers, rosieposieeee, Roudenn, router, ruddygreat, rumaks-xyz, RumiTiger, Ruzihm, rwrv, S1rFl0, S1ss3l, Saakra, SabreML, Sadie-silly, saga3152, saintmuntzer, salarua, Salex08, sam, samgithubaccount, Samuka-C, SaphireLattice, SapphicOverload, sarahon, sativaleanne, SaveliyM360, sBasalto, ScalyChimp, ScarKy0, ScholarNZL, schrodinger71, scrato, Scribbles0, scrivoy, scruq445, scuffedjays, ScumbagDog, SeamLesss, Segonist, semensponge, sephtasm, ser1-1y, Serkket, sewerpig, SG6732, sh18rw, Shaddap1, ShadeAware, ShadowCommander, shadowtheprotogen546, shaeone, shampunj, shariathotpatrol, SharkSnake98, Shegare, shepardtothestars, shibechef, Siginanto, signalsender, SignalWalker, siigiil, silicon14wastaken, Silverfur-underscore, Simyon264, sirdragooon, Sirionaut, SirWarock, Sk1tch, SkaldetSkaeg, Skarletto, skeeka-dev, skrybl, Skybailey-dev, skye, Skyedra, SlamBamActionman, slarticodefast, Slava0135, sleepyyapril, slimmslamm, Slyfox333, Smugman, SnappingOpossum, snebl, snicket, sniperchance, Snowni, snowsignal, SolidSyn, SolidusSnek, solstar2, SomegnihT, SonarZeBat, SonicHDC, SoulFN, SoulSloth, Soundwavesghost, soupkilove, southbridge-fur, sowelipililimute, Soydium, SpaceLizard24, SpaceLizardSky, SpaceManiac, SpaceRox1244, SpaceyLady, Spangs04, spanky-spanky, Sparlight, spartak, SpartanKadence, spderman3333, SpeltIncorrectyl, Spessmann, SphiraI, SplinterGP, spoogemonster, sporekto, sporkyz, ssdaniel24, stalengd, stanberytrask, Stanislav4ix, StanTheCarpenter, starbuckss14, Stealthbomber16, steel, Steffo99, stellar-novas, stewie523, stomf, Stop-Signs, stopbreaking, stopka-html, StrawberryMoses, Stray-Pyramid, strO0pwafel, Strol20, StStevens, Subversionary, sunbear-dev, SuperGDPWYL, superjj18, Supernorn, SurrealShibe, SweetAplle, SweptWasTaken, SyaoranFox, Sybil, SYNCHRONIC, Synthestra, Szunti, t, Tainakov, takemysoult, taonewt, tap, TaralGit, Taran, taserthefox, taurie, Tayrtahn, tday93, teamaki, TeenSarlacc, TekuNut, telavivgamers, telyonok, temm1ie, TemporalOroboros, tentekal, terezi4real, Terraspark4941, texcruize, Tezzaide, TGODiamond, TGRCdev, tgrkzus, thanosdegraf, ThatGuyUSA, ThatOneGoblin25, thatrandomcanadianguy, TheArturZh, TheBlueYowie, thecopbennet, TheCze, TheDarkElites, thedraccx, TheEmber, theexetron, TheFlyingSentry, thefoty, TheGrimbeeper, TheIntoxicatedCat, thekilk, themias, theomund, TheProNoob678, TherapyGoth, ThereDrD0, TheSecondLord, TheShuEd, thetolbean, thevinter, TheWaffleJesus, Thinbug0, ThunderBear2006, timothyteakettle, TimrodDX, timurjavid, tin-man-tim, TiniestShark, Titian3, tk-a369, tkdrg, tmtmtl30, ToastEnjoyer, Toby222, TokenStyle, Tollhouse, Toly65, tom-leys, tomasalves8, Tomeno, Tonydatguy, topy, tornado-technology, TornadoTechnology, tosatur, TotallyLemon, ToxicSonicFan04, Tr1bute, travis-g-reid, treytipton, TriviaSolari, trixxedbit, TrixxedHeart, tropicalhibi, truepaintgit, Truoizys, Tryded, TsjipTsjip, tuchila-adi-bogdan, Tunguso4ka, TurboTrackerss14, TVK-04, tyashley, Tyler-IN, TytosB, Tyzemol, UbaserB, Uberration, ubis1, UBlueberry, uhbg, UKNOWH, UltimateJester, Unbelievable-Salmon, underscorex5, UnicornOnLSD, Unisol, Unkn0wnGh0st333, unusualcrow, UpAndLeaves, Uriende, UristMcDorf, user424242420, Utmanarn, Vaaankas, valentfingerov, valquaint, Varen, Vasilis, VasilisThePikachu, veliebm, Velken, VelonacepsCalyxEggs, veprolet, VerinSenpai, veritable-calamity, Veritius, Vermidia, vero5123, verslebas, vexerot, vgskye, viceemargo, VigersRay, violet754, Visne, vitopigno, vitusveit, vlad, vlados1408, VMSolidus, vmzd, VoidMeticulous, voidnull000, volotomite, volundr-, Voomra, Vordenburg, vorkathbruh, Vortebo, vulppine, wachte1, wafehling, walksanatora, Warentan, WarMechanic, Watermelon914, weaversam8, wertanchik, whateverusername0, whatston3, widgetbeck, Will-Oliver-Br, Willhelm53, WilliamECrew, willicassi, Winkarst-cpu, wirdal, wixoaGit, WlarusFromDaSpace, Wolfkey-SomeoneElseTookMyUsername, Worldwaker, wrexbe, wtcwr68, xeri7, xkreksx, xprospero, xRiriq, xsainteer, YanehCheck, yathxyz, Ygg01, YotaXP, youarereadingthis, YoungThugSS14, Yousifb26, youtissoum, yunii, YuriyKiss, yuriykiss, zach-hill, Zadeon, Zalycon, zamp, Zandario, Zap527, Zealith-Gamer, ZelteHonor, zero, ZeroDiamond, ZeWaka, zHonys, zionnBE, ZNixian, Zokkie, ZoldorfTheWizard, zonespace27, Zylofan, Zymem, zzylex From e9e497103a22d7cd45111aa9c90b7264e77973ff Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Sun, 29 Mar 2026 00:13:38 -0400 Subject: [PATCH 014/126] Update RT to 275.2.0 (#43378) --- RobustToolbox | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RobustToolbox b/RobustToolbox index a12555988a..3136118b53 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit a12555988ae75a6c77b916df5c448b9a5d19f519 +Subproject commit 3136118b5338ef2d9580178caf5c723e65eb76e7 From 29d9c1786d62307ee55f40a4c035815c8435b222 Mon Sep 17 00:00:00 2001 From: Jessica M Date: Sun, 29 Mar 2026 08:02:51 -0700 Subject: [PATCH 015/126] Fix changeling transform ability (#42107) * fix changeling transform * Add store UI on MapInit instead * Apply suggestion from @slarticodefast * Apply suggestion from @slarticodefast --------- Co-authored-by: Jessica M Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --- Content.Server/Store/Systems/StoreSystem.cs | 6 ++++++ Resources/Prototypes/Entities/Mobs/Player/changeling.yml | 5 ----- Resources/Prototypes/GameRules/roundstart.yml | 5 ----- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Content.Server/Store/Systems/StoreSystem.cs b/Content.Server/Store/Systems/StoreSystem.cs index 7c5c99b5b4..3806842507 100644 --- a/Content.Server/Store/Systems/StoreSystem.cs +++ b/Content.Server/Store/Systems/StoreSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Implants.Components; using Content.Shared.Interaction; using Content.Shared.Popups; using Content.Shared.Stacks; +using Content.Shared.Store; using Content.Shared.Store.Components; using Content.Shared.Store.Events; using Content.Shared.UserInterface; @@ -23,6 +24,7 @@ public sealed partial class StoreSystem : EntitySystem [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; public override void Initialize() { @@ -47,6 +49,10 @@ public sealed partial class StoreSystem : EntitySystem { RefreshAllListings(component); component.StartingMap = Transform(uid).MapUid; + + // Add the bui key if it does not exist already (the check is needed to make sure that we don't overwrite existing InterfaceData). + if (!_uiSystem.HasUi(uid, StoreUiKey.Key)) + _uiSystem.SetUi(uid, StoreUiKey.Key, new InterfaceData("StoreBoundUserInterface")); } private void OnStartup(EntityUid uid, StoreComponent component, ComponentStartup args) diff --git a/Resources/Prototypes/Entities/Mobs/Player/changeling.yml b/Resources/Prototypes/Entities/Mobs/Player/changeling.yml index 6ee9de8d01..424c942c66 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/changeling.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/changeling.yml @@ -13,8 +13,3 @@ - type: Store balance: ChangelingDNA: 50 - - type: UserInterface - interfaces: - enum.StoreUiKey.Key: - type: StoreBoundUserInterface - requireInputValidation: false diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index b03b5b03cc..76c2ade9d4 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -286,11 +286,6 @@ - ChangelingDNA balance: ChangelingDNA: 50 - - type: UserInterface - interfaces: - enum.StoreUiKey.Key: - type: StoreBoundUserInterface - requireInputValidation: false mindRoles: - MindRoleChangeling - type: AntagObjectives From ef2ea25cdf6472a2ded88948feb011c4de6b6107 Mon Sep 17 00:00:00 2001 From: NotActuallyMarty <27968892+NotActuallyMarty@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:14:08 +0300 Subject: [PATCH 016/126] Kill unused terminator files (#43381) terminate the terminator --- .../Locale/en-US/administration/smites.ftl | 1 - .../Species/Terminator/parts.rsi/full.png | Bin 2187 -> 0 bytes .../Species/Terminator/parts.rsi/head_f.png | Bin 997 -> 0 bytes .../Species/Terminator/parts.rsi/head_m.png | Bin 997 -> 0 bytes .../Species/Terminator/parts.rsi/l_arm.png | Bin 807 -> 0 bytes .../Species/Terminator/parts.rsi/l_foot.png | Bin 746 -> 0 bytes .../Species/Terminator/parts.rsi/l_hand.png | Bin 748 -> 0 bytes .../Species/Terminator/parts.rsi/l_leg.png | Bin 737 -> 0 bytes .../Species/Terminator/parts.rsi/meta.json | 66 ------------------ .../Species/Terminator/parts.rsi/r_arm.png | Bin 839 -> 0 bytes .../Species/Terminator/parts.rsi/r_foot.png | Bin 782 -> 0 bytes .../Species/Terminator/parts.rsi/r_hand.png | Bin 759 -> 0 bytes .../Species/Terminator/parts.rsi/r_leg.png | Bin 848 -> 0 bytes .../Terminator/parts.rsi/skull_icon.png | Bin 1128 -> 0 bytes .../Species/Terminator/parts.rsi/torso_f.png | Bin 1803 -> 0 bytes .../Species/Terminator/parts.rsi/torso_m.png | Bin 1803 -> 0 bytes 16 files changed, 67 deletions(-) delete mode 100644 Resources/Textures/Mobs/Species/Terminator/parts.rsi/full.png delete mode 100644 Resources/Textures/Mobs/Species/Terminator/parts.rsi/head_f.png delete mode 100644 Resources/Textures/Mobs/Species/Terminator/parts.rsi/head_m.png delete mode 100644 Resources/Textures/Mobs/Species/Terminator/parts.rsi/l_arm.png delete mode 100644 Resources/Textures/Mobs/Species/Terminator/parts.rsi/l_foot.png delete mode 100644 Resources/Textures/Mobs/Species/Terminator/parts.rsi/l_hand.png delete mode 100644 Resources/Textures/Mobs/Species/Terminator/parts.rsi/l_leg.png delete mode 100644 Resources/Textures/Mobs/Species/Terminator/parts.rsi/meta.json delete mode 100644 Resources/Textures/Mobs/Species/Terminator/parts.rsi/r_arm.png delete mode 100644 Resources/Textures/Mobs/Species/Terminator/parts.rsi/r_foot.png delete mode 100644 Resources/Textures/Mobs/Species/Terminator/parts.rsi/r_hand.png delete mode 100644 Resources/Textures/Mobs/Species/Terminator/parts.rsi/r_leg.png delete mode 100644 Resources/Textures/Mobs/Species/Terminator/parts.rsi/skull_icon.png delete mode 100644 Resources/Textures/Mobs/Species/Terminator/parts.rsi/torso_f.png delete mode 100644 Resources/Textures/Mobs/Species/Terminator/parts.rsi/torso_m.png diff --git a/Resources/Locale/en-US/administration/smites.ftl b/Resources/Locale/en-US/administration/smites.ftl index 0702afb33c..2de4e79081 100644 --- a/Resources/Locale/en-US/administration/smites.ftl +++ b/Resources/Locale/en-US/administration/smites.ftl @@ -107,7 +107,6 @@ admin-smite-disarm-prone-description = Makes them get disarmed 100% of the time admin-smite-garbage-can-description = Turn them into a garbage bin to emphasize what they remind you of. admin-smite-super-bonk-description = Slams them on every single table on the Station and beyond. admin-smite-super-bonk-lite-description= Slams them on every single table on the Station and beyond. Stops when the target is dead. -admin-smite-terminate-description = Creates a Terminator ghost role with the sole objective of killing them. admin-smite-super-slip-description = Slips them really, really hard. admin-smite-omni-accent-description = Makes the target speak with almost every accent available. admin-smite-crawler-description = Makes the target fall down and be unable to stand up. Remove their hands too for added effect! diff --git a/Resources/Textures/Mobs/Species/Terminator/parts.rsi/full.png b/Resources/Textures/Mobs/Species/Terminator/parts.rsi/full.png deleted file mode 100644 index 44e3df3e91b378de6c18fc0617a87996c248a5d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2187 zcmV;62z2*}P)Px#1ZP1_K>z@;j|==^1pojDHAzH4RCr$PntyDS)fvaXloq)C0d1|U-Mt{CO2uj| z?LeTS5dR=z<}~{QaW2HnB_>PE{?P=cn_F}yTe2)>f4IekI2{EOlMNFP6D**$2&3(; zcI@^C1$sMbTcIue0kqiN@6-3Wd0T$H=iGZ=;>IVrIp;j@dEf7Ip7)&Ry!X8qH@V6G zH#3~lm^BYQG~rb7@xFc5*Rds8`c@}WeEpL@1p+}}vFJ5||)Pr|^2N%c25h|@jG}3@s8R_Wy_dn#=@nfH*BnUe7zaIY@ zV;4tpVeA63v*%)HXb`hAW+P)(0BPxI#!JCdM~au-zX`kFdd;x_OYbhnoSY(@KX)3( zz6d*3V8iAuX#S)osY`Ro(bE;hw?elekd=?(sz+o7uwD1gz^gVq|Co1 z2cx~sQfJ9F(c`Hk(om0+$Btm|%LqQ)_Og_0({96c1HkF8@a_gOXQB7YP8^SfWva7e zn|2;|x=uMS5!}<6cegN9hzp}<(AnC6JMJo#l5N`EiO=gYxNZQlzrFIZ>8u0QX94%X zb=mN|WU*}RQ*nLf7#*nUv#mLb{n+0to|B*kVP;x-+!;Oq5x1TFl6CjVK<_E}EU8>C z*PoZ4e?@(@v!9;h1y4(WcXRKbpO+`SHIM%U%hvr+j_Dm>n|2;&KlV>%5Wec|L`%b7 zsWZ9pLsZ{wV?Xxy$~6PPsl^?DgJPYJ!OD`dN-VyoN=k*t*^m8so=#O;c>eL`gBa-P zxa?Y2(kJZ4{yfi%zLA46#bScPgX7XZTDKcj)f=RqH8&kC&8huTG+n=|GmPMZa;e+u zd&&WBidSwz^8*pk&1pGH`x4Sgq4;gK+=|r@DUOVd-8PexY|d(HUnI z046&()7#%9j04bds3!J>=zjTn%r;B5Y4?Nn!#iZ%>sH8;ZQ5-x4nWrYGPH#2oYo|4 z)9wdc>sQkEB?stK9~mB!Qqi_wOW|tzHh2zzPWhg;f8y4nBC#oK(@wNsve79|uB5NH z`+31r2IWPoAO1c@&SxTX&WMzawQoxX<}p4As-0yr9)jG6M(p{%u(&+_B}cy^>oKCo z^_Up1gXaJ^)hVb0PIC&$4uDggf?xVKxyk=9*6U>Y+VgwOB}?DxB#LWR783l8-W2M) zeZ~Ax_PDf8z>smWowQBW5X;}pZ z2hPSEI4QQH^`EA6@QNlo-hiBW`3b#?G+qK6_)C8}AcN1EzZ6AxtVZQJf8GZwBO{}@ z^S;M0FIa)J?1flRTnhR`-<&RGc;m$<;vYKV7u0fV(--IMB^o-xO{7KwpT>GBnycm|)g!{CPYFa3_c~H#tWqN%dAZ zJ<+NUni%)*{FTv`Xfy+O;oaM|NS*7B!ei%0r9E+BT%0h?!Y1(A)SwojHhs|KN8e1J z2dt8yLT8Bku8;KfJGJEFzP4(r54KnbK$k(roIre99{u2LWESMg@l?QTQ-e^G+PVeW zV!U46Ymie<1EKht19aW%vLvSZ#xsBdnVA7pZ}>3+nb|15Yc<%WJ=KFJszXg`(+B#Z zJ~-VxlTZ#Id$zsc)cl9qUjGetHLL=_OdEeEaD(A{N!xb;B;gtGO~)z#+>iOMES-L~ z(N6~Sfxg&2e^}1|3ddiI-$2|ynA%_+fOGv_PVL5%h%X7}0DT{y%17YoP>Z^p`mK$7 z(AHGz95Kf_06DoqnR<-A{&UoiAn4$!OMUv02v*7KS_4gWLQJkU@L$|xn!Ybfs}BGG N002ovPDHLkV1n=*R1*LI diff --git a/Resources/Textures/Mobs/Species/Terminator/parts.rsi/head_f.png b/Resources/Textures/Mobs/Species/Terminator/parts.rsi/head_f.png deleted file mode 100644 index dada5727bf6261b51b3df9e29910a92972386fe9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 997 zcmVAge`>x4_Hc93`YRXlW1{qLyw$ z|3R%mYe5hcK}1`7w)i)DR`LgyY-%9izWVp)|_XnTgMT-~3SoMyg6FV{8C5xBPD;<~QI z_Th2|hMx4O5v@vXvhB$vyq`mF1p4m4u~y6K@0_zo2ye+Rx`l&77?b<1^UE1-g^h(fA_t2_l9=>(SI$V{bNsm|BX9eUs(zG?s%#4E41OyhhGCV z%|Px%nr?g+LGlNN#(?TDaFw}Nen1m$!tD8EJcT#IQP=L}Nhlh;V~X%;VOo_RRNZ+g zIR@?Bw?{+T3m?m}4gS@;;=v1ZE$y0o)fA9B|JEEA-;V(CSxjQWO$Bc*vR`q1l|xl^ z4eCSf{uV(u1wKsA;Ze95-!dl{9ee3@3Osxq#gB_K>};-~v%41$>KpMrol@low4HeJ zG>X=aUL5YO;mdvsz8AscgQMH)mbS=Wc@q&45fKp)5fKp)5fS}UO0qvd`2I3es(khS zfM3!!QebVUfIBbb`~XbX`{CcG6Od9a@!xS9Vu%QMJkj{Lp_ z#N}lU6_r)kSW1`<9NV_7&l_cb3%KITri@THg4((UTL2;=A|fIpA|fIp`U5`!d=2XX T9$_Z%00000NkvXXu0mjf<5$Vb diff --git a/Resources/Textures/Mobs/Species/Terminator/parts.rsi/head_m.png b/Resources/Textures/Mobs/Species/Terminator/parts.rsi/head_m.png deleted file mode 100644 index dada5727bf6261b51b3df9e29910a92972386fe9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 997 zcmVAge`>x4_Hc93`YRXlW1{qLyw$ z|3R%mYe5hcK}1`7w)i)DR`LgyY-%9izWVp)|_XnTgMT-~3SoMyg6FV{8C5xBPD;<~QI z_Th2|hMx4O5v@vXvhB$vyq`mF1p4m4u~y6K@0_zo2ye+Rx`l&77?b<1^UE1-g^h(fA_t2_l9=>(SI$V{bNsm|BX9eUs(zG?s%#4E41OyhhGCV z%|Px%nr?g+LGlNN#(?TDaFw}Nen1m$!tD8EJcT#IQP=L}Nhlh;V~X%;VOo_RRNZ+g zIR@?Bw?{+T3m?m}4gS@;;=v1ZE$y0o)fA9B|JEEA-;V(CSxjQWO$Bc*vR`q1l|xl^ z4eCSf{uV(u1wKsA;Ze95-!dl{9ee3@3Osxq#gB_K>};-~v%41$>KpMrol@low4HeJ zG>X=aUL5YO;mdvsz8AscgQMH)mbS=Wc@q&45fKp)5fKp)5fS}UO0qvd`2I3es(khS zfM3!!QebVUfIBbb`~XbX`{CcG6Od9a@!xS9Vu%QMJkj{Lp_ z#N}lU6_r)kSW1`<9NV_7&l_cb3%KITri@THg4((UTL2;=A|fIpA|fIp`U5`!d=2XX T9$_Z%00000NkvXXu0mjf<5$Vb diff --git a/Resources/Textures/Mobs/Species/Terminator/parts.rsi/l_arm.png b/Resources/Textures/Mobs/Species/Terminator/parts.rsi/l_arm.png deleted file mode 100644 index bb7425405cf99b761b0b095748bf740cb7fbd973..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 807 zcmV+?1K9kDP)Age`>x4_Hc93`YRXlW1{qLyw$ z|3R%mYe5hcK}1`7w)i)DR`LgyY-%9izWVp)|_XnTgMT-~3SoMyg6FV{8C5xBPD;<~QI z_Th2|hMx4O5v@vXvhB$vyq`mF1p4m4u~y6K@0_zo2ye+Rx`l&77?b<1^ncNU$6an6#hp7^hQ?_yWi&SZmw%b<2G~r_Y;sk)lYEVpWy@t l!+P^dUyJ|%0002|%p2p7YPS_z1BL(q002ovPDHLkV1l*XY*_#R diff --git a/Resources/Textures/Mobs/Species/Terminator/parts.rsi/l_foot.png b/Resources/Textures/Mobs/Species/Terminator/parts.rsi/l_foot.png deleted file mode 100644 index 8e0b3f150767e0cb3c9920fab3982345dc405fac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 746 zcmVAge`>x4_Hc93`YRXlW1{qLyw$ z|3R%mYe5hcK}1`7w)i)DR`LgyY-%9izWVp)|_XnTgMT-~3SoMyg6FV{8C5xBPD;<~QI z_Th2|hMx4O5v@vXvhB$vyq`mF1p4m4u~y6K@0_zo2ye+Rx`l&77?b<1^_z3^#5%xa|X^m zXYSpdkpKVy000000002s9|$&0U>eP&P%No(RM+Fft&hdiyCCaA&%!WqYx}aRVed-X zlWs<%z{aId8}D>=9W~a@o7=RoLb)W zo(z5t!9Kfq8o4YAjK}>yzpB3B$uI8!0000000000007IS8ezL}b@jBRnrZv7w42Um zISyZC|HZicDIV&1Huk;P8rII0dOTh%SA6sDZ!iA;04v$XM_4rP_SO8mj-A|qW%<&a c!Y=pz0?f}?PP0Wa$^ZZW07*qoM6N<$g2F>t{{R30 diff --git a/Resources/Textures/Mobs/Species/Terminator/parts.rsi/l_hand.png b/Resources/Textures/Mobs/Species/Terminator/parts.rsi/l_hand.png deleted file mode 100644 index cf93432a5e797cb3f22ffeb6ada16a28f8ddc6af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 748 zcmVAge`>x4_Hc93`YRXlW1{qLyw$ z|3R%mYe5hcK}1`7w)i)DR`LgyY-%9izWVp)|_XnTgMT-~3SoMyg6FV{8C5xBPD;<~QI z_Th2|hMx4O5v@vXvhB$vyq`mF1p4m4u~y6K@0_zo2ye+Rx`l&77?b<1^00HOdTu~d;>{}pdhHtMX4cB{FancZqn1ONa40002V;+PJ&>3Lyo??|t^?S@?SdQOyBrL7&kzPmh)6zeA` z(*afTZ^K(-@m_<09&2t*6+~iR0M+%SLfL2$#=(<3^tAC!?1? z>D(+)J-gNINO97pH@3K(x10E^`^x%8MbE11ZZ$>)uzCFToy~r4S#^J9wdkB5AB489 eGynhq{;Mf0j8IAge`>x4_Hc93`YRXlW1{qLyw$ z|3R%mYe5hcK}1`7w)i)DR`LgyY-%9izWVp)|_XnTgMT-~3SoMyg6FV{8C5xBPD;<~QI z_Th2|hMx4O5v@vXvhB$vyq`mF1p4m4u~y6K@0_zo2ye+Rx`l&77?b<1^vOA zLE#7niwFU$yn%rfpgCwR*8c^9Ljr62`7i>99E7<5o7KXbHufnO;K+Odht^d( Tds-Bv00000NkvXXu0mjf=juxp diff --git a/Resources/Textures/Mobs/Species/Terminator/parts.rsi/meta.json b/Resources/Textures/Mobs/Species/Terminator/parts.rsi/meta.json deleted file mode 100644 index 688877a32d..0000000000 --- a/Resources/Textures/Mobs/Species/Terminator/parts.rsi/meta.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "version": 1, - "license": "CC-BY-SA-3.0", - "copyright": "Created by ps3moira#9488 (discord) for SS14.", - "size": { - "x": 32, - "y": 32 - }, - "states": [ - { - "name": "full" - }, - { - "name": "head_f", - "directions": 4 - }, - { - "name": "head_m", - "directions": 4 - }, - { - "name": "l_arm", - "directions": 4 - }, - { - "name": "l_foot", - "directions": 4 - }, - { - "name": "l_hand", - "directions": 4 - }, - { - "name": "l_leg", - "directions": 4 - }, - { - "name": "r_arm", - "directions": 4 - }, - { - "name": "r_foot", - "directions": 4 - }, - { - "name": "r_hand", - "directions": 4 - }, - { - "name": "r_leg", - "directions": 4 - }, - { - "name": "skull_icon", - "directions": 1 - }, - { - "name": "torso_f", - "directions": 4 - }, - { - "name": "torso_m", - "directions": 4 - } - ] -} diff --git a/Resources/Textures/Mobs/Species/Terminator/parts.rsi/r_arm.png b/Resources/Textures/Mobs/Species/Terminator/parts.rsi/r_arm.png deleted file mode 100644 index 51f05a477380a3bebd9e5d2834e82a5f49117441..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 839 zcmV-N1GxN&P)Age`>x4_Hc93`YRXlW1{qLyw$ z|3R%mYe5hcK}1`7w)i)DR`LgyY-%9izWVp)|_XnTgMT-~3SoMyg6FV{8C5xBPD;<~QI z_Th2|hMx4O5v@vXvhB$vyq`mF1p4m4u~y6K@0_zo2ye+Rx`l&77?b<1^0zaK7mu0V(cakc9JSoTa-2usgNNkA`%=5f+FG& zlR`5}QXF#I{~HoQa>KoU&Pj$40RR910Ki`+#c6YGW5@UNT!_a_FW2*KZ#uiWMYpAb z?*kAi_jq=BQ@2 z(hP?qOax%hSISjQH&wuUxp8PH)NwC&-c9+psEX2kZ2$lO00000000000013=^EyDB zn>Q1wJu;qD5+dWyc>B0kl&M<2W-5Symy7Mct&7QBXR*(^PyilSnoI|8u!zPXRiBq@D( z-YorK-ynUyJun$STP-lPv`%N)v>6+l;I#T3^$+l*Yxnr1000000000ZegW3*bYW$< Ri825H002ovPDHLkV1m>CbISk# diff --git a/Resources/Textures/Mobs/Species/Terminator/parts.rsi/r_foot.png b/Resources/Textures/Mobs/Species/Terminator/parts.rsi/r_foot.png deleted file mode 100644 index 19ac240da3ad9bb75bd86dc85b5cad0ed453a20b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 782 zcmV+p1M&QcP)Age`>x4_Hc93`YRXlW1{qLyw$ z|3R%mYe5hcK}1`7w)i)DR`LgyY-%9izWVp)|_XnTgMT-~3SoMyg6FV{8C5xBPD;<~QI z_Th2|hMx4O5v@vXvhB$vyq`mF1p4m4u~y6K@0_zo2ye+Rx`l&77?b<1^2%jSz8axxU{^f{zFeMgJ=2OODl{ko6D;>U)Ig_ zxrt4*;KOED-cenemd;u&ZB?s*-X!;+A@8^ztqJ3?&t=D1WRh&SI~u-bl$jZ7wzML@ zeG<)O3wb$}-BG-|H_;1tyzj}W*5r4Zqn8sto4C8*h{jM5_Y&;5i7&jzwlLi0)000000001hv8*JUOuM|aTG688M(ya2?yP0{^=UBqo$Z?D7RtKyJ>6Zk z6X%~wT12i;j1E6|>PP{Qz9M`8$%U|Yx2Q}Sn@oadB2mk;8 M07*qoM6N<$f_3_BDgXcg diff --git a/Resources/Textures/Mobs/Species/Terminator/parts.rsi/r_hand.png b/Resources/Textures/Mobs/Species/Terminator/parts.rsi/r_hand.png deleted file mode 100644 index 6cd2eb37ccbe12bef6a484660d4be6459db0a47d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 759 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 Date: Mon, 30 Mar 2026 00:01:49 +0100 Subject: [PATCH 017/126] Remove PDA equip sprites (#40498) * we have never had pda equip sprites. * I didn't know the rsi validator would complain about that one tbh --------- Co-authored-by: ruddygreat --- .../Objects/Devices/pda.rsi/equipped-BELT.png | Bin 1156 -> 0 bytes .../Objects/Devices/pda.rsi/equipped-IDCARD.png | Bin 1156 -> 0 bytes .../Textures/Objects/Devices/pda.rsi/meta.json | 8 -------- 3 files changed, 8 deletions(-) delete mode 100644 Resources/Textures/Objects/Devices/pda.rsi/equipped-BELT.png delete mode 100644 Resources/Textures/Objects/Devices/pda.rsi/equipped-IDCARD.png diff --git a/Resources/Textures/Objects/Devices/pda.rsi/equipped-BELT.png b/Resources/Textures/Objects/Devices/pda.rsi/equipped-BELT.png deleted file mode 100644 index 6901e6c33b16b6a58f4a635b276edaa70a7f9845..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1156 zcmZ`(PiWIn7*BObEp*JAY@#8?n0g=RwSp*ETFoV)9BOcvyEE zJBWDj;MLQ>!SE_fL=biu=-H`w5p~1ME`m(oOS<$A?SuE;_rCmo-}k-WdoQbnxtVxu zG)7TWJU5#y;(3Df=m|Wo>kmHTDdHAqGStS-nICvD3`@CkK2Kf1b`*EgKHhqY$CLK# zf=f}cU!+G?*5Y?5>NwKpOQ@8;DXLbDQ#5N4a$e2$S&}DWvj&kudo{y!MK8_x2oc-F z4H(*oprtfZ$`|M<%Yk%~<2jy@V>C@mj#d_n+3PJjUZt5TLbeD%qtW0RmpIF*fJ7>l z0=xhOfyD^ceP|-ZV@-EF2yzrB3tiRGZKPW!P2wtxRvo1oh9nAGP^YGbdF;Al_*pa+ zKm*pGiCmE25}+Rs>E(8XrrVMO3j@d(Oym2~ayC*gW2L4&Tu*C%xKP#4>mm(w0D!>r zS6N6pf6mwA78;q19f`|o=9NmL|M^L%u1T^SPhnyx`7y3H>--N zv5sD;A_)Y@?g)XmJFo=$)F6?aiwi~QT1K5{FJDPWz8qc7_8|m>Qa?hcvzJh)TFAn8 z%F)%FiJ)UU_?n0g=RwSp*ETFoV)9BOcvyEE zJBWDj;MLQ>!SE_fL=biu=-H`w5p~1ME`m(oOS<$A?SuE;_rCmo-}k-WdoQbnxtVxu zG)7TWJU5#y;(3Df=m|Wo>kmHTDdHAqGStS-nICvD3`@CkK2Kf1b`*EgKHhqY$CLK# zf=f}cU!+G?*5Y?5>NwKpOQ@8;DXLbDQ#5N4a$e2$S&}DWvj&kudo{y!MK8_x2oc-F z4H(*oprtfZ$`|M<%Yk%~<2jy@V>C@mj#d_n+3PJjUZt5TLbeD%qtW0RmpIF*fJ7>l z0=xhOfyD^ceP|-ZV@-EF2yzrB3tiRGZKPW!P2wtxRvo1oh9nAGP^YGbdF;Al_*pa+ zKm*pGiCmE25}+Rs>E(8XrrVMO3j@d(Oym2~ayC*gW2L4&Tu*C%xKP#4>mm(w0D!>r zS6N6pf6mwA78;q19f`|o=9NmL|M^L%u1T^SPhnyx`7y3H>--N zv5sD;A_)Y@?g)XmJFo=$)F6?aiwi~QT1K5{FJDPWz8qc7_8|m>Qa?hcvzJh)TFAn8 z%F)%FiJ)UU_ Date: Sun, 29 Mar 2026 16:13:55 -0700 Subject: [PATCH 018/126] Lathe Menu Title Enhancement (#43392) * DefaultWindow -> FancyWindow, use SetInfoFromEntity * code smell fix --- Content.Client/Lathe/UI/LatheMenu.xaml | 5 +++-- Content.Client/Lathe/UI/LatheMenu.xaml.cs | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Content.Client/Lathe/UI/LatheMenu.xaml b/Content.Client/Lathe/UI/LatheMenu.xaml index a5c8f6a85c..84f1d7835a 100644 --- a/Content.Client/Lathe/UI/LatheMenu.xaml +++ b/Content.Client/Lathe/UI/LatheMenu.xaml @@ -1,8 +1,9 @@ - @@ -156,4 +157,4 @@ - + diff --git a/Content.Client/Lathe/UI/LatheMenu.xaml.cs b/Content.Client/Lathe/UI/LatheMenu.xaml.cs index f6688a63af..adb115d352 100644 --- a/Content.Client/Lathe/UI/LatheMenu.xaml.cs +++ b/Content.Client/Lathe/UI/LatheMenu.xaml.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Text; using Content.Client.Materials; +using Content.Client.UserInterface.Controls; using Content.Shared.Lathe; using Content.Shared.Lathe.Prototypes; using Content.Shared.Research.Prototypes; @@ -8,7 +9,6 @@ using Robust.Client.AutoGenerated; using Robust.Client.GameObjects; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Prototypes; using Robust.Shared.Utility; @@ -16,7 +16,7 @@ using Robust.Shared.Utility; namespace Content.Client.Lathe.UI; [GenerateTypedNameReferences] -public sealed partial class LatheMenu : DefaultWindow +public sealed partial class LatheMenu : FancyWindow { [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; @@ -75,6 +75,7 @@ public sealed partial class LatheMenu : DefaultWindow public void SetEntity(EntityUid uid) { Entity = uid; + this.SetInfoFromEntity(_entityManager, Entity); if (_entityManager.TryGetComponent(Entity, out var latheComponent)) { From f812ee23be300dfc2c80cc097eb709e4ac5ff51f Mon Sep 17 00:00:00 2001 From: PJBot Date: Sun, 29 Mar 2026 23:27:57 +0000 Subject: [PATCH 019/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 84a758166f..dfa00ca08e 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: DrSmugleaf - changes: - - message: Fixed species not being ordered alphabetically in the character customization - UI. - type: Fix - id: 9084 - time: '2025-10-12T11:14:46.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/39359 - author: Callmore changes: - message: Bullet casings can now be picked up again. @@ -4035,3 +4027,10 @@ id: 9595 time: '2026-03-29T02:36:35.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/42973 +- author: HoofedEar + changes: + - message: Lathe machine interfaces now display their proper names + type: Tweak + id: 9596 + time: '2026-03-29T23:26:46.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43392 From f44e9999b41dd2bdc68d99ce0a5a5a7568e7eefb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=81da?= Date: Sun, 29 Mar 2026 18:15:28 -0500 Subject: [PATCH 020/126] Reduce speso value of tech disks (#43395) commit Co-authored-by: iaada --- .../TechnologyDisk/Components/TechnologyDiskComponent.cs | 6 +++--- .../Structures/Machines/Computers/techdiskterminal.yml | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Content.Shared/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs b/Content.Shared/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs index 02ffc77616..0afb845a9f 100644 --- a/Content.Shared/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs +++ b/Content.Shared/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs @@ -39,8 +39,8 @@ public sealed partial class TechnologyDiskComponent : Component [DataField] public Dictionary DiskPricePerTier = new() { - [1] = 100, - [2] = 500, - [3] = 1500 + [1] = 50, + [2] = 135, + [3] = 1000, }; } diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/techdiskterminal.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/techdiskterminal.yml index dd58b9709d..5b250969ce 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/techdiskterminal.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/techdiskterminal.yml @@ -31,6 +31,8 @@ energy: 0.5 color: "#b53ca1" +# The value per-disk equate to an average of 1000 research points -> 100 spesos +# Average is calculated from this table, default speso value in TechnologyDiskComponent, and default point cost in DiskConsoleComponent - type: weightedRandom id: TechDiskTierWeights weights: From 79b55a4f65f1cce7d978414059e8a8302dad0214 Mon Sep 17 00:00:00 2001 From: PJBot Date: Sun, 29 Mar 2026 23:42:54 +0000 Subject: [PATCH 021/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index dfa00ca08e..4c53d81f13 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Callmore - changes: - - message: Bullet casings can now be picked up again. - type: Fix - id: 9085 - time: '2025-10-12T18:45:39.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40829 - author: CoconutThunder changes: - message: Fixed lubed items being thrown when looking at the pickup verb. @@ -4034,3 +4027,10 @@ id: 9596 time: '2026-03-29T23:26:46.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43392 +- author: aada + changes: + - message: The value of tier 1 tech disks has been lowered. + type: Tweak + id: 9597 + time: '2026-03-29T23:41:45.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43395 From e27f51762b5e0476c7d8f9041bb7db4bfaaa4070 Mon Sep 17 00:00:00 2001 From: InsoPL Date: Mon, 30 Mar 2026 04:04:14 +0200 Subject: [PATCH 022/126] Bugfix of Heat distortion shader (#43397) * revert of the revert * tests * changes * more fun * test * ccvvvar * works but bad * now its better * more fixes * more cleanup * cleaning * last fixes before move to glasses activ * x * glasses only * working * fix toolbox * cleanup * ThermalByte added * small fix * small optimalisations * float bux fix * comments add * more comments * more comments * last fix * revert cvar delete * wrong blue shades * cvar refactor * Update Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * Update Content.Client/Atmos/Overlays/GasTileDangerousTemperatureOverlay.cs Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * tweak to TryGetTemperature comment * Factors are now const * renames * Interface for ThermalByte * tile color vaccum and more comments * saving yeeted * integration test * rename and cleanup * fix * cleanup * switch * UT fix (hopefully) * small bug+ rename * vaccum limit + space is now invalid * typo * typo * fix * init, works * move method around * renames and split * renames * heatblur * cleanup * more docs * resource cache * No dynamic perlin noise generation [no fun allowed] and new blur softening method * magic numbers and rename * misc cleanup * removed init values because display compat mode broken * misc cleanup * bugfix * fix --------- Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> --- Content.Client/Atmos/Overlays/GasTileHeatBlurOverlay.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Content.Client/Atmos/Overlays/GasTileHeatBlurOverlay.cs b/Content.Client/Atmos/Overlays/GasTileHeatBlurOverlay.cs index 2f540b6360..d849319875 100644 --- a/Content.Client/Atmos/Overlays/GasTileHeatBlurOverlay.cs +++ b/Content.Client/Atmos/Overlays/GasTileHeatBlurOverlay.cs @@ -152,7 +152,9 @@ public sealed class GasTileHeatBlurOverlay : Overlay worldHandle.SetTransform(gridEntToViewportLocal); // We only care about tiles that fit in these bounds - var floatBounds = worldToViewportLocal.TransformBox(worldBounds).Enlarged(grid.Comp.TileSize); + var worldToGridLocal = _xformSys.GetInvWorldMatrix(grid.Owner); + var floatBounds = worldToGridLocal.TransformBox(worldBounds).Enlarged(grid.Comp.TileSize); + var localBounds = new Box2i( (int)MathF.Floor(floatBounds.Left), (int)MathF.Floor(floatBounds.Bottom), From 76e4d48273e445e8a6c27a1342b9c3c6b8176eca Mon Sep 17 00:00:00 2001 From: MissKay1994 <15877268+MissKay1994@users.noreply.github.com> Date: Mon, 30 Mar 2026 20:18:47 -0400 Subject: [PATCH 023/126] Greatly reduce spawn rate of salt ore (#43012) * Disintegrate salt * Salt is forever * Forgot to exclude this file --- Resources/Prototypes/Procedural/Magnet/asteroid_ore_gens.yml | 2 +- Resources/Prototypes/Procedural/vgroid.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Resources/Prototypes/Procedural/Magnet/asteroid_ore_gens.yml b/Resources/Prototypes/Procedural/Magnet/asteroid_ore_gens.yml index 661350f6de..8d4632c0f3 100644 --- a/Resources/Prototypes/Procedural/Magnet/asteroid_ore_gens.yml +++ b/Resources/Prototypes/Procedural/Magnet/asteroid_ore_gens.yml @@ -4,7 +4,7 @@ OreIron: 1.0 OreQuartz: 1.0 OreCoal: 0.33 - OreSalt: 0.25 + OreSalt: 0.20 OreGold: 0.25 OreSilver: 0.25 OrePlasma: 0.20 diff --git a/Resources/Prototypes/Procedural/vgroid.yml b/Resources/Prototypes/Procedural/vgroid.yml index 0caa9f0e1f..7cc40dbb8f 100644 --- a/Resources/Prototypes/Procedural/vgroid.yml +++ b/Resources/Prototypes/Procedural/vgroid.yml @@ -50,7 +50,7 @@ - !type:OreDunGen replacement: IronRock entity: IronRockSalt - count: 50 + count: 25 minGroupSize: 8 maxGroupSize: 12 - !type:OreDunGen From 74c8fdde7221c54372836c51475ea545f99fef93 Mon Sep 17 00:00:00 2001 From: PJBot Date: Tue, 31 Mar 2026 00:32:33 +0000 Subject: [PATCH 024/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 4c53d81f13..9748cd79d6 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,15 +1,4 @@ Entries: -- author: CoconutThunder - changes: - - message: Fixed lubed items being thrown when looking at the pickup verb. - type: Fix - - message: Fixed multihanded items showing a popup when looking at the pickup verb. - type: Fix - - message: Fixed a bug with lubed handcuffs. - type: Fix - id: 9086 - time: '2025-05-25T05:10:58.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/38705 - author: GitHubUser53123 changes: - message: Zombies that aren't supposed to spread the zombie virus now don't do @@ -4034,3 +4023,10 @@ id: 9597 time: '2026-03-29T23:41:45.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43395 +- author: MissKay1994 + changes: + - message: Salt is now significantly less common in ore generation + type: Tweak + id: 9598 + time: '2026-03-31T00:31:21.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43012 From 03b46abef034c4186953b53e57e6fd3341ffb2f0 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Mon, 30 Mar 2026 23:28:24 -0400 Subject: [PATCH 025/126] Fix ghost time of death (#43411) * Use RealTime when setting ghost TimeOfDeath * Don't pause with RealTime * Oops. Also get rid of AutoGenerateComponentPause --- Content.Server/Ghost/GhostSystem.cs | 2 +- Content.Shared/Ghost/GhostComponent.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs index 9d3d05c607..964ba99ac3 100644 --- a/Content.Server/Ghost/GhostSystem.cs +++ b/Content.Server/Ghost/GhostSystem.cs @@ -205,7 +205,7 @@ namespace Content.Server.Ghost } _eye.RefreshVisibilityMask(uid); - var time = _gameTiming.CurTime; + var time = _gameTiming.RealTime; component.TimeOfDeath = time; } diff --git a/Content.Shared/Ghost/GhostComponent.cs b/Content.Shared/Ghost/GhostComponent.cs index 3fc9c081cb..cd5aa9be22 100644 --- a/Content.Shared/Ghost/GhostComponent.cs +++ b/Content.Shared/Ghost/GhostComponent.cs @@ -10,7 +10,7 @@ namespace Content.Shared.Ghost; /// Handles limiting interactions, using ghost abilities, ghost visibility, and ghost warping. /// [RegisterComponent, NetworkedComponent, Access(typeof(SharedGhostSystem))] -[AutoGenerateComponentState(true), AutoGenerateComponentPause] +[AutoGenerateComponentState(true)] public sealed partial class GhostComponent : Component { // Actions @@ -54,7 +54,7 @@ public sealed partial class GhostComponent : Component /// May not reflect actual time of death if this entity has been paused, /// but will give an accurate length of time since death. /// - [DataField, AutoPausedField] + [DataField] public TimeSpan TimeOfDeath = TimeSpan.Zero; /// From 4b9e7352d57374f138fb8cee1186d7a2c6023b6d Mon Sep 17 00:00:00 2001 From: PJBot Date: Tue, 31 Mar 2026 03:42:21 +0000 Subject: [PATCH 026/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 9748cd79d6..62b7a50860 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: GitHubUser53123 - changes: - - message: Zombies that aren't supposed to spread the zombie virus now don't do - so on mobs in critical state. - type: Fix - id: 9087 - time: '2025-10-13T01:24:58.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40857 - author: PotentiallyTom changes: - message: Added a page in the guidebook listing common AI and silicon lawsets. @@ -4030,3 +4022,10 @@ id: 9598 time: '2026-03-31T00:31:21.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43012 +- author: Tayrtahn + changes: + - message: Ghost time of death display is now accurate. + type: Fix + id: 9599 + time: '2026-03-31T03:41:12.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43411 From d42adbf05df1902c0ab3fb90995ee64737df383b Mon Sep 17 00:00:00 2001 From: Moony Date: Wed, 1 Apr 2026 09:06:26 -0700 Subject: [PATCH 027/126] Gametest Part 2: Preliminary refactor every test to use GameTest as the framework. (#43207) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Pass 1. * i'm FREE * Prevent hangups. * okay fine here's an attribute for settings, will polish later and prolly remove the overridable thing. * sigh. * fix singular trigger bug so LatheTest doesn't flake. * Remove SystemAttribute usage. * Poke * I used the shotgun. You know why? Cause the shot gun doesn’t miss, and unlike the shitty hybrid taser it stops a criminal in their tracks in two hits. Bang, bang, and they’re fucking done. I use four shots just to make damn sure. Because, once again, I’m not there to coddle a buncha criminal scum sucking f------, I’m there to 1) Survive the fucking round. 2) Guard the armory. So you can absolutely get fucked. If I get unbanned, which I won’t, you can guarantee I will continue to use the shotgun to apprehend criminals. Because it’s quick, clean and effective as fuck. Why in the seven hells would I fuck around with the disabler shots, which take half a clip just to bring someone down, or with the tazer bolts which are slow as balls, impossible to aim and do about next to jack shit, fuck all. The shotgun is the superior law enforcement weapon. Because it stops crime. And it stops crime by reducing the number of criminals roaming the fucking halls. * Change the faulty store test into two tests, one of which is ignored for failing. --- Content.Benchmarks/ComponentQueryBenchmark.cs | 3 +- Content.Benchmarks/DeltaPressureBenchmark.cs | 3 +- Content.Benchmarks/DestructibleBenchmark.cs | 3 +- .../DeviceNetworkingBenchmark.cs | 3 +- Content.Benchmarks/GasReactionBenchmark.cs | 3 +- Content.Benchmarks/HeatCapacityBenchmark.cs | 5 +- Content.Benchmarks/MapLoadBenchmark.cs | 3 +- Content.Benchmarks/PvsBenchmark.cs | 3 +- Content.Benchmarks/RaiseEventBenchmark.cs | 3 +- .../SpawnEquipDeleteBenchmark.cs | 5 +- .../Tests/Access/AccessReaderTest.cs | 159 +++++++-------- .../Tests/Actions/ActionPvsDetachTest.cs | 17 +- .../Tests/Actions/ActionsAddedTest.cs | 10 +- .../Tests/Administration/Logs/AddTests.cs | 188 +++++++++--------- .../Tests/Administration/Logs/FilterTests.cs | 13 +- .../Tests/Administration/Logs/QueryTests.cs | 14 +- .../Tests/Atmos/AlarmThresholdTest.cs | 6 +- .../Tests/Atmos/ConstantsTest.cs | 6 +- .../Tests/Atmos/GasArrayTest.cs | 6 +- .../Tests/Atmos/GasMixtureTest.cs | 13 +- .../Tests/Atmos/GridJoinTest.cs | 7 +- .../Tests/Atmos/SharedGasSpecificHeatsTest.cs | 2 +- .../Tests/Body/GibbingTest.cs | 7 +- .../Tests/Body/HandOrganTest.cs | 7 +- .../Tests/Buckle/BuckleTest.Interact.cs | 8 +- .../Tests/Buckle/BuckleTest.cs | 14 +- Content.IntegrationTests/Tests/CargoTest.cs | 27 +-- .../Tests/Chemistry/ReagentDataTest.cs | 7 +- .../Tests/Chemistry/SolutionRoundingTest.cs | 7 +- .../Tests/Chemistry/SolutionSystemTests.cs | 23 +-- .../Tests/Chemistry/TryAllReactionsTest.cs | 7 +- .../Tests/Cleanup/EuiManagerTest.cs | 42 ++-- .../Tests/ClickableTest.cs | 9 +- .../Cloning/CloningSettingsPrototypeTest.cs | 7 +- .../Tests/Commands/ForceMapTest.cs | 7 +- .../Tests/Commands/ObjectiveCommandsTest.cs | 12 +- .../Tests/Commands/PardonCommand.cs | 7 +- .../Tests/Commands/RejuvenateTest.cs | 8 +- .../Tests/Commands/RestartRoundTest.cs | 20 +- .../Tests/Commands/SuicideCommandTests.cs | 55 ++--- .../Tests/ConfigPresetTests.cs | 7 +- .../Construction/ConstructionActionValid.cs | 9 +- .../Construction/ConstructionPrototypeTest.cs | 10 +- .../Tests/ContainerOcclusionTest.cs | 15 +- .../Tests/ContrabandTest.cs | 7 +- .../Tests/Damageable/DamageableTest.cs | 6 +- .../Tests/Damageable/MobThresholdsTest.cs | 7 +- .../Tests/Damageable/StaminaComponentTest.cs | 7 +- .../Tests/DeleteInventoryTest.cs | 6 +- .../DestructibleDamageGroupTest.cs | 6 +- .../DestructibleDamageTypeTest.cs | 6 +- .../DestructibleDestructionTest.cs | 6 +- .../DestructibleThresholdActivationTest.cs | 6 +- .../Tests/DeviceLinking/DeviceLinkingTest.cs | 7 +- .../Tests/DeviceNetwork/DeviceNetworkTest.cs | 14 +- .../Tests/Disposal/DisposalUnitTest.cs | 7 +- .../Tests/DoAfter/DoAfterServerTest.cs | 19 +- .../Tests/Doors/AirlockTest.cs | 12 +- .../Tests/DummyIconTest.cs | 6 +- Content.IntegrationTests/Tests/EntityTest.cs | 48 +++-- .../Tests/Explosion/ExplosionPrototypeTest.cs | 7 +- .../Tests/FillLevelSpriteTest.cs | 7 +- .../Tests/Fluids/AbsorbentTest.cs | 13 +- .../Tests/Fluids/FluidSpillTest.cs | 7 +- .../Tests/Fluids/PuddleTest.cs | 12 +- .../Tests/FollowerSystemTest.cs | 6 +- .../Components/ActionBlocking/HandCuffTest.cs | 7 +- .../EntityPrototypeComponentsTest.cs | 11 +- .../Components/Mobs/AlertsComponentTests.cs | 17 +- .../Tests/GameRules/AntagPreferenceTest.cs | 18 +- .../Tests/GameRules/FailAndStartPresetTest.cs | 20 +- .../Tests/GameRules/NukeOpsTest.cs | 21 +- .../Tests/GameRules/RuleMaxTimeRestartTest.cs | 9 +- .../Tests/GameRules/SecretStartsTest.cs | 9 +- .../Tests/GameRules/StartEndGameRulesTest.cs | 17 +- .../Tests/GameRules/TraitorRuleTest.cs | 22 +- .../Tests/Gibbing/GibTest.cs | 7 +- .../Tests/Gravity/WeightlessStatusTests.cs | 7 +- .../Tests/GravityGridTest.cs | 7 +- .../Tests/Guidebook/DocumentParsingTest.cs | 7 +- .../Guidebook/GuideEntryPrototypeTests.cs | 7 +- .../Tests/Hands/HandTests.cs | 23 +-- .../Tests/HumanInventoryUniformSlotsTest.cs | 7 +- .../Humanoid/HideablePrototypeValidation.cs | 12 +- .../Tests/Humanoid/HumanoidProfileTests.cs | 7 +- .../Click/InteractionSystemTests.cs | 18 +- .../Tests/Interaction/InRangeUnobstructed.cs | 7 +- .../Tests/Interaction/InteractionTest.cs | 2 +- .../Tests/Internals/AutoInternalsTests.cs | 11 +- .../Tests/InventoryHelpersTest.cs | 7 +- .../Tests/Lathe/LatheTest.cs | 11 +- .../Tests/Linter/StaticFieldValidationTest.cs | 7 +- .../Tests/Lobby/CharacterCreationTest.cs | 8 +- .../Tests/Lobby/ServerReloginTest.cs | 17 +- .../EntityPrototypeLocalizationTest.cs | 7 +- .../LocalizedDatasetPrototypeTest.cs | 7 +- .../Tests/MachineBoardTest.cs | 15 +- .../Tests/MagazineVisualsSpriteTest.cs | 7 +- .../Tests/Mapping/MappingTests.cs | 9 +- .../Tests/MappingEditorTest.cs | 10 +- .../Tests/Markings/MarkingManagerTests.cs | 23 +-- .../Tests/Markings/MarkingsViewModelTests.cs | 2 +- .../Tests/MaterialArbitrageTest.cs | 6 +- .../Tests/Materials/MaterialTests.cs | 7 +- .../Tests/Minds/GhostRoleTests.cs | 20 +- .../Tests/Minds/GhostTests.cs | 28 ++- .../Minds/MindTest.DeleteAllThenGhost.cs | 10 +- .../Tests/Minds/MindTests.EntityDeletion.cs | 24 +-- .../Tests/Minds/MindTests.Helpers.cs | 18 +- .../Tests/Minds/MindTests.ReconnectTests.cs | 20 +- .../Tests/Minds/MindTests.cs | 44 +--- .../Tests/Minds/RoleTests.cs | 14 +- Content.IntegrationTests/Tests/NPC/NPCTest.cs | 7 +- .../Tests/Networking/NetworkIdsMatchTest.cs | 6 +- .../Tests/Networking/PvsCommandTest.cs | 9 +- .../Tests/Networking/ReconnectTest.cs | 6 +- .../Networking/SimplePredictReconcileTest.cs | 6 +- .../Tests/Physics/AnchorPrototypeTest.cs | 7 +- .../Tests/PostMapInitTest.cs | 68 ++----- .../Tests/Power/PowerStatePrototypeTest.cs | 7 +- .../Tests/Power/PowerStateTest.cs | 15 +- .../Tests/Power/PowerTest.cs | 63 ++---- .../Tests/Power/StationPowerTests.cs | 22 +- .../Tests/Preferences/LoadoutTests.cs | 15 +- .../Tests/Preferences/ServerDbSqliteTests.cs | 20 +- .../Tests/Procedural/DungeonTests.cs | 11 +- .../Tests/PrototypeSaveTest.cs | 6 +- .../Tests/PrototypeTests/PrototypeTests.cs | 15 +- .../PrototypeTests/PrototypeUploadTest.cs | 7 +- .../Tests/Puller/PullerTest.cs | 7 +- .../Tests/Replays/ReplayTests.cs | 18 +- .../Tests/ResearchTest.cs | 11 +- .../Tests/ResettingEntitySystemTests.cs | 20 +- .../Tests/Respirator/LungTest.cs | 11 +- .../Tests/RestartRoundTest.cs | 20 +- .../Tests/Roles/StartingGearStorageTests.cs | 10 +- .../Tests/Round/JobTest.cs | 42 ++-- .../Tests/RoundEndTest.cs | 19 +- Content.IntegrationTests/Tests/SalvageTest.cs | 14 +- .../Tests/SaveLoadMapTest.cs | 11 +- .../Tests/SaveLoadSaveTest.cs | 14 +- .../Tests/Serialization/SerializationTest.cs | 7 +- .../Tests/Shuttle/DockTest.cs | 11 +- Content.IntegrationTests/Tests/ShuttleTest.cs | 6 +- .../Tests/Sprite/ItemSpriteTest.cs | 8 +- Content.IntegrationTests/Tests/StartTest.cs | 7 +- .../Tests/Station/EvacShuttleTest.cs | 12 +- .../Tests/Station/JobTests.cs | 6 +- .../Tests/Station/StationJobsTest.cs | 12 +- .../Tests/Storage/EntityStorageTests.cs | 7 +- .../Tests/Storage/StorageTest.cs | 19 +- Content.IntegrationTests/Tests/StoreTests.cs | 25 ++- Content.IntegrationTests/Tests/Tag/TagTest.cs | 6 +- .../Tests/Tiles/TileStackRecursionTest.cs | 6 +- .../Tests/UserInterface/UiControlTest.cs | 10 +- .../Utility/EntitySystemExtensionsTest.cs | 6 +- .../Tests/Utility/EntityWhitelistTest.cs | 6 +- .../Tests/VendingMachineRestockTest.cs | 21 +- .../Tests/Wires/WireLayoutTest.cs | 9 +- .../WizdenContentFreeze.cs | 7 +- .../Tests/XenoArtifactTest.cs | 33 +-- Content.MapRenderer/Program.cs | 2 +- .../Trigger/Systems/TriggerSystem.cs | 3 + 163 files changed, 1005 insertions(+), 1317 deletions(-) diff --git a/Content.Benchmarks/ComponentQueryBenchmark.cs b/Content.Benchmarks/ComponentQueryBenchmark.cs index bfe367790a..c9aebf7ba3 100644 --- a/Content.Benchmarks/ComponentQueryBenchmark.cs +++ b/Content.Benchmarks/ComponentQueryBenchmark.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.IO; using System.Runtime.CompilerServices; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; @@ -44,7 +45,7 @@ public class ComponentQueryBenchmark ProgramShared.PathOffset = "../../../../"; PoolManager.Startup(typeof(QueryBenchSystem).Assembly); - _pair = PoolManager.GetServerClient().GetAwaiter().GetResult(); + _pair = PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).GetAwaiter().GetResult(); _entMan = _pair.Server.ResolveDependency(); _itemQuery = _entMan.GetEntityQuery(); diff --git a/Content.Benchmarks/DeltaPressureBenchmark.cs b/Content.Benchmarks/DeltaPressureBenchmark.cs index 8d4929c47f..dac79e0376 100644 --- a/Content.Benchmarks/DeltaPressureBenchmark.cs +++ b/Content.Benchmarks/DeltaPressureBenchmark.cs @@ -1,3 +1,4 @@ +using System.IO; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Content.IntegrationTests; @@ -68,7 +69,7 @@ public class DeltaPressureBenchmark { ProgramShared.PathOffset = "../../../../"; PoolManager.Startup(); - _pair = await PoolManager.GetServerClient(); + _pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)); var server = _pair.Server; var mapdata = await _pair.CreateTestMap(); diff --git a/Content.Benchmarks/DestructibleBenchmark.cs b/Content.Benchmarks/DestructibleBenchmark.cs index aa759c35fc..58c834c0ae 100644 --- a/Content.Benchmarks/DestructibleBenchmark.cs +++ b/Content.Benchmarks/DestructibleBenchmark.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Content.IntegrationTests; @@ -69,7 +70,7 @@ public class DestructibleBenchmark { ProgramShared.PathOffset = "../../../../"; PoolManager.Startup(); - _pair = await PoolManager.GetServerClient(); + _pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)); var server = _pair.Server; _entMan = server.ResolveDependency(); diff --git a/Content.Benchmarks/DeviceNetworkingBenchmark.cs b/Content.Benchmarks/DeviceNetworkingBenchmark.cs index bb2a22312e..fcde4feb64 100644 --- a/Content.Benchmarks/DeviceNetworkingBenchmark.cs +++ b/Content.Benchmarks/DeviceNetworkingBenchmark.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Content.IntegrationTests; @@ -60,7 +61,7 @@ public class DeviceNetworkingBenchmark { ProgramShared.PathOffset = "../../../../"; PoolManager.Startup(typeof(DeviceNetworkingBenchmark).Assembly); - _pair = await PoolManager.GetServerClient(); + _pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)); var server = _pair.Server; await server.WaitPost(() => diff --git a/Content.Benchmarks/GasReactionBenchmark.cs b/Content.Benchmarks/GasReactionBenchmark.cs index 9ed30373d1..f298869366 100644 --- a/Content.Benchmarks/GasReactionBenchmark.cs +++ b/Content.Benchmarks/GasReactionBenchmark.cs @@ -1,3 +1,4 @@ +using System.IO; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Content.IntegrationTests; @@ -51,7 +52,7 @@ public class GasReactionBenchmark { ProgramShared.PathOffset = "../../../../"; PoolManager.Startup(); - _pair = await PoolManager.GetServerClient(); + _pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)); var server = _pair.Server; // Create test map and grid diff --git a/Content.Benchmarks/HeatCapacityBenchmark.cs b/Content.Benchmarks/HeatCapacityBenchmark.cs index cef5bc10c7..18366306d7 100644 --- a/Content.Benchmarks/HeatCapacityBenchmark.cs +++ b/Content.Benchmarks/HeatCapacityBenchmark.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.IO; +using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Content.IntegrationTests; using Content.IntegrationTests.Pair; @@ -27,7 +28,7 @@ public class HeatCapacityBenchmark { ProgramShared.PathOffset = "../../../../"; PoolManager.Startup(); - _pair = await PoolManager.GetServerClient(); + _pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)); await _pair.Connect(); _cEntMan = _pair.Client.ResolveDependency(); _sEntMan = _pair.Server.ResolveDependency(); diff --git a/Content.Benchmarks/MapLoadBenchmark.cs b/Content.Benchmarks/MapLoadBenchmark.cs index 261d408aac..9ea95c84b6 100644 --- a/Content.Benchmarks/MapLoadBenchmark.cs +++ b/Content.Benchmarks/MapLoadBenchmark.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; @@ -29,7 +30,7 @@ public class MapLoadBenchmark ProgramShared.PathOffset = "../../../../"; PoolManager.Startup(); - _pair = PoolManager.GetServerClient().GetAwaiter().GetResult(); + _pair = PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).GetAwaiter().GetResult(); var server = _pair.Server; Paths = server.ResolveDependency() diff --git a/Content.Benchmarks/PvsBenchmark.cs b/Content.Benchmarks/PvsBenchmark.cs index 51a013539e..af1ec8d9ef 100644 --- a/Content.Benchmarks/PvsBenchmark.cs +++ b/Content.Benchmarks/PvsBenchmark.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.IO; using System.Linq; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; @@ -50,7 +51,7 @@ public class PvsBenchmark #endif PoolManager.Startup(); - _pair = PoolManager.GetServerClient().GetAwaiter().GetResult(); + _pair = PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).GetAwaiter().GetResult(); _entMan = _pair.Server.ResolveDependency(); _pair.Server.CfgMan.SetCVar(CVars.NetPVS, true); _pair.Server.CfgMan.SetCVar(CVars.ThreadParallelCount, 0); diff --git a/Content.Benchmarks/RaiseEventBenchmark.cs b/Content.Benchmarks/RaiseEventBenchmark.cs index e3d377ccb3..99e032d0b5 100644 --- a/Content.Benchmarks/RaiseEventBenchmark.cs +++ b/Content.Benchmarks/RaiseEventBenchmark.cs @@ -1,4 +1,5 @@ #nullable enable +using System.IO; using System.Runtime.CompilerServices; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; @@ -21,7 +22,7 @@ public class RaiseEventBenchmark { ProgramShared.PathOffset = "../../../../"; PoolManager.Startup(typeof(BenchSystem).Assembly); - _pair = PoolManager.GetServerClient().GetAwaiter().GetResult(); + _pair = PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).GetAwaiter().GetResult(); var entMan = _pair.Server.EntMan; var fact = _pair.Server.ResolveDependency(); var bus = (EntityEventBus)entMan.EventBus; diff --git a/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs b/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs index 9b878eac40..b6d5427480 100644 --- a/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs +++ b/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.IO; +using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Content.IntegrationTests; using Content.IntegrationTests.Pair; @@ -36,7 +37,7 @@ public class SpawnEquipDeleteBenchmark { ProgramShared.PathOffset = "../../../../"; PoolManager.Startup(); - _pair = await PoolManager.GetServerClient(); + _pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)); var server = _pair.Server; var mapData = await _pair.CreateTestMap(); diff --git a/Content.IntegrationTests/Tests/Access/AccessReaderTest.cs b/Content.IntegrationTests/Tests/Access/AccessReaderTest.cs index a0c8c775b1..0279673910 100644 --- a/Content.IntegrationTests/Tests/Access/AccessReaderTest.cs +++ b/Content.IntegrationTests/Tests/Access/AccessReaderTest.cs @@ -1,16 +1,17 @@ +#nullable enable using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; using Content.Shared.Access; using Content.Shared.Access.Components; using Content.Shared.Access.Systems; using Robust.Shared.GameObjects; -using Robust.Shared.Map; using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Access { - [TestFixture] [TestOf(typeof(AccessReaderComponent))] - public sealed class AccessReaderTest + public sealed class AccessReaderTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -21,91 +22,85 @@ namespace Content.IntegrationTests.Tests.Access - type: AccessReader "; + [SidedDependency(Side.Server)] private readonly AccessReaderSystem _system = null!; + [Test] + [RunOnSide(Side.Server)] public async Task TestTags() { - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; - var entityManager = server.ResolveDependency(); + var ent = SSpawn("TestAccessReader"); + var reader = new Entity(ent, SComp(ent)); - await server.WaitAssertion(() => + // test empty + Assert.Multiple(() => { - var system = entityManager.System(); - var ent = entityManager.SpawnEntity("TestAccessReader", MapCoordinates.Nullspace); - var reader = new Entity(ent, entityManager.GetComponent(ent)); - - // test empty - Assert.Multiple(() => - { - Assert.That(system.AreAccessTagsAllowed(new List> { "Foo" }, reader), Is.True); - Assert.That(system.AreAccessTagsAllowed(new List> { "Bar" }, reader), Is.True); - Assert.That(system.AreAccessTagsAllowed(Array.Empty>(), reader), Is.True); - }); - - // test deny - system.AddDenyTag(reader, "A"); - Assert.Multiple(() => - { - Assert.That(system.AreAccessTagsAllowed(new List> { "Foo" }, reader), Is.True); - Assert.That(system.AreAccessTagsAllowed(new List> { "A" }, reader), Is.False); - Assert.That(system.AreAccessTagsAllowed(new List> { "A", "Foo" }, reader), Is.False); - Assert.That(system.AreAccessTagsAllowed(Array.Empty>(), reader), Is.True); - }); - system.ClearDenyTags(reader); - - // test one list - system.TryAddAccess(reader, "A"); - Assert.Multiple(() => - { - Assert.That(system.AreAccessTagsAllowed(new List> { "A" }, reader), Is.True); - Assert.That(system.AreAccessTagsAllowed(new List> { "B" }, reader), Is.False); - Assert.That(system.AreAccessTagsAllowed(new List> { "A", "B" }, reader), Is.True); - Assert.That(system.AreAccessTagsAllowed(Array.Empty>(), reader), Is.False); - }); - system.TryClearAccesses(reader); - - // test one list - two items - system.TryAddAccess(reader, new HashSet> { "A", "B" }); - Assert.Multiple(() => - { - Assert.That(system.AreAccessTagsAllowed(new List> { "A" }, reader), Is.False); - Assert.That(system.AreAccessTagsAllowed(new List> { "B" }, reader), Is.False); - Assert.That(system.AreAccessTagsAllowed(new List> { "A", "B" }, reader), Is.True); - Assert.That(system.AreAccessTagsAllowed(Array.Empty>(), reader), Is.False); - }); - system.TryClearAccesses(reader); - - // test two list - var accesses = new List>>() { - new HashSet> () { "A" }, - new HashSet> () { "B", "C" } - }; - system.TryAddAccesses(reader, accesses); - Assert.Multiple(() => - { - Assert.That(system.AreAccessTagsAllowed(new List> { "A" }, reader), Is.True); - Assert.That(system.AreAccessTagsAllowed(new List> { "B" }, reader), Is.False); - Assert.That(system.AreAccessTagsAllowed(new List> { "A", "B" }, reader), Is.True); - Assert.That(system.AreAccessTagsAllowed(new List> { "C", "B" }, reader), Is.True); - Assert.That(system.AreAccessTagsAllowed(new List> { "C", "B", "A" }, reader), Is.True); - Assert.That(system.AreAccessTagsAllowed(Array.Empty>(), reader), Is.False); - }); - system.TryClearAccesses(reader); - - // test deny list - system.TryAddAccess(reader, new HashSet> { "A" }); - system.AddDenyTag(reader, "B"); - Assert.Multiple(() => - { - Assert.That(system.AreAccessTagsAllowed(new List> { "A" }, reader), Is.True); - Assert.That(system.AreAccessTagsAllowed(new List> { "B" }, reader), Is.False); - Assert.That(system.AreAccessTagsAllowed(new List> { "A", "B" }, reader), Is.False); - Assert.That(system.AreAccessTagsAllowed(Array.Empty>(), reader), Is.False); - }); - system.TryClearAccesses(reader); - system.ClearDenyTags(reader); + Assert.That(_system.AreAccessTagsAllowed(new List> { "Foo" }, reader), Is.True); + Assert.That(_system.AreAccessTagsAllowed(new List> { "Bar" }, reader), Is.True); + Assert.That(_system.AreAccessTagsAllowed(Array.Empty>(), reader), Is.True); }); - await pair.CleanReturnAsync(); + + // test deny + _system.AddDenyTag(reader, "A"); + Assert.Multiple(() => + { + Assert.That(_system.AreAccessTagsAllowed(new List> { "Foo" }, reader), Is.True); + Assert.That(_system.AreAccessTagsAllowed(new List> { "A" }, reader), Is.False); + Assert.That(_system.AreAccessTagsAllowed(new List> { "A", "Foo" }, reader), Is.False); + Assert.That(_system.AreAccessTagsAllowed(Array.Empty>(), reader), Is.True); + }); + _system.ClearDenyTags(reader); + + // test one list + _system.TryAddAccess(reader, "A"); + Assert.Multiple(() => + { + Assert.That(_system.AreAccessTagsAllowed(new List> { "A" }, reader), Is.True); + Assert.That(_system.AreAccessTagsAllowed(new List> { "B" }, reader), Is.False); + Assert.That(_system.AreAccessTagsAllowed(new List> { "A", "B" }, reader), Is.True); + Assert.That(_system.AreAccessTagsAllowed(Array.Empty>(), reader), Is.False); + }); + _system.TryClearAccesses(reader); + + // test one list - two items + _system.TryAddAccess(reader, new HashSet> { "A", "B" }); + Assert.Multiple(() => + { + Assert.That(_system.AreAccessTagsAllowed(new List> { "A" }, reader), Is.False); + Assert.That(_system.AreAccessTagsAllowed(new List> { "B" }, reader), Is.False); + Assert.That(_system.AreAccessTagsAllowed(new List> { "A", "B" }, reader), Is.True); + Assert.That(_system.AreAccessTagsAllowed(Array.Empty>(), reader), Is.False); + }); + _system.TryClearAccesses(reader); + + // test two list + var accesses = new List>>() { + new HashSet> () { "A" }, + new HashSet> () { "B", "C" } + }; + _system.TryAddAccesses(reader, accesses); + Assert.Multiple(() => + { + Assert.That(_system.AreAccessTagsAllowed(new List> { "A" }, reader), Is.True); + Assert.That(_system.AreAccessTagsAllowed(new List> { "B" }, reader), Is.False); + Assert.That(_system.AreAccessTagsAllowed(new List> { "A", "B" }, reader), Is.True); + Assert.That(_system.AreAccessTagsAllowed(new List> { "C", "B" }, reader), Is.True); + Assert.That(_system.AreAccessTagsAllowed(new List> { "C", "B", "A" }, reader), Is.True); + Assert.That(_system.AreAccessTagsAllowed(Array.Empty>(), reader), Is.False); + }); + _system.TryClearAccesses(reader); + + // test deny list + _system.TryAddAccess(reader, new HashSet> { "A" }); + _system.AddDenyTag(reader, "B"); + Assert.Multiple(() => + { + Assert.That(_system.AreAccessTagsAllowed(new List> { "A" }, reader), Is.True); + Assert.That(_system.AreAccessTagsAllowed(new List> { "B" }, reader), Is.False); + Assert.That(_system.AreAccessTagsAllowed(new List> { "A", "B" }, reader), Is.False); + Assert.That(_system.AreAccessTagsAllowed(Array.Empty>(), reader), Is.False); + }); + _system.TryClearAccesses(reader); + _system.ClearDenyTags(reader); } } diff --git a/Content.IntegrationTests/Tests/Actions/ActionPvsDetachTest.cs b/Content.IntegrationTests/Tests/Actions/ActionPvsDetachTest.cs index 45addff00b..9680e48939 100644 --- a/Content.IntegrationTests/Tests/Actions/ActionPvsDetachTest.cs +++ b/Content.IntegrationTests/Tests/Actions/ActionPvsDetachTest.cs @@ -1,4 +1,7 @@ +#nullable enable using System.Linq; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; using Content.Shared.Actions; using Content.Shared.Eye; using Robust.Server.GameObjects; @@ -7,15 +10,18 @@ using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests.Actions; [TestFixture] -public sealed class ActionPvsDetachTest +public sealed class ActionPvsDetachTest : GameTest { + [SidedDependency(Side.Server)] private readonly SharedActionsSystem _sActionsSys = null!; + [SidedDependency(Side.Client)] private readonly SharedActionsSystem _cActionsSys = null!; + [Test] public async Task TestActionDetach() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); - var (server, client) = pair; - var sys = server.System(); - var cSys = client.System(); + var pair = Pair; + var (server, client) = (Server, Client); + var sys = _sActionsSys; + var cSys = _cActionsSys; // Spawn mob that has some actions EntityUid ent = default; @@ -60,6 +66,5 @@ public sealed class ActionPvsDetachTest Assert.That(cSys.GetActions(cEnt).Count(), Is.EqualTo(initActions)); await server.WaitPost(() => server.EntMan.DeleteEntity(map.MapUid)); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Actions/ActionsAddedTest.cs b/Content.IntegrationTests/Tests/Actions/ActionsAddedTest.cs index 0dbec0c83a..1fa005496e 100644 --- a/Content.IntegrationTests/Tests/Actions/ActionsAddedTest.cs +++ b/Content.IntegrationTests/Tests/Actions/ActionsAddedTest.cs @@ -1,4 +1,6 @@ +#nullable enable using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Actions; using Content.Shared.Actions.Components; using Content.Shared.CombatMode; @@ -11,15 +13,17 @@ namespace Content.IntegrationTests.Tests.Actions; /// This tests checks that actions properly get added to an entity's actions component.. /// [TestFixture] -public sealed class ActionsAddedTest +public sealed class ActionsAddedTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings { Connected = true, DummyTicker = false }; + // TODO add magboot test (inventory action) // TODO add ghost toggle-fov test (client-side action) [Test] public async Task TestCombatActionsAdded() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true, DummyTicker = false }); + var pair = Pair; var server = pair.Server; var client = pair.Client; var sEntMan = server.ResolveDependency(); @@ -67,7 +71,5 @@ public sealed class ActionsAddedTest // required, because integration tests do not respect the [NonSerialized] attribute and will simply events by reference. Assert.That(ReferenceEquals(sAct.Comp, cAct.Comp), Is.False); Assert.That(ReferenceEquals(sQuery.GetComponent(sAct).Event, cQuery.GetComponent(cAct).Event), Is.False); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs b/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs index 772af337a1..71cb0fd7ed 100644 --- a/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs +++ b/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +#nullable enable +using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Administration.Logs; using Content.Server.Database; using Content.Server.GameTicking; @@ -12,9 +14,9 @@ namespace Content.IntegrationTests.Tests.Administration.Logs; [TestFixture] [TestOf(typeof(AdminLogSystem))] -public sealed class AddTests +public sealed class AddTests : GameTest { - public static PoolSettings LogTestSettings = new() + public override PoolSettings PoolSettings => new() { AdminLogsEnabled = true, DummyTicker = false, @@ -24,7 +26,7 @@ public sealed class AddTests [Test] public async Task AddAndGetSingleLog() { - await using var pair = await PoolManager.GetServerClient(LogTestSettings); + var pair = Pair; var server = pair.Server; var sEntities = server.ResolveDependency(); @@ -33,7 +35,7 @@ public sealed class AddTests var guid = Guid.NewGuid(); await pair.CreateTestMap(); - var coordinates = pair.TestMap.GridCoords; + var coordinates = pair.TestMap!.GridCoords; await server.WaitPost(() => { var entity = sEntities.SpawnEntity(null, coordinates); @@ -62,14 +64,12 @@ public sealed class AddTests return false; }); - - await pair.CleanReturnAsync(); } [Test] public async Task AddAndGetUnformattedLog() { - await using var pair = await PoolManager.GetServerClient(LogTestSettings); + var pair = Pair; var server = pair.Server; var sDatabase = server.ResolveDependency(); @@ -127,15 +127,13 @@ public sealed class AddTests json.Dispose(); } - - await pair.CleanReturnAsync(); } [Test] [TestCase(500)] public async Task BulkAddLogs(int amount) { - await using var pair = await PoolManager.GetServerClient(LogTestSettings); + var pair = Pair; var server = pair.Server; var sEntities = server.ResolveDependency(); @@ -158,14 +156,12 @@ public sealed class AddTests var messages = await sAdminLogSystem.CurrentRoundLogs(); return messages.Count >= amount; }); - - await pair.CleanReturnAsync(); } [Test] public async Task AddPlayerSessionLog() { - await using var pair = await PoolManager.GetServerClient(LogTestSettings); + var pair = Pair; var server = pair.Server; var sPlayers = server.ResolveDependency(); @@ -195,20 +191,91 @@ public sealed class AddTests Assert.That(logs.First().Players, Does.Contain(playerGuid)); return true; }); - await pair.CleanReturnAsync(); } + [Test] + public async Task DuplicatePlayerDoesNotThrowTest() + { + var pair = Pair; + var server = pair.Server; + + var sPlayers = server.ResolveDependency(); + var sAdminLogSystem = server.ResolveDependency(); + + var guid = Guid.NewGuid(); + + await server.WaitPost(() => + { + var player = sPlayers.Sessions.Single(); + + sAdminLogSystem.Add(LogType.Unknown, $"{player} {player} test log: {guid}"); + }); + + await PoolManager.WaitUntil(server, async () => + { + var logs = await sAdminLogSystem.CurrentRoundLogs(new LogFilter + { + Search = guid.ToString() + }); + + if (logs.Count == 0) + { + return false; + } + + return true; + }); + } + + [Test] + public async Task DuplicatePlayerIdDoesNotThrowTest() + { + var pair = Pair; + var server = pair.Server; + + var sPlayers = server.ResolveDependency(); + + var sAdminLogSystem = server.ResolveDependency(); + + var guid = Guid.NewGuid(); + + await server.WaitPost(() => + { + var player = sPlayers.Sessions.Single(); + + sAdminLogSystem.Add(LogType.Unknown, $"{player:first} {player:second} test log: {guid}"); + }); + + await PoolManager.WaitUntil(server, async () => + { + var logs = await sAdminLogSystem.CurrentRoundLogs(new LogFilter + { + Search = guid.ToString() + }); + + if (logs.Count == 0) + { + return false; + } + + return true; + }); + } +} + +public sealed class PreRoundAddTests : GameTest +{ + public override PoolSettings PoolSettings => new PoolSettings + { + Dirty = true, + InLobby = true, + AdminLogsEnabled = true + }; + [Test] public async Task PreRoundAddAndGetSingle() { - var setting = new PoolSettings - { - Dirty = true, - InLobby = true, - AdminLogsEnabled = true - }; - - await using var pair = await PoolManager.GetServerClient(setting); + var pair = Pair; var server = pair.Server; var sDatabase = server.ResolveDependency(); @@ -262,81 +329,6 @@ public sealed class AddTests json.Dispose(); } - await pair.CleanReturnAsync(); } - [Test] - public async Task DuplicatePlayerDoesNotThrowTest() - { - await using var pair = await PoolManager.GetServerClient(LogTestSettings); - var server = pair.Server; - - var sPlayers = server.ResolveDependency(); - var sAdminLogSystem = server.ResolveDependency(); - - var guid = Guid.NewGuid(); - - await server.WaitPost(() => - { - var player = sPlayers.Sessions.Single(); - - sAdminLogSystem.Add(LogType.Unknown, $"{player} {player} test log: {guid}"); - }); - - await PoolManager.WaitUntil(server, async () => - { - var logs = await sAdminLogSystem.CurrentRoundLogs(new LogFilter - { - Search = guid.ToString() - }); - - if (logs.Count == 0) - { - return false; - } - - return true; - }); - - await pair.CleanReturnAsync(); - Assert.Pass(); - } - - [Test] - public async Task DuplicatePlayerIdDoesNotThrowTest() - { - await using var pair = await PoolManager.GetServerClient(LogTestSettings); - var server = pair.Server; - - var sPlayers = server.ResolveDependency(); - - var sAdminLogSystem = server.ResolveDependency(); - - var guid = Guid.NewGuid(); - - await server.WaitPost(() => - { - var player = sPlayers.Sessions.Single(); - - sAdminLogSystem.Add(LogType.Unknown, $"{player:first} {player:second} test log: {guid}"); - }); - - await PoolManager.WaitUntil(server, async () => - { - var logs = await sAdminLogSystem.CurrentRoundLogs(new LogFilter - { - Search = guid.ToString() - }); - - if (logs.Count == 0) - { - return false; - } - - return true; - }); - - await pair.CleanReturnAsync(); - Assert.Pass(); - } } diff --git a/Content.IntegrationTests/Tests/Administration/Logs/FilterTests.cs b/Content.IntegrationTests/Tests/Administration/Logs/FilterTests.cs index 6f907f425e..2517defe96 100644 --- a/Content.IntegrationTests/Tests/Administration/Logs/FilterTests.cs +++ b/Content.IntegrationTests/Tests/Administration/Logs/FilterTests.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.Administration.Logs; using Content.Shared.Administration.Logs; using Content.Shared.Database; @@ -7,14 +8,21 @@ namespace Content.IntegrationTests.Tests.Administration.Logs; [TestFixture] [TestOf(typeof(AdminLogSystem))] -public sealed class FilterTests +public sealed class FilterTests : GameTest { + public override PoolSettings PoolSettings => new() + { + AdminLogsEnabled = true, + DummyTicker = false, + Connected = true + }; + [Test] [TestCase(DateOrder.Ascending)] [TestCase(DateOrder.Descending)] public async Task Date(DateOrder order) { - await using var pair = await PoolManager.GetServerClient(AddTests.LogTestSettings); + var pair = Pair; var server = pair.Server; var sEntities = server.ResolveDependency(); @@ -96,6 +104,5 @@ public sealed class FilterTests return firstFound && secondFound; }); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Administration/Logs/QueryTests.cs b/Content.IntegrationTests/Tests/Administration/Logs/QueryTests.cs index 5a58757d53..55b36ebae1 100644 --- a/Content.IntegrationTests/Tests/Administration/Logs/QueryTests.cs +++ b/Content.IntegrationTests/Tests/Administration/Logs/QueryTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Administration.Logs; using Content.Server.GameTicking; using Content.Shared.Database; @@ -11,12 +12,19 @@ namespace Content.IntegrationTests.Tests.Administration.Logs; [TestFixture] [TestOf(typeof(AdminLogSystem))] -public sealed class QueryTests +public sealed class QueryTests : GameTest { + public override PoolSettings PoolSettings => new() + { + AdminLogsEnabled = true, + DummyTicker = false, + Connected = true + }; + [Test] public async Task QuerySingleLog() { - await using var pair = await PoolManager.GetServerClient(AddTests.LogTestSettings); + var pair = Pair; var server = pair.Server; var sSystems = server.ResolveDependency(); @@ -55,7 +63,5 @@ public sealed class QueryTests return false; }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Atmos/AlarmThresholdTest.cs b/Content.IntegrationTests/Tests/Atmos/AlarmThresholdTest.cs index b74c35ba11..2eaa073819 100644 --- a/Content.IntegrationTests/Tests/Atmos/AlarmThresholdTest.cs +++ b/Content.IntegrationTests/Tests/Atmos/AlarmThresholdTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Atmos.Monitor; using Robust.Shared.Prototypes; @@ -5,7 +6,7 @@ namespace Content.IntegrationTests.Tests.Atmos { [TestFixture] [TestOf(typeof(AtmosAlarmThreshold))] - public sealed class AlarmThresholdTest + public sealed class AlarmThresholdTest : GameTest { private const string AlarmThresholdTestDummyId = "AlarmThresholdTestDummy"; @@ -26,7 +27,7 @@ namespace Content.IntegrationTests.Tests.Atmos [Test] public async Task TestAlarmThreshold() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var prototypeManager = server.ResolveDependency(); @@ -136,7 +137,6 @@ namespace Content.IntegrationTests.Tests.Atmos Assert.That(alarmType, Is.EqualTo(AtmosAlarmType.Normal)); } }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Atmos/ConstantsTest.cs b/Content.IntegrationTests/Tests/Atmos/ConstantsTest.cs index 4112acac50..d44bfe7ae4 100644 --- a/Content.IntegrationTests/Tests/Atmos/ConstantsTest.cs +++ b/Content.IntegrationTests/Tests/Atmos/ConstantsTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Atmos.EntitySystems; using Content.Shared.Atmos; using Content.Shared.Atmos.Prototypes; @@ -6,12 +7,12 @@ using Content.Shared.Atmos.Prototypes; namespace Content.IntegrationTests.Tests.Atmos; [TestOf(typeof(Atmospherics))] -public sealed class ConstantsTest +public sealed class ConstantsTest : GameTest { [Test] public async Task TotalGasesTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entityManager = server.EntMan; var protoManager = server.ProtoMan; @@ -42,7 +43,6 @@ public sealed class ConstantsTest } }); }); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs b/Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs index 07caf447bd..eda9061281 100644 --- a/Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs +++ b/Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; using Robust.Shared.GameObjects; @@ -8,7 +9,7 @@ namespace Content.IntegrationTests.Tests.Atmos; [TestFixture] [TestOf(typeof(Atmospherics))] -public sealed class GasArrayTest +public sealed class GasArrayTest : GameTest { private const string GasTankTestDummyId = "GasTankTestDummy"; @@ -42,7 +43,7 @@ public sealed class GasArrayTest [Test] public async Task TestGasArrayDeserialization() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var compFactory = server.ResolveDependency(); @@ -80,6 +81,5 @@ public sealed class GasArrayTest } }); }); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Atmos/GasMixtureTest.cs b/Content.IntegrationTests/Tests/Atmos/GasMixtureTest.cs index 1cb8fd8b6f..70bbd0ac0c 100644 --- a/Content.IntegrationTests/Tests/Atmos/GasMixtureTest.cs +++ b/Content.IntegrationTests/Tests/Atmos/GasMixtureTest.cs @@ -1,4 +1,5 @@ -using Content.Server.Atmos; +using Content.IntegrationTests.Fixtures; +using Content.Server.Atmos; using Content.Server.Atmos.EntitySystems; using Content.Shared.Atmos; using Robust.Shared.GameObjects; @@ -7,12 +8,12 @@ namespace Content.IntegrationTests.Tests.Atmos { [TestFixture] [TestOf(typeof(GasMixture))] - public sealed class GasMixtureTest + public sealed class GasMixtureTest : GameTest { [Test] public async Task TestMerge() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var atmosphereSystem = server.ResolveDependency().GetEntitySystem(); @@ -56,8 +57,6 @@ namespace Content.IntegrationTests.Tests.Atmos Assert.That(a.GetMoles(Gas.Oxygen), Is.EqualTo(50)); }); }); - - await pair.CleanReturnAsync(); } [Test] @@ -69,7 +68,7 @@ namespace Content.IntegrationTests.Tests.Atmos [TestCase(Atmospherics.BreathPercentage)] public async Task RemoveRatio(float ratio) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitAssertion(() => @@ -103,8 +102,6 @@ namespace Content.IntegrationTests.Tests.Atmos Assert.That(a.GetMoles(Gas.Nitrogen), Is.EqualTo(100 - b.GetMoles(Gas.Nitrogen))); }); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Atmos/GridJoinTest.cs b/Content.IntegrationTests/Tests/Atmos/GridJoinTest.cs index 45ccddfad9..9b43e85396 100644 --- a/Content.IntegrationTests/Tests/Atmos/GridJoinTest.cs +++ b/Content.IntegrationTests/Tests/Atmos/GridJoinTest.cs @@ -1,4 +1,5 @@ using Content.Server.Atmos.EntitySystems; +using Content.IntegrationTests.Fixtures; using Content.Server.Atmos.Piping.EntitySystems; using Content.Shared.Atmos.Components; using Robust.Shared.GameObjects; @@ -6,14 +7,14 @@ using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests.Atmos; [TestFixture] -public sealed class GridJoinTest +public sealed class GridJoinTest : GameTest { private const string CanisterProtoId = "AirCanister"; [Test] public async Task TestGridJoinAtmosphere() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.EntMan; @@ -46,7 +47,5 @@ public sealed class GridJoinTest // Make sure that the canister is now properly tracked as on-grid Assert.That(atmosDeviceSystem.IsJoinedOffGrid(canisterEnt), Is.False); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs b/Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs index 406f7fa10d..7269b1b473 100644 --- a/Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs +++ b/Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs @@ -37,7 +37,7 @@ public sealed class SharedGasSpecificHeatsTest { Connected = true, }; - _pair = await PoolManager.GetServerClient(poolSettings); + _pair = await PoolManager.GetServerClient(poolSettings, new NUnitTestContextWrap(TestContext.CurrentContext, TestContext.Out)); _sEntMan = Server.ResolveDependency(); _cEntMan = Client.ResolveDependency(); diff --git a/Content.IntegrationTests/Tests/Body/GibbingTest.cs b/Content.IntegrationTests/Tests/Body/GibbingTest.cs index a727487940..a3f3c1bcc3 100644 --- a/Content.IntegrationTests/Tests/Body/GibbingTest.cs +++ b/Content.IntegrationTests/Tests/Body/GibbingTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Body; using Content.Shared.Gibbing; using Robust.Shared.GameObjects; @@ -6,7 +7,7 @@ namespace Content.IntegrationTests.Tests.Body; [TestFixture] [TestOf(typeof(GibbableOrganSystem))] -public sealed class GibletTest +public sealed class GibletTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -33,7 +34,7 @@ public sealed class GibletTest [Test] public async Task GibletCountTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -54,7 +55,5 @@ public sealed class GibletTest Assert.That(entityManager.HasComponent(giblet), Is.True); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Body/HandOrganTest.cs b/Content.IntegrationTests/Tests/Body/HandOrganTest.cs index 560dfbf64a..3d885186f1 100644 --- a/Content.IntegrationTests/Tests/Body/HandOrganTest.cs +++ b/Content.IntegrationTests/Tests/Body/HandOrganTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Body; using Content.Shared.Hands.Components; using Robust.Shared.Containers; @@ -9,7 +10,7 @@ namespace Content.IntegrationTests.Tests.Body; [TestFixture] [TestOf(typeof(HandOrganSystem))] -public sealed class HandOrganTest +public sealed class HandOrganTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -46,7 +47,7 @@ public sealed class HandOrganTest [Test] public async Task HandInsertionAndRemovalTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -81,7 +82,5 @@ public sealed class HandOrganTest Assert.That(hands.Count, Is.EqualTo(expectedCount)); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleTest.Interact.cs b/Content.IntegrationTests/Tests/Buckle/BuckleTest.Interact.cs index d9cce764ab..444bd159f3 100644 --- a/Content.IntegrationTests/Tests/Buckle/BuckleTest.Interact.cs +++ b/Content.IntegrationTests/Tests/Buckle/BuckleTest.Interact.cs @@ -12,7 +12,7 @@ public sealed partial class BuckleTest [Test] public async Task BuckleInteractUnbuckleOther() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -55,14 +55,12 @@ public sealed partial class BuckleTest Assert.That(strap.BuckledEntities, Does.Not.Contain(victim)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task BuckleInteractBuckleUnbuckleSelf() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -102,7 +100,5 @@ public sealed partial class BuckleTest Assert.That(strap.BuckledEntities, Does.Not.Contain(user)); }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs index b42f42922a..9c12da3fe8 100644 --- a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs +++ b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Shared.Buckle; using Content.Shared.ActionBlocker; using Content.Shared.Buckle.Components; @@ -12,7 +13,7 @@ namespace Content.IntegrationTests.Tests.Buckle [TestFixture] [TestOf(typeof(BuckleComponent))] [TestOf(typeof(StrapComponent))] - public sealed partial class BuckleTest + public sealed partial class BuckleTest : GameTest { private const string BuckleDummyId = "BuckleDummy"; private const string StrapDummyId = "StrapDummy"; @@ -50,7 +51,7 @@ namespace Content.IntegrationTests.Tests.Buckle [Test] public async Task BuckleUnbuckleCooldownRangeTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -228,14 +229,12 @@ namespace Content.IntegrationTests.Tests.Buckle Assert.That(strap.BuckledEntities, Is.Empty); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task BuckledDyingDropItemsTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -298,14 +297,12 @@ namespace Content.IntegrationTests.Tests.Buckle buckleSystem.Unbuckle(human, human); Assert.That(buckle.Buckled, Is.False); }); - - await pair.CleanReturnAsync(); } [Test] public async Task ForceUnbuckleBuckleTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -373,7 +370,6 @@ namespace Content.IntegrationTests.Tests.Buckle Assert.That(buckle.Buckled); }); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/CargoTest.cs b/Content.IntegrationTests/Tests/CargoTest.cs index df85e61550..0c8fc2f0fb 100644 --- a/Content.IntegrationTests/Tests/CargoTest.cs +++ b/Content.IntegrationTests/Tests/CargoTest.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Server.Cargo.Components; using Content.Server.Cargo.Systems; using Content.Server.Nutrition.Components; @@ -17,7 +18,7 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests; [TestFixture] -public sealed class CargoTest +public sealed class CargoTest : GameTest { private static readonly HashSet> Ignored = [ @@ -28,7 +29,7 @@ public sealed class CargoTest [Test] public async Task NoCargoOrderArbitrage() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -54,13 +55,11 @@ public sealed class CargoTest } }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task NoCargoBountyArbitrageTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -94,14 +93,12 @@ public sealed class CargoTest mapSystem.DeleteMap(mapId); }); - - await pair.CleanReturnAsync(); } [Test] public async Task NoStaticPriceAndStackPrice() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoManager = server.ProtoMan; @@ -133,8 +130,6 @@ public sealed class CargoTest } } }); - - await pair.CleanReturnAsync(); } /// @@ -144,7 +139,7 @@ public sealed class CargoTest [Test] public async Task NoSliceableBountyArbitrageTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -209,8 +204,6 @@ public sealed class CargoTest } mapSystem.DeleteMap(mapId); }); - - await pair.CleanReturnAsync(); } [TestPrototypes] @@ -233,7 +226,7 @@ public sealed class CargoTest [Test] public async Task StackPrice() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); @@ -245,14 +238,12 @@ public sealed class CargoTest var price = priceSystem.GetPrice(ent); Assert.That(price, Is.EqualTo(100.0)); }); - - await pair.CleanReturnAsync(); } [Test] public async Task MobPrice() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var componentFactory = pair.Server.ResolveDependency(); @@ -266,7 +257,5 @@ public sealed class CargoTest } }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Chemistry/ReagentDataTest.cs b/Content.IntegrationTests/Tests/Chemistry/ReagentDataTest.cs index 59948c8b17..9ac45549a2 100644 --- a/Content.IntegrationTests/Tests/Chemistry/ReagentDataTest.cs +++ b/Content.IntegrationTests/Tests/Chemistry/ReagentDataTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.IntegrationTests.Tests.Interaction; using Content.Shared.Chemistry.Reagent; using Robust.Shared.Reflection; @@ -8,12 +9,12 @@ namespace Content.IntegrationTests.Tests.Chemistry; [TestFixture] [TestOf(typeof(ReagentData))] -public sealed class ReagentDataTest +public sealed class ReagentDataTest : GameTest { [Test] public async Task ReagentDataIsSerializable() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var reflection = pair.Server.ResolveDependency(); Assert.Multiple(() => @@ -24,7 +25,5 @@ public sealed class ReagentDataTest Assert.That(instance.HasCustomAttribute(), $"{instance} must have the serializable attribute."); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Chemistry/SolutionRoundingTest.cs b/Content.IntegrationTests/Tests/Chemistry/SolutionRoundingTest.cs index 5b5829d386..f188fd0f66 100644 --- a/Content.IntegrationTests/Tests/Chemistry/SolutionRoundingTest.cs +++ b/Content.IntegrationTests/Tests/Chemistry/SolutionRoundingTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Reaction; @@ -9,7 +10,7 @@ namespace Content.IntegrationTests.Tests.Chemistry; [TestFixture] [TestOf(typeof(ChemicalReactionSystem))] -public sealed class SolutionRoundingTest +public sealed class SolutionRoundingTest : GameTest { // This test tests two things: // * A rounding error in reaction code while I was making chloral hydrate @@ -72,7 +73,7 @@ public sealed class SolutionRoundingTest [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -121,7 +122,5 @@ public sealed class SolutionRoundingTest Is.EqualTo((FixedPoint2) 30)); }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs b/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs index 6f50f54103..ffaca012a3 100644 --- a/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs +++ b/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.FixedPoint; @@ -12,7 +13,7 @@ namespace Content.IntegrationTests.Tests.Chemistry; // reactions can change this assumption [TestFixture] [TestOf(typeof(SharedSolutionContainerSystem))] -public sealed class SolutionSystemTests +public sealed class SolutionSystemTests : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -53,7 +54,7 @@ public sealed class SolutionSystemTests [Test] public async Task TryAddTwoNonReactiveReagent() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entityManager = server.ResolveDependency(); @@ -88,8 +89,6 @@ public sealed class SolutionSystemTests Assert.That(oil, Is.EqualTo(oilQuantity)); }); }); - - await pair.CleanReturnAsync(); } // This test mimics current behavior @@ -97,7 +96,7 @@ public sealed class SolutionSystemTests [Test] public async Task TryAddTooMuchNonReactiveReagent() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -133,15 +132,13 @@ public sealed class SolutionSystemTests Assert.That(oil, Is.EqualTo(FixedPoint2.Zero)); }); }); - - await pair.CleanReturnAsync(); } // Unlike TryAddSolution this adds and two solution without then splits leaving only threshold in original [Test] public async Task TryMixAndOverflowTooMuchReagent() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; @@ -188,15 +185,13 @@ public sealed class SolutionSystemTests Assert.That(oilOverFlow, Is.EqualTo(oilQuantity - oilMix)); }); }); - - await pair.CleanReturnAsync(); } // TryMixAndOverflow will fail if Threshold larger than MaxVolume [Test] public async Task TryMixAndOverflowTooBigOverflow() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entityManager = server.ResolveDependency(); @@ -226,14 +221,12 @@ public sealed class SolutionSystemTests .TryMixAndOverflow(solutionEnt.Value, oilAdded, threshold, out _), Is.False); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestTemperatureCalculations() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ResolveDependency(); const float temp = 100.0f; @@ -264,7 +257,5 @@ public sealed class SolutionSystemTests solutionOne.AddSolution(solutionTwo, protoMan); Assert.That(solutionOne.GetHeatCapacity(protoMan) * solutionOne.Temperature, Is.EqualTo(thermalEnergyOne + thermalEnergyTwo)); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs b/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs index 0037670556..e720387425 100644 --- a/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs +++ b/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs @@ -5,6 +5,7 @@ using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Utility; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.IntegrationTests.Utility; using Content.Shared.Chemistry.EntitySystems; @@ -12,7 +13,7 @@ namespace Content.IntegrationTests.Tests.Chemistry { [TestFixture] [TestOf(typeof(ReactionPrototype))] - public sealed class TryAllReactionsTest + public sealed class TryAllReactionsTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -33,7 +34,7 @@ namespace Content.IntegrationTests.Tests.Chemistry [Description("Tries an individual reaction to see if it succeeds.")] public async Task TryReaction(string reaction) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entityManager = server.ResolveDependency(); @@ -134,8 +135,6 @@ namespace Content.IntegrationTests.Tests.Chemistry server.EntMan.DeleteEntity(beaker); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Cleanup/EuiManagerTest.cs b/Content.IntegrationTests/Tests/Cleanup/EuiManagerTest.cs index e2bff03501..037224fec5 100644 --- a/Content.IntegrationTests/Tests/Cleanup/EuiManagerTest.cs +++ b/Content.IntegrationTests/Tests/Cleanup/EuiManagerTest.cs @@ -1,35 +1,37 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Administration.UI; using Content.Server.EUI; using Robust.Server.Player; namespace Content.IntegrationTests.Tests.Cleanup; -public sealed class EuiManagerTest +public sealed class EuiManagerTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings + { + Connected = true, + Dirty = true + }; + [Test] + [Retry(2)] + // Even though we are using the server EUI here, we actually want to see if the client EUIManager crashes public async Task EuiManagerRecycleWithOpenWindowTest() { - // Even though we are using the server EUI here, we actually want to see if the client EUIManager crashes - for (var i = 0; i < 2; i++) + var pair = Pair; + var server = pair.Server; + + var sPlayerManager = server.ResolveDependency(); + var eui = server.ResolveDependency(); + + await server.WaitAssertion(() => { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - Dirty = true - }); - var server = pair.Server; + var clientSession = sPlayerManager.Sessions.Single(); + var ui = new AdminAnnounceEui(); + eui.OpenEui(ui, clientSession); + }); - var sPlayerManager = server.ResolveDependency(); - var eui = server.ResolveDependency(); - - await server.WaitAssertion(() => - { - var clientSession = sPlayerManager.Sessions.Single(); - var ui = new AdminAnnounceEui(); - eui.OpenEui(ui, clientSession); - }); - await pair.CleanReturnAsync(); - } + await RunUntilSynced(); } } diff --git a/Content.IntegrationTests/Tests/ClickableTest.cs b/Content.IntegrationTests/Tests/ClickableTest.cs index aaac421ed1..fb20cd9038 100644 --- a/Content.IntegrationTests/Tests/ClickableTest.cs +++ b/Content.IntegrationTests/Tests/ClickableTest.cs @@ -1,5 +1,6 @@ using System.Numerics; using Content.Client.Clickable; +using Content.IntegrationTests.Fixtures; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Shared.GameObjects; @@ -7,7 +8,7 @@ using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class ClickableTest + public sealed class ClickableTest : GameTest { private const double DirSouth = 0; private const double DirNorth = Math.PI; @@ -44,7 +45,7 @@ namespace Content.IntegrationTests.Tests [TestCase("ClickTestRotatingCornerInvisibleNoRot", 0.25f, 0.25f, DirSouthEastJustShy, 1, ExpectedResult = true)] public async Task Test(string prototype, float clickPosX, float clickPosY, double angle, float scale) { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -66,7 +67,7 @@ namespace Content.IntegrationTests.Tests }); // Let client sync up. - await pair.RunTicksSync(5); + await RunUntilSynced(); var hit = false; var clientEnt = clientEntManager.GetEntity(serverEntManager.GetNetEntity(serverEnt)); @@ -89,8 +90,6 @@ namespace Content.IntegrationTests.Tests serverEntManager.DeleteEntity(serverEnt); }); - await pair.CleanReturnAsync(); - return hit; } } diff --git a/Content.IntegrationTests/Tests/Cloning/CloningSettingsPrototypeTest.cs b/Content.IntegrationTests/Tests/Cloning/CloningSettingsPrototypeTest.cs index 9edc115eee..c21bcf922a 100644 --- a/Content.IntegrationTests/Tests/Cloning/CloningSettingsPrototypeTest.cs +++ b/Content.IntegrationTests/Tests/Cloning/CloningSettingsPrototypeTest.cs @@ -1,8 +1,9 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Cloning; namespace Content.IntegrationTests.Tests.Cloning; -public sealed class CloningSettingsPrototypeTest +public sealed class CloningSettingsPrototypeTest : GameTest { /// /// Checks that the components named in every are valid components known to the server. @@ -12,7 +13,7 @@ public sealed class CloningSettingsPrototypeTest [Test] public async Task ValidatePrototypes() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ProtoMan; var compFactory = server.EntMan.ComponentFactory; @@ -40,7 +41,5 @@ public sealed class CloningSettingsPrototypeTest } }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Commands/ForceMapTest.cs b/Content.IntegrationTests/Tests/Commands/ForceMapTest.cs index 3fa7e64f1a..566edd47ca 100644 --- a/Content.IntegrationTests/Tests/Commands/ForceMapTest.cs +++ b/Content.IntegrationTests/Tests/Commands/ForceMapTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.Maps; using Content.Shared.CCVar; using Robust.Shared.Configuration; @@ -6,7 +7,7 @@ using Robust.Shared.Console; namespace Content.IntegrationTests.Tests.Commands; [TestFixture] -public sealed class ForceMapTest +public sealed class ForceMapTest : GameTest { private const string DefaultMapName = "Empty"; private const string BadMapName = "asdf_asd-fa__sdfAsd_f"; // Hopefully no one ever names a map this... @@ -44,7 +45,7 @@ public sealed class ForceMapTest [Test] public async Task TestForceMapCommand() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.EntMan; @@ -82,7 +83,5 @@ public sealed class ForceMapTest // Cleanup configManager.SetCVar(CCVars.GameMap, DefaultMapName); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs b/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs index d430325e31..eb18d8e1ae 100644 --- a/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs +++ b/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs @@ -1,5 +1,6 @@ #nullable enable using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Objectives; using Content.Shared.Mind; using Robust.Shared.GameObjects; @@ -7,7 +8,7 @@ using Robust.Shared.Player; namespace Content.IntegrationTests.Tests.Commands; -public sealed class ObjectiveCommandsTest +public sealed class ObjectiveCommandsTest : GameTest { private const string ObjectiveProtoId = "MindCommandsTestObjective"; @@ -27,6 +28,11 @@ public sealed class ObjectiveCommandsTest - type: DieCondition """; + public override PoolSettings PoolSettings => new () + { + Connected = false + }; + /// /// Creates a dummy session, and assigns it a mind, then /// tests using addobjective, lsobjectives, @@ -35,7 +41,7 @@ public sealed class ObjectiveCommandsTest [Test] public async Task AddListRemoveObjectiveTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.EntMan; var playerMan = server.ResolveDependency(); @@ -66,7 +72,5 @@ public sealed class ObjectiveCommandsTest await pair.WaitCommand($"rmobjective {playerSession.Name} 0"); Assert.That(mindComp.Objectives, Is.Empty, "rmobjective failed to remove objective"); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Commands/PardonCommand.cs b/Content.IntegrationTests/Tests/Commands/PardonCommand.cs index 5f77af1b10..2d84eebf96 100644 --- a/Content.IntegrationTests/Tests/Commands/PardonCommand.cs +++ b/Content.IntegrationTests/Tests/Commands/PardonCommand.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Database; using Robust.Server.Console; using Robust.Server.Player; @@ -8,14 +9,14 @@ namespace Content.IntegrationTests.Tests.Commands { [TestFixture] [TestOf(typeof(PardonCommand))] - public sealed class PardonCommand + public sealed class PardonCommand : GameTest { private static readonly TimeSpan MarginOfError = TimeSpan.FromMinutes(1); [Test] public async Task PardonTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -148,8 +149,6 @@ namespace Content.IntegrationTests.Tests.Commands await client.WaitPost(() => netMan.ClientConnect(null!, 0, null!)); await pair.RunTicksSync(5); Assert.That(sPlayerManager.Sessions, Has.Length.EqualTo(1)); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs b/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs index 308f2797c4..15d93c40ae 100644 --- a/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs +++ b/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs @@ -1,4 +1,5 @@ -using Content.Shared.Administration.Systems; +using Content.IntegrationTests.Fixtures; +using Content.Shared.Administration.Systems; using Content.Shared.Damage; using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; @@ -14,7 +15,7 @@ namespace Content.IntegrationTests.Tests.Commands { [TestFixture] [TestOf(typeof(RejuvenateSystem))] - public sealed class RejuvenateTest + public sealed class RejuvenateTest : GameTest { private static readonly ProtoId TestDamageGroup = "Toxin"; @@ -36,7 +37,7 @@ namespace Content.IntegrationTests.Tests.Commands [Test] public async Task RejuvenateDeadTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); var prototypeManager = server.ResolveDependency(); @@ -92,7 +93,6 @@ namespace Content.IntegrationTests.Tests.Commands Assert.That(damSystem.GetTotalDamage((human, damageable)), Is.EqualTo(FixedPoint2.Zero)); }); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs b/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs index 72a05b5246..bcacc8011b 100644 --- a/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs +++ b/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Server.GameTicking.Commands; using Content.Shared.CCVar; @@ -10,25 +11,27 @@ namespace Content.IntegrationTests.Tests.Commands { [TestFixture] [TestOf(typeof(RestartRoundNowCommand))] - public sealed class RestartRoundNowTest + public sealed class RestartRoundNowTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings + { + DummyTicker = false, + Dirty = true + }; + [Test] [TestCase(true)] [TestCase(false)] public async Task RestartRoundAfterStart(bool lobbyEnabled) { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Dirty = true - }); + var pair = Pair; var server = pair.Server; var configManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); var gameTicker = entityManager.System(); - await pair.RunTicksSync(5); + await pair.RunUntilSynced(); GameTick tickBeforeRestart = default; @@ -58,8 +61,7 @@ namespace Content.IntegrationTests.Tests.Commands Assert.That(tickBeforeRestart, Is.LessThan(tickAfterRestart)); }); - await pair.RunTicksSync(5); - await pair.CleanReturnAsync(); + await pair.RunUntilSynced(); } } } diff --git a/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs b/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs index 32f1f6128e..f78bd4949e 100644 --- a/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs +++ b/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Damage; using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; @@ -21,7 +22,7 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Commands; [TestFixture] -public sealed class SuicideCommandTests +public sealed class SuicideCommandTests : GameTest { [TestPrototypes] @@ -57,6 +58,13 @@ public sealed class SuicideCommandTests private static readonly ProtoId CannotSuicideTag = "CannotSuicide"; private static readonly ProtoId DamageType = "Slash"; + public override PoolSettings PoolSettings => new PoolSettings + { + Connected = true, + Dirty = true, + DummyTicker = false + }; + /// /// Run the suicide command in the console /// Should successfully kill the player and ghost them @@ -64,12 +72,7 @@ public sealed class SuicideCommandTests [Test] public async Task TestSuicide() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - Dirty = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var consoleHost = server.ResolveDependency(); var entManager = server.ResolveDependency(); @@ -104,8 +107,6 @@ public sealed class SuicideCommandTests !ghostComp.CanReturnToBody); }); }); - - await pair.CleanReturnAsync(); } /// @@ -115,12 +116,7 @@ public sealed class SuicideCommandTests [Test] public async Task TestSuicideWhileDamaged() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - Dirty = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var consoleHost = server.ResolveDependency(); var entManager = server.ResolveDependency(); @@ -166,8 +162,6 @@ public sealed class SuicideCommandTests Assert.That(damageableSystem.GetTotalDamage(player), Is.EqualTo(lethalDamageThreshold)); }); }); - - await pair.CleanReturnAsync(); } /// @@ -177,12 +171,7 @@ public sealed class SuicideCommandTests [Test] public async Task TestSuicideWhenCannotSuicide() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - Dirty = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var consoleHost = server.ResolveDependency(); var entManager = server.ResolveDependency(); @@ -217,8 +206,6 @@ public sealed class SuicideCommandTests !ghostComp.CanReturnToBody); }); }); - - await pair.CleanReturnAsync(); } @@ -228,12 +215,7 @@ public sealed class SuicideCommandTests [Test] public async Task TestSuicideByHeldItem() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - Dirty = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var consoleHost = server.ResolveDependency(); var entManager = server.ResolveDependency(); @@ -292,8 +274,6 @@ public sealed class SuicideCommandTests Assert.That(damageableSystem.GetAllDamage((player, damageableComp)).DamageDict["Slash"], Is.EqualTo(lethalDamageThreshold)); }); }); - - await pair.CleanReturnAsync(); } /// @@ -303,12 +283,7 @@ public sealed class SuicideCommandTests [Test] public async Task TestSuicideByHeldItemSpreadDamage() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - Dirty = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var consoleHost = server.ResolveDependency(); var entManager = server.ResolveDependency(); @@ -367,7 +342,5 @@ public sealed class SuicideCommandTests Assert.That(damageableSystem.GetAllDamage((player, damageableComp)).DamageDict["Slash"], Is.EqualTo(lethalDamageThreshold / 2)); }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/ConfigPresetTests.cs b/Content.IntegrationTests/Tests/ConfigPresetTests.cs index ebeea7f391..224a5677c0 100644 --- a/Content.IntegrationTests/Tests/ConfigPresetTests.cs +++ b/Content.IntegrationTests/Tests/ConfigPresetTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using Content.IntegrationTests.Fixtures; using Content.Server.Entry; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; @@ -7,12 +8,12 @@ using Robust.Shared.ContentPack; namespace Content.IntegrationTests.Tests; [TestFixture] -public sealed class ConfigPresetTests +public sealed class ConfigPresetTests : GameTest { [Test] public async Task TestLoadAll() { - var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var resources = server.ResolveDependency(); @@ -70,7 +71,5 @@ public sealed class ConfigPresetTests Assert.Fail($"CVar {name} was not reset to its original value."); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Construction/ConstructionActionValid.cs b/Content.IntegrationTests/Tests/Construction/ConstructionActionValid.cs index c59b42d638..8c68e5b192 100644 --- a/Content.IntegrationTests/Tests/Construction/ConstructionActionValid.cs +++ b/Content.IntegrationTests/Tests/Construction/ConstructionActionValid.cs @@ -1,4 +1,5 @@ using System.Text; +using Content.IntegrationTests.Fixtures; using Content.Server.Construction.Completions; using Content.Shared.Construction; using Content.Shared.Construction.Prototypes; @@ -7,7 +8,7 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Construction { [TestFixture] - public sealed class ConstructionActionValid + public sealed class ConstructionActionValid : GameTest { private bool IsValid(IGraphAction action, IPrototypeManager protoMan, out string prototype) { @@ -47,7 +48,7 @@ namespace Content.IntegrationTests.Tests.Construction [Test] public async Task ConstructionGraphSpawnPrototypeValid() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ResolveDependency(); @@ -84,13 +85,12 @@ namespace Content.IntegrationTests.Tests.Construction }); Assert.That(valid, Is.True, $"One or more SpawnPrototype actions specified invalid entity prototypes!\n{message}"); - await pair.CleanReturnAsync(); } [Test] public async Task ConstructionGraphEdgeValid() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ResolveDependency(); @@ -118,7 +118,6 @@ namespace Content.IntegrationTests.Tests.Construction }); Assert.That(valid, Is.True, $"One or more edges specified invalid node targets!\n{message}"); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Construction/ConstructionPrototypeTest.cs b/Content.IntegrationTests/Tests/Construction/ConstructionPrototypeTest.cs index 9441443b22..49cc76e945 100644 --- a/Content.IntegrationTests/Tests/Construction/ConstructionPrototypeTest.cs +++ b/Content.IntegrationTests/Tests/Construction/ConstructionPrototypeTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.IntegrationTests.Utility; using Content.Server.Construction.Components; using Content.Shared.Construction.Prototypes; @@ -7,7 +8,7 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Construction { [TestFixture] - public sealed class ConstructionPrototypeTest + public sealed class ConstructionPrototypeTest : GameTest { // discount linter for construction graphs // TODO: Create serialization validators for these? @@ -25,7 +26,7 @@ namespace Content.IntegrationTests.Tests.Construction [Description("Tests that a given entity specifies a valid node for construction, and optionally a valid one for deconstruction.")] public async Task ConstructionComponentValid(string protoKey) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ResolveDependency(); @@ -49,8 +50,6 @@ namespace Content.IntegrationTests.Tests.Construction $"Invalid deconstruction node \"{target}\" on graph \"{graph.ID}\" for construction entity \"{proto.ID}\"!"); } }); - - await pair.CleanReturnAsync(); } [Test] @@ -59,7 +58,7 @@ namespace Content.IntegrationTests.Tests.Construction [Description("Tests that a given construction prototype has a valid starting and target node, and a valid path between them.")] public async Task ConstructionFormsValidGraph(string protoKey) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ResolveDependency(); @@ -95,7 +94,6 @@ namespace Content.IntegrationTests.Tests.Construction $"The next node ({next.Name}) in the path from the start node ({start}) to the target node ({target}) specified an entity prototype ({next.Entity}) without a ConstructionComponent."); #pragma warning restore NUnit2045 }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs b/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs index 37c4b0c9b5..cb7e401601 100644 --- a/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs +++ b/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Server.Storage.EntitySystems; using Robust.Client.GameObjects; using Robust.Shared.GameObjects; @@ -7,7 +8,7 @@ using Robust.Shared.Maths; namespace Content.IntegrationTests.Tests { - public sealed class ContainerOcclusionTest + public sealed class ContainerOcclusionTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -34,7 +35,7 @@ namespace Content.IntegrationTests.Tests [Test] public async Task TestA() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -69,14 +70,12 @@ namespace Content.IntegrationTests.Tests Assert.That(light.ContainerOccluded); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestB() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -112,14 +111,12 @@ namespace Content.IntegrationTests.Tests Assert.That(light.ContainerOccluded, Is.False); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestAb() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -157,8 +154,6 @@ namespace Content.IntegrationTests.Tests Assert.That(light.ContainerOccluded); }); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/ContrabandTest.cs b/Content.IntegrationTests/Tests/ContrabandTest.cs index c52ef293e1..9f0fb1d499 100644 --- a/Content.IntegrationTests/Tests/ContrabandTest.cs +++ b/Content.IntegrationTests/Tests/ContrabandTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Contraband; using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; @@ -5,12 +6,12 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests; [TestFixture] -public sealed class ContrabandTest +public sealed class ContrabandTest : GameTest { [Test] public async Task EntityShowDepartmentsAndJobs() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var client = pair.Client; var protoMan = client.ResolveDependency(); var componentFactory = client.ResolveDependency(); @@ -41,7 +42,5 @@ public sealed class ContrabandTest } }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs index 9777054625..758bbe0a50 100644 --- a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs +++ b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Damage; using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; @@ -12,7 +13,7 @@ namespace Content.IntegrationTests.Tests.Damageable [TestFixture] [TestOf(typeof(DamageableComponent))] [TestOf(typeof(DamageableSystem))] - public sealed class DamageableTest + public sealed class DamageableTest : GameTest { private const string TestDamageableEntityId = "TestDamageableEntityId"; private const string TestGroup1 = "TestGroup1"; @@ -95,7 +96,7 @@ namespace Content.IntegrationTests.Tests.Damageable [Test] public async Task TestDamageableComponents() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntityManager = server.ResolveDependency(); @@ -254,7 +255,6 @@ namespace Content.IntegrationTests.Tests.Damageable sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(group3, -100)); Assert.That(sDamageableSystem.GetTotalDamage(ent), Is.EqualTo(FixedPoint2.Zero)); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Damageable/MobThresholdsTest.cs b/Content.IntegrationTests/Tests/Damageable/MobThresholdsTest.cs index b359f05569..c4179135cd 100644 --- a/Content.IntegrationTests/Tests/Damageable/MobThresholdsTest.cs +++ b/Content.IntegrationTests/Tests/Damageable/MobThresholdsTest.cs @@ -1,10 +1,11 @@ +using Content.IntegrationTests.Fixtures; using Content.IntegrationTests.Utility; using Content.Shared.Alert; using Content.Shared.Mobs.Components; namespace Content.IntegrationTests.Tests.Damageable; -public sealed class MobThresholdsTest +public sealed class MobThresholdsTest : GameTest { private static string[] _entitiesWithThresholds = GameDataScrounger.EntitiesWithComponent("MobThresholds"); @@ -14,7 +15,7 @@ public sealed class MobThresholdsTest [Description("Ensures every entity with mob thresholds has valid mob state configuration corresponding to some AlertPrototype.")] public async Task ValidateMobThresholds(string protoKey) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ProtoMan; @@ -33,7 +34,5 @@ public sealed class MobThresholdsTest Assert.That(alertStates, Does.Contain(state), $"{proto.ID} does not have an alert state for mob state {state}"); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Damageable/StaminaComponentTest.cs b/Content.IntegrationTests/Tests/Damageable/StaminaComponentTest.cs index 09be373a4c..b4a278529d 100644 --- a/Content.IntegrationTests/Tests/Damageable/StaminaComponentTest.cs +++ b/Content.IntegrationTests/Tests/Damageable/StaminaComponentTest.cs @@ -1,11 +1,12 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.IntegrationTests.Utility; using Content.Shared.Damage.Components; using Content.Shared.FixedPoint; namespace Content.IntegrationTests.Tests.Damageable; -public sealed class StaminaComponentTest +public sealed class StaminaComponentTest : GameTest { private static string[] _entitiesWithStamina = GameDataScrounger.EntitiesWithComponent("Stamina"); @@ -15,7 +16,7 @@ public sealed class StaminaComponentTest [Description("Ensures every entity with Stamina has a valid stamina configuration.")] public async Task ValidateStamina(string protoKey) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ProtoMan; @@ -46,7 +47,5 @@ public sealed class StaminaComponentTest #pragma warning restore NUnit2041 } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/DeleteInventoryTest.cs b/Content.IntegrationTests/Tests/DeleteInventoryTest.cs index 49d54bbecf..bbaf11a052 100644 --- a/Content.IntegrationTests/Tests/DeleteInventoryTest.cs +++ b/Content.IntegrationTests/Tests/DeleteInventoryTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Clothing.Components; using Content.Shared.Clothing.EntitySystems; using Content.Shared.Inventory; @@ -7,14 +8,14 @@ using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class DeleteInventoryTest + public sealed class DeleteInventoryTest : GameTest { // Test that when deleting an entity with an InventoryComponent, // any equipped items also get deleted. [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); var entMgr = server.ResolveDependency(); @@ -44,7 +45,6 @@ namespace Content.IntegrationTests.Tests // Assert that child item was also deleted. Assert.That(item.Deleted, Is.True); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs index f5010eefdc..a83891b8fc 100644 --- a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs +++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Damage; using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; @@ -13,12 +14,12 @@ namespace Content.IntegrationTests.Tests.Destructible [TestFixture] [TestOf(typeof(DamageGroupTrigger))] [TestOf(typeof(AndTrigger))] - public sealed class DestructibleDamageGroupTest + public sealed class DestructibleDamageGroupTest : GameTest { [Test] public async Task AndTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -193,7 +194,6 @@ namespace Content.IntegrationTests.Tests.Destructible // No new thresholds reached as triggers once is set to true and it already triggered before Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs index 70baaea95a..47f44ad496 100644 --- a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs +++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Damage; using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; @@ -12,12 +13,12 @@ namespace Content.IntegrationTests.Tests.Destructible [TestFixture] [TestOf(typeof(DamageTypeTrigger))] [TestOf(typeof(AndTrigger))] - public sealed class DestructibleDamageTypeTest + public sealed class DestructibleDamageTypeTest : GameTest { [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -188,7 +189,6 @@ namespace Content.IntegrationTests.Tests.Destructible // No new thresholds reached as triggers once is set to true and it already triggered before Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs index df98294ee9..0aa0449682 100644 --- a/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs +++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Destructible.Thresholds.Behaviors; using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; @@ -10,12 +11,12 @@ using static Content.IntegrationTests.Tests.Destructible.DestructibleTestPrototy namespace Content.IntegrationTests.Tests.Destructible { - public sealed class DestructibleDestructionTest + public sealed class DestructibleDestructionTest : GameTest { [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -89,7 +90,6 @@ namespace Content.IntegrationTests.Tests.Destructible Assert.That(found, Is.True, $"Unable to find {SpawnedEntityId} nearby for destructible test; found {entitiesInRange.Count} entities."); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs index 4460affedf..da56a43a4a 100644 --- a/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs +++ b/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Destructible; using Content.Server.Destructible.Thresholds; using Content.Server.Destructible.Thresholds.Behaviors; @@ -19,12 +20,12 @@ namespace Content.IntegrationTests.Tests.Destructible [TestFixture] [TestOf(typeof(DestructibleComponent))] [TestOf(typeof(DamageThreshold))] - public sealed class DestructibleThresholdActivationTest + public sealed class DestructibleThresholdActivationTest : GameTest { [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntityManager = server.ResolveDependency(); @@ -289,7 +290,6 @@ namespace Content.IntegrationTests.Tests.Destructible Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty); }); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/DeviceLinking/DeviceLinkingTest.cs b/Content.IntegrationTests/Tests/DeviceLinking/DeviceLinkingTest.cs index c62b46ab6d..7a2a880bae 100644 --- a/Content.IntegrationTests/Tests/DeviceLinking/DeviceLinkingTest.cs +++ b/Content.IntegrationTests/Tests/DeviceLinking/DeviceLinkingTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.IntegrationTests.Utility; using Content.Server.DeviceLinking.Systems; using Content.Shared.DeviceLinking; @@ -7,7 +8,7 @@ using Robust.Shared.Maths; namespace Content.IntegrationTests.Tests.DeviceLinking; -public sealed class DeviceLinkingTest +public sealed class DeviceLinkingTest : GameTest { private const string PortTesterProtoId = "DeviceLinkingSinkPortTester"; @@ -29,7 +30,7 @@ public sealed class DeviceLinkingTest [Description("Ensures all devices that can sink signals will not cause exceptions when signaled.")] public async Task DeviceLinkSinkAllPortsTest(string protoKey) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ProtoMan; var compFact = server.ResolveDependency(); @@ -77,7 +78,5 @@ public sealed class DeviceLinkingTest } } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/DeviceNetwork/DeviceNetworkTest.cs b/Content.IntegrationTests/Tests/DeviceNetwork/DeviceNetworkTest.cs index fdc0e1a4d4..6ecdffc58c 100644 --- a/Content.IntegrationTests/Tests/DeviceNetwork/DeviceNetworkTest.cs +++ b/Content.IntegrationTests/Tests/DeviceNetwork/DeviceNetworkTest.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Server.DeviceNetwork.Components; using Content.Server.DeviceNetwork.Systems; using Content.Shared.DeviceNetwork; @@ -12,7 +13,7 @@ namespace Content.IntegrationTests.Tests.DeviceNetwork [TestOf(typeof(DeviceNetworkComponent))] [TestOf(typeof(WiredNetworkComponent))] [TestOf(typeof(WirelessNetworkComponent))] - public sealed class DeviceNetworkTest + public sealed class DeviceNetworkTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -50,7 +51,7 @@ namespace Content.IntegrationTests.Tests.DeviceNetwork [Test] public async Task NetworkDeviceSendAndReceive() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); @@ -104,13 +105,12 @@ namespace Content.IntegrationTests.Tests.DeviceNetwork { Assert.That(payload, Is.EquivalentTo(deviceNetTestSystem.LastPayload)); }); - await pair.CleanReturnAsync(); } [Test] public async Task WirelessNetworkDeviceSendAndReceive() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); var coordinates = testMap.GridCoords; @@ -188,14 +188,12 @@ namespace Content.IntegrationTests.Tests.DeviceNetwork { Assert.That(payload, Is.Not.EqualTo(deviceNetTestSystem.LastPayload).AsCollection); }); - - await pair.CleanReturnAsync(); } [Test] public async Task WiredNetworkDeviceSendAndReceive() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); var coordinates = testMap.GridCoords; @@ -271,8 +269,6 @@ namespace Content.IntegrationTests.Tests.DeviceNetwork { Assert.That(payload, Is.EqualTo(deviceNetTestSystem.LastPayload).AsCollection); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs b/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs index 52b669b09d..f7b20819c2 100644 --- a/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs +++ b/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs @@ -1,6 +1,7 @@ #nullable enable annotations using System.Linq; using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Server.Disposal.Unit; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; @@ -16,7 +17,7 @@ namespace Content.IntegrationTests.Tests.Disposal [TestOf(typeof(DisposalHolderComponent))] [TestOf(typeof(DisposalEntryComponent))] [TestOf(typeof(DisposalUnitComponent))] - public sealed class DisposalUnitTest + public sealed class DisposalUnitTest : GameTest { [Reflect(false)] private sealed class DisposalUnitTestSystem : EntitySystem @@ -145,7 +146,7 @@ namespace Content.IntegrationTests.Tests.Disposal [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -240,8 +241,6 @@ namespace Content.IntegrationTests.Tests.Disposal // Re-pressurizing Flush(disposalUnit, unitComponent, false, disposalSystem); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs b/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs index 32f8b58542..d9619076e0 100644 --- a/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs +++ b/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; using Content.Shared.DoAfter; using Content.Shared.Interaction; using Robust.Shared.GameObjects; @@ -12,7 +13,7 @@ namespace Content.IntegrationTests.Tests.DoAfter { [TestFixture] [TestOf(typeof(DoAfterComponent))] - public sealed partial class DoAfterServerTest + public sealed partial class DoAfterServerTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -35,7 +36,7 @@ namespace Content.IntegrationTests.Tests.DoAfter [Test] public async Task TestSerializable() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); var refMan = server.ResolveDependency(); @@ -55,14 +56,12 @@ namespace Content.IntegrationTests.Tests.DoAfter } }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestFinished() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -84,14 +83,12 @@ namespace Content.IntegrationTests.Tests.DoAfter await server.WaitRunTicks(1); Assert.That(ev.Cancelled, Is.False); - - await pair.CleanReturnAsync(); } [Test] public async Task TestCancelled() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entityManager = server.EntMan; var timing = server.ResolveDependency(); @@ -113,8 +110,6 @@ namespace Content.IntegrationTests.Tests.DoAfter await server.WaitRunTicks(3); Assert.That(ev.Cancelled); - - await pair.CleanReturnAsync(); } /// @@ -124,7 +119,7 @@ namespace Content.IntegrationTests.Tests.DoAfter [Test] public async Task TestGetInteractingEntities() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entityManager = server.EntMan; var timing = server.ResolveDependency(); @@ -175,8 +170,6 @@ namespace Content.IntegrationTests.Tests.DoAfter entityManager.DeleteEntity(target); entityManager.DeleteEntity(target2); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Doors/AirlockTest.cs b/Content.IntegrationTests/Tests/Doors/AirlockTest.cs index 69fe66039b..af03de169f 100644 --- a/Content.IntegrationTests/Tests/Doors/AirlockTest.cs +++ b/Content.IntegrationTests/Tests/Doors/AirlockTest.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Server.Doors.Systems; using Content.Shared.Doors.Components; using Robust.Shared.GameObjects; @@ -11,7 +12,7 @@ namespace Content.IntegrationTests.Tests.Doors { [TestFixture] [TestOf(typeof(AirlockComponent))] - public sealed class AirlockTest + public sealed class AirlockTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -54,7 +55,7 @@ namespace Content.IntegrationTests.Tests.Doors [Test] public async Task OpenCloseDestroyTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entityManager = server.ResolveDependency(); @@ -104,16 +105,12 @@ namespace Content.IntegrationTests.Tests.Doors entityManager.DeleteEntity(airlock); }); }); - - server.RunTicks(5); - - await pair.CleanReturnAsync(); } [Test] public async Task AirlockBlockTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -179,7 +176,6 @@ namespace Content.IntegrationTests.Tests.Doors { Assert.That(Math.Abs(xformSystem.GetWorldPosition(airlockPhysicsDummy).X - 1), Is.GreaterThan(0.01f)); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/DummyIconTest.cs b/Content.IntegrationTests/Tests/DummyIconTest.cs index 62197bb319..dcb5d315dc 100644 --- a/Content.IntegrationTests/Tests/DummyIconTest.cs +++ b/Content.IntegrationTests/Tests/DummyIconTest.cs @@ -1,5 +1,6 @@ #nullable enable using System.Linq; +using Content.IntegrationTests.Fixtures; using Robust.Client.GameObjects; using Robust.Client.ResourceManagement; using Robust.Shared.Prototypes; @@ -7,12 +8,12 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class DummyIconTest + public sealed class DummyIconTest : GameTest { [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var client = pair.Client; var prototypeManager = client.ResolveDependency(); var resourceCache = client.ResolveDependency(); @@ -32,7 +33,6 @@ namespace Content.IntegrationTests.Tests proto.ID); } }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/EntityTest.cs b/Content.IntegrationTests/Tests/EntityTest.cs index 9b0e7729f5..59dfec1060 100644 --- a/Content.IntegrationTests/Tests/EntityTest.cs +++ b/Content.IntegrationTests/Tests/EntityTest.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Text; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; using Robust.Shared; using Robust.Shared.Audio.Components; using Robust.Shared.Configuration; @@ -16,17 +18,28 @@ namespace Content.IntegrationTests.Tests { [TestFixture] [TestOf(typeof(EntityUid))] - public sealed class EntityTest + public sealed class EntityTest : GameTest { private static readonly ProtoId SpawnerCategory = "Spawner"; + public override PoolSettings PoolSettings => new() + { + Connected = true, + Dirty = true + }; + + public static PoolSettings Disconnected => new() + { + Dirty = true, + }; + [Test] + [PairConfig(nameof(Disconnected))] public async Task SpawnAndDeleteAllEntitiesOnDifferentMaps() { // This test dirties the pair as it simply deletes ALL entities when done. Overhead of restarting the round // is minimal relative to the rest of the test. - var settings = new PoolSettings { Dirty = true }; - await using var pair = await PoolManager.GetServerClient(settings); + var pair = Pair; var server = pair.Server; var entityMan = server.ResolveDependency(); @@ -79,17 +92,14 @@ namespace Content.IntegrationTests.Tests Assert.That(entityMan.EntityCount, Is.Zero); }); - - await pair.CleanReturnAsync(); } [Test] + [PairConfig(nameof(Disconnected))] public async Task SpawnAndDeleteAllEntitiesInTheSameSpot() { - // This test dirties the pair as it simply deletes ALL entities when done. Overhead of restarting the round - // is minimal relative to the rest of the test. - var settings = new PoolSettings { Dirty = true }; - await using var pair = await PoolManager.GetServerClient(settings); + var pair = Pair; + Assert.That(pair.Client.Session, Is.Null); var server = pair.Server; var map = await pair.CreateTestMap(); @@ -134,8 +144,6 @@ namespace Content.IntegrationTests.Tests Assert.That(entityMan.EntityCount, Is.Zero); }); - - await pair.CleanReturnAsync(); } /// @@ -145,10 +153,7 @@ namespace Content.IntegrationTests.Tests [Test] public async Task SpawnAndDirtyAllEntities() { - // This test dirties the pair as it simply deletes ALL entities when done. Overhead of restarting the round - // is minimal relative to the rest of the test. - var settings = new PoolSettings { Connected = true, Dirty = true }; - await using var pair = await PoolManager.GetServerClient(settings); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -182,7 +187,7 @@ namespace Content.IntegrationTests.Tests } }); - await pair.RunTicksSync(15); + await pair.RunUntilSynced(); // Make sure the client actually received the entities // 500 is completely arbitrary. Note that the client & sever entity counts aren't expected to match. @@ -209,8 +214,6 @@ namespace Content.IntegrationTests.Tests Assert.That(sEntMan.EntityCount, Is.Zero); }); - - await pair.CleanReturnAsync(); } /// @@ -230,8 +233,7 @@ namespace Content.IntegrationTests.Tests [Test] public async Task SpawnAndDeleteEntityCountTest() { - var settings = new PoolSettings { Connected = true, Dirty = true }; - await using var pair = await PoolManager.GetServerClient(settings); + var pair = Pair; var mapSys = pair.Server.System(); var server = pair.Server; var client = pair.Client; @@ -317,8 +319,6 @@ namespace Content.IntegrationTests.Tests BuildDiffString(clientEntities, Entities(client.EntMan), client.EntMan)); } }); - - await pair.CleanReturnAsync(); } private static string BuildDiffString(IEnumerable oldEnts, IEnumerable newEnts, IEntityManager entMan) @@ -392,7 +392,7 @@ namespace Content.IntegrationTests.Tests "ActivatableUI", // Requires enum key }; - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entityManager = server.ResolveDependency(); var componentFactory = server.ResolveDependency(); @@ -445,8 +445,6 @@ namespace Content.IntegrationTests.Tests } }); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Explosion/ExplosionPrototypeTest.cs b/Content.IntegrationTests/Tests/Explosion/ExplosionPrototypeTest.cs index 01f52f7d83..7a486e6839 100644 --- a/Content.IntegrationTests/Tests/Explosion/ExplosionPrototypeTest.cs +++ b/Content.IntegrationTests/Tests/Explosion/ExplosionPrototypeTest.cs @@ -1,9 +1,10 @@ +using Content.IntegrationTests.Fixtures; using Content.IntegrationTests.Utility; using Content.Shared.Explosion; namespace Content.IntegrationTests.Tests.Explosion; -public sealed class ExplosionPrototypeTest +public sealed class ExplosionPrototypeTest : GameTest { private static string[] _explosionKinds = GameDataScrounger.PrototypesOfKind(); @@ -13,7 +14,7 @@ public sealed class ExplosionPrototypeTest [Description("Ensures various properties of ExplosionPrototype are correctly configured.")] public async Task Validate(string protoKey) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ProtoMan; @@ -40,7 +41,5 @@ public sealed class ExplosionPrototypeTest Assert.That(proto.IntensityPerState, Is.Positive); Assert.That(proto.FireStates, Is.Positive); } - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/FillLevelSpriteTest.cs b/Content.IntegrationTests/Tests/FillLevelSpriteTest.cs index 99354e16c1..142ec21ddb 100644 --- a/Content.IntegrationTests/Tests/FillLevelSpriteTest.cs +++ b/Content.IntegrationTests/Tests/FillLevelSpriteTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Components; using Content.Shared.Prototypes; @@ -12,7 +13,7 @@ namespace Content.IntegrationTests.Tests; /// Tests to see if any entity prototypes specify solution fill level sprites that don't exist. /// [TestFixture] -public sealed class FillLevelSpriteTest +public sealed class FillLevelSpriteTest : GameTest { private static readonly string[] HandStateNames = ["left", "right"]; private static readonly string[] EquipStateNames = ["back", "suitstorage"]; @@ -20,7 +21,7 @@ public sealed class FillLevelSpriteTest [Test] public async Task FillLevelSpritesExist() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var client = pair.Client; var protoMan = client.ResolveDependency(); var componentFactory = client.ResolveDependency(); @@ -101,7 +102,5 @@ public sealed class FillLevelSpriteTest } }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs b/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs index 1afed38966..3d9ff61a2d 100644 --- a/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs +++ b/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs @@ -7,12 +7,13 @@ using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; namespace Content.IntegrationTests.Tests.Fluids; [TestFixture] [TestOf(typeof(AbsorbentComponent))] -public sealed class AbsorbentTest +public sealed class AbsorbentTest : GameTest { private const string UserDummyId = "UserDummy"; private const string AbsorbentDummyId = "AbsorbentDummy"; @@ -73,7 +74,7 @@ public sealed class AbsorbentTest [TestCaseSource(nameof(TestCasesToRun))] public async Task AbsorbentOnRefillableTest(TestSolutionCase testCase) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -123,15 +124,12 @@ public sealed class AbsorbentTest Assert.That(VolumeOfPrototypeInComposition(refillableComposition, NonEvaporablePrototypeId), Is.EqualTo(testCase.ExpectedRefillableSolution.VolumeOfNonEvaporable)); }); }); - await pair.RunTicksSync(5); - - await pair.CleanReturnAsync(); } [TestCaseSource(nameof(TestCasesToRunOnSmallRefillable))] public async Task AbsorbentOnSmallRefillableTest(TestSolutionCase testCase) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -180,9 +178,6 @@ public sealed class AbsorbentTest Assert.That(VolumeOfPrototypeInComposition(refillableComposition, NonEvaporablePrototypeId), Is.EqualTo(testCase.ExpectedRefillableSolution.VolumeOfNonEvaporable)); }); }); - await pair.RunTicksSync(5); - - await pair.CleanReturnAsync(); } private static FixedPoint2 VolumeOfPrototypeInComposition(Dictionary composition, string prototypeId) diff --git a/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs b/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs index d6f9bf3598..c073020e9c 100644 --- a/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs +++ b/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs @@ -1,4 +1,5 @@ #nullable enable +using Content.IntegrationTests.Fixtures; using Content.Server.Fluids.EntitySystems; using Content.Server.Spreader; using Content.Shared.Chemistry.Components; @@ -14,7 +15,7 @@ namespace Content.IntegrationTests.Tests.Fluids; [TestFixture] [TestOf(typeof(SpreaderSystem))] -public sealed class FluidSpill +public sealed class FluidSpill : GameTest { private static PuddleComponent? GetPuddle(IEntityManager entityManager, Entity mapGrid, Vector2i pos) { @@ -36,7 +37,7 @@ public sealed class FluidSpill [Test] public async Task SpillCorner() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -110,7 +111,5 @@ public sealed class FluidSpill } } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs b/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs index ee2d0cb1f7..71fbefa241 100644 --- a/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs +++ b/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.Fluids.EntitySystems; using Content.Shared.Chemistry.Components; using Content.Shared.Coordinates; @@ -10,12 +11,12 @@ namespace Content.IntegrationTests.Tests.Fluids { [TestFixture] [TestOf(typeof(PuddleComponent))] - public sealed class PuddleTest + public sealed class PuddleTest : GameTest { [Test] public async Task TilePuddleTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -32,15 +33,12 @@ namespace Content.IntegrationTests.Tests.Fluids Assert.That(spillSystem.TrySpillAt(coordinates, solution, out _), Is.True); }); - await pair.RunTicksSync(5); - - await pair.CleanReturnAsync(); } [Test] public async Task SpaceNoPuddleTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -69,8 +67,6 @@ namespace Content.IntegrationTests.Tests.Fluids Assert.That(spillSystem.TrySpillAt(coordinates, solution, out _), Is.False); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/FollowerSystemTest.cs b/Content.IntegrationTests/Tests/FollowerSystemTest.cs index f4447426c7..464b3306f2 100644 --- a/Content.IntegrationTests/Tests/FollowerSystemTest.cs +++ b/Content.IntegrationTests/Tests/FollowerSystemTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Shared.Follower; using Robust.Shared.GameObjects; @@ -7,7 +8,7 @@ using Robust.Shared.Map; namespace Content.IntegrationTests.Tests; [TestFixture, TestOf(typeof(FollowerSystem))] -public sealed class FollowerSystemTest +public sealed class FollowerSystemTest : GameTest { /// /// This test ensures that deleting a map while an entity follows another doesn't throw any exceptions. @@ -15,7 +16,7 @@ public sealed class FollowerSystemTest [Test] public async Task FollowerMapDeleteTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -44,6 +45,5 @@ public sealed class FollowerSystemTest entMan.DeleteEntity(mapSys.GetMap(map)); }); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs index dae3203f9f..97681ad469 100644 --- a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs +++ b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs @@ -1,4 +1,5 @@ #nullable enable +using Content.IntegrationTests.Fixtures; using Content.Server.Cuffs; using Content.Shared.Cuffs.Components; using Content.Shared.Hands.Components; @@ -10,7 +11,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking [TestFixture] [TestOf(typeof(CuffableComponent))] [TestOf(typeof(HandcuffComponent))] - public sealed class HandCuffTest + public sealed class HandCuffTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -40,7 +41,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; EntityUid human; @@ -98,8 +99,6 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking cuffableSys.TryAddNewCuffs(human, human, secondCuffs, cuffed); Assert.That(cuffed.CuffedHandCount, Is.EqualTo(4), "Player doesn't have correct amount of hands cuffed"); }); - - await pair.CleanReturnAsync(); } private static void AddHand(NetEntity to, IServerConsoleHost host) diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/EntityPrototypeComponentsTest.cs b/Content.IntegrationTests/Tests/GameObjects/Components/EntityPrototypeComponentsTest.cs index ef94cf0f00..3473f1bc9c 100644 --- a/Content.IntegrationTests/Tests/GameObjects/Components/EntityPrototypeComponentsTest.cs +++ b/Content.IntegrationTests/Tests/GameObjects/Components/EntityPrototypeComponentsTest.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using Content.IntegrationTests.Fixtures; using Robust.Shared.ContentPack; using Robust.Shared.GameObjects; using Robust.Shared.Utility; @@ -11,12 +12,12 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components { [TestFixture] [TestOf(typeof(Server.Entry.IgnoredComponents))] - public sealed class EntityPrototypeComponentsTest + public sealed class EntityPrototypeComponentsTest : GameTest { [Test] public async Task PrototypesHaveKnownComponents() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -100,7 +101,6 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components if (unknownComponentsClient.Count + unknownComponentsServer.Count + doubleIgnoredComponents.Count == 0) { - await pair.CleanReturnAsync(); Assert.Pass($"Validated {entitiesValidated} entities with {componentsValidated} components in {paths.Length} files."); return; } @@ -131,11 +131,11 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components [Test] public async Task IgnoredComponentsExistInTheCorrectPlaces() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var client = pair.Client; var serverComponents = server.ResolveDependency(); - var ignoredServerNames = Server.Entry.IgnoredComponents.List; + var ignoredServerNames = Content.Server.Entry.IgnoredComponents.List; var clientComponents = client.ResolveDependency(); var failureMessages = ""; @@ -151,7 +151,6 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components } } Assert.That(failureMessages, Is.Empty); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs index 6c103bf6ae..71c37076ee 100644 --- a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs +++ b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs @@ -1,6 +1,7 @@ using System.Linq; using Content.Client.UserInterface.Systems.Alerts.Controls; using Content.Client.UserInterface.Systems.Alerts.Widgets; +using Content.IntegrationTests.Fixtures; using Content.Shared.Alert; using Robust.Client.UserInterface; using Robust.Server.Player; @@ -10,16 +11,18 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs { [TestFixture] [TestOf(typeof(AlertsComponent))] - public sealed class AlertsComponentTests + public sealed class AlertsComponentTests : GameTest { + public override PoolSettings PoolSettings => new() + { + Connected = true, + DummyTicker = false + }; + [Test] public async Task AlertsTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -107,8 +110,6 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs var expectedIDs = new[] { "HumanHealth", "Debug2" }; Assert.That(alertIDs, Is.SupersetOf(expectedIDs)); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs b/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs index b215584c57..4a059eb1cc 100644 --- a/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Antag; using Content.Server.Antag.Components; using Content.Server.GameTicking; @@ -14,17 +15,19 @@ namespace Content.IntegrationTests.Tests.GameRules; // Once upon a time, players in the lobby weren't ever considered eligible for antag roles. // Lets not let that happen again. [TestFixture] -public sealed class AntagPreferenceTest +public sealed class AntagPreferenceTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings + { + DummyTicker = false, + Connected = true, + InLobby = true + }; + [Test] public async Task TestLobbyPlayersValid() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - InLobby = true - }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -71,6 +74,5 @@ public sealed class AntagPreferenceTest Assert.That(pool.Count, Is.EqualTo(0)); await server.WaitPost(() => server.EntMan.DeleteEntity(uid)); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs b/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs index b9a02339fb..b8efb64b8b 100644 --- a/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs @@ -1,4 +1,5 @@ #nullable enable +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Server.GameTicking.Presets; using Content.Shared.CCVar; @@ -9,7 +10,7 @@ using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests.GameRules; [TestFixture] -public sealed class FailAndStartPresetTest +public sealed class FailAndStartPresetTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -52,19 +53,21 @@ public sealed class FailAndStartPresetTest - type: TestRule "; + public override PoolSettings PoolSettings => new() + { + Dirty = true, + DummyTicker = false, + Connected = true, + InLobby = true + }; + /// /// Test that a nuke ops gamemode can start after failing to start once. /// [Test] public async Task FailAndStartTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Dirty = true, - DummyTicker = false, - Connected = true, - InLobby = true - }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -115,7 +118,6 @@ public sealed class FailAndStartPresetTest server.CfgMan.SetCVar(CCVars.GameLobbyFallbackEnabled, true); server.CfgMan.SetCVar(CCVars.GameLobbyDefaultPreset, "secret"); server.System().Run = false; - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs index 53165adca0..f9074f7dc6 100644 --- a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Body.Components; using Content.Server.GameTicking; using Content.Server.GameTicking.Presets; @@ -30,24 +31,27 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.GameRules; [TestFixture] -public sealed class NukeOpsTest +public sealed class NukeOpsTest : GameTest { private static readonly ProtoId SyndicateFaction = "Syndicate"; private static readonly ProtoId NanotrasenFaction = "NanoTrasen"; + public override PoolSettings PoolSettings => new() + { + Dirty = true, + DummyTicker = false, + Connected = true, + InLobby = true + }; + + /// /// Check that a nuke ops game mode can start without issue. I.e., that the nuke station and such all get loaded. /// [Test] public async Task TryStopNukeOpsFromConstantlyFailing() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Dirty = true, - DummyTicker = false, - Connected = true, - InLobby = true - }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -260,6 +264,5 @@ public sealed class NukeOpsTest }); ticker.SetGamePreset((GamePresetPrototype?) null); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs index c805d04a71..0bbf2fae4f 100644 --- a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Server.GameTicking.Rules; using Content.Server.GameTicking.Rules.Components; @@ -9,12 +10,14 @@ namespace Content.IntegrationTests.Tests.GameRules { [TestFixture] [TestOf(typeof(MaxTimeRestartRuleSystem))] - public sealed class RuleMaxTimeRestartTest + public sealed class RuleMaxTimeRestartTest : GameTest { + public override PoolSettings PoolSettings => new() { InLobby = true }; + [Test] public async Task RestartTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { InLobby = true }); + var pair = Pair; var server = pair.Server; Assert.That(server.EntMan.Count(), Is.Zero); @@ -64,8 +67,6 @@ namespace Content.IntegrationTests.Tests.GameRules { Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby)); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs index 5d7ae8efbf..521b043c68 100644 --- a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs @@ -1,19 +1,22 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests.GameRules; [TestFixture] -public sealed class SecretStartsTest +public sealed class SecretStartsTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings { Dirty = true }; + /// /// Tests that when secret is started, all of the game rules it successfully adds are also started. /// [Test] public async Task TestSecretStarts() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true }); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -38,7 +41,5 @@ public sealed class SecretStartsTest // End all rules gameTicker.ClearGameRules(); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs b/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs index bda931397b..1eef1ab0a1 100644 --- a/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Shared.CCVar; using Robust.Shared.Configuration; @@ -7,19 +8,21 @@ using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests.GameRules; [TestFixture] -public sealed class StartEndGameRulesTest +public sealed class StartEndGameRulesTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings + { + Dirty = true, + DummyTicker = false + }; + /// /// Tests that all game rules can be added/started/ended at the same time without exceptions. /// [Test] public async Task TestAllConcurrent() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Dirty = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); var gameTicker = server.ResolveDependency().GetEntitySystem(); @@ -47,7 +50,5 @@ public sealed class StartEndGameRulesTest gameTicker.ClearGameRules(); Assert.That(!gameTicker.GetAddedGameRules().Any()); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/GameRules/TraitorRuleTest.cs b/Content.IntegrationTests/Tests/GameRules/TraitorRuleTest.cs index 97fe1c8762..318c4b6b42 100644 --- a/Content.IntegrationTests/Tests/GameRules/TraitorRuleTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/TraitorRuleTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Antag.Components; using Content.Server.GameTicking; using Content.Server.GameTicking.Rules; @@ -17,23 +18,25 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.GameRules; [TestFixture] -public sealed class TraitorRuleTest +public sealed class TraitorRuleTest : GameTest { private const string TraitorGameRuleProtoId = "Traitor"; private const string TraitorAntagRoleName = "Traitor"; private static readonly ProtoId SyndicateFaction = "Syndicate"; private static readonly ProtoId NanotrasenFaction = "NanoTrasen"; + public override PoolSettings PoolSettings => new() + { + Dirty = true, + DummyTicker = false, + Connected = true, + InLobby = true, + }; + [Test] public async Task TestTraitorObjectives() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings() - { - Dirty = true, - DummyTicker = false, - Connected = true, - InLobby = true, - }); + var pair = Pair; var server = pair.Server; var client = pair.Client; var entMan = server.EntMan; @@ -123,9 +126,6 @@ public sealed class TraitorRuleTest $"MaxDifficulty exceeded! Objectives: {string.Join(", ", mindComp.Objectives.Select(o => FormatObjective(o, entMan)))}"); Assert.That(mindComp.Objectives, Is.Not.Empty, $"No objectives assigned!"); - - - await pair.CleanReturnAsync(); } private static string FormatObjective(Entity entity, IEntityManager entMan) diff --git a/Content.IntegrationTests/Tests/Gibbing/GibTest.cs b/Content.IntegrationTests/Tests/Gibbing/GibTest.cs index ee0f7a742d..23be8f028a 100644 --- a/Content.IntegrationTests/Tests/Gibbing/GibTest.cs +++ b/Content.IntegrationTests/Tests/Gibbing/GibTest.cs @@ -1,16 +1,17 @@ #nullable enable +using Content.IntegrationTests.Fixtures; using Content.Shared.Gibbing; using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests.Body; [TestFixture] -public sealed class GibTest +public sealed class GibTest : GameTest { [Test] public async Task TestGib() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var (server, client) = (pair.Server, pair.Client); var map = await pair.CreateTestMap(); @@ -30,7 +31,5 @@ public sealed class GibTest await pair.RunTicksSync(5); Assert.That(!client.EntMan.EntityExists(nuid)); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs index 0951e7e260..0ef52a12a6 100644 --- a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs +++ b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.Gravity; using Content.Shared.Alert; using Content.Shared.Gravity; @@ -8,7 +9,7 @@ namespace Content.IntegrationTests.Tests.Gravity [TestFixture] [TestOf(typeof(GravitySystem))] [TestOf(typeof(GravityGeneratorComponent))] - public sealed class WeightlessStatusTests + public sealed class WeightlessStatusTests : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -38,7 +39,7 @@ namespace Content.IntegrationTests.Tests.Gravity [Test] public async Task WeightlessStatusTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entityManager = server.ResolveDependency(); @@ -86,8 +87,6 @@ namespace Content.IntegrationTests.Tests.Gravity }); await pair.RunTicksSync(10); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/GravityGridTest.cs b/Content.IntegrationTests/Tests/GravityGridTest.cs index 047ec0259a..10804ea201 100644 --- a/Content.IntegrationTests/Tests/GravityGridTest.cs +++ b/Content.IntegrationTests/Tests/GravityGridTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.Power.Components; using Content.Shared.Gravity; using Robust.Shared.GameObjects; @@ -11,7 +12,7 @@ namespace Content.IntegrationTests.Tests /// making sure that gravity is applied to the correct grids. [TestFixture] [TestOf(typeof(GravityGeneratorComponent))] - public sealed class GravityGridTest + public sealed class GravityGridTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -31,7 +32,7 @@ namespace Content.IntegrationTests.Tests [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -96,8 +97,6 @@ namespace Content.IntegrationTests.Tests Assert.That(entityMan.GetComponent(grid2).Enabled, Is.False); }); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Guidebook/DocumentParsingTest.cs b/Content.IntegrationTests/Tests/Guidebook/DocumentParsingTest.cs index dec2c40c0a..98b7056d00 100644 --- a/Content.IntegrationTests/Tests/Guidebook/DocumentParsingTest.cs +++ b/Content.IntegrationTests/Tests/Guidebook/DocumentParsingTest.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Content.Client.Guidebook; using Content.Client.Guidebook.Richtext; +using Content.IntegrationTests.Fixtures; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; @@ -13,7 +14,7 @@ namespace Content.IntegrationTests.Tests.Guidebook; /// [TestFixture] [TestOf(typeof(DocumentParsingManager))] -public sealed class DocumentParsingTest +public sealed class DocumentParsingTest : GameTest { public string TestDocument = @"multiple @@ -45,7 +46,7 @@ whitespace before newlines are ignored. [Test] public async Task ParseTestDocument() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var client = pair.Client; await client.WaitIdleAsync(); var parser = client.ResolveDependency(); @@ -133,8 +134,6 @@ whitespace before newlines are ignored. subTest2.Params.TryGetValue("k", out val); Assert.That(val, Is.EqualTo(@"<>\>=""=<-_?*3.0//")); - - await pair.CleanReturnAsync(); } public sealed class TestControl : Control, IDocumentTag diff --git a/Content.IntegrationTests/Tests/Guidebook/GuideEntryPrototypeTests.cs b/Content.IntegrationTests/Tests/Guidebook/GuideEntryPrototypeTests.cs index 13d0ad1497..016b06abe8 100644 --- a/Content.IntegrationTests/Tests/Guidebook/GuideEntryPrototypeTests.cs +++ b/Content.IntegrationTests/Tests/Guidebook/GuideEntryPrototypeTests.cs @@ -1,5 +1,6 @@ using Content.Client.Guidebook; using Content.Client.Guidebook.Richtext; +using Content.IntegrationTests.Fixtures; using Robust.Shared.ContentPack; using Robust.Shared.Prototypes; using Content.IntegrationTests.Utility; @@ -12,7 +13,7 @@ namespace Content.IntegrationTests.Tests.Guidebook; [TestOf(typeof(GuidebookSystem))] [TestOf(typeof(GuideEntryPrototype))] [TestOf(typeof(DocumentParsingManager))] -public sealed class GuideEntryPrototypeTests +public sealed class GuideEntryPrototypeTests : GameTest { private static string[] _guideEntries = GameDataScrounger.PrototypesOfKind(); @@ -21,7 +22,7 @@ public sealed class GuideEntryPrototypeTests [Description("Ensures a given guidebook entry is valid, checking the document/etc.")] public async Task Validate(string protoKey) { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var client = pair.Client; await client.WaitIdleAsync(); var protoMan = client.ResolveDependency(); @@ -36,7 +37,5 @@ public sealed class GuideEntryPrototypeTests Assert.That(parser.TryAddMarkup(new Document(), text), $"Failed to parse the guide entry's document."); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Hands/HandTests.cs b/Content.IntegrationTests/Tests/Hands/HandTests.cs index d5cf75c463..95f053a1cb 100644 --- a/Content.IntegrationTests/Tests/Hands/HandTests.cs +++ b/Content.IntegrationTests/Tests/Hands/HandTests.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Storage.EntitySystems; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; @@ -10,7 +11,7 @@ using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests.Hands; [TestFixture] -public sealed class HandTests +public sealed class HandTests : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -25,14 +26,16 @@ public sealed class HandTests "; + public override PoolSettings PoolSettings => new() + { + Connected = true, + DummyTicker = false + }; + [Test] public async Task TestPickupDrop() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -69,17 +72,12 @@ public sealed class HandTests Assert.That(sys.GetActiveItem((player, hands)), Is.Null); await server.WaitPost(() => mapSystem.DeleteMap(data.MapId)); - await pair.CleanReturnAsync(); } [Test] public async Task TestPickUpThenDropInContainer() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var map = await pair.CreateTestMap(); await pair.RunTicksSync(5); @@ -134,6 +132,5 @@ public sealed class HandTests Assert.That(containerSystem.IsInSameOrNoContainer((player, xform), (item, itemXform))); await server.WaitPost(() => mapSystem.DeleteMap(map.MapId)); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs b/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs index 929a231159..accdd4667c 100644 --- a/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs +++ b/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Inventory; using Robust.Shared.GameObjects; @@ -7,7 +8,7 @@ namespace Content.IntegrationTests.Tests // i.e. the interaction between uniforms and the pocket/ID slots. // and also how big items don't fit in pockets. [TestFixture] - public sealed class HumanInventoryUniformSlotsTest + public sealed class HumanInventoryUniformSlotsTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -55,7 +56,7 @@ namespace Content.IntegrationTests.Tests [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); var coordinates = testMap.GridCoords; @@ -130,8 +131,6 @@ namespace Content.IntegrationTests.Tests mapSystem.DeleteMap(testMap.MapId); }); - - await pair.CleanReturnAsync(); } private static bool IsDescendant(EntityUid descendant, EntityUid parent, IEntityManager entManager) diff --git a/Content.IntegrationTests/Tests/Humanoid/HideablePrototypeValidation.cs b/Content.IntegrationTests/Tests/Humanoid/HideablePrototypeValidation.cs index d95992bda2..4ee65c9c32 100644 --- a/Content.IntegrationTests/Tests/Humanoid/HideablePrototypeValidation.cs +++ b/Content.IntegrationTests/Tests/Humanoid/HideablePrototypeValidation.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Body; using Content.Shared.Clothing.Components; using Content.Shared.Humanoid; @@ -8,13 +9,12 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Humanoid; [TestFixture] -public sealed class HideablePrototypeValidation +public sealed class HideablePrototypeValidation : GameTest { [Test] public async Task NoOrgansWithoutClothing() { - await using var pair = await PoolManager.GetServerClient(); - + var pair = Pair; var requirements = new Dictionary>(); foreach (var (proto, component) in pair.GetPrototypesWithComponent()) { @@ -42,14 +42,12 @@ public sealed class HideablePrototypeValidation { Assert.That(provided, Does.Contain(key), $"No clothing will hide {key} that can be hidden on {string.Join(", ", requirement.Select(it => it.Id))}"); } - - await pair.CleanReturnAsync(); } [Test] public async Task NoClothingWithoutOrgans() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var requirements = new Dictionary>(); foreach (var (proto, component) in pair.GetPrototypesWithComponent()) @@ -74,7 +72,5 @@ public sealed class HideablePrototypeValidation { Assert.That(provided, Does.Contain(key), $"No organ will hide {key} that can be hidden by {string.Join(", ", requirement.Select(it => it.Id))}"); } - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Humanoid/HumanoidProfileTests.cs b/Content.IntegrationTests/Tests/Humanoid/HumanoidProfileTests.cs index 1e4a094b79..48eb521c5e 100644 --- a/Content.IntegrationTests/Tests/Humanoid/HumanoidProfileTests.cs +++ b/Content.IntegrationTests/Tests/Humanoid/HumanoidProfileTests.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Prototypes; using Content.Shared.Preferences; @@ -10,14 +11,14 @@ namespace Content.IntegrationTests.Tests.Humanoid; [TestFixture] [TestOf(typeof(HumanoidProfileSystem))] -public sealed class HumanoidProfileTests +public sealed class HumanoidProfileTests : GameTest { private static readonly ProtoId Vox = "Vox"; [Test] public async Task EnsureValidLoading() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -43,7 +44,5 @@ public sealed class HumanoidProfileTests Assert.That(voiceComponent.Sounds, Is.Not.Null, message: "the MobHuman spawned by this test needs to have sex-specific sound set"); Assert.That(voiceComponent.Sounds![Sex.Female], Is.EqualTo(voiceComponent.EmoteSounds)); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs index 6ac40e92a1..e4bd4615c5 100644 --- a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs +++ b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs @@ -1,5 +1,6 @@ #nullable enable annotations using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Server.Interaction; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; @@ -16,7 +17,7 @@ namespace Content.IntegrationTests.Tests.Interaction.Click { [TestFixture] [TestOf(typeof(InteractionSystem))] - public sealed class InteractionSystemTests + public sealed class InteractionSystemTests : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -40,7 +41,7 @@ namespace Content.IntegrationTests.Tests.Interaction.Click [Test] public async Task InteractionTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntities = server.ResolveDependency(); @@ -101,13 +102,12 @@ namespace Content.IntegrationTests.Tests.Interaction.Click }); testInteractionSystem.ClearHandlers(); - await pair.CleanReturnAsync(); } [Test] public async Task InteractionObstructionTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntities = server.ResolveDependency(); @@ -168,13 +168,12 @@ namespace Content.IntegrationTests.Tests.Interaction.Click }); testInteractionSystem.ClearHandlers(); - await pair.CleanReturnAsync(); } [Test] public async Task InteractionInRangeTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntities = server.ResolveDependency(); @@ -234,14 +233,13 @@ namespace Content.IntegrationTests.Tests.Interaction.Click }); testInteractionSystem.ClearHandlers(); - await pair.CleanReturnAsync(); } [Test] public async Task InteractionOutOfRangeTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntities = server.ResolveDependency(); @@ -300,13 +298,12 @@ namespace Content.IntegrationTests.Tests.Interaction.Click }); testInteractionSystem.ClearHandlers(); - await pair.CleanReturnAsync(); } [Test] public async Task InsideContainerInteractionBlockTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntities = server.ResolveDependency(); @@ -388,7 +385,6 @@ namespace Content.IntegrationTests.Tests.Interaction.Click }); testInteractionSystem.ClearHandlers(); - await pair.CleanReturnAsync(); } public sealed class TestInteractionSystem : EntitySystem diff --git a/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs b/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs index 801433ae72..966f13675d 100644 --- a/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs +++ b/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Shared.Interaction; using Robust.Server.GameObjects; using Robust.Shared.Containers; @@ -10,7 +11,7 @@ namespace Content.IntegrationTests.Tests.Interaction { [TestFixture] [TestOf(typeof(SharedInteractionSystem))] - public sealed class InRangeUnobstructed + public sealed class InRangeUnobstructed : GameTest { private const string HumanId = "MobHuman"; @@ -27,7 +28,7 @@ namespace Content.IntegrationTests.Tests.Interaction [Test] public async Task EntityEntityTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntities = server.ResolveDependency(); @@ -109,8 +110,6 @@ namespace Content.IntegrationTests.Tests.Interaction Assert.That(interactionSys.InRangeUnobstructed(mapCoordinates, origin, InteractionRangeDivided15Times3)); }); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs index 62a03b0abe..5231055c69 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs @@ -175,7 +175,7 @@ public abstract partial class InteractionTest [SetUp] public virtual async Task Setup() { - Pair = await PoolManager.GetServerClient(Settings); + Pair = await PoolManager.GetServerClient(Settings, new NUnitTestContextWrap(TestContext.CurrentContext, TestContext.Out)); // server dependencies SEntMan = Server.ResolveDependency(); diff --git a/Content.IntegrationTests/Tests/Internals/AutoInternalsTests.cs b/Content.IntegrationTests/Tests/Internals/AutoInternalsTests.cs index d153536873..d2f67e9830 100644 --- a/Content.IntegrationTests/Tests/Internals/AutoInternalsTests.cs +++ b/Content.IntegrationTests/Tests/Internals/AutoInternalsTests.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.Atmos.EntitySystems; using Content.Server.Body.Systems; using Content.Server.Station.Systems; @@ -7,12 +8,12 @@ namespace Content.IntegrationTests.Tests.Internals; [TestFixture] [TestOf(typeof(InternalsSystem))] -public sealed class AutoInternalsTests +public sealed class AutoInternalsTests : GameTest { [Test] public async Task TestInternalsAutoActivateInSpaceForStationSpawn() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -31,14 +32,12 @@ public sealed class AutoInternalsTests server.EntMan.DeleteEntity(dummy); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestInternalsAutoActivateInSpaceForEntitySpawn() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -55,8 +54,6 @@ public sealed class AutoInternalsTests server.EntMan.DeleteEntity(dummy); }); - - await pair.CleanReturnAsync(); } [TestPrototypes] diff --git a/Content.IntegrationTests/Tests/InventoryHelpersTest.cs b/Content.IntegrationTests/Tests/InventoryHelpersTest.cs index 39761ad089..c841e23365 100644 --- a/Content.IntegrationTests/Tests/InventoryHelpersTest.cs +++ b/Content.IntegrationTests/Tests/InventoryHelpersTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.Stunnable; using Content.Shared.Inventory; using Robust.Shared.GameObjects; @@ -7,7 +8,7 @@ using Robust.Shared.Map; namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class InventoryHelpersTest + public sealed class InventoryHelpersTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -39,7 +40,7 @@ namespace Content.IntegrationTests.Tests [Test] public async Task SpawnItemInSlotTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntities = server.ResolveDependency(); @@ -87,8 +88,6 @@ namespace Content.IntegrationTests.Tests #pragma warning restore NUnit2045 sEntities.DeleteEntity(human); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Lathe/LatheTest.cs b/Content.IntegrationTests/Tests/Lathe/LatheTest.cs index c335f8d6c8..f0a5c04237 100644 --- a/Content.IntegrationTests/Tests/Lathe/LatheTest.cs +++ b/Content.IntegrationTests/Tests/Lathe/LatheTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Lathe; using Content.Shared.Materials; using Content.Shared.Prototypes; @@ -11,12 +12,12 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Lathe; [TestFixture] -public sealed class LatheTest +public sealed class LatheTest : GameTest { [Test] public async Task TestLatheRecipeIngredientsFitLathe() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapData = await pair.CreateTestMap(); @@ -111,14 +112,12 @@ public sealed class LatheTest } }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task AllLatheRecipesValidTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var proto = server.ProtoMan; @@ -131,7 +130,5 @@ public sealed class LatheTest Assert.That(recipe.ResultReagents, Is.Not.Null, $"Recipe '{recipe.ID}' has no result or result reagents."); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs b/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs index b75b81ab3c..bd35c1f08e 100644 --- a/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs +++ b/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Tag; using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; @@ -11,12 +12,12 @@ namespace Content.IntegrationTests.Tests.Linter; /// Verify that the yaml linter successfully validates static fields /// [TestFixture] -public sealed class StaticFieldValidationTest +public sealed class StaticFieldValidationTest : GameTest { [Test] public async Task TestStaticFieldValidation() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var protoMan = pair.Server.ProtoMan; var protos = new Dictionary>(); @@ -49,8 +50,6 @@ public sealed class StaticFieldValidationTest Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdListInvalid), protos), Has.Count.EqualTo(2)); Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdSetInvalid), protos), Has.Count.EqualTo(2)); Assert.That(protoMan.ValidateStaticFields(typeof(PrivateProtoIdArrayInvalid), protos), Has.Count.EqualTo(2)); - - await pair.CleanReturnAsync(); } [TestPrototypes] diff --git a/Content.IntegrationTests/Tests/Lobby/CharacterCreationTest.cs b/Content.IntegrationTests/Tests/Lobby/CharacterCreationTest.cs index 2fa3c9961c..fb48797616 100644 --- a/Content.IntegrationTests/Tests/Lobby/CharacterCreationTest.cs +++ b/Content.IntegrationTests/Tests/Lobby/CharacterCreationTest.cs @@ -1,4 +1,5 @@ using Content.Client.Lobby; +using Content.IntegrationTests.Fixtures; using Content.Server.Preferences.Managers; using Content.Shared.Humanoid; using Content.Shared.Preferences; @@ -9,12 +10,14 @@ namespace Content.IntegrationTests.Tests.Lobby; [TestFixture] [TestOf(typeof(ClientPreferencesManager))] [TestOf(typeof(ServerPreferencesManager))] -public sealed class CharacterCreationTest +public sealed class CharacterCreationTest : GameTest { + public override PoolSettings PoolSettings => new() { InLobby = true }; + [Test] public async Task CreateDeleteCreateTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { InLobby = true }); + var pair = Pair; var server = pair.Server; var client = pair.Client; var user = pair.Client.User!.Value; @@ -72,7 +75,6 @@ public sealed class CharacterCreationTest serverCharacters = serverPrefManager.GetPreferences(user).Characters; Assert.That(serverCharacters, Has.Count.EqualTo(2)); AssertEqual(serverCharacters[1], profile); - await pair.CleanReturnAsync(); } private void AssertEqual(HumanoidCharacterProfile a, HumanoidCharacterProfile b) diff --git a/Content.IntegrationTests/Tests/Lobby/ServerReloginTest.cs b/Content.IntegrationTests/Tests/Lobby/ServerReloginTest.cs index 3dc2075887..e59bc269ad 100644 --- a/Content.IntegrationTests/Tests/Lobby/ServerReloginTest.cs +++ b/Content.IntegrationTests/Tests/Lobby/ServerReloginTest.cs @@ -1,20 +1,23 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.CCVar; using Robust.Server.Player; using Robust.Shared.Configuration; using Robust.Shared.Network; namespace Content.IntegrationTests.Tests.Lobby; -public sealed class ServerReloginTest +public sealed class ServerReloginTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings + { + Connected = true, + DummyTicker = false + }; + [Test] public async Task Relogin() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var client = pair.Client; var originalMaxPlayers = 0; @@ -62,7 +65,5 @@ public sealed class ServerReloginTest //Put the cvar back, so other tests can still use this server serverConfig.SetCVar(CCVars.SoftMaxPlayers, originalMaxPlayers); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Localization/EntityPrototypeLocalizationTest.cs b/Content.IntegrationTests/Tests/Localization/EntityPrototypeLocalizationTest.cs index 69d44fd08b..1b8fa16543 100644 --- a/Content.IntegrationTests/Tests/Localization/EntityPrototypeLocalizationTest.cs +++ b/Content.IntegrationTests/Tests/Localization/EntityPrototypeLocalizationTest.cs @@ -1,9 +1,10 @@ +using Content.IntegrationTests.Fixtures; using Robust.Shared.Localization; using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Localization; -public sealed class EntityPrototypeLocalizationTest +public sealed class EntityPrototypeLocalizationTest : GameTest { /// /// An explanation of why LocIds should not be used for entity prototype names/descriptions. @@ -18,7 +19,7 @@ public sealed class EntityPrototypeLocalizationTest [Test] public async Task TestNoManualEntityLocStrings() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ProtoMan; var locMan = server.ResolveDependency(); @@ -44,7 +45,5 @@ public sealed class EntityPrototypeLocalizationTest } } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Localization/LocalizedDatasetPrototypeTest.cs b/Content.IntegrationTests/Tests/Localization/LocalizedDatasetPrototypeTest.cs index 05f98c3d19..14aeaf648c 100644 --- a/Content.IntegrationTests/Tests/Localization/LocalizedDatasetPrototypeTest.cs +++ b/Content.IntegrationTests/Tests/Localization/LocalizedDatasetPrototypeTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Dataset; using Robust.Shared.Localization; using Robust.Shared.Prototypes; @@ -6,12 +7,12 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Localization; [TestFixture] -public sealed class LocalizedDatasetPrototypeTest +public sealed class LocalizedDatasetPrototypeTest : GameTest { [Test] public async Task ValidProtoIdsTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ResolveDependency(); @@ -36,7 +37,5 @@ public sealed class LocalizedDatasetPrototypeTest Assert.That(localizationMan.HasString(nextId), Is.False, $"LocalizedDataset {proto.ID} with prefix \"{proto.Values.Prefix}\" specifies {proto.Values.Count} entries, but a localized string exists with ID {nextId}! Does count need to be raised?"); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/MachineBoardTest.cs b/Content.IntegrationTests/Tests/MachineBoardTest.cs index e1533bbb8d..9554357ece 100644 --- a/Content.IntegrationTests/Tests/MachineBoardTest.cs +++ b/Content.IntegrationTests/Tests/MachineBoardTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Construction.Components; using Content.Shared.Construction.Components; using Robust.Shared.GameObjects; @@ -7,7 +8,7 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests; -public sealed class MachineBoardTest +public sealed class MachineBoardTest : GameTest { /// /// A list of machine boards that can be ignored by this test. @@ -32,7 +33,7 @@ public sealed class MachineBoardTest [Test] public async Task TestMachineBoardHasValidMachine() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ResolveDependency(); @@ -60,8 +61,6 @@ public sealed class MachineBoardTest }); } }); - - await pair.CleanReturnAsync(); } /// @@ -71,7 +70,7 @@ public sealed class MachineBoardTest [Test] public async Task TestComputerBoardHasValidComputer() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ResolveDependency(); @@ -100,8 +99,6 @@ public sealed class MachineBoardTest }); } }); - - await pair.CleanReturnAsync(); } /// @@ -111,7 +108,7 @@ public sealed class MachineBoardTest [Test] public async Task TestValidateBoardComponentRequirements() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -136,7 +133,5 @@ public sealed class MachineBoardTest }); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/MagazineVisualsSpriteTest.cs b/Content.IntegrationTests/Tests/MagazineVisualsSpriteTest.cs index 6d48a668a5..43fea6d6c0 100644 --- a/Content.IntegrationTests/Tests/MagazineVisualsSpriteTest.cs +++ b/Content.IntegrationTests/Tests/MagazineVisualsSpriteTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Content.Client.Weapons.Ranged.Components; +using Content.IntegrationTests.Fixtures; using Robust.Client.GameObjects; using Robust.Shared.GameObjects; @@ -9,12 +10,12 @@ namespace Content.IntegrationTests.Tests; /// Tests all entity prototypes with the MagazineVisualsComponent. /// [TestFixture] -public sealed class MagazineVisualsSpriteTest +public sealed class MagazineVisualsSpriteTest : GameTest { [Test] public async Task MagazineVisualsSpritesExist() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var client = pair.Client; var toTest = new List<(int, string)>(); var protos = pair.GetPrototypesWithComponent(); @@ -67,7 +68,5 @@ public sealed class MagazineVisualsSpriteTest } }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Mapping/MappingTests.cs b/Content.IntegrationTests/Tests/Mapping/MappingTests.cs index be8bad229b..0bf637cf19 100644 --- a/Content.IntegrationTests/Tests/Mapping/MappingTests.cs +++ b/Content.IntegrationTests/Tests/Mapping/MappingTests.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Robust.Server.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.Map; @@ -5,15 +6,18 @@ using Robust.Shared.Map; namespace Content.IntegrationTests.Tests.Mapping; [TestFixture] -public sealed class MappingTests +public sealed class MappingTests : GameTest { + public override PoolSettings PoolSettings => + new() { Dirty = true, Connected = true, DummyTicker = false }; + /// /// Checks that the mapping command creates paused & uninitialized maps. /// [Test] public async Task MappingTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true, Connected = true, DummyTicker = false }); + var pair = Pair; var server = pair.Server; var entMan = server.EntMan; @@ -97,6 +101,5 @@ public sealed class MappingTests Assert.That(server.MetaData(ent).EntityPaused, Is.True); await server.WaitPost(() => entMan.DeleteEntity(map)); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/MappingEditorTest.cs b/Content.IntegrationTests/Tests/MappingEditorTest.cs index bd930aef9e..987fe7ad73 100644 --- a/Content.IntegrationTests/Tests/MappingEditorTest.cs +++ b/Content.IntegrationTests/Tests/MappingEditorTest.cs @@ -1,19 +1,17 @@ using Content.Client.Gameplay; using Content.Client.Mapping; +using Content.IntegrationTests.Fixtures; using Robust.Client.State; namespace Content.IntegrationTests.Tests; [TestFixture] -public sealed class MappingEditorTest +public sealed class MappingEditorTest : GameTest { [Test] public async Task StopHardCodingWidgetsJesusChristTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true - }); + var pair = Pair; var client = pair.Client; var state = client.ResolveDependency(); @@ -35,7 +33,5 @@ public sealed class MappingEditorTest state.RequestStateChange(); }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Markings/MarkingManagerTests.cs b/Content.IntegrationTests/Tests/Markings/MarkingManagerTests.cs index c81ff6a698..af5c6c2018 100644 --- a/Content.IntegrationTests/Tests/Markings/MarkingManagerTests.cs +++ b/Content.IntegrationTests/Tests/Markings/MarkingManagerTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; using Content.Shared.Body; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Markings; @@ -9,7 +10,7 @@ namespace Content.IntegrationTests.Tests.Markings; [TestFixture] [TestOf(typeof(MarkingManager))] -public sealed class MarkingManagerTests +public sealed class MarkingManagerTests : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -76,7 +77,7 @@ public sealed class MarkingManagerTests [Test] public async Task HairConvesion() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -96,14 +97,12 @@ public sealed class MarkingManagerTests Assert.That(hairMarkings[0].MarkingId, Is.EqualTo("HumanHairLongBedhead2")); Assert.That(hairMarkings[0].MarkingColors[0], Is.EqualTo(Color.Red)); }); - - await pair.CleanReturnAsync(); } [Test] public async Task LimitsFilling() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -118,14 +117,12 @@ public sealed class MarkingManagerTests Assert.That(dict[HumanoidVisualLayers.Eyes], Has.Count.EqualTo(1)); Assert.That(dict[HumanoidVisualLayers.Eyes][0].MarkingId, Is.EqualTo("EyesMarking")); }); - - await pair.CleanReturnAsync(); } [Test] public async Task LimitsTruncations() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -146,14 +143,12 @@ public sealed class MarkingManagerTests Assert.That(dict[HumanoidVisualLayers.Eyes], Has.Count.EqualTo(1)); Assert.That(dict[HumanoidVisualLayers.Eyes][0].MarkingId, Is.EqualTo("MenOnlyMarking")); }); - - await pair.CleanReturnAsync(); } [Test] public async Task EnsureValidGroupAndSex() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -191,14 +186,12 @@ public sealed class MarkingManagerTests Assert.That(testingMenMarkings[HumanoidVisualLayers.Eyes][1].MarkingId, Is.EqualTo("TestingOnlyMarking")); Assert.That(testingMenMarkings[HumanoidVisualLayers.Eyes][2].MarkingId, Is.EqualTo("TestingMenOnlyMarking")); }); - - await pair.CleanReturnAsync(); } [Test] public async Task EnsureValidColors() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -232,7 +225,5 @@ public sealed class MarkingManagerTests Assert.That(eyeMarkings[1].MarkingColors[0], Is.EqualTo(Color.Red)); Assert.That(eyeMarkings[3].MarkingColors[0], Is.EqualTo(Color.Green)); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Markings/MarkingsViewModelTests.cs b/Content.IntegrationTests/Tests/Markings/MarkingsViewModelTests.cs index f69d3356c2..026d2ecf3b 100644 --- a/Content.IntegrationTests/Tests/Markings/MarkingsViewModelTests.cs +++ b/Content.IntegrationTests/Tests/Markings/MarkingsViewModelTests.cs @@ -59,7 +59,7 @@ public sealed class MarkingsViewModelTests [SetUp] public async Task SetUp() { - Pair = await PoolManager.GetServerClient(); + Pair = await PoolManager.GetServerClient(testContext: new NUnitTestContextWrap(TestContext.CurrentContext, TestContext.Out)); await Client.WaitPost(() => { Model = new MarkingsViewModel(); diff --git a/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs b/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs index 3c6c372b75..460146357c 100644 --- a/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs +++ b/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; using Content.Server.Cargo.Systems; using Content.Server.Construction.Completions; using Content.Server.Construction.Components; @@ -26,7 +27,7 @@ namespace Content.IntegrationTests.Tests; /// create them. /// [TestFixture] -public sealed class MaterialArbitrageTest +public sealed class MaterialArbitrageTest : GameTest { // These sets are for selectively excluding recipes from arbitrage. // You should NOT be adding to these. They exist here for downstreams and potential future issues. @@ -36,7 +37,7 @@ public sealed class MaterialArbitrageTest [Test] public async Task NoMaterialArbitrage() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -439,7 +440,6 @@ public sealed class MaterialArbitrageTest }); await server.WaitPost(() => mapSystem.DeleteMap(testMap.MapId)); - await pair.CleanReturnAsync(); async Task GetSpawnedPrice(Dictionary ents) { diff --git a/Content.IntegrationTests/Tests/Materials/MaterialTests.cs b/Content.IntegrationTests/Tests/Materials/MaterialTests.cs index a177869e7f..43c7dab2b0 100644 --- a/Content.IntegrationTests/Tests/Materials/MaterialTests.cs +++ b/Content.IntegrationTests/Tests/Materials/MaterialTests.cs @@ -1,4 +1,5 @@ #nullable enable +using Content.IntegrationTests.Fixtures; using Content.Server.Stack; using Content.Shared.Stacks; using Content.Shared.Materials; @@ -14,12 +15,12 @@ namespace Content.IntegrationTests.Tests.Materials [TestFixture] [TestOf(typeof(StackSystem))] [TestOf(typeof(MaterialPrototype))] - public sealed class MaterialPrototypeSpawnsStackMaterialTest + public sealed class MaterialPrototypeSpawnsStackMaterialTest : GameTest { [Test] public async Task MaterialPrototypeSpawnsStackMaterial() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -60,8 +61,6 @@ namespace Content.IntegrationTests.Tests.Materials mapSystem.DeleteMap(testMap.MapId); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Minds/GhostRoleTests.cs b/Content.IntegrationTests/Tests/Minds/GhostRoleTests.cs index 32fcf9c1ad..7369fece35 100644 --- a/Content.IntegrationTests/Tests/Minds/GhostRoleTests.cs +++ b/Content.IntegrationTests/Tests/Minds/GhostRoleTests.cs @@ -1,5 +1,6 @@ #nullable enable using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Ghost.Roles; using Content.Server.Ghost.Roles.Components; using Content.Shared.Ghost; @@ -12,7 +13,7 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Minds; [TestFixture] -public sealed class GhostRoleTests +public sealed class GhostRoleTests : GameTest { private const string GhostRoleProtoId = "GhostRoleTestEntity"; private const string TestMobProtoId = "GhostRoleTestMob"; @@ -33,6 +34,13 @@ public sealed class GhostRoleTests - type: MobState # MobState is required for correct determination of if the player can return to body or not """; + public override PoolSettings PoolSettings => new() + { + Dirty = true, + DummyTicker = false, + Connected = true + }; + /// /// This is a simple test that just checks if a player can take a ghost role and then regain control of their /// original entity without encountering errors. @@ -43,12 +51,7 @@ public sealed class GhostRoleTests { var ghostCommand = adminGhost ? "aghost" : "ghost"; - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Dirty = true, - DummyTicker = false, - Connected = true - }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -210,7 +213,6 @@ public sealed class GhostRoleTests if (!adminGhost) { // End of the normal player ghost role test - await pair.CleanReturnAsync(); return; } @@ -242,7 +244,5 @@ public sealed class GhostRoleTests // Check that there is are no lingereing ghosts Assert.That(entMan.Count(), Is.Zero); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Minds/GhostTests.cs b/Content.IntegrationTests/Tests/Minds/GhostTests.cs index 3a860267e5..26991c08ad 100644 --- a/Content.IntegrationTests/Tests/Minds/GhostTests.cs +++ b/Content.IntegrationTests/Tests/Minds/GhostTests.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.IntegrationTests.Pair; using Content.Shared.Ghost; using Content.Shared.Mind; @@ -12,7 +13,7 @@ using Robust.UnitTesting; namespace Content.IntegrationTests.Tests.Minds; [TestFixture] -public sealed class GhostTests +public sealed class GhostTests : GameTest { private struct GhostTestData { @@ -45,17 +46,20 @@ public sealed class GhostTests } } + // Client is needed to create a session for the ghost system. Creating a dummy session was too difficult. + public override PoolSettings PoolSettings => new() + { + DummyTicker = false, + Connected = true, + Dirty = true + }; + private async Task SetupData() { var data = new GhostTestData { - // Client is needed to create a session for the ghost system. Creating a dummy session was too difficult. - Pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - Dirty = true - }) + // ..Just use the gametest pair, please. + Pair = Pair, }; data.SEntMan = data.Pair.Server.ResolveDependency(); @@ -126,8 +130,6 @@ public sealed class GhostTests // Ensure the position is the same var ghostPosition = data.SEntMan.GetComponent(ghost).Coordinates; Assert.That(ghostPosition, Is.EqualTo(oldPosition)); - - await data.Pair.CleanReturnAsync(); } /// @@ -154,8 +156,6 @@ public sealed class GhostTests // Ensure the position is the same var ghostPosition = data.SEntMan.GetComponent(ghost).Coordinates; Assert.That(ghostPosition, Is.EqualTo(oldPosition)); - - await data.Pair.CleanReturnAsync(); } [Test] @@ -168,10 +168,6 @@ public sealed class GhostTests // Delete the grid await data.Server.WaitPost(() => data.SEntMan.DeleteEntity(data.MapData.Grid.Owner)); }); - - await data.Pair.RunTicksSync(5); - - await data.Pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs b/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs index ad4ddc2612..c207e7b489 100644 --- a/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs +++ b/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs @@ -11,13 +11,7 @@ public sealed partial class MindTests [Test] public async Task DeleteAllThenGhost() { - var settings = new PoolSettings - { - Dirty = true, - DummyTicker = false, - Connected = true - }; - await using var pair = await PoolManager.GetServerClient(settings); + var pair = Pair; // Client is connected with a valid entity & mind Assert.That(pair.Client.EntMan.EntityExists(pair.Client.AttachedEntity)); @@ -56,7 +50,5 @@ public sealed partial class MindTests Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind)); var xform = pair.Client.Transform(pair.Client.AttachedEntity!.Value); Assert.That(xform.MapID, Is.EqualTo(mapId)); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs b/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs index 513049bcad..cf87b3ea76 100644 --- a/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs +++ b/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs @@ -23,7 +23,7 @@ public sealed partial class MindTests [Test] public async Task TestDeleteVisiting() { - await using var pair = await SetupPair(); + var pair = await SetupPair(); var server = pair.Server; var entMan = server.ResolveDependency(); @@ -67,15 +67,13 @@ public sealed partial class MindTests // This used to throw so make sure it doesn't. await server.WaitPost(() => entMan.DeleteEntity(mind.OwnedEntity!.Value)); await pair.RunTicksSync(5); - - await pair.CleanReturnAsync(); } // this is a variant of TestGhostOnDelete that just deletes the whole map. [Test] public async Task TestGhostOnDeleteMap() { - await using var pair = await SetupPair(dirty: true); + var pair = await SetupPair(dirty: true); var server = pair.Server; var testMap = await pair.CreateTestMap(); var testMap2 = await pair.CreateTestMap(); @@ -117,8 +115,6 @@ public sealed partial class MindTests Assert.That(transform.MapID, Is.Not.EqualTo(testMap.MapId)); #pragma warning restore NUnit2045 }); - - await pair.CleanReturnAsync(); } /// @@ -130,7 +126,7 @@ public sealed partial class MindTests public async Task TestGhostOnDelete() { // Client is needed to spawn session - await using var pair = await SetupPair(dirty: true); + var pair = await SetupPair(dirty: true); var server = pair.Server; var entMan = server.ResolveDependency(); @@ -145,8 +141,6 @@ public sealed partial class MindTests await pair.RunTicksSync(5); Assert.That(entMan.HasComponent(player.AttachedEntity), "Player did not become a ghost"); - - await pair.CleanReturnAsync(); } /// @@ -162,7 +156,7 @@ public sealed partial class MindTests public async Task TestOriginalDeletedWhileGhostingKeepsGhost() { // Client is needed to spawn session - await using var pair = await SetupPair(); + var pair = await SetupPair(); var server = pair.Server; var entMan = server.ResolveDependency(); @@ -207,8 +201,6 @@ public sealed partial class MindTests Assert.That(mind.Comp.VisitingEntity, Is.Null); Assert.That(mind.Comp.OwnedEntity, Is.EqualTo(ghost)); }); - - await pair.CleanReturnAsync(); } /// @@ -220,7 +212,7 @@ public sealed partial class MindTests [Test] public async Task TestGhostToAghost() { - await using var pair = await SetupPair(); + var pair = await SetupPair(); var server = pair.Server; var entMan = server.ResolveDependency(); var playerMan = server.ResolveDependency(); @@ -250,8 +242,6 @@ public sealed partial class MindTests var mind = entMan.GetComponent(mindId!.Value); Assert.That(mind.VisitingEntity, Is.Null); - - await pair.CleanReturnAsync(); } /// @@ -264,7 +254,7 @@ public sealed partial class MindTests public async Task TestGhostDeletedSpawnsNewGhost() { // Client is needed to spawn session - await using var pair = await SetupPair(); + var pair = await SetupPair(); var server = pair.Server; var entMan = server.ResolveDependency(); @@ -308,7 +298,5 @@ public sealed partial class MindTests Assert.That(entMan.HasComponent(player.AttachedEntity!.Value)); #pragma warning restore NUnit2045 }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs b/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs index cebbed8ee8..e22f6c1e4d 100644 --- a/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs +++ b/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs @@ -18,6 +18,14 @@ namespace Content.IntegrationTests.Tests.Minds; // This partial class contains misc helper functions for other tests. public sealed partial class MindTests { + // TODO GAMETEST: Rewrite this test to use improved GameTest pair management when I have an API for that figured out. + public override PoolSettings PoolSettings => new() + { + DummyTicker = false, + Connected = true, + Dirty = true, + }; + /// /// Gets a server-client pair and ensures that the client is attached to a simple mind test entity. /// @@ -26,15 +34,9 @@ public sealed partial class MindTests /// the player's mind's current entity, likely because some previous test directly changed the players attached /// entity. /// - private static async Task SetupPair(bool dirty = false) + private async Task SetupPair(bool dirty = false) { - var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - Dirty = dirty - }); - + var pair = Pair; var entMan = pair.Server.ResolveDependency(); var playerMan = pair.Server.ResolveDependency(); var mindSys = entMan.System(); diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.ReconnectTests.cs b/Content.IntegrationTests/Tests/Minds/MindTests.ReconnectTests.cs index db87797553..fbe79fe044 100644 --- a/Content.IntegrationTests/Tests/Minds/MindTests.ReconnectTests.cs +++ b/Content.IntegrationTests/Tests/Minds/MindTests.ReconnectTests.cs @@ -17,7 +17,7 @@ public sealed partial class MindTests [Test] public async Task TestGhostsCanReconnect() { - await using var pair = await SetupPair(); + var pair = await SetupPair(); var entMan = pair.Server.ResolveDependency(); var mind = GetMind(pair); @@ -32,8 +32,6 @@ public sealed partial class MindTests Assert.That(entMan.HasComponent(mind.Comp.OwnedEntity)); Assert.That(mind.Comp.VisitingEntity, Is.Null); }); - - await pair.CleanReturnAsync(); } // This test will do the following: @@ -44,7 +42,7 @@ public sealed partial class MindTests [Test] public async Task TestDeletedCanReconnect() { - await using var pair = await SetupPair(); + var pair = await SetupPair(); var entMan = pair.Server.ResolveDependency(); var mind = GetMind(pair); @@ -82,8 +80,6 @@ public sealed partial class MindTests Assert.That(mind.Comp.OwnedEntity, Is.Not.EqualTo(entity)); Assert.That(entMan.HasComponent(mind.Comp.OwnedEntity)); }); - - await pair.CleanReturnAsync(); } // This test will do the following: @@ -94,7 +90,7 @@ public sealed partial class MindTests [Test] public async Task TestVisitingGhostReconnect() { - await using var pair = await SetupPair(); + var pair = await SetupPair(); var entMan = pair.Server.ResolveDependency(); var mind = GetMind(pair); @@ -110,8 +106,6 @@ public sealed partial class MindTests Assert.That(entMan.Deleted(original), Is.False); Assert.That(entMan.Deleted(ghost)); }); - - await pair.CleanReturnAsync(); } // This test will do the following: @@ -122,7 +116,7 @@ public sealed partial class MindTests [Test] public async Task TestVisitingReconnect() { - await using var pair = await SetupPair(true); + var pair = await SetupPair(true); var entMan = pair.Server.ResolveDependency(); var mindSys = entMan.System(); var mind = GetMind(pair); @@ -150,8 +144,6 @@ public sealed partial class MindTests Assert.That(entMan.Deleted(visiting), Is.False); Assert.That(mind.Comp.CurrentEntity, Is.EqualTo(visiting)); }); - - await pair.CleanReturnAsync(); } // This test will do the following @@ -162,7 +154,7 @@ public sealed partial class MindTests [Test] public async Task TestReconnect() { - await using var pair = await SetupPair(); + var pair = await SetupPair(); var mind = GetMind(pair); Assert.That(mind.Comp.VisitingEntity, Is.Null); @@ -178,7 +170,5 @@ public sealed partial class MindTests Assert.That(newMind.Comp.VisitingEntity, Is.Null); Assert.That(newMind.Comp.OwnedEntity, Is.EqualTo(entity)); Assert.That(newMind.Id, Is.EqualTo(mind.Id)); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.cs b/Content.IntegrationTests/Tests/Minds/MindTests.cs index 35069339ba..08cfbaed5e 100644 --- a/Content.IntegrationTests/Tests/Minds/MindTests.cs +++ b/Content.IntegrationTests/Tests/Minds/MindTests.cs @@ -1,5 +1,6 @@ #nullable enable using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Ghost.Roles; using Content.Server.Ghost.Roles.Components; using Content.Server.Mind; @@ -23,7 +24,7 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Minds; [TestFixture] -public sealed partial class MindTests +public sealed partial class MindTests : GameTest { private static readonly ProtoId BluntDamageType = "Blunt"; @@ -56,7 +57,7 @@ public sealed partial class MindTests [Test] public async Task TestCreateAndTransferMindToNewEntity() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -75,14 +76,12 @@ public sealed partial class MindTests mindSystem.TransferTo(mind, entity, mind: mind); Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind.Owner)); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestReplaceMind() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -107,14 +106,12 @@ public sealed partial class MindTests Assert.That(mind.OwnedEntity, Is.Not.EqualTo(entity)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestEntityDeadWhenGibbed() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -160,14 +157,12 @@ public sealed partial class MindTests var mind = entMan.GetComponent(mindId); Assert.That(mindSystem.IsCharacterDeadPhysically(mind)); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestMindTransfersToOtherEntity() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -194,18 +189,12 @@ public sealed partial class MindTests Assert.That(mindSystem.GetMind(targetEntity), Is.EqualTo(mind)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestOwningPlayerCanBeChanged() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -253,14 +242,12 @@ public sealed partial class MindTests }); await pair.RunTicksSync(5); - - await pair.CleanReturnAsync(); } [Test] public async Task TestAddRemoveHasRoles() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -323,15 +310,13 @@ public sealed partial class MindTests Assert.That(roleSystem.MindHasRole(mindId), Is.False); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestPlayerCanGhost() { // Client is needed to spawn session - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true, DummyTicker = false }); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -399,19 +384,12 @@ public sealed partial class MindTests Assert.That(mId, Is.Not.EqualTo(mindId)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestGhostDoesNotInfiniteLoop() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - Dirty = true - }); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -482,7 +460,5 @@ public sealed partial class MindTests Assert.That(player.AttachedEntity, Is.Not.Null); Assert.That(player.AttachedEntity!.Value, Is.EqualTo(ghost)); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Minds/RoleTests.cs b/Content.IntegrationTests/Tests/Minds/RoleTests.cs index f0a7268a3d..3204dc1c2f 100644 --- a/Content.IntegrationTests/Tests/Minds/RoleTests.cs +++ b/Content.IntegrationTests/Tests/Minds/RoleTests.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Roles.Components; using Robust.Shared.GameObjects; using Robust.Shared.Reflection; @@ -6,7 +7,7 @@ using Robust.Shared.Reflection; namespace Content.IntegrationTests.Tests.Minds; [TestFixture] -public sealed class RoleTests +public sealed class RoleTests : GameTest { /// /// Check that any prototype with a is properly configured @@ -14,7 +15,7 @@ public sealed class RoleTests [Test] public async Task ValidateRolePrototypes() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var jobComp = pair.Server.ResolveDependency().GetComponentName(); @@ -35,7 +36,6 @@ public sealed class RoleTests } }); - await pair.CleanReturnAsync(); } /// @@ -45,7 +45,7 @@ public sealed class RoleTests [Test] public async Task ValidateJobPrototypes() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var mindCompId = pair.Server.ResolveDependency().GetComponentName(); @@ -57,8 +57,6 @@ public sealed class RoleTests Assert.That(((MindRoleComponent)mindComp).JobPrototype, Is.Not.Null); } }); - - await pair.CleanReturnAsync(); } /// @@ -68,7 +66,7 @@ public sealed class RoleTests [Test] public async Task ValidateRolesHaveMindRoleComp() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var refMan = pair.Server.ResolveDependency(); var mindCompId = pair.Server.ResolveDependency().GetComponentName(); @@ -87,7 +85,5 @@ public sealed class RoleTests } } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/NPC/NPCTest.cs b/Content.IntegrationTests/Tests/NPC/NPCTest.cs index 064fd6c5bf..1568f360e7 100644 --- a/Content.IntegrationTests/Tests/NPC/NPCTest.cs +++ b/Content.IntegrationTests/Tests/NPC/NPCTest.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; using Content.Server.NPC.HTN; using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; @@ -7,12 +8,12 @@ using Robust.Shared.Utility; namespace Content.IntegrationTests.Tests.NPC; [TestFixture] -public sealed class NPCTest +public sealed class NPCTest : GameTest { [Test] public async Task CompoundRecursion() { - var pool = await PoolManager.GetServerClient(); + var pool = Pair; var server = pool.Server; await server.WaitIdleAsync(); @@ -30,8 +31,6 @@ public sealed class NPCTest counts.Clear(); } }); - - await pool.CleanReturnAsync(); } private static void Count(HTNCompoundPrototype compound, Dictionary counts, HTNSystem htnSystem, IPrototypeManager protoManager) diff --git a/Content.IntegrationTests/Tests/Networking/NetworkIdsMatchTest.cs b/Content.IntegrationTests/Tests/Networking/NetworkIdsMatchTest.cs index c72be944fd..7b4f048939 100644 --- a/Content.IntegrationTests/Tests/Networking/NetworkIdsMatchTest.cs +++ b/Content.IntegrationTests/Tests/Networking/NetworkIdsMatchTest.cs @@ -1,14 +1,15 @@ +using Content.IntegrationTests.Fixtures; using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests.Networking { [TestFixture] - public sealed class NetworkIdsMatchTest + public sealed class NetworkIdsMatchTest : GameTest { [Test] public async Task TestConnect() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -38,7 +39,6 @@ namespace Content.IntegrationTests.Tests.Networking Assert.That(clientNetComps[netId].Name, Is.EqualTo(serverNetComps[netId].Name)); } }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Networking/PvsCommandTest.cs b/Content.IntegrationTests/Tests/Networking/PvsCommandTest.cs index b395569848..786e14ae42 100644 --- a/Content.IntegrationTests/Tests/Networking/PvsCommandTest.cs +++ b/Content.IntegrationTests/Tests/Networking/PvsCommandTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Robust.Shared.GameObjects; using Robust.Shared.Map.Components; using Robust.Shared.Prototypes; @@ -5,16 +6,17 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Networking; [TestFixture] -public sealed class PvsCommandTest +public sealed class PvsCommandTest : GameTest { private static readonly EntProtoId TestEnt = "MobHuman"; + public override PoolSettings PoolSettings => new() { Connected = true, DummyTicker = false }; + [Test] public async Task TestPvsCommands() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true, DummyTicker = false }); + var pair = Pair; var (server, client) = pair; - await pair.RunTicksSync(5); // Spawn a complex entity. EntityUid entity = default; @@ -46,6 +48,5 @@ public sealed class PvsCommandTest Assert.That(meta.LastStateApplied, Is.GreaterThan(lastApplied)); await server.WaitPost(() => server.EntMan.DeleteEntity(entity)); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Networking/ReconnectTest.cs b/Content.IntegrationTests/Tests/Networking/ReconnectTest.cs index 4539ca81a5..26e76376ce 100644 --- a/Content.IntegrationTests/Tests/Networking/ReconnectTest.cs +++ b/Content.IntegrationTests/Tests/Networking/ReconnectTest.cs @@ -1,15 +1,16 @@ +using Content.IntegrationTests.Fixtures; using Robust.Client.Console; using Robust.Shared.Network; namespace Content.IntegrationTests.Tests.Networking { [TestFixture] - public sealed class ReconnectTest + public sealed class ReconnectTest : GameTest { [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -32,7 +33,6 @@ namespace Content.IntegrationTests.Tests.Networking await pair.RunTicksSync(10); await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs b/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs index 3e49499845..15c7373b63 100644 --- a/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs +++ b/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Generic; using System.Numerics; +using Content.IntegrationTests.Fixtures; using Robust.Client.GameStates; using Robust.Client.Timing; using Robust.Shared; @@ -27,12 +28,12 @@ namespace Content.IntegrationTests.Tests.Networking // the tick where the server *should* have, but did not, acknowledge the state change. // Finally, we run two events inside the prediction area to ensure reconciling does for incremental stuff. [TestFixture] - public sealed class SimplePredictReconcileTest + public sealed class SimplePredictReconcileTest : GameTest { [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -386,7 +387,6 @@ namespace Content.IntegrationTests.Tests.Networking } cfg.SetCVar(CVars.NetLogging, log); - await pair.CleanReturnAsync(); } public sealed class PredictionTestEntitySystem : EntitySystem diff --git a/Content.IntegrationTests/Tests/Physics/AnchorPrototypeTest.cs b/Content.IntegrationTests/Tests/Physics/AnchorPrototypeTest.cs index a65e7d1fd6..f65bd324a0 100644 --- a/Content.IntegrationTests/Tests/Physics/AnchorPrototypeTest.cs +++ b/Content.IntegrationTests/Tests/Physics/AnchorPrototypeTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Robust.Shared.GameObjects; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; @@ -6,7 +7,7 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Physics; [TestFixture] -public sealed class AnchorPrototypeTest +public sealed class AnchorPrototypeTest : GameTest { /// /// Asserts that entityprototypes marked as anchored are also static physics bodies. @@ -14,7 +15,7 @@ public sealed class AnchorPrototypeTest [Test] public async Task TestStaticAnchorPrototypes() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var protoManager = pair.Server.ResolveDependency(); @@ -37,7 +38,5 @@ public sealed class AnchorPrototypeTest Assert.That(physics.BodyType, Is.EqualTo(BodyType.Static), $"Found entity prototype {ent} marked as anchored but not static for physics."); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs index 6147fb6e17..a02179efc6 100644 --- a/Content.IntegrationTests/Tests/PostMapInitTest.cs +++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; using Content.IntegrationTests.Utility; using YamlDotNet.RepresentationModel; using Content.Server.Administration.Systems; @@ -28,8 +30,14 @@ using Robust.Shared.Utility; namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class PostMapInitTest + public sealed class PostMapInitTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings() + { + Connected = true, + Dirty = true, + }; + private const bool SkipTestMaps = true; private const string TestMapsPath = "/Maps/Test/"; @@ -93,16 +101,16 @@ namespace Content.IntegrationTests.Tests /// Asserts that specific files have been saved as grids and not maps. /// [Test, TestCaseSource(nameof(Grids))] + [EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)] public async Task GridsLoadableTest(string mapFile) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); var mapLoader = entManager.System(); var mapSystem = entManager.System(); var cfg = server.ResolveDependency(); - Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False); var path = new ResPath(mapFile); await server.WaitPost(() => @@ -119,9 +127,6 @@ namespace Content.IntegrationTests.Tests mapSystem.DeleteMap(mapId); }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); } /// @@ -129,16 +134,16 @@ namespace Content.IntegrationTests.Tests /// [Test] [TestCaseSource(nameof(ShuttleMapFiles))] + [EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)] public async Task ShuttlesLoadableTest(ResPath path) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); var mapLoader = entManager.System(); var mapSystem = entManager.System(); var cfg = server.ResolveDependency(); - Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False); await server.WaitPost(() => { @@ -158,17 +163,13 @@ namespace Content.IntegrationTests.Tests mapSystem.DeleteMap(mapId); }); }); - - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); } [Test] [TestCaseSource(nameof(AllMapFiles))] public async Task NoSavedPostMapInitTest(ResPath map) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var resourceManager = server.ResolveDependency(); @@ -182,7 +183,6 @@ namespace Content.IntegrationTests.Tests // ReSharper disable once RedundantLogicalConditionalExpressionOperand if (SkipTestMaps && rootedPath.ToString().StartsWith(TestMapsPath, StringComparison.Ordinal)) { - await pair.CleanReturnAsync(); return; // We just pass immediately. } @@ -239,8 +239,6 @@ namespace Content.IntegrationTests.Tests await server.WaitPost(() => mapSys.InitializeMap(id)); Assert.That(loader.TrySaveMap(id, path)); Assert.That(IsPreInit(path, loader, deps, ev.RenamedPrototypes, ev.DeletedPrototypes), Is.False); - - await pair.CleanReturnAsync(); } private bool IsWhitelistedForMap(EntProtoId protoId, ResPath map) @@ -325,12 +323,10 @@ namespace Content.IntegrationTests.Tests } [Test, TestCaseSource(nameof(GameMaps))] + [EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)] public async Task GameMapsLoadableTest(string mapProto) { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Dirty = true // Stations spawn a bunch of nullspace entities and maps like centcomm. - }); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); @@ -341,7 +337,6 @@ namespace Content.IntegrationTests.Tests var ticker = entManager.EntitySysManager.GetEntitySystem(); var shuttleSystem = entManager.EntitySysManager.GetEntitySystem(); var cfg = server.ResolveDependency(); - Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False); await server.WaitPost(() => { @@ -440,9 +435,6 @@ namespace Content.IntegrationTests.Tests throw new Exception($"Failed to delete map {mapProto}", ex); } }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); } @@ -471,37 +463,18 @@ namespace Content.IntegrationTests.Tests return resultCount; } - [Test] - public async Task AllMapsTested() - { - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; - var protoMan = server.ResolveDependency(); - - var gameMaps = protoMan.EnumeratePrototypes() - .Where(x => !pair.IsTestPrototype(x)) - .Select(x => x.ID) - .ToHashSet(); - - Assert.That(gameMaps.Remove(PoolManager.TestMap)); - - Assert.That(gameMaps, Is.EquivalentTo(GameMaps.ToHashSet()), "Game map prototype missing from test cases."); - - await pair.CleanReturnAsync(); - } - [Test] [TestCaseSource(nameof(AllMapFiles))] + [EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)] public async Task NonGameMapsLoadableTest(ResPath mapPath) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapLoader = server.ResolveDependency().GetEntitySystem(); var resourceManager = server.ResolveDependency(); var protoManager = server.ResolveDependency(); var cfg = server.ResolveDependency(); - Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False); var gameMaps = protoManager.EnumeratePrototypes().Select(o => o.MapPath).ToHashSet(); @@ -510,7 +483,6 @@ namespace Content.IntegrationTests.Tests { // TODO: You might be able to save like, 1-2 seconds of test time if you eliminate these before // actually needing a pair. - await pair.CleanReturnAsync(); return; } @@ -518,7 +490,6 @@ namespace Content.IntegrationTests.Tests if (SkipTestMaps && rootedPath.ToString().StartsWith(TestMapsPath, StringComparison.Ordinal)) { - await pair.CleanReturnAsync(); return; } @@ -561,9 +532,6 @@ namespace Content.IntegrationTests.Tests } }); }); - - await server.WaitRunTicks(1); - await pair.CleanReturnAsync(); } /// diff --git a/Content.IntegrationTests/Tests/Power/PowerStatePrototypeTest.cs b/Content.IntegrationTests/Tests/Power/PowerStatePrototypeTest.cs index 288e976e9b..e991801330 100644 --- a/Content.IntegrationTests/Tests/Power/PowerStatePrototypeTest.cs +++ b/Content.IntegrationTests/Tests/Power/PowerStatePrototypeTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Power.Components; using Content.Shared.Power.Components; using Content.Shared.Power.EntitySystems; @@ -8,7 +9,7 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Power; [TestFixture, TestOf(typeof(SharedPowerStateSystem))] -public sealed class PowerStatePrototypeTest +public sealed class PowerStatePrototypeTest : GameTest { /// /// Asserts that the 's load is the same @@ -18,7 +19,7 @@ public sealed class PowerStatePrototypeTest [Test] public async Task AssertApcPowerMatchesPowerState() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ResolveDependency(); @@ -53,7 +54,5 @@ public sealed class PowerStatePrototypeTest } }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Power/PowerStateTest.cs b/Content.IntegrationTests/Tests/Power/PowerStateTest.cs index edec6f3d21..a265de532f 100644 --- a/Content.IntegrationTests/Tests/Power/PowerStateTest.cs +++ b/Content.IntegrationTests/Tests/Power/PowerStateTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Coordinates; using Content.Shared.Power.Components; using Content.Shared.Power.EntitySystems; @@ -8,7 +9,7 @@ using Robust.Shared.Maths; namespace Content.IntegrationTests.Tests.Power; [TestFixture] -public sealed class PowerStateTest +public sealed class PowerStateTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -31,7 +32,7 @@ public sealed class PowerStateTest [Test] public async Task SetWorkingState_IdleToWorking_UpdatesLoad() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); @@ -65,8 +66,6 @@ public sealed class PowerStateTest Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f)); }); }); - - await pair.CleanReturnAsync(); } /// @@ -75,7 +74,7 @@ public sealed class PowerStateTest [Test] public async Task SetWorkingState_WorkingToIdle_UpdatesLoad() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); @@ -118,8 +117,6 @@ public sealed class PowerStateTest Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f)); }); }); - - await pair.CleanReturnAsync(); } /// @@ -128,7 +125,7 @@ public sealed class PowerStateTest [Test] public async Task SetWorkingState_AlreadyInState_NoChange() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); @@ -179,8 +176,6 @@ public sealed class PowerStateTest Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f)); }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Power/PowerTest.cs b/Content.IntegrationTests/Tests/Power/PowerTest.cs index a28f646ef8..db6c0b7c89 100644 --- a/Content.IntegrationTests/Tests/Power/PowerTest.cs +++ b/Content.IntegrationTests/Tests/Power/PowerTest.cs @@ -1,4 +1,5 @@ #nullable enable +using Content.IntegrationTests.Fixtures; using Content.Server.NodeContainer.EntitySystems; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; @@ -14,7 +15,7 @@ using Robust.Shared.Timing; namespace Content.IntegrationTests.Tests.Power { [TestFixture] - public sealed class PowerTest + public sealed class PowerTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -167,7 +168,7 @@ namespace Content.IntegrationTests.Tests.Power [Test] public async Task TestSimpleSurplus() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -218,8 +219,6 @@ namespace Content.IntegrationTests.Tests.Power Assert.That(supplier.CurrentSupply, Is.EqualTo(loadPower * 2).Within(0.1)); }); }); - - await pair.CleanReturnAsync(); } @@ -229,7 +228,7 @@ namespace Content.IntegrationTests.Tests.Power [Test] public async Task TestSimpleDeficit() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -280,14 +279,12 @@ namespace Content.IntegrationTests.Tests.Power Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestSupplyRamp() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -368,14 +365,12 @@ namespace Content.IntegrationTests.Tests.Power Assert.That(consumer.ReceivedPower, Is.EqualTo(400).Within(tickDev)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestBatteryRamp() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -472,8 +467,6 @@ namespace Content.IntegrationTests.Tests.Power Assert.That(currentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev)); }); }); - - await pair.CleanReturnAsync(); } [Test] @@ -481,7 +474,7 @@ namespace Content.IntegrationTests.Tests.Power { // checks that batteries and supplies properly ramp down if the load is disconnected/disabled. - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -571,14 +564,12 @@ namespace Content.IntegrationTests.Tests.Power Assert.That(consumer.ReceivedPower, Is.EqualTo(0).Within(0.1)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestSimpleBatteryChargeDeficit() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var gameTiming = server.ResolveDependency(); @@ -631,14 +622,12 @@ namespace Content.IntegrationTests.Tests.Power Assert.That(supplier.CurrentSupply, Is.EqualTo(500).Within(0.1)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestFullBattery() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -713,14 +702,12 @@ namespace Content.IntegrationTests.Tests.Power Assert.That(currentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestFullBatteryEfficiencyPassThrough() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -795,14 +782,12 @@ namespace Content.IntegrationTests.Tests.Power Assert.That(currentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestFullBatteryEfficiencyDemandPassThrough() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -888,8 +873,6 @@ namespace Content.IntegrationTests.Tests.Power Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1)); }); }); - - await pair.CleanReturnAsync(); } /// @@ -899,7 +882,7 @@ namespace Content.IntegrationTests.Tests.Power [Test] public async Task TestSupplyPrioritized() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -988,8 +971,6 @@ namespace Content.IntegrationTests.Tests.Power Assert.That(netBattery2.SupplyRampPosition, Is.EqualTo(500).Within(0.1)); }); }); - - await pair.CleanReturnAsync(); } /// @@ -998,7 +979,7 @@ namespace Content.IntegrationTests.Tests.Power [Test] public async Task TestBatteriesProportional() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -1079,14 +1060,12 @@ namespace Content.IntegrationTests.Tests.Power Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestBatteryEngineCut() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -1161,8 +1140,6 @@ namespace Content.IntegrationTests.Tests.Power Assert.That(netBattery.CurrentSupply, Is.GreaterThan(0)); }); }); - - await pair.CleanReturnAsync(); } /// @@ -1171,7 +1148,7 @@ namespace Content.IntegrationTests.Tests.Power [Test] public async Task TestTerminalNodeGroups() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -1230,14 +1207,12 @@ namespace Content.IntegrationTests.Tests.Power Assert.That(leftNode.NodeGroup, Is.Not.EqualTo(rightNode.NodeGroup)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task ApcChargingTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -1290,14 +1265,12 @@ namespace Content.IntegrationTests.Tests.Power Assert.That(currentCharge, Is.GreaterThan(0)); //apc battery should have gained charge }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task ApcNetTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -1355,8 +1328,6 @@ namespace Content.IntegrationTests.Tests.Power Assert.That(apcNetBattery.CurrentSupply, Is.EqualTo(1).Within(0.1)); }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Power/StationPowerTests.cs b/Content.IntegrationTests/Tests/Power/StationPowerTests.cs index 9d79abf480..763cec328e 100644 --- a/Content.IntegrationTests/Tests/Power/StationPowerTests.cs +++ b/Content.IntegrationTests/Tests/Power/StationPowerTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; @@ -13,7 +14,7 @@ using Robust.Shared.EntitySerialization; namespace Content.IntegrationTests.Tests.Power; -public sealed class StationPowerTests +public sealed class StationPowerTests : GameTest { /// /// How long the station should be able to survive on stored power if nothing is changed from round start. @@ -36,14 +37,16 @@ public sealed class StationPowerTests "Exo", ]; + public override PoolSettings PoolSettings => new () + { + Dirty = true, + }; + [Explicit] [Test, TestCaseSource(nameof(GameMaps))] public async Task TestStationStartingPowerWindow(string mapProtoId) { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Dirty = true, - }); + var pair = Pair; var server = pair.Server; var entMan = server.EntMan; @@ -96,17 +99,12 @@ public sealed class StationPowerTests Assert.That(totalStartingCharge, Is.GreaterThanOrEqualTo(requiredStoredPower), $"Needs at least {requiredStoredPower - totalStartingCharge} more stored power!"); }); - - await pair.CleanReturnAsync(); } [Test, TestCaseSource(nameof(GameMaps))] public async Task TestApcLoad(string mapProtoId) { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Dirty = true, - }); + var pair = Pair; var server = pair.Server; var entMan = server.EntMan; @@ -145,7 +143,5 @@ public sealed class StationPowerTests } } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs b/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs index 267b3637e0..62c9f1a460 100644 --- a/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs +++ b/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; using Content.Server.Station.Systems; using Content.Shared.Inventory; using Content.Shared.Preferences; @@ -9,7 +10,7 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Preferences; [TestFixture] -public sealed class LoadoutTests +public sealed class LoadoutTests : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -42,16 +43,18 @@ public sealed class LoadoutTests ["jumpsuit"] = "ClothingUniformJumpsuitColorGrey" }; + public override PoolSettings PoolSettings => new() + { + Dirty = true, + }; + /// /// Checks that an empty loadout still spawns with default gear and not naked. /// [Test] public async Task TestEmptyLoadout() { - var pair = await PoolManager.GetServerClient(new PoolSettings() - { - Dirty = true, - }); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); @@ -87,7 +90,5 @@ public sealed class LoadoutTests entManager.DeleteEntity(tester); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs b/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs index 6208804e3a..ad93e083fb 100644 --- a/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs +++ b/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading; +using Content.IntegrationTests.Fixtures; using Content.Server.Database; using Content.Server.Preferences.Managers; using Content.Shared.Body; @@ -20,7 +21,7 @@ using Robust.UnitTesting; namespace Content.IntegrationTests.Tests.Preferences { [TestFixture] - public sealed class ServerDbSqliteTests + public sealed class ServerDbSqliteTests : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -69,12 +70,10 @@ namespace Content.IntegrationTests.Tests.Preferences [Test] public async Task TestUserDoesNotExist() { - var pair = await PoolManager.GetServerClient(); + var pair = Pair; var db = GetDb(pair.Server); // Database should be empty so a new GUID should do it. Assert.That(await db.GetPlayerPreferencesAsync(NewUserId()), Is.Null); - - await pair.CleanReturnAsync(); } [Test] @@ -113,7 +112,7 @@ namespace Content.IntegrationTests.Tests.Preferences [Test] public async Task TestInitPrefs() { - var pair = await PoolManager.GetServerClient(); + var pair = Pair; var db = GetDb(pair.Server); var preferences = (ServerPreferencesManager)pair.Server.ResolveDependency(); var username = new NetUserId(new Guid("640bd619-fc8d-4fe2-bf3c-4a5fb17d6ddd")); @@ -123,13 +122,12 @@ namespace Content.IntegrationTests.Tests.Preferences var prefs = await db.GetPlayerPreferencesAsync(username); var profile = preferences.ConvertProfiles(prefs!.Profiles.Find(p => p.Slot == slot)); Assert.That(profile.MemberwiseEquals(originalProfile)); - await pair.CleanReturnAsync(); } [Test] public async Task TestDeleteCharacter() { - var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var db = GetDb(server); var username = new NetUserId(new Guid("640bd619-fc8d-4fe2-bf3c-4a5fb17d6ddd")); @@ -139,18 +137,16 @@ namespace Content.IntegrationTests.Tests.Preferences await db.SaveCharacterSlotAsync(username, null, 1); var prefs = await db.GetPlayerPreferencesAsync(username); Assert.That(prefs!.Profiles, Has.Count.EqualTo(1)); - await pair.CleanReturnAsync(); } [Test] public async Task TestNoPendingDatabaseChanges() { - var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var db = GetDb(server); Assert.That(async () => await db.HasPendingModelChanges(), Is.False, "The database has pending model changes. Add a new migration to apply them. See https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations"); - await pair.CleanReturnAsync(); } private static NetUserId NewUserId() @@ -166,7 +162,7 @@ namespace Content.IntegrationTests.Tests.Preferences [TestCaseSource(nameof(_trueFalse))] public async Task InvalidSpeciesConversion(bool legacy) { - var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var db = GetDb(pair.Server); var preferences = (ServerPreferencesManager)pair.Server.ResolveDependency(); @@ -198,8 +194,6 @@ namespace Content.IntegrationTests.Tests.Preferences Assert.That(converted.Characters[0].Species, Is.Not.EqualTo(InvalidSpecies)); Assert.That(converted.Characters[0].Species, Is.EqualTo(HumanoidCharacterProfile.DefaultSpecies)); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Procedural/DungeonTests.cs b/Content.IntegrationTests/Tests/Procedural/DungeonTests.cs index 6bede9660a..d3afcc7253 100644 --- a/Content.IntegrationTests/Tests/Procedural/DungeonTests.cs +++ b/Content.IntegrationTests/Tests/Procedural/DungeonTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; using Content.Server.Procedural; using Content.Shared.Procedural; using Robust.Shared.Maths; @@ -7,12 +8,12 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Procedural; [TestOf(typeof(DungeonSystem))] -public sealed class DungeonTests +public sealed class DungeonTests : GameTest { [Test] public async Task TestDungeonRoomPackBounds() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var protoManager = pair.Server.ResolveDependency(); await pair.Server.WaitAssertion(() => @@ -55,14 +56,12 @@ public sealed class DungeonTests } } }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestDungeonPresets() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var protoManager = pair.Server.ResolveDependency(); await pair.Server.WaitAssertion(() => @@ -92,7 +91,5 @@ public sealed class DungeonTests } } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/PrototypeSaveTest.cs b/Content.IntegrationTests/Tests/PrototypeSaveTest.cs index 9ff8ca2900..5977e7deee 100644 --- a/Content.IntegrationTests/Tests/PrototypeSaveTest.cs +++ b/Content.IntegrationTests/Tests/PrototypeSaveTest.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Coordinates; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -27,12 +28,12 @@ namespace Content.IntegrationTests.Tests; /// spawn it into a new empty map and seeing what the map yml looks like. /// [TestFixture] -public sealed class PrototypeSaveTest +public sealed class PrototypeSaveTest : GameTest { [Test] public async Task UninitializedSaveTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entityMan = server.ResolveDependency(); @@ -156,7 +157,6 @@ public sealed class PrototypeSaveTest } }); }); - await pair.CleanReturnAsync(); } public sealed class TestEntityUidContext : ISerializationContext, diff --git a/Content.IntegrationTests/Tests/PrototypeTests/PrototypeTests.cs b/Content.IntegrationTests/Tests/PrototypeTests/PrototypeTests.cs index 440d9e636e..3b4bed882d 100644 --- a/Content.IntegrationTests/Tests/PrototypeTests/PrototypeTests.cs +++ b/Content.IntegrationTests/Tests/PrototypeTests/PrototypeTests.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Markdown; @@ -8,7 +9,7 @@ using Robust.UnitTesting; namespace Content.IntegrationTests.Tests.PrototypeTests; -public sealed class PrototypeTests +public sealed class PrototypeTests : GameTest { /// /// This test writes all known server prototypes as yaml files, then validates that the result is valid yaml. @@ -17,10 +18,9 @@ public sealed class PrototypeTests [Test] public async Task TestAllServerPrototypesAreSerializable() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var context = new PrototypeSaveTest.TestEntityUidContext(); await SaveThenValidatePrototype(pair.Server, "server", context); - await pair.CleanReturnAsync(); } /// @@ -30,10 +30,9 @@ public sealed class PrototypeTests [Test] public async Task TestAllClientPrototypesAreSerializable() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var context = new PrototypeSaveTest.TestEntityUidContext(); await SaveThenValidatePrototype(pair.Client, "client", context); - await pair.CleanReturnAsync(); } public async Task SaveThenValidatePrototype(RobustIntegrationTest.IntegrationInstance instance, string instanceId, @@ -69,10 +68,9 @@ public sealed class PrototypeTests [Test] public async Task ServerPrototypeSaveLoadSaveTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var context = new PrototypeSaveTest.TestEntityUidContext(); await SaveLoadSavePrototype(pair.Server, context); - await pair.CleanReturnAsync(); } /// @@ -81,10 +79,9 @@ public sealed class PrototypeTests [Test] public async Task ClientPrototypeSaveLoadSaveTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var context = new PrototypeSaveTest.TestEntityUidContext(); await SaveLoadSavePrototype(pair.Client, context); - await pair.CleanReturnAsync(); } private async Task SaveLoadSavePrototype( diff --git a/Content.IntegrationTests/Tests/PrototypeTests/PrototypeUploadTest.cs b/Content.IntegrationTests/Tests/PrototypeTests/PrototypeUploadTest.cs index c641cd9bd1..ac7bdcd24d 100644 --- a/Content.IntegrationTests/Tests/PrototypeTests/PrototypeUploadTest.cs +++ b/Content.IntegrationTests/Tests/PrototypeTests/PrototypeUploadTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Tag; using Robust.Client.Upload.Commands; using Robust.Shared.GameObjects; @@ -6,7 +7,7 @@ using Robust.Shared.Upload; namespace Content.IntegrationTests.Tests.PrototypeTests; -public sealed class PrototypeUploadTest +public sealed class PrototypeUploadTest : GameTest { public const string IdA = "UploadTestPrototype"; public const string IdB = $"{IdA}NoParent"; @@ -36,7 +37,7 @@ public sealed class PrototypeUploadTest [TestOf(typeof(LoadPrototypeCommand))] public async Task TestFileUpload() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings {Connected = true}); + var pair = Pair; var sCompFact = pair.Server.ResolveDependency(); var cCompFact = pair.Client.ResolveDependency(); @@ -79,7 +80,5 @@ public sealed class PrototypeUploadTest Assert.That(cProtoB!.TryGetComponent(out _, cCompFact), Is.False); Assert.That(cProtoD!.TryGetComponent(out _, cCompFact), Is.True); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Puller/PullerTest.cs b/Content.IntegrationTests/Tests/Puller/PullerTest.cs index a4fde86dbf..541417354a 100644 --- a/Content.IntegrationTests/Tests/Puller/PullerTest.cs +++ b/Content.IntegrationTests/Tests/Puller/PullerTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Hands.Components; using Content.Shared.Movement.Pulling.Components; using Content.Shared.Prototypes; @@ -9,7 +10,7 @@ namespace Content.IntegrationTests.Tests.Puller; #nullable enable [TestFixture] -public sealed class PullerTest +public sealed class PullerTest : GameTest { /// /// Checks that needsHands on PullerComponent is not set on mobs that don't even have hands. @@ -17,7 +18,7 @@ public sealed class PullerTest [Test] public async Task PullerSanityTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var compFactory = server.ResolveDependency(); @@ -39,7 +40,5 @@ public sealed class PullerTest } }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Replays/ReplayTests.cs b/Content.IntegrationTests/Tests/Replays/ReplayTests.cs index dcff6c3b45..4bd5f62e78 100644 --- a/Content.IntegrationTests/Tests/Replays/ReplayTests.cs +++ b/Content.IntegrationTests/Tests/Replays/ReplayTests.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Shared.CCVar; using Robust.Shared; @@ -6,20 +7,21 @@ using Robust.Shared.Replays; namespace Content.IntegrationTests.Tests.Replays; [TestFixture] -public sealed class ReplayTests +public sealed class ReplayTests : GameTest { + public override PoolSettings PoolSettings => new() + { + DummyTicker = false, + Dirty = true + }; + /// /// Simple test that just makes sure that automatic replay recording on round restarts works without any issues. /// [Test] public async Task AutoRecordReplayTest() { - var settings = new PoolSettings - { - DummyTicker = false, - Dirty = true - }; - await using var pair = await PoolManager.GetServerClient(settings); + var pair = Pair; var server = pair.Server; Assert.That(server.CfgMan.GetCVar(CVars.ReplayServerRecordingEnabled), Is.False); @@ -54,7 +56,5 @@ public sealed class ReplayTests await server.WaitPost(() => ticker.RestartRound()); await pair.RunTicksSync(25); Assert.That(recordMan.IsRecording, Is.False); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/ResearchTest.cs b/Content.IntegrationTests/Tests/ResearchTest.cs index 4661a1ea9e..c95d68d9e2 100644 --- a/Content.IntegrationTests/Tests/ResearchTest.cs +++ b/Content.IntegrationTests/Tests/ResearchTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Lathe; using Content.Shared.Research.Prototypes; using Robust.Shared.GameObjects; @@ -8,12 +9,12 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests; [TestFixture] -public sealed class ResearchTest +public sealed class ResearchTest : GameTest { [Test] public async Task DisciplineValidTierPrerequesitesTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoManager = server.ResolveDependency(); @@ -42,14 +43,12 @@ public sealed class ResearchTest } }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task AllTechPrintableTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -99,7 +98,5 @@ public sealed class ResearchTest } }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs b/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs index 40457f5488..13bb3cfd74 100644 --- a/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs +++ b/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs @@ -1,4 +1,5 @@ -using Content.Server.GameTicking; +using Content.IntegrationTests.Fixtures; +using Content.Server.GameTicking; using Content.Shared.GameTicking; using Robust.Shared.GameObjects; using Robust.Shared.Reflection; @@ -7,7 +8,7 @@ namespace Content.IntegrationTests.Tests { [TestFixture] [TestOf(typeof(RoundRestartCleanupEvent))] - public sealed class ResettingEntitySystemTests + public sealed class ResettingEntitySystemTests : GameTest { public sealed class TestRoundRestartCleanupEvent : EntitySystem { @@ -26,15 +27,17 @@ namespace Content.IntegrationTests.Tests } } + public override PoolSettings PoolSettings => new PoolSettings + { + DummyTicker = false, + Connected = true, + Dirty = true + }; + [Test] public async Task ResettingEntitySystemResetTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - Dirty = true - }); + var pair = Pair; var server = pair.Server; var entitySystemManager = server.ResolveDependency(); @@ -52,7 +55,6 @@ namespace Content.IntegrationTests.Tests Assert.That(system.HasBeenReset); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Respirator/LungTest.cs b/Content.IntegrationTests/Tests/Respirator/LungTest.cs index ae6b50ff0f..097fe5e3a0 100644 --- a/Content.IntegrationTests/Tests/Respirator/LungTest.cs +++ b/Content.IntegrationTests/Tests/Respirator/LungTest.cs @@ -6,6 +6,7 @@ using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.Map; using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Shared.Atmos.Components; using Robust.Shared.EntitySerialization.Systems; using Robust.Shared.Utility; @@ -14,7 +15,7 @@ namespace Content.IntegrationTests.Tests.Respirator; [TestFixture] [TestOf(typeof(LungSystem))] -public sealed class LungTest +public sealed class LungTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -54,7 +55,7 @@ public sealed class LungTest public async Task AirConsistencyTest() { // --- Setup - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -123,14 +124,12 @@ public sealed class LungTest "Did not exhale as much gas as was inhaled" ); } - - await pair.CleanReturnAsync(); } [Test] public async Task NoSuffocationTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); @@ -183,7 +182,5 @@ public sealed class LungTest $"Entity {entityManager.GetComponent(human).EntityName} is suffocating on tick {tick}"); }); } - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/RestartRoundTest.cs b/Content.IntegrationTests/Tests/RestartRoundTest.cs index 69c9a7dedf..1ed43b0951 100644 --- a/Content.IntegrationTests/Tests/RestartRoundTest.cs +++ b/Content.IntegrationTests/Tests/RestartRoundTest.cs @@ -1,20 +1,23 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class RestartRoundTest + public sealed class RestartRoundTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings + { + DummyTicker = false, + Connected = true, + Dirty = true + }; + [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - Dirty = true - }); + var pair = Pair; var server = pair.Server; var sysManager = server.ResolveDependency(); @@ -23,8 +26,7 @@ namespace Content.IntegrationTests.Tests sysManager.GetEntitySystem().RestartRound(); }); - await pair.RunTicksSync(10); - await pair.CleanReturnAsync(); + await pair.RunUntilSynced(); } } } diff --git a/Content.IntegrationTests/Tests/Roles/StartingGearStorageTests.cs b/Content.IntegrationTests/Tests/Roles/StartingGearStorageTests.cs index de89f16be7..e4f80eebb7 100644 --- a/Content.IntegrationTests/Tests/Roles/StartingGearStorageTests.cs +++ b/Content.IntegrationTests/Tests/Roles/StartingGearStorageTests.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Roles; using Content.Server.Storage.EntitySystems; using Robust.Shared.GameObjects; @@ -7,16 +8,17 @@ using Robust.Shared.Collections; namespace Content.IntegrationTests.Tests.Roles; [TestFixture] -public sealed class StartingGearPrototypeStorageTest +public sealed class StartingGearPrototypeStorageTest : GameTest { + public override PoolSettings PoolSettings => new() { Connected = true, Dirty = true }; + /// /// Checks that a storage fill on a StartingGearPrototype will properly fill /// [Test] public async Task TestStartingGearStorage() { - var settings = new PoolSettings { Connected = true, Dirty = true }; - await using var pair = await PoolManager.GetServerClient(settings); + var pair = Pair; var server = pair.Server; var mapSystem = server.System(); var storageSystem = server.System(); @@ -65,7 +67,5 @@ public sealed class StartingGearPrototypeStorageTest mapSystem.DeleteMap(testMap.MapId); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Round/JobTest.cs b/Content.IntegrationTests/Tests/Round/JobTest.cs index 215890791d..927f34a9ef 100644 --- a/Content.IntegrationTests/Tests/Round/JobTest.cs +++ b/Content.IntegrationTests/Tests/Round/JobTest.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.IntegrationTests.Pair; using Content.Server.GameTicking; using Content.Server.Mind; @@ -16,7 +17,7 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Round; [TestFixture] -public sealed class JobTest +public sealed class JobTest : GameTest { private static readonly ProtoId Passenger = "Passenger"; private static readonly ProtoId Engineer = "StationEngineer"; @@ -44,6 +45,13 @@ public sealed class JobTest {Captain}: [ 1, 1 ] "; + public override PoolSettings PoolSettings => new() + { + DummyTicker = false, + Connected = true, + InLobby = true + }; + private void AssertJob(TestPair pair, ProtoId job, NetUserId? user = null, bool isAntag = false) { var jobSys = pair.Server.System(); @@ -71,12 +79,7 @@ public sealed class JobTest [Test] public async Task StartRoundTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - InLobby = true - }); + var pair = Pair; pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map); var ticker = pair.Server.System(); @@ -95,7 +98,6 @@ public sealed class JobTest AssertJob(pair, Passenger); await pair.Server.WaitPost(() => ticker.RestartRound()); - await pair.CleanReturnAsync(); } /// @@ -104,12 +106,7 @@ public sealed class JobTest [Test] public async Task JobPreferenceTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - InLobby = true - }); + var pair = Pair; pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map); var ticker = pair.Server.System(); @@ -133,7 +130,6 @@ public sealed class JobTest AssertJob(pair, Passenger); await pair.Server.WaitPost(() => ticker.RestartRound()); - await pair.CleanReturnAsync(); } /// @@ -143,12 +139,7 @@ public sealed class JobTest [Test] public async Task JobWeightTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - InLobby = true - }); + var pair = Pair; pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map); var ticker = pair.Server.System(); @@ -169,7 +160,6 @@ public sealed class JobTest AssertJob(pair, Captain); await pair.Server.WaitPost(() => ticker.RestartRound()); - await pair.CleanReturnAsync(); } /// @@ -178,12 +168,7 @@ public sealed class JobTest [Test] public async Task JobPriorityTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - InLobby = true - }); + var pair = Pair; pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map); var ticker = pair.Server.System(); @@ -217,6 +202,5 @@ public sealed class JobTest }); await pair.Server.WaitPost(() => ticker.RestartRound()); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/RoundEndTest.cs b/Content.IntegrationTests/Tests/RoundEndTest.cs index 5de6de381d..16c460c3da 100644 --- a/Content.IntegrationTests/Tests/RoundEndTest.cs +++ b/Content.IntegrationTests/Tests/RoundEndTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Server.RoundEnd; using Content.Shared.CCVar; @@ -7,7 +8,7 @@ using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class RoundEndTest + public sealed class RoundEndTest : GameTest { private sealed class RoundEndTestSystem : EntitySystem { @@ -25,15 +26,18 @@ namespace Content.IntegrationTests.Tests } } + + public override PoolSettings PoolSettings => new PoolSettings + { + DummyTicker = false, + Connected = true, + Dirty = true + }; + [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - Dirty = true - }); + var pair = Pair; var server = pair.Server; @@ -151,7 +155,6 @@ namespace Content.IntegrationTests.Tests roundEndSystem.DefaultCountdownDuration = TimeSpan.FromMinutes(4); ticker.RestartRound(); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/SalvageTest.cs b/Content.IntegrationTests/Tests/SalvageTest.cs index 0059db6292..68bdf4726c 100644 --- a/Content.IntegrationTests/Tests/SalvageTest.cs +++ b/Content.IntegrationTests/Tests/SalvageTest.cs @@ -1,4 +1,6 @@ -using Content.Shared.CCVar; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; +using Content.Shared.CCVar; using Content.Shared.Salvage; using Robust.Shared.Configuration; using Robust.Shared.EntitySerialization.Systems; @@ -8,15 +10,16 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests; [TestFixture] -public sealed class SalvageTest +public sealed class SalvageTest : GameTest { /// /// Asserts that all salvage maps have been saved as grids and are loadable. /// [Test] + [EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)] public async Task AllSalvageMapsLoadableTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); @@ -24,7 +27,6 @@ public sealed class SalvageTest var prototypeManager = server.ResolveDependency(); var cfg = server.ResolveDependency(); var mapSystem = entManager.System(); - Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False); await server.WaitPost(() => { @@ -50,8 +52,6 @@ public sealed class SalvageTest } } }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); + await RunUntilSynced(); } } diff --git a/Content.IntegrationTests/Tests/SaveLoadMapTest.cs b/Content.IntegrationTests/Tests/SaveLoadMapTest.cs index eb3dc14720..914a1e21c2 100644 --- a/Content.IntegrationTests/Tests/SaveLoadMapTest.cs +++ b/Content.IntegrationTests/Tests/SaveLoadMapTest.cs @@ -1,4 +1,6 @@ using System.Numerics; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; using Content.Shared.CCVar; using Robust.Server.GameObjects; using Robust.Shared.Configuration; @@ -12,14 +14,15 @@ using Robust.Shared.Utility; namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class SaveLoadMapTest + public sealed class SaveLoadMapTest : GameTest { [Test] + [EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)] public async Task SaveLoadMultiGridMap() { var mapPath = new ResPath("/Maps/Test/TestMap.yml"); - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var sEntities = server.ResolveDependency(); @@ -27,8 +30,6 @@ namespace Content.IntegrationTests.Tests var mapSystem = sEntities.System(); var xformSystem = sEntities.EntitySysManager.GetEntitySystem(); var resManager = server.ResolveDependency(); - var cfg = server.ResolveDependency(); - Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False); await server.WaitAssertion(() => { @@ -94,8 +95,6 @@ namespace Content.IntegrationTests.Tests }); } }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs b/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs index b41aa0bf2f..9339d61270 100644 --- a/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs +++ b/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs @@ -1,5 +1,6 @@ using System.IO; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.CCVar; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; @@ -16,12 +17,12 @@ namespace Content.IntegrationTests.Tests /// Tests that a grid's yaml does not change when saved consecutively. /// [TestFixture] - public sealed class SaveLoadSaveTest + public sealed class SaveLoadSaveTest : GameTest { [Test] public async Task CreateSaveLoadSaveGrid() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); var mapLoader = entManager.System(); @@ -85,10 +86,9 @@ namespace Content.IntegrationTests.Tests } }); testSystem.Enabled = false; - await pair.CleanReturnAsync(); } - private const string TestMap = "Maps/bagel.yml"; + private new const string TestMap = "Maps/bagel.yml"; /// /// Loads the default map, runs it for 5 ticks, then assert that it did not change. @@ -96,7 +96,7 @@ namespace Content.IntegrationTests.Tests [Test] public async Task LoadSaveTicksSaveBagel() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapLoader = server.ResolveDependency().GetEntitySystem(); var mapSys = server.System(); @@ -167,7 +167,6 @@ namespace Content.IntegrationTests.Tests testSystem.Enabled = false; await server.WaitPost(() => mapSys.DeleteMap(mapId)); - await pair.CleanReturnAsync(); } /// @@ -183,7 +182,7 @@ namespace Content.IntegrationTests.Tests [Test] public async Task LoadTickLoadBagel() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapLoader = server.System(); @@ -241,7 +240,6 @@ namespace Content.IntegrationTests.Tests testSystem.Enabled = false; await server.WaitPost(() => mapSys.DeleteMap(mapId1)); await server.WaitPost(() => mapSys.DeleteMap(mapId2)); - await pair.CleanReturnAsync(); } /// diff --git a/Content.IntegrationTests/Tests/Serialization/SerializationTest.cs b/Content.IntegrationTests/Tests/Serialization/SerializationTest.cs index 339420362c..daf14e42a0 100644 --- a/Content.IntegrationTests/Tests/Serialization/SerializationTest.cs +++ b/Content.IntegrationTests/Tests/Serialization/SerializationTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Robust.Shared.Reflection; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Manager.Attributes; @@ -8,7 +9,7 @@ using Robust.Shared.Serialization.Markdown.Value; namespace Content.IntegrationTests.Tests.Serialization; [TestFixture] -public sealed partial class SerializationTest +public sealed partial class SerializationTest : GameTest { /// /// Check that serializing generic enums works as intended. This should really be in engine, but engine @@ -17,7 +18,7 @@ public sealed partial class SerializationTest [Test] public async Task SerializeGenericEnums() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var seriMan = server.ResolveDependency(); var refMan = server.ResolveDependency(); @@ -67,8 +68,6 @@ public sealed partial class SerializationTest Assert.That(seriMan.ValidateNode(genericNode).GetErrors().Any(), Is.True); Assert.That(seriMan.ValidateNode(typedNode).GetErrors().Any(), Is.True); Assert.That(seriMan.ValidateNode(typedNode).GetErrors().Any(), Is.False); - - await pair.CleanReturnAsync(); } private enum TestEnum : byte { Aa, Bb, Cc, Dd } diff --git a/Content.IntegrationTests/Tests/Shuttle/DockTest.cs b/Content.IntegrationTests/Tests/Shuttle/DockTest.cs index ab82a3d2f9..927f973472 100644 --- a/Content.IntegrationTests/Tests/Shuttle/DockTest.cs +++ b/Content.IntegrationTests/Tests/Shuttle/DockTest.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Server.Shuttles.Systems; using Content.Tests; using Robust.Server.GameObjects; @@ -13,7 +14,7 @@ using Robust.Shared.Utility; namespace Content.IntegrationTests.Tests.Shuttle; -public sealed class DockTest : ContentUnitTest +public sealed class DockTest : GameTest { private static IEnumerable TestSource() { @@ -26,7 +27,7 @@ public sealed class DockTest : ContentUnitTest [TestCaseSource(nameof(TestSource))] public async Task TestDockingConfig(Vector2 dock1Pos, Vector2 dock2Pos, Angle dock1Angle, Angle dock2Angle, bool result) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var map = await pair.CreateTestMap(); @@ -83,14 +84,12 @@ public sealed class DockTest : ContentUnitTest Assert.That(result, Is.EqualTo(config != null)); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestPlanetDock() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var map = await pair.CreateTestMap(); @@ -126,7 +125,5 @@ public sealed class DockTest : ContentUnitTest var dockingConfig = dockingSystem.GetDockingConfig(shuttle, map.MapUid); Assert.That(dockingConfig, Is.Not.EqualTo(null)); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/ShuttleTest.cs b/Content.IntegrationTests/Tests/ShuttleTest.cs index da5b82d91e..1c447db871 100644 --- a/Content.IntegrationTests/Tests/ShuttleTest.cs +++ b/Content.IntegrationTests/Tests/ShuttleTest.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Server.Shuttles.Components; using Robust.Shared.GameObjects; using Robust.Shared.Map; @@ -9,12 +10,12 @@ using Robust.Shared.Physics.Systems; namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class ShuttleTest + public sealed class ShuttleTest : GameTest { [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -50,7 +51,6 @@ namespace Content.IntegrationTests.Tests { Assert.That(entManager.GetComponent(map.Grid).LocalPosition, Is.Not.EqualTo(Vector2.Zero)); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Sprite/ItemSpriteTest.cs b/Content.IntegrationTests/Tests/Sprite/ItemSpriteTest.cs index da7e1e8e9b..ca667f2e7a 100644 --- a/Content.IntegrationTests/Tests/Sprite/ItemSpriteTest.cs +++ b/Content.IntegrationTests/Tests/Sprite/ItemSpriteTest.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; using Content.Shared.Item; using Robust.Client.GameObjects; using Robust.Shared.GameObjects; @@ -22,7 +23,7 @@ namespace Content.IntegrationTests.Tests.Sprite; /// /// [TestFixture] -public sealed class PrototypeSaveTest +public sealed class PrototypeSaveTest : GameTest { private static readonly HashSet Ignored = new() { @@ -34,8 +35,7 @@ public sealed class PrototypeSaveTest [Test] public async Task AllItemsHaveSpritesTest() { - var settings = new PoolSettings() { Connected = true }; // client needs to be in-game - await using var pair = await PoolManager.GetServerClient(settings); + var pair = Pair; List badPrototypes = []; await pair.Client.WaitPost(() => @@ -58,7 +58,5 @@ public sealed class PrototypeSaveTest Assert.Fail($"Item prototype has no sprite: {proto.ID}. It should probably either be marked as abstract, not be an item, or have a valid sprite"); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/StartTest.cs b/Content.IntegrationTests/Tests/StartTest.cs index e2bf5e8ff1..223f4676b0 100644 --- a/Content.IntegrationTests/Tests/StartTest.cs +++ b/Content.IntegrationTests/Tests/StartTest.cs @@ -1,9 +1,10 @@ +using Content.IntegrationTests.Fixtures; using Robust.Shared.Exceptions; namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class StartTest + public sealed class StartTest : GameTest { /// /// Test that the server, and client start, and stop. @@ -11,7 +12,7 @@ namespace Content.IntegrationTests.Tests [Test] public async Task TestClientStart() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var client = pair.Client; Assert.That(client.IsAlive); await client.WaitRunTicks(5); @@ -27,8 +28,6 @@ namespace Content.IntegrationTests.Tests Assert.That(sRuntimeLog.ExceptionCount, Is.EqualTo(0), "No exceptions must be logged on server."); await server.WaitIdleAsync(); Assert.That(server.IsAlive); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs b/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs index 02552669f7..8da97cbab7 100644 --- a/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs +++ b/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Systems; @@ -12,15 +13,21 @@ namespace Content.IntegrationTests.Tests.Station; [TestFixture] [TestOf(typeof(EmergencyShuttleSystem))] -public sealed class EvacShuttleTest +public sealed class EvacShuttleTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings() + { + DummyTicker = true, + Dirty = true, + }; + /// /// Ensure that the emergency shuttle can be called, and that it will travel to centcomm /// [Test] public async Task EmergencyEvacTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { DummyTicker = true, Dirty = true }); + var pair = Pair; var server = pair.Server; var entMan = server.EntMan; var ticker = server.System(); @@ -122,6 +129,5 @@ public sealed class EvacShuttleTest server.CfgMan.SetCVar(CCVars.EmergencyShuttleDockTime, dockTime); pair.Server.CfgMan.SetCVar(CCVars.EmergencyShuttleEnabled, false); pair.Server.CfgMan.SetCVar(CCVars.GameMap, gameMap); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Station/JobTests.cs b/Content.IntegrationTests/Tests/Station/JobTests.cs index 5172049a99..0961db1c67 100644 --- a/Content.IntegrationTests/Tests/Station/JobTests.cs +++ b/Content.IntegrationTests/Tests/Station/JobTests.cs @@ -2,12 +2,13 @@ using Content.Shared.Roles; using Content.Shared.Roles.Jobs; using Robust.Shared.Prototypes; using System.Linq; +using Content.IntegrationTests.Fixtures; namespace Content.IntegrationTests.Tests.Station; [TestFixture] [TestOf(typeof(SharedJobSystem))] -public sealed class JobTest +public sealed class JobTest : GameTest { /// /// Ensures that every job belongs to at most 1 primary department. @@ -16,7 +17,7 @@ public sealed class JobTest [Test] public async Task PrimaryDepartmentsTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var prototypeManager = server.ResolveDependency(); @@ -43,6 +44,5 @@ public sealed class JobTest } } }); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Station/StationJobsTest.cs b/Content.IntegrationTests/Tests/Station/StationJobsTest.cs index 4abd32bda0..c0f0b19b19 100644 --- a/Content.IntegrationTests/Tests/Station/StationJobsTest.cs +++ b/Content.IntegrationTests/Tests/Station/StationJobsTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Station.Components; using Content.Server.Station.Systems; using Content.Shared.Maps; @@ -15,7 +16,7 @@ namespace Content.IntegrationTests.Tests.Station; [TestFixture] [TestOf(typeof(StationJobsSystem))] -public sealed class StationJobsTest +public sealed class StationJobsTest : GameTest { private const string StationMapId = "FooStation"; @@ -85,7 +86,7 @@ public sealed class StationJobsTest [Test] public async Task AssignJobsTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var prototypeManager = server.ResolveDependency(); @@ -153,13 +154,12 @@ public sealed class StationJobsTest Assert.That(assigned.Values.Select(x => x.Item1).ToList(), Does.Contain("TCaptain")); }); }); - await pair.CleanReturnAsync(); } [Test] public async Task AdjustJobsTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var prototypeManager = server.ResolveDependency(); @@ -203,13 +203,12 @@ public sealed class StationJobsTest Assert.That(stationJobs.IsJobUnlimited(station, "TChaplain"), "Could not make TChaplain unlimited."); }); }); - await pair.CleanReturnAsync(); } [Test] public async Task InvalidRoundstartJobsTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var prototypeManager = server.ResolveDependency(); @@ -247,7 +246,6 @@ public sealed class StationJobsTest } }); }); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Storage/EntityStorageTests.cs b/Content.IntegrationTests/Tests/Storage/EntityStorageTests.cs index f80cc089de..6b100f89fd 100644 --- a/Content.IntegrationTests/Tests/Storage/EntityStorageTests.cs +++ b/Content.IntegrationTests/Tests/Storage/EntityStorageTests.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.Storage.EntitySystems; using Content.Shared.Damage; using Content.Shared.Damage.Systems; @@ -7,7 +8,7 @@ using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests.Storage; [TestFixture] -public sealed class EntityStorageTests +public sealed class EntityStorageTests : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -31,7 +32,7 @@ public sealed class EntityStorageTests [Test] public async Task TestContainerDestruction() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var map = await pair.CreateTestMap(); @@ -76,7 +77,5 @@ public sealed class EntityStorageTests await server.WaitRunTicks(5); Assert.That(server.EntMan.Deleted(box)); Assert.That(server.EntMan.Deleted(crowbar), Is.False); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Storage/StorageTest.cs b/Content.IntegrationTests/Tests/Storage/StorageTest.cs index 95d94906bb..c0ef6a3691 100644 --- a/Content.IntegrationTests/Tests/Storage/StorageTest.cs +++ b/Content.IntegrationTests/Tests/Storage/StorageTest.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Containers; using Content.Shared.Item; using Content.Shared.Prototypes; @@ -12,7 +13,7 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Storage; -public sealed class StorageTest +public sealed class StorageTest : GameTest { /// /// Can an item store more than itself weighs. @@ -21,7 +22,7 @@ public sealed class StorageTest [Test] public async Task StorageSizeArbitrageTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoManager = server.ResolveDependency(); @@ -44,13 +45,12 @@ public sealed class StorageTest $"Found storage arbitrage on {proto.ID}"); } }); - await pair.CleanReturnAsync(); } [Test] public async Task TestStorageFillPrototypes() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoManager = server.ResolveDependency(); @@ -72,13 +72,12 @@ public sealed class StorageTest } }); }); - await pair.CleanReturnAsync(); } [Test] public async Task TestSufficientSpaceForFill() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -159,14 +158,12 @@ public sealed class StorageTest } } }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestSufficientSpaceForEntityStorageFill() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -194,7 +191,6 @@ public sealed class StorageTest $"{proto.ID} storage fill is too large."); }); } - await pair.CleanReturnAsync(); } private int GetEntrySize(EntitySpawnEntry entry, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem) @@ -242,7 +238,7 @@ public sealed class StorageTest [Test] public async Task NoMultipleContainerFillsTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var compFact = pair.Server.ResolveDependency(); Assert.Multiple(() => @@ -258,6 +254,5 @@ public sealed class StorageTest Assert.That(!proto.HasComponent(compFact), $"Prototype {proto.ID} has both {nameof(ContainerFillComponent)} and {nameof(StorageFillComponent)}."); } }); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/StoreTests.cs b/Content.IntegrationTests/Tests/StoreTests.cs index 39df0fc8cc..811bf40548 100644 --- a/Content.IntegrationTests/Tests/StoreTests.cs +++ b/Content.IntegrationTests/Tests/StoreTests.cs @@ -1,7 +1,6 @@ -using System.Collections.Generic; using System.Linq; -using System.Threading; -using Content.Server.Store.Systems; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; using Content.Server.Traitor.Uplink; using Content.Shared.FixedPoint; using Content.Shared.Inventory; @@ -10,13 +9,12 @@ using Content.Shared.Store; using Content.Shared.Store.Components; using Content.Shared.StoreDiscount.Components; using Robust.Shared.GameObjects; -using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.IntegrationTests.Tests; [TestFixture] -public sealed class StoreTests +public sealed class StoreTests : GameTest { [TestPrototypes] @@ -32,10 +30,23 @@ public sealed class StoreTests - idcard - type: Pda "; + [Test] + [Ignore(""" + This currently causes the client to crash, failing the test. + When this is fixed, this test should be removed and StoreDiscountAndRefund + should just use the default pair config. + """)] + public async Task StoreDiscountAndRefundWithClient() + { + await StoreDiscountAndRefund(); + } + + [Test] + [PairConfig(nameof(PsDisconnected))] public async Task StoreDiscountAndRefund() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -168,7 +179,5 @@ public sealed class StoreTests } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Tag/TagTest.cs b/Content.IntegrationTests/Tests/Tag/TagTest.cs index e6cd2accaf..4632e3ea2e 100644 --- a/Content.IntegrationTests/Tests/Tag/TagTest.cs +++ b/Content.IntegrationTests/Tests/Tag/TagTest.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; using Content.Shared.Tag; using Robust.Shared.GameObjects; using Robust.Shared.Map; @@ -10,7 +11,7 @@ namespace Content.IntegrationTests.Tests.Tag { [TestFixture] [TestOf(typeof(TagComponent))] - public sealed class TagTest + public sealed class TagTest : GameTest { private const string TagEntityId = "TagTestDummy"; @@ -44,7 +45,7 @@ namespace Content.IntegrationTests.Tests.Tag [Test] public async Task TagComponentTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntityManager = server.ResolveDependency(); @@ -295,7 +296,6 @@ namespace Content.IntegrationTests.Tests.Tag Assert.Throws(() => { tagSystem.AddTags(sTagEntity, new HashSet> { UnregisteredTag }); }); #endif }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Tiles/TileStackRecursionTest.cs b/Content.IntegrationTests/Tests/Tiles/TileStackRecursionTest.cs index 52c5b03265..d1c26e9bec 100644 --- a/Content.IntegrationTests/Tests/Tiles/TileStackRecursionTest.cs +++ b/Content.IntegrationTests/Tests/Tiles/TileStackRecursionTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.CCVar; using Content.Shared.Maps; using Robust.Shared.Configuration; @@ -7,12 +8,12 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Tiles; -public sealed class TileStackRecursionTest +public sealed class TileStackRecursionTest : GameTest { [Test] public async Task TestBaseTurfRecursion() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var protoMan = pair.Server.ResolveDependency(); var cfg = pair.Server.ResolveDependency(); var maxTileHistoryLength = cfg.GetCVar(CCVars.TileStackLimit); @@ -40,7 +41,6 @@ public sealed class TileStackRecursionTest (possibleTurf, new ProtoId(ctdef.ID)))); } Bfs(nodes, edges, maxTileHistoryLength); - await pair.CleanReturnAsync(); } private void Bfs(List<(ProtoId, int)> nodes, List<(ProtoId, ProtoId)> edges, int depthLimit) diff --git a/Content.IntegrationTests/Tests/UserInterface/UiControlTest.cs b/Content.IntegrationTests/Tests/UserInterface/UiControlTest.cs index 5efa009ca7..2aea2986ec 100644 --- a/Content.IntegrationTests/Tests/UserInterface/UiControlTest.cs +++ b/Content.IntegrationTests/Tests/UserInterface/UiControlTest.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Client.LateJoin; +using Content.IntegrationTests.Fixtures; using Robust.Client.UserInterface.CustomControls; using Robust.Shared.ContentPack; using Robust.Shared.IoC; @@ -8,7 +9,7 @@ using Robust.Shared.Reflection; namespace Content.IntegrationTests.Tests.UserInterface; [TestFixture] -public sealed class UiControlTest +public sealed class UiControlTest : GameTest { // You should not be adding to this. private Type[] _ignored = new Type[] @@ -22,10 +23,7 @@ public sealed class UiControlTest [Test] public async Task TestWindows() { - var pair = await PoolManager.GetServerClient(new PoolSettings() - { - Connected = true, - }); + var pair = Pair; var activator = pair.Client.ResolveDependency(); var refManager = pair.Client.ResolveDependency(); var loader = pair.Client.ResolveDependency(); @@ -50,7 +48,5 @@ public sealed class UiControlTest activator.CreateInstance(type, oneOff: true, inject: false); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Utility/EntitySystemExtensionsTest.cs b/Content.IntegrationTests/Tests/Utility/EntitySystemExtensionsTest.cs index d460fd354f..6ab5ca1691 100644 --- a/Content.IntegrationTests/Tests/Utility/EntitySystemExtensionsTest.cs +++ b/Content.IntegrationTests/Tests/Utility/EntitySystemExtensionsTest.cs @@ -1,4 +1,5 @@ #nullable enable +using Content.IntegrationTests.Fixtures; using Content.Shared.Physics; using Content.Shared.Spawning; using Robust.Shared.GameObjects; @@ -8,7 +9,7 @@ namespace Content.IntegrationTests.Tests.Utility { [TestFixture] [TestOf(typeof(EntitySystemExtensions))] - public sealed class EntitySystemExtensionsTest + public sealed class EntitySystemExtensionsTest : GameTest { private const string BlockerDummyId = "BlockerDummy"; @@ -32,7 +33,7 @@ namespace Content.IntegrationTests.Tests.Utility [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -95,7 +96,6 @@ namespace Content.IntegrationTests.Tests.Utility Assert.That(entity, Is.Not.Null); }); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Utility/EntityWhitelistTest.cs b/Content.IntegrationTests/Tests/Utility/EntityWhitelistTest.cs index 19b25816fa..d7e1239603 100644 --- a/Content.IntegrationTests/Tests/Utility/EntityWhitelistTest.cs +++ b/Content.IntegrationTests/Tests/Utility/EntityWhitelistTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Containers.ItemSlots; using Content.Shared.Whitelist; using Robust.Shared.GameObjects; @@ -7,7 +8,7 @@ namespace Content.IntegrationTests.Tests.Utility { [TestFixture] [TestOf(typeof(EntityWhitelist))] - public sealed class EntityWhitelistTest + public sealed class EntityWhitelistTest : GameTest { private const string InvalidComponent = "Sprite"; private const string ValidComponent = "Physics"; @@ -58,7 +59,7 @@ namespace Content.IntegrationTests.Tests.Utility [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -111,7 +112,6 @@ namespace Content.IntegrationTests.Tests.Utility Assert.That(sys.IsValid(whitelistSer, WhitelistTestInvalidTag), Is.False); }); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs b/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs index 7058cfab6a..dfaaae05ad 100644 --- a/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs +++ b/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; using Content.Server.VendingMachines; using Content.Server.Wires; using Content.Shared.Cargo.Prototypes; @@ -21,7 +22,7 @@ namespace Content.IntegrationTests.Tests [TestFixture] [TestOf(typeof(VendingMachineRestockComponent))] [TestOf(typeof(VendingMachineSystem))] - public sealed class VendingMachineRestockTest : EntitySystem + public sealed class VendingMachineRestockTest : GameTest { private static readonly ProtoId TestDamageType = "Blunt"; @@ -111,7 +112,7 @@ namespace Content.IntegrationTests.Tests [Test] public async Task TestAllRestocksAreAvailableToBuy() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -194,14 +195,12 @@ namespace Content.IntegrationTests.Tests $"Some entities with {restockCompName} are unavailable for purchase: \n - {string.Join("\n - ", restockEntities)}"); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestCompleteRestockProcess() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -280,14 +279,12 @@ namespace Content.IntegrationTests.Tests mapSystem.DeleteMap(testMap.MapId); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestRestockBreaksOpen() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -342,14 +339,12 @@ namespace Content.IntegrationTests.Tests mapSystem.DeleteMap(testMap.MapId); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestRestockInventoryBounds() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -388,10 +383,6 @@ namespace Content.IntegrationTests.Tests Assert.That(vendingMachineSystem.GetAvailableInventory(machine)[0].Amount, Is.EqualTo(3), "Machine's available inventory did not stay the same after a third restock."); }); - - await pair.CleanReturnAsync(); } } } - -#nullable disable diff --git a/Content.IntegrationTests/Tests/Wires/WireLayoutTest.cs b/Content.IntegrationTests/Tests/Wires/WireLayoutTest.cs index 920dc08818..d9801275b9 100644 --- a/Content.IntegrationTests/Tests/Wires/WireLayoutTest.cs +++ b/Content.IntegrationTests/Tests/Wires/WireLayoutTest.cs @@ -1,4 +1,5 @@ -using Content.Server.Doors; +using Content.IntegrationTests.Fixtures; +using Content.Server.Doors; using Content.Server.Power; using Content.Server.Wires; using Robust.Shared.GameObjects; @@ -10,7 +11,7 @@ namespace Content.IntegrationTests.Tests.Wires; [TestFixture] [Parallelizable(ParallelScope.All)] [TestOf(typeof(WiresSystem))] -public sealed class WireLayoutTest +public sealed class WireLayoutTest : GameTest { [TestPrototypes] public const string Prototypes = """ @@ -53,7 +54,7 @@ public sealed class WireLayoutTest [Test] public async Task TestLayoutInheritance() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -89,8 +90,6 @@ public sealed class WireLayoutTest Assert.That(ent3.Comp.WiresList, Has.One.With.Property("Action").InstanceOf(), "1 door bolt wire"); }); }); - - await pair.CleanReturnAsync(); } private static Entity SpawnWithComp(IEntityManager entityManager, string prototype, MapCoordinates coords) diff --git a/Content.IntegrationTests/Tests/WizdenContentFreeze/WizdenContentFreeze.cs b/Content.IntegrationTests/Tests/WizdenContentFreeze/WizdenContentFreeze.cs index 891525a25b..801c6c49b6 100644 --- a/Content.IntegrationTests/Tests/WizdenContentFreeze/WizdenContentFreeze.cs +++ b/Content.IntegrationTests/Tests/WizdenContentFreeze/WizdenContentFreeze.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Kitchen; namespace Content.IntegrationTests.Tests.WizdenContentFreeze; @@ -5,7 +6,7 @@ namespace Content.IntegrationTests.Tests.WizdenContentFreeze; /// /// These tests are limited to adding a specific type of content, essentially freezing it. If you are a fork developer, you may want to disable these tests. /// -public sealed class WizdenContentFreeze +public sealed class WizdenContentFreeze : GameTest { /// /// This freeze prohibits the addition of new microwave recipes. @@ -18,7 +19,7 @@ public sealed class WizdenContentFreeze [Test] public async Task MicrowaveRecipesFreezeTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ProtoMan; @@ -35,7 +36,5 @@ public sealed class WizdenContentFreeze { Assert.Fail($"Oh, you deleted the microwave recipes? YOU ARE SO COOL! Please lower the number of recipes in MicrowaveRecipesFreezeTest from {recipesLimit} to {recipesCount} so that future contributors cannot add new recipes back."); } - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/XenoArtifactTest.cs b/Content.IntegrationTests/Tests/XenoArtifactTest.cs index ac4c58c52c..35b03470e9 100644 --- a/Content.IntegrationTests/Tests/XenoArtifactTest.cs +++ b/Content.IntegrationTests/Tests/XenoArtifactTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Xenoarchaeology.Artifact; using Content.Shared.Xenoarchaeology.Artifact.Components; using Robust.Shared.GameObjects; @@ -6,7 +7,7 @@ using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests; [TestFixture] -public sealed class XenoArtifactTest +public sealed class XenoArtifactTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -90,7 +91,7 @@ public sealed class XenoArtifactTest [Test] public async Task XenoArtifactAddNodeTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); @@ -133,9 +134,6 @@ public sealed class XenoArtifactTest Assert.That(artifactSystem.GetDirectPredecessorNodes(artifactEnt, node3!.Value), Has.Count.EqualTo(1)); Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node3!.Value), Has.Count.EqualTo(2)); }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); } /// @@ -144,7 +142,7 @@ public sealed class XenoArtifactTest [Test] public async Task XenoArtifactRemoveNodeTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); @@ -182,9 +180,6 @@ public sealed class XenoArtifactTest Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node2!.Value), Is.Empty); Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node4!.Value), Is.Empty); }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); } /// @@ -193,7 +188,7 @@ public sealed class XenoArtifactTest [Test] public async Task XenoArtifactResizeTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); @@ -245,9 +240,6 @@ public sealed class XenoArtifactTest Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node4!.Value), Is.Empty); Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node4!.Value), Is.Empty); }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); } /// @@ -256,7 +248,7 @@ public sealed class XenoArtifactTest [Test] public async Task XenoArtifactReplaceTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); @@ -304,9 +296,6 @@ public sealed class XenoArtifactTest Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node4!.Value), Is.Empty); }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); } /// @@ -315,7 +304,7 @@ public sealed class XenoArtifactTest [Test] public async Task XenoArtifactBuildActiveNodesTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); @@ -367,15 +356,12 @@ public sealed class XenoArtifactTest Assert.That(artifactEnt.Comp.CachedActiveNodes, Has.Count.EqualTo(expectedActiveNodes.Length)); }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); } [Test] public async Task XenoArtifactGenerateSegmentsTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); @@ -412,8 +398,5 @@ public sealed class XenoArtifactTest Assert.That(grouped[2].Count(), Is.LessThanOrEqualTo(2)); // maintain same width or, if we used 3 nodes on previous layer - we only have 1 left! }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); } } diff --git a/Content.MapRenderer/Program.cs b/Content.MapRenderer/Program.cs index 90f97a5786..668e04f63d 100644 --- a/Content.MapRenderer/Program.cs +++ b/Content.MapRenderer/Program.cs @@ -145,7 +145,7 @@ namespace Content.MapRenderer { Console.Write($"Following map files did not exist on disk directly, searching through prototypes: {string.Join(", ", lookupPrototypeFiles)}"); - await using var pair = await PoolManager.GetServerClient(); + await using var pair = await PoolManager.GetServerClient(testContext: testContext); var mapPrototypes = pair.Server .ResolveDependency() .EnumeratePrototypes() diff --git a/Content.Shared/Trigger/Systems/TriggerSystem.cs b/Content.Shared/Trigger/Systems/TriggerSystem.cs index 1e7261043f..3ba9e76186 100644 --- a/Content.Shared/Trigger/Systems/TriggerSystem.cs +++ b/Content.Shared/Trigger/Systems/TriggerSystem.cs @@ -90,6 +90,9 @@ public sealed partial class TriggerSystem : EntitySystem if (!Resolve(ent, ref ent.Comp)) return false; + if (Terminating(ent)) + return false; // Stop trying to resurrect a dead horse. + if (HasComp(ent)) return false; // already activated From 0b8ff0ffa276b6c59c17d415f360c959739ba1be Mon Sep 17 00:00:00 2001 From: ThatGuyUSA Date: Thu, 2 Apr 2026 14:56:08 -0700 Subject: [PATCH 028/126] New Wizard robe and hat in-hand sprites (#43429) * save station * Revert "Merge branch 'wiz-guardian-deck'" This reverts commit 78fa318583b6c93110c47e3b9e23f7222747f89a, reversing changes made to 4d5dab1098bcfdbce14906d9c77dbc669e295760. * resprite! * i forgot to credit myself oops * you had it set to W for Wumbo, when it should have been set to M for Mini --- .../Head/Hats/redwizard.rsi/inhand-left.png | Bin 516 -> 305 bytes .../Head/Hats/redwizard.rsi/inhand-right.png | Bin 490 -> 299 bytes .../Clothing/Head/Hats/redwizard.rsi/meta.json | 2 +- .../Hats/violetwizard.rsi/equipped-HELMET.png | Bin 515 -> 511 bytes .../Head/Hats/violetwizard.rsi/inhand-left.png | Bin 462 -> 377 bytes .../Head/Hats/violetwizard.rsi/inhand-right.png | Bin 486 -> 365 bytes .../Head/Hats/violetwizard.rsi/meta.json | 2 +- .../Head/Hats/wizardhat.rsi/equipped-HELMET.png | Bin 605 -> 619 bytes .../Head/Hats/wizardhat.rsi/inhand-left.png | Bin 300 -> 234 bytes .../Head/Hats/wizardhat.rsi/inhand-right.png | Bin 297 -> 236 bytes .../Clothing/Head/Hats/wizardhat.rsi/meta.json | 2 +- .../Misc/redwizard.rsi/inhand-left.png | Bin 385 -> 423 bytes .../Misc/redwizard.rsi/inhand-right.png | Bin 412 -> 432 bytes .../Misc/wizard.rsi/inhand-left.png | Bin 356 -> 272 bytes .../Misc/wizard.rsi/inhand-right.png | Bin 354 -> 275 bytes 15 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Resources/Textures/Clothing/Head/Hats/redwizard.rsi/inhand-left.png b/Resources/Textures/Clothing/Head/Hats/redwizard.rsi/inhand-left.png index dabd7cb3d368dbebe2e6293f3b83ca4f0e568a7e..bac0bd75d95143b9fe250d96e3d14e5f8bc12371 100644 GIT binary patch delta 279 zcmV+y0qFjO1hE2;BYyw^b5ch_0Itp)=>Px#>q$gGRCt{2+RYJyFc5~}Psf_4ut d008Xq23G)uJP-?DoL>L{002ovPDHLkV1gm=fhGU| delta 491 zcmVUNxCejs8iglxAE zC;Aci0(wr|y>SiT!x)rW2Q#5ksws@YLu+J?EdYFv*nCFD@E&6<6vhAmo?4>@aBauJ z6~IGl6vm(o+%-RV0LbtwLf(FswL38*Ye2LBH)}5vRACIRt#dviQsIbGwxSGC1Dz0l z;Bwtq-+_&H0e^WWhnx#lcYkh3l(E2LcY$5E!LX%DKj17- zfYc78h61E^AT<;qwF86@LI@#*5JCtcujXa%-`6s9@Uo;#_8p9IDgl=C z4$S3{M`JE-fVuO%9GlCL``S!6d<|%fX&%mTd*;o%&14roH)}6bHQ?|W;Pwzaogm%* z0ifS^FgGI(p8?j|?lB1U4G2s72M(VBz+X^9?KQExz$pX}_UIcp)i$^X6d<(&si6R= h9Y_rYNbNvsegF+_vR~2qKzslI002ovPDHLkV1gn{+CKmQ diff --git a/Resources/Textures/Clothing/Head/Hats/redwizard.rsi/inhand-right.png b/Resources/Textures/Clothing/Head/Hats/redwizard.rsi/inhand-right.png index af141d7eec53158f41695fc626dacb04bdf767f0..cd3e95bb9a3310115850dc2d497c67be586ad038 100644 GIT binary patch delta 273 zcmV+s0q*|l1FHg%BYyw^b5ch_0Itp)=>Px#hBYy$0Nklq{glLYU zhJ*=B1;4{3f$I9($9B>hy|qnat-Vb$1f4NMWQremENp-?W|k0dzy!>U=%U#CN}inp z-iXvP&%Ir1IDZ9VZMkFt_p!BM&_!cswfM?U zfz|JTEw%U>*?d3Xm4Z$2_LU(TP+CA~Xh3NJrJ(_(1(fCoV-365Gc(B&00000NkvXX Hu0mjf6b{w! diff --git a/Resources/Textures/Clothing/Head/Hats/redwizard.rsi/meta.json b/Resources/Textures/Clothing/Head/Hats/redwizard.rsi/meta.json index 4e1c6c10da..1d42deb94c 100644 --- a/Resources/Textures/Clothing/Head/Hats/redwizard.rsi/meta.json +++ b/Resources/Textures/Clothing/Head/Hats/redwizard.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/4f6190e2895e09116663ef282d3ce1d8b35c032e.", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/4f6190e2895e09116663ef282d3ce1d8b35c032e. In-hand by ThatGuyUSA (github).", "size": { "x": 32, "y": 32 diff --git a/Resources/Textures/Clothing/Head/Hats/violetwizard.rsi/equipped-HELMET.png b/Resources/Textures/Clothing/Head/Hats/violetwizard.rsi/equipped-HELMET.png index f457d22f2f5b837ab7ec9175c2940d8147d2bc97..ca99276fce6a2c47c04fbf0b12884a719e50fa95 100644 GIT binary patch delta 486 zcmZo>`OiE-xt@WsILO_JVcj{ImkbPyJ3U<-Ln`LHopmtquz`T9Ige<;LpN?sg?0;B zgDDfZUL5daU#Xy)^5VcV2gWTEq{X8Q*wZ9abx%%P`oZ|aecfq`YkvjKIkQGgXu<1U z7cTJs-u3qG_4B*WCVlapd1h^7=+y7OeEUvh?VNhGXtv6v?rRrs)tgNSS;W|)cKxWf zs>`xTLDgNC-%e@zHcPueujqyTPT@C?a^G4Ov!#BO+_3)kzs$=W9Vd=I_jGZ2_g;?e z`|BIGe#oKDuGn{JYf0N5azj5bynSxrO3-^Db;= zDrl{le7dz>@XZ}Fkxrc$O`!wzTOa(c>S2}RO8BB!(L3c%?B@UMe?%o1q8NCY9k>@H zGGs8C@G$5$v@tGVyU^BZeT^_ z-OAWScOOpr`!nW^WUC>+t9^$OyUG;HzdwU+DAe$IZ4_2g>d00f?{elF{r5}E+gLe{qc literal 515 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEU_9mN;uumf=k2V6zK0zIT#t8f z?I>8>Dl&oDi+joht``iK8AX@0Y)OdMXb>xU?8N<0pm$5on~!~auT4I*Cpsnl`P=VN zc4tg#@1KsbySJ9Wo1m4dpiUA$GP5O;>*nAr8R+NLgE zAFt>YW~~WN$Y~5wc>4MEhF6{^%;#U1OFDI&$Dw=Qnz;oa9?m?*uvmTEaT5_hIf1G71Ee=rv2LC@9_5DAEB%>8atR* z+?}t#a{HCThrV+4e0{Eel)t_!e%IEy*V88i|F1b`eP~+ybCn)5_qG4_v~IVLKXjp| zsWsg#vB*i~?eA!&m&evN6`xEOS!Oe3CHq6E2?3u1woEwjE56mHLy>*yt(FrrTDC{{ qW?3F-V9|9DX1u^;BFI1jW4~E9E2%i!ti=d#Wzp$PzDq~US^ diff --git a/Resources/Textures/Clothing/Head/Hats/violetwizard.rsi/inhand-left.png b/Resources/Textures/Clothing/Head/Hats/violetwizard.rsi/inhand-left.png index 759e6b3ed975b6eef8a641a0d5751858934f5c15..27ff39da48480b0dfb9ad054dd34a0e7cebd8f87 100644 GIT binary patch delta 351 zcmV-l0igcQ1Nj1wBYyw^b5ch_0Itp)=>Px$Gf6~2RCt{2+Obi?Fcik|&m0q^Nf$=Q z04a2Apkn}v3_zFKqNZa2iVTorgB%3|Faqh)WCc+qGCJd&mGd)c{=dqxeV?DKxOz_l z00000002yRS4Z#ach-RYq;-gKf7nTqKEK|Y`})x&3Y0000C@dL09h0BD--ADic002ovPDHLkV1nf`qpJV_ delta 437 zcmV;m0ZRV)0?q@FBYy#vNklRW)pjZwrvN; z>Z0k)-^VHs{=KPlvMdYT((5j2wntqw-F|n#es=&cu6Xl0nSW~sGPj!qAjOL*g-n?{ zM>x@s!1GhlTZCDF>ErX{-3fN__FR{@_bRG}Sps;;O8~(4ehwhzJ z02-2g4K4)((0^WlHU^-*0BsCFdjZ;39{>>%5fKp)5v@=3hml(w`tUNgk0A8nWRd?7 z`fKlj%i;ov={;~s{t>YI52p4WgdTz919&aF4!C|&_i^N44?uQ+-GpJYn<~aO zP$VCKyantg43phZVLl3>0Jd?<`#(}tU6j28FXk<Px$CrLy>RCt{2+OciIKorLD&rv3zp@R`T zfD0=dSQ#LN2cV<&QPX9B6byi}0ZNepG6HlotZ)?3Ij8t6h;zt?^#4`Z#!ufp$<=!j z00000007XdDjQpsjU7A3Xzuawe&$gA$IuJ!AFC`>tjKFg>VJKnBA2=EdFLR0mx{dB z@^zj`(#BYOJRKxSpI>k7|5Is(pW9_xFZNcFrjwbrI}^+r3}2OveSR|S^>+9D`}e(6 z4)1J?ZJTgB9Wjnz{h~r1hHldkk(|@Aebi&O!`-Dl}yN9*e)T;KtxF l3@S9%{5`Y<0ssIo)EQzLh07$jmKgv5002ovPDHLkV1gfHnZ*DA delta 461 zcmaFM^o)6eay{c(PZ!6KiaBp@?GHR`Aj0;+KSpiA!sJ$-2~5?@eT*7L49|IUuA9s| zp#0>PA9{q z-;~x`ef=}Vs`(kqvSqq+)fJcMCg|V0vL*HXdvk^lLg@?*pP#ZYRP3lPv8&iOXWwO= zw+CFdxBa|-wfp_*`*PMHl9O!0R;_zI-?K!ZU8`8_Lh=FA70Yx#ew^9O_TgXIn}++{ z`$G52UH0F!NnWw|!jt;y-EA9KkL~$co4vb|r%%v{q5h#Q+w~?!4{?QFIFb96Q9uw3 zF8<=#wza0WePeCK%VQ@e+`P}>`}S~qRvCl$KIep6XMQTj%iA6K_H)9)N6TUs7`!a| zn!T)vFX02H$EOI<%&RlMGdH}JWt?5fn8>L%m+70_b~k6nJKP)#=QB;d$M`KUe96qs yw$qAc`^^3-bYU{dvTaSW-L z^LCbF)@=ug*7;GKPx5wsJT_ZTN`SHXe8PoA+3_xx-bI3Q6&4ta@F+ge+jZ{PY=sG@ zO9bm&CHs#}GdU-J>-*ay9-sGD8Z$Tjcl{6$IyGOSGH}+r&wuZ#eX0{^Ff_gXy7c{x z<(f`u-J+K&%eGAr@R(aNFL7IhtfZ01ddAwx&s{SwRkB^aB48NYI;Z=JSk2b+7oXhS zdVaO^bC%6VU!C2YnN>4ey`ttu+!vK6u9=%&tJc|@c1+r`mY3n{<;9<8|K9EUWH(dX z#?VkjeJ|noTn2`dpZS)pEc~&{rpo+Yezb6mlArFmV_U1gJz_Of^a?VGU&p}E9((iJ z+!TlK9*M#mQ{CU6l3=Luk1I=l)j2_yy&?YW>5cv0qg_0?E?>#-d7&pW?d4?N7hIdv zw;ykr5Popp?(A(3qIL+JY3OfyUG;F7_*i*dFVdQ&MBb@0B@8Ts{jB1 literal 605 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEU{djPaSW-L^LCbRR&#(zYrpHk zl1rVFeXKGZnHqBrIUc{+QJrc2h>Po{ivr6f$K!KTgjkr@H-B1lQM}e}>gD;r&-OgB zeg9eg(B6A#n;W$~XRUno?e*zf#deY_pPikcu=yufqDa5c5wTeHo}@F+R<}*qw}op* z*(a5=AreOeT=`_vG6bSaa($z7XZ!5)J@_ks^UqgF(V~`*jtT*d(tO_nRJbSN(ci@l z;w?|#JL@%E|H|ia$>(Q(HiJWfm4m3|x$@J;3jY;$|JFEAw>|ew@x-L437H=mPFY{% zb`sg%&(QE$ar3I%nhT`dx-H~49W$(wXRu+ZW&kRd{gQVfT!!IZ*ZSM{?DHP7Duzsq z?^vNHvtfFk>H?`3w@xnqsbX4@Evet z*}%L(kns&ejthe|gET=QtJ#ayB#Ie>pBC(5KKAb7Zi8zzO_LioR)5JYZM*h<(Rbww z%iZ&{lD<8CDY@!i?>fD>@~+tD!co=DdbU*zPyaOJ_V4R`Rnt`eXP&LW)$*DCFH98{ zO73>Q?^r5>H&Uc4mshA(3NCUj(GsCAj?iJADC1 u0(*io(+$QO9Smg*W=ajR4ZQ?~=2uOS4!t_LhXa^w7(8A5T-G@yGywolg9v8; diff --git a/Resources/Textures/Clothing/Head/Hats/wizardhat.rsi/inhand-left.png b/Resources/Textures/Clothing/Head/Hats/wizardhat.rsi/inhand-left.png index 4a3bb5d4b85d1ee09e1f4e16a619e7a8b8eb3c61..8c45376672f245ce2384882f570058c8af075c14 100644 GIT binary patch delta 218 zcmZ3(^ontUL_G&H0|Ud{?Tv4M6k~CayA#8@b22Z19H9W85LY10te4O2G?mAFu4dRQ z_P_(CuixFS$kcif|(++YSP~dU)Rrvq^bhF=$l(}Mi zawe2(2qmqIn{}-{tiTzl=le)wP1=i8 zk@EdlC6ar78o!j&VQY!FP}zLrPxifL1{bkM=T$f!@Nj+peOckbjD#S*?ePpJ6yi!; QfDT~rboFyt=akR{0O3tpZ2$lO delta 285 zcmaFGxQ1zhL_HHT0|Uc#_L;swiZj3`#1%+0FeETAoY4Ih;s|6imIV0)GdMiEkp|>s zSA|5BxTF>*7iAWdWaj5FFjUM54l5`s{r)Am;N#aPTHd-^=gyoD-VkbV(fGk5o%24L zCmD)*dUseD2N_>B_EMPhXws2MAqv4OZ8mEuZ*=Lb?>1K5yh370cen9c4MTI$BW6#6 z!V10?GtTB_h*>PPRK9eL4ba{wPZ!4!i{9h}3Dy-2tTP`o99iR0<2Hd|GP6?yuZsce z0oRBl2Tp|@VvPuIVx6YcbD|}~aR%eHro_`45^EY*V;e+c8%{FDDlqu6Xtp}APFwUT hFo>fpnvGeEfkBmzeWuw4-DaRQ44$rjF6*2UngG?rWxxOc diff --git a/Resources/Textures/Clothing/Head/Hats/wizardhat.rsi/inhand-right.png b/Resources/Textures/Clothing/Head/Hats/wizardhat.rsi/inhand-right.png index 4f669b4bb74606d02dcdced92a498a59a0231bd1..d4a9e495ffd67f571051dc46edd8093feac0e2d9 100644 GIT binary patch delta 220 zcmZ3<^oDVQL_G&H0|Ud{?Tv4M6k~CayA#8@b22Z19H9W85LY10te4O2G?mAFu4dRQ z_P_(CuixmD6=GU+PYw!mdWfPy;6!zSnQml&*R!a_(Hy;$7o;xm5H2 zwE1>B439~C77zF%JZ<-eRTXDuTkG3Z)fXtJ^PPI6->h&pZN`^<_N>dEFzx=raH1kW RtqJG^22WQ%mvv4FO#q+cRlEQI delta 282 zcmaFExRPmtL_HHT0|Uc#_L;swiZj3`#1%+0FeETAoY4Ih;s|6imIV0)GdMiEkp|>s zSA|5BxTF>*7iAWdWaj5FFjUM54l5`s{r)Am;N#aPTHd-^=gyoD-VkbV(fGk5o%24L zCmD)*dUseD2N_>B_EMPhXws2MAqv4OZ8mEuZ*=Lb?>1K5yh370cen9c4MTI$BW6#6 z!V10?GtTB_h*>PPRK9eL4ba|jPZ!4!i{9h}3D$@PW@ct?qxpgw2fB0)upRN?gQ2jCj;j&#BG&o3ixUEv8A}=&Oze`GrRIyqDx7BY^%mL1uuIG* enoWp-VUBPZN7#eS4Xi*r7(8A5T-G@yGywp&;a+k8 diff --git a/Resources/Textures/Clothing/Head/Hats/wizardhat.rsi/meta.json b/Resources/Textures/Clothing/Head/Hats/wizardhat.rsi/meta.json index 03229e284d..4bce2899c5 100644 --- a/Resources/Textures/Clothing/Head/Hats/wizardhat.rsi/meta.json +++ b/Resources/Textures/Clothing/Head/Hats/wizardhat.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/4f6190e2895e09116663ef282d3ce1d8b35c032e. equipped-HELMET-dog modified from equipped-HELMET by Sparlight (GitHub).", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/4f6190e2895e09116663ef282d3ce1d8b35c032e. equipped-HELMET-dog modified from equipped-HELMET by Sparlight (GitHub). In-hand by ThatGuyUSA (github).", "size": { "x": 32, "y": 32 diff --git a/Resources/Textures/Clothing/OuterClothing/Misc/redwizard.rsi/inhand-left.png b/Resources/Textures/Clothing/OuterClothing/Misc/redwizard.rsi/inhand-left.png index eec3320f0f7a6316f76c6a89a04f396908ba2583..991445e5824fe2fcb19ffe1866770967bdf5cca6 100644 GIT binary patch delta 398 zcmZo*OY#?#$qy38T0(LchAD+|| z5f;p+bZeFu^vX4?bQWaxo?e#mY>KkNUY^259h&o(EIjYur4wBxTffJPP1*$xmdF>M z3o>XBIWuSTwvW%|ME2xL+po-epm-y3zZv(!SswM5f5@CUP`2@mwaz1zOBOd|&g{`w zdr*5dEX{TCk6V{b_I2N0XkBt|GxLc}ixf{=9R0TJ{P_rz1ZyN&Fobtkr)?|bDOVHf+4$@bB0303B2Pttn) z%oDsH7+-teFuh*v{jH{}m;Xsu$YrikLI5Lvv8JWA{=MndMkP1aul8hr{$dx$X6tgD zy|y(4@Ap@{xp_Xvn4xPoyT9k5*FSB(di;x%^6z{7RqyW^GiE1_2e!}tKD{q1<^1dQ qzXMlO4+Q)>%Csl`v3mg9C*~@rEqA6DGqExNfhW7GpUXO@geCy`3$}9r delta 359 zcmZ3^+{ipZxt`I()5S5QV$R!Jws}m30&Ne?1;Y!vLiRDn&iYbv@m`blTT2UO)|Hon zH|SQkdBk(&ICXwkfAGQf(y28|BilY^XyzCg!@%Z3k)|jC2ksyat=@2spf26s{VR(f zz217uWYq!=E=OCB1+h}U8C~~unyKDy7J2-V{YXXqywkCTp?{8+=Kc{lA8c=M z*M7pW#}&`UOYc8#?#p~&pTa-AoKISM(_aN^Pd}2cAGVi0H$CEK&L=*G3PwH#!+FP? zx@G1cJL*#Tcjo7AgLvnEtZql9&sfTw^~vP8v-tw?2ikY{Gs&~w-^v=h?XURS3xl;q;`~CE0r6z48Mtt8^XgoP?crWXgr@md{h!$g$w|TVn-BYyw^b5ch_0Itp)=>Px$YDq*vRCt{2+A&VUFc^m67t|}b1Cw`# zGE^*_0p%9HQ@BRAE=(PY7+AWrLo+8=K>8W#giuOF+LSird!)#+l~|vgKUr)L5fKp) z5fKp)5$QW~&QdUd6wI8nb7yM8oHKLIEM{+%zj`nPTQ0_CA%9d8oiuez%sI=!3~V0T z{I~hjcykVB-ft%Gz8Wj5;!=IdU%U3@zuq2j-ZKZtvduu2v5wF zaur7-%VQ~>B<+dJYHvLUo9)T}tpWgsz#72&P1`YpGJk9bO(&>+gyq$dA7H6T{+$)j zCs(@$CtyS1Er0fJfX5&1y+kvxdfC4N4;{)cA|fIpA|fIpA|i5W(zx4dA#`fjXxl+M zLn&BruIeur2>PUc(!x4$a%7*QJ^}OI z@SYGtdx^TBJULeGzxTER{)2-T;H4ss!#4q78^uFL_|bHL_|bH#?G9x91I`_ zGw1Bsnffs2%$zfe*?Z-09}L0rI9fuBV9r?yW?;)WmcPxP$A6nkF!R3t0q@&d(-o)c zOa9KaEC2as!FkUTpeX7>QLv8n6gcl$HIpm@U`5aTvt|{iQ!8UD4V?2&Ejs^t3AWgg z|62zDOn^0j_w|^V3utq%(zeN+>!BKSz*sNeR=R<}BlpzLE3$#QNcTb_F^i+d~)-Kt{5wnRbY z+@1&CcQrX$IJVu{{^zpfqf7lh|3e*~D>*D-`5u1$pHu9CC(dR-o%ebk)mX6Y)T~bT w{Wr;BZ&~|J%c(5Ni_|>UYx#5h%L}YvtSV$&-1jkk7SKryp00i_>zopr0PPrR>i_@% delta 341 zcmbQh^n_`GL_G^L0|P^Zd(K-RB@y5g;tHf492^)JPT1MmSy)&wFeCs)Zrr#LdiQ7q zki}RMeukTAW;zSx}OhpU1#ZF(){zps4iwm*9erU!Q1s>uQ}l zb3S-OsKG_!2aj~l`)HnIDC+6mVPPC(eA(DbVNU&{Nk=AyCD9yuACnPNpRw5e+w7ALB#WBRAGdV$mb#X(OLqLFl zhY=SSSHkpkBauMXpfz1hPB%nZG&LJVH8eF@CoEUc(beT$!r<}H$dNavNh(j9LxY1i zh?iBkmZj?fox|Yi>gTe~DWM4fnM8BE diff --git a/Resources/Textures/Clothing/OuterClothing/Misc/wizard.rsi/inhand-right.png b/Resources/Textures/Clothing/OuterClothing/Misc/wizard.rsi/inhand-right.png index e2542d772cd650319a602ef95fa8bf4bdcd16970..84f70a58a6537b3409e487ea7afc27bdbd26a249 100644 GIT binary patch delta 259 zcmaFFG?{6FL_G&H0|Ud{?Tv4M6k~CayA#8@b22Z19K`^i5LY10te4MZR4r^#$n7+h z$9=A5*ev$I1Mz*=dX}BLb=BnUcOLFMGa64Cm0+CeU6 zLk?DBn+YlZ|2Ln`%gi)u4b{^R8 BYApZ& delta 339 zcmV-Z0j&O$0^$OY7=Hu<00013M{Ml?00000787K0Y`&H~;_u z*x1-lgK%~L0004WQchCV=-0C=2JR&a84_w-Y6 z@%7{?OD!tS%+FJ>RWQ*r;NmRLOex6#a*U0*I5Sc+(=$pSoPXky#FA7XQ>i?&swh7x zRf&r;C9|j)q>YO+ttc@!6~s2=QdV&Fa{(I+0Dcl15LR>@Q2+n{fJsC_R7i>Klrai` zFbqXoy+DJwOA78b(A~@Q3cbFyN^wY}gW!_4WcZJa&lpAd77O6`1pv-7Y%sC=xX+;q zM6_^%C+ZYZN=&S)9rF9(h=5|rl)gp&XO002ovPDHLkV1idZhEM Date: Thu, 2 Apr 2026 22:12:37 +0000 Subject: [PATCH 029/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 62b7a50860..cf60d97d83 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: PotentiallyTom - changes: - - message: Added a page in the guidebook listing common AI and silicon lawsets. - type: Add - id: 9088 - time: '2025-10-13T11:55:26.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/38225 - author: meganerobot changes: - message: SmartFridges are now airtight. @@ -4029,3 +4022,10 @@ id: 9599 time: '2026-03-31T03:41:12.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43411 +- author: ThatGuyUSA + changes: + - message: Wizard robes and hats have been given proper in-hand sprites. + type: Tweak + id: 9600 + time: '2026-04-02T22:11:23.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43429 From 81be6f25713f5770636368e0d1c6782922d5e7dd Mon Sep 17 00:00:00 2001 From: Booblesnoot42 <108703193+Booblesnoot42@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:03:23 -0400 Subject: [PATCH 030/126] Force Vent Critters to Attack (#42399) * Create force attack component, system and add to spider * Add check for incapacitated enemies * minor cleanup * Fix networking stuff, hopefully * Add component to slimes * Move system and component to server, add localization * Fixed attack animation not playing for server-forced attacks, hopefully didn't break anything godo * Add feedback popup * pretty big optimization --------- Co-authored-by: SlamBamActionman Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> --- .../ForceAttack/ForceAttackComponent.cs | 35 ++++++++ .../ForceAttack/ForceAttackSystem.cs | 82 +++++++++++++++++++ .../Weapons/Melee/SharedMeleeWeaponSystem.cs | 12 +-- .../components/force-attack-component.ftl | 1 + .../Prototypes/Entities/Mobs/NPCs/animals.yml | 1 + .../Entities/Mobs/NPCs/elemental.yml | 1 + .../Prototypes/Entities/Mobs/NPCs/slimes.yml | 3 + .../FeedbackPopup/feedbackpopups.yml | 13 +++ 8 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 Content.Server/ForceAttack/ForceAttackComponent.cs create mode 100644 Content.Server/ForceAttack/ForceAttackSystem.cs create mode 100644 Resources/Locale/en-US/components/force-attack-component.ftl diff --git a/Content.Server/ForceAttack/ForceAttackComponent.cs b/Content.Server/ForceAttack/ForceAttackComponent.cs new file mode 100644 index 0000000000..ce24554714 --- /dev/null +++ b/Content.Server/ForceAttack/ForceAttackComponent.cs @@ -0,0 +1,35 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Server.ForceAttack; + +/// +/// This is used to force a player-controlled mob to attack nearby enemies, preventing "friendly antag"ing. +/// +[RegisterComponent] +[AutoGenerateComponentPause] +public sealed partial class ForceAttackComponent : Component +{ + /// + /// The next time this component will attempt to force an attack. + /// + [AutoPausedField] + public TimeSpan NextAttack = TimeSpan.MaxValue; + + /// + /// Whether an enemy is in range. + /// + public bool InRange = false; + + /// + /// The time this component will wait before forcing an attack when an enemy is in range. + /// + [DataField] + public TimeSpan PassiveTime = TimeSpan.FromSeconds(5); + + /// + /// The message displayed on forced attack + /// + [DataField] + public LocId Message = "force-attack-component-message"; +} diff --git a/Content.Server/ForceAttack/ForceAttackSystem.cs b/Content.Server/ForceAttack/ForceAttackSystem.cs new file mode 100644 index 0000000000..33512b0e68 --- /dev/null +++ b/Content.Server/ForceAttack/ForceAttackSystem.cs @@ -0,0 +1,82 @@ +using System.Linq; +using Content.Shared.CombatMode; +using Content.Shared.Mobs.Systems; +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Systems; +using Content.Shared.Popups; +using Content.Shared.Weapons.Melee; +using Content.Shared.Weapons.Melee.Events; +using Robust.Shared.Player; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Server.ForceAttack; + +/// +/// This handles forcing a player-controlled mob to attack nearby enemies. +/// +public sealed class ForceAttackSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedMeleeWeaponSystem _melee = default!; + [Dependency] private readonly NpcFactionSystem _faction = default!; + [Dependency] private readonly SharedCombatModeSystem _mode = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly MobStateSystem _mob = default!; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnMeleeAttack); + } + + private void OnMeleeAttack(Entity ent, ref MeleeAttackEvent args) + { + ent.Comp.NextAttack = _timing.CurTime + ent.Comp.PassiveTime; + } + + /// + public override void Update(float frameTime) + { + base.Update(frameTime); + + var curTime = _timing.CurTime; + + // Query includes ActorComponent to only get mobs currently controlled by players + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var forceComp, out var factionComp, out var modeComp, out _)) + { + // Check if we have a weapon + if (!_melee.TryGetWeapon(uid, out var weaponUid, out var weapon) || weapon.NextAttack > curTime) + continue; + + // Find a target in range that isn't critical or dead + if (!_faction.GetNearbyHostiles((uid, factionComp), weapon.Range) + .Where((potTarget) => !_mob.IsIncapacitated(potTarget)) + .TryFirstOrNull(out var target)) + { + forceComp.InRange = false; + continue; + } + + if (!forceComp.InRange) // Just entered range + { + forceComp.InRange = true; + forceComp.NextAttack = curTime + forceComp.PassiveTime; + continue; + } + + if (forceComp.NextAttack > curTime) + continue; + + // Force mob to enter combat mode (necessary for AttemptAttack to succeed). + _mode.SetInCombatMode(uid, true, modeComp); + + var popupMessage = Loc.GetString(forceComp.Message); + if (popupMessage.Length != 0) + _popup.PopupEntity(popupMessage, uid, uid); + + _melee.AttemptLightAttack(uid, weaponUid, weapon, target.Value, false); + } + } +} diff --git a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs index 6f4b078fe2..f827249dd3 100644 --- a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs +++ b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs @@ -344,12 +344,12 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem AttemptAttack(user, weaponUid, weapon, new LightAttackEvent(null, GetNetEntity(weaponUid), GetNetCoordinates(coordinates)), null); } - public bool AttemptLightAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, EntityUid target) + public bool AttemptLightAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, EntityUid target, bool predicted = true) { if (!TryComp(target, out TransformComponent? targetXform)) return false; - return AttemptAttack(user, weaponUid, weapon, new LightAttackEvent(GetNetEntity(target), GetNetEntity(weaponUid), GetNetCoordinates(targetXform.Coordinates)), null); + return AttemptAttack(user, weaponUid, weapon, new LightAttackEvent(GetNetEntity(target), GetNetEntity(weaponUid), GetNetCoordinates(targetXform.Coordinates)), null, predicted); } public bool AttemptDisarmAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, EntityUid target) @@ -364,7 +364,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem /// Called when a windup is finished and an attack is tried. /// /// True if attack successful - private bool AttemptAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, AttackEvent attack, ICommonSession? session) + private bool AttemptAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, AttackEvent attack, ICommonSession? session, bool predicted = true) { var curTime = Timing.CurTime; @@ -471,7 +471,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem throw new NotImplementedException(); } - DoLungeAnimation(user, weaponUid, weapon.Angle, TransformSystem.ToMapCoordinates(GetCoordinates(attack.Coordinates)), weapon.Range, animation); + DoLungeAnimation(user, weaponUid, weapon.Angle, TransformSystem.ToMapCoordinates(GetCoordinates(attack.Coordinates)), weapon.Range, animation, predicted); } var attackEv = new MeleeAttackEvent(weaponUid); @@ -954,7 +954,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem return true; } - private void DoLungeAnimation(EntityUid user, EntityUid weapon, Angle angle, MapCoordinates coordinates, float length, string? animation) + private void DoLungeAnimation(EntityUid user, EntityUid weapon, Angle angle, MapCoordinates coordinates, float length, string? animation, bool predicted = true) { // TODO: Assert that offset eyes are still okay. if (!TryComp(user, out TransformComponent? userXform)) @@ -975,7 +975,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem if (localPos.Length() > visualLength) localPos = localPos.Normalized() * visualLength; - DoLunge(user, weapon, angle, localPos, animation); + DoLunge(user, weapon, angle, localPos, animation, predicted); } public abstract void DoLunge(EntityUid user, EntityUid weapon, Angle angle, Vector2 localPos, string? animation, bool predicted = true); diff --git a/Resources/Locale/en-US/components/force-attack-component.ftl b/Resources/Locale/en-US/components/force-attack-component.ftl new file mode 100644 index 0000000000..fc033e1edc --- /dev/null +++ b/Resources/Locale/en-US/components/force-attack-component.ftl @@ -0,0 +1 @@ +force-attack-component-message = Your anger overwhelms you! diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 1379095e4e..0c8e4beedb 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -2619,6 +2619,7 @@ raffle: settings: short - type: GhostTakeoverAvailable + - type: ForceAttack - type: entity name: tarantula diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml index b3f7749246..49f53654f2 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml @@ -386,6 +386,7 @@ solution: bloodstream - type: DrainableSolution solution: bloodstream + - type: ForceAttack - type: entity parent: MarkerBase diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml index 8d8b4f763e..ef1203e458 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml @@ -168,6 +168,7 @@ - MindRoleGhostRoleTeamAntagonist raffle: settings: short + - type: ForceAttack - type: entity name: green slime @@ -208,6 +209,7 @@ - MindRoleGhostRoleTeamAntagonist raffle: settings: short + - type: ForceAttack - type: entity name: yellow slime @@ -247,3 +249,4 @@ - MindRoleGhostRoleTeamAntagonist raffle: settings: short + - type: ForceAttack diff --git a/Resources/Prototypes/FeedbackPopup/feedbackpopups.yml b/Resources/Prototypes/FeedbackPopup/feedbackpopups.yml index d424a24244..a71276bffc 100644 --- a/Resources/Prototypes/FeedbackPopup/feedbackpopups.yml +++ b/Resources/Prototypes/FeedbackPopup/feedbackpopups.yml @@ -17,3 +17,16 @@ responseType: "General Feedback" responseLink: "https://forum.spacestation14.com/c/development/feedback/51" showRoundEnd: false + +- type: feedbackPopup + id: ForceVentCrittersToAttackFeedback + popupOrigin: wizden_master + title: "[bold]Played an antagonist vent critter role?[/bold]" + description: >- + If you've played or interacted with a player-controlled antagonist vent critter (spiders, slimes, clown spiders), please leave your feedback on how the new auto-attack feature went. + responseType: "Feedback Thread" + responseLink: "https://forum.spacestation14.com/t/force-vent-critters-to-attack-feedback/26861" + showRoundEnd: true + ruleWhitelist: + components: + - VentHordeRule From 9504c1e0bb66294937dfd5037fe5c7a08e420c68 Mon Sep 17 00:00:00 2001 From: PJBot Date: Fri, 3 Apr 2026 06:19:23 +0000 Subject: [PATCH 031/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index cf60d97d83..f6d5385c12 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: meganerobot - changes: - - message: SmartFridges are now airtight. - type: Tweak - id: 9089 - time: '2025-10-13T16:19:03.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40196 - author: TrixxedHeart changes: - message: Renamed "trash" reagent to "reprocessed material" @@ -4029,3 +4022,11 @@ id: 9600 time: '2026-04-02T22:11:23.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43429 +- author: Booblesnoot42 + changes: + - message: 'EXPERIMENTAL: Player-controlled spiders and slimes are now forced to + attack if they are passive near enemies for too long.' + type: Tweak + id: 9601 + time: '2026-04-03T06:18:14.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/42399 From 17ccf1dc70d7a931c798bf8987c343d65afb96ed Mon Sep 17 00:00:00 2001 From: alexalexmax <149889301+alexalexmax@users.noreply.github.com> Date: Fri, 3 Apr 2026 06:21:04 -0400 Subject: [PATCH 032/126] Hijack the Automated Trade Station objective (#42135) * predict TradeStationComponent * hijack beacon first pass * add Objective uplink category * add HijackTradeStationCondition, objective, and uplink entry * add sprite and cleanups * fix + make cooldown sane * use TimeSpans and update logic accordingly + private state-related fields * remove temporary reward * tiny loc changes * buncha changes * Change success message, beacon physics properties, formatting --------- Co-authored-by: seanpimble <149889301+seanpimble@users.noreply.github.com> Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> Co-authored-by: SlamBamActionman --- .../Cargo/Components/TradeStationComponent.cs | 10 - .../Cargo/Systems/CargoSystem.Shuttle.cs | 3 +- Content.Server/Cargo/Systems/CargoSystem.cs | 1 + .../HijackTradeStationConditionComponent.cs | 11 + .../HijackTradeStationConditionSystem.cs | 35 ++ .../Cargo/Components/TradeStationComponent.cs | 17 + Content.Shared/Cargo/SharedCargoSystem.cs | 19 + .../EntitySystems/AnchorableSystem.cs | 23 +- .../ActiveHijackBeaconComponent.cs | 16 + .../HijackBeacon/HijackBeaconComponent.cs | 53 +++ .../HijackBeacon/HijackBeaconSystem.cs | 353 ++++++++++++++++++ .../en-US/hijack-beacon/hijack-beacon.ftl | 16 + Resources/Locale/en-US/store/categories.ftl | 1 + .../Locale/en-US/store/uplink-catalog.ftl | 4 + .../Prototypes/Catalog/uplink_catalog.yml | 17 + .../Entities/Objects/Tools/hijack_beacon.yml | 34 ++ .../Prototypes/Objectives/objectiveGroups.yml | 8 + Resources/Prototypes/Objectives/traitor.yml | 17 + Resources/Prototypes/Store/categories.yml | 5 + Resources/Prototypes/Store/presets.yml | 1 + .../hijack_beacon.rsi/extraction_point.png | Bin 0 -> 1389 bytes .../Objects/Tools/hijack_beacon.rsi/meta.json | 20 + 22 files changed, 648 insertions(+), 16 deletions(-) delete mode 100644 Content.Server/Cargo/Components/TradeStationComponent.cs create mode 100644 Content.Server/Objectives/Components/HijackTradeStationConditionComponent.cs create mode 100644 Content.Server/Objectives/Systems/HijackTradeStationConditionSystem.cs create mode 100644 Content.Shared/Cargo/Components/TradeStationComponent.cs create mode 100644 Content.Shared/HijackBeacon/ActiveHijackBeaconComponent.cs create mode 100644 Content.Shared/HijackBeacon/HijackBeaconComponent.cs create mode 100644 Content.Shared/HijackBeacon/HijackBeaconSystem.cs create mode 100644 Resources/Locale/en-US/hijack-beacon/hijack-beacon.ftl create mode 100644 Resources/Prototypes/Entities/Objects/Tools/hijack_beacon.yml create mode 100644 Resources/Textures/Objects/Tools/hijack_beacon.rsi/extraction_point.png create mode 100644 Resources/Textures/Objects/Tools/hijack_beacon.rsi/meta.json diff --git a/Content.Server/Cargo/Components/TradeStationComponent.cs b/Content.Server/Cargo/Components/TradeStationComponent.cs deleted file mode 100644 index 0422470cc9..0000000000 --- a/Content.Server/Cargo/Components/TradeStationComponent.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Content.Server.Cargo.Components; - -/// -/// Target for approved orders to spawn at. -/// -[RegisterComponent] -public sealed partial class TradeStationComponent : Component -{ - -} diff --git a/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs b/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs index 351451123c..635eadea1d 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs @@ -6,6 +6,7 @@ using Content.Shared.Cargo.Components; using Content.Shared.Cargo.Events; using Content.Shared.Cargo.Prototypes; using Content.Shared.CCVar; +using Content.Shared.HijackBeacon; using Robust.Shared.Audio; using Robust.Shared.Prototypes; @@ -14,7 +15,7 @@ namespace Content.Server.Cargo.Systems; public sealed partial class CargoSystem { /* - * Handles cargo shuttle / trade mechanics. + * Handles automated trade station / trade mechanics. */ private static readonly SoundPathSpecifier ApproveSound = new("/Audio/Effects/Cargo/ping.ogg"); diff --git a/Content.Server/Cargo/Systems/CargoSystem.cs b/Content.Server/Cargo/Systems/CargoSystem.cs index 9f3a4d5bf3..a35f1e7ea3 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.cs @@ -7,6 +7,7 @@ using Content.Shared.Access.Systems; using Content.Shared.Administration.Logs; using Content.Server.Radio.EntitySystems; using Content.Shared.Cargo; +using Content.Shared.Cargo.Components; using Content.Shared.Containers.ItemSlots; using Content.Shared.Mobs.Components; using Content.Shared.Paper; diff --git a/Content.Server/Objectives/Components/HijackTradeStationConditionComponent.cs b/Content.Server/Objectives/Components/HijackTradeStationConditionComponent.cs new file mode 100644 index 0000000000..416c422837 --- /dev/null +++ b/Content.Server/Objectives/Components/HijackTradeStationConditionComponent.cs @@ -0,0 +1,11 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Objective condition that requires the player to hijack the trade station. +/// +[RegisterComponent, Access(typeof(HijackTradeStationConditionSystem))] +public sealed partial class HijackTradeStationConditionComponent : Component +{ +} diff --git a/Content.Server/Objectives/Systems/HijackTradeStationConditionSystem.cs b/Content.Server/Objectives/Systems/HijackTradeStationConditionSystem.cs new file mode 100644 index 0000000000..e5f88a28dd --- /dev/null +++ b/Content.Server/Objectives/Systems/HijackTradeStationConditionSystem.cs @@ -0,0 +1,35 @@ +using Content.Server.Objectives.Components; +using Content.Shared.Cargo.Components; +using Content.Shared.Mind; +using Content.Shared.Objectives.Components; +using NetCord; + +namespace Content.Server.Objectives.Systems; + +/// +/// Handles the Hijack Trade Station objective. +/// +public sealed class HijackTradeStationConditionSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetProgress); + } + + private void OnGetProgress(Entity ent, ref ObjectiveGetProgressEvent args) + { + var enumerator = EntityQueryEnumerator(); + args.Progress = 0f; + // If there's any hacked trade station, succeed. + while (enumerator.MoveNext(out var comp)) + { + if (!comp.Hacked) + continue; + + args.Progress = 1f; + return; + } + } +} diff --git a/Content.Shared/Cargo/Components/TradeStationComponent.cs b/Content.Shared/Cargo/Components/TradeStationComponent.cs new file mode 100644 index 0000000000..ec0cba5386 --- /dev/null +++ b/Content.Shared/Cargo/Components/TradeStationComponent.cs @@ -0,0 +1,17 @@ +using Content.Shared.HijackBeacon; +using Robust.Shared.GameStates; + +namespace Content.Shared.Cargo.Components; + +/// +/// Target for approved orders to spawn at. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class TradeStationComponent : Component +{ + /// + /// The Trade Station's current hijack state. Modified by HijackBeaconSystem. + /// + [DataField, AutoNetworkedField] + public bool Hacked = false; +} diff --git a/Content.Shared/Cargo/SharedCargoSystem.cs b/Content.Shared/Cargo/SharedCargoSystem.cs index 9d044d1850..291d3abbc3 100644 --- a/Content.Shared/Cargo/SharedCargoSystem.cs +++ b/Content.Shared/Cargo/SharedCargoSystem.cs @@ -1,5 +1,6 @@ using Content.Shared.Cargo.Components; using Content.Shared.Cargo.Prototypes; +using Content.Shared.HijackBeacon; using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; @@ -17,6 +18,7 @@ public abstract class SharedCargoSystem : EntitySystem base.Initialize(); SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnHijackSuccess); } private void OnMapInit(Entity ent, ref MapInitEvent args) @@ -25,6 +27,23 @@ public abstract class SharedCargoSystem : EntitySystem Dirty(ent); } + private void OnHijackSuccess(ref HijackBeaconSuccessEvent args) + { + var stationQuery = EntityQueryEnumerator(); + while (stationQuery.MoveNext(out var uid, out var comp)) + { + foreach (var (account, cash) in comp.Accounts) + { + comp.Accounts[account] = cash - args.Fine; + args.Total += args.Fine; + } + + var ev = new BankBalanceUpdatedEvent(uid, comp.Accounts); + RaiseLocalEvent(uid, ref ev, true); + Dirty(uid, comp); + } + } + /// /// For a given station, retrieves the balance in a specific account. /// diff --git a/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs b/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs index 3985bd3051..b290aae404 100644 --- a/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs +++ b/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs @@ -235,12 +235,8 @@ public sealed partial class AnchorableSystem : EntitySystem // Log anchor attempt (server only) _adminLogger.Add(LogType.Anchor, LogImpact.Low, $"{ToPrettyString(userUid):user} is trying to anchor {ToPrettyString(uid):entity} to {transform.Coordinates:targetlocation}"); - if (TryComp(uid, out var anchorBody) && - !TileFree(transform.Coordinates, anchorBody)) - { - _popup.PopupClient(Loc.GetString("anchorable-occupied"), uid, userUid); + if (!CanAnchorAt(uid, transform.Coordinates, userUid)) return; - } if (AnyUnstackable(uid, transform.Coordinates)) { @@ -285,6 +281,23 @@ public sealed partial class AnchorableSystem : EntitySystem return !attempt.Cancelled; } + public bool CanAnchorAt(Entity entity, EntityUid? user = null) + { + return CanAnchorAt(entity, Transform(entity).Coordinates, user); + } + + public bool CanAnchorAt(Entity entity, EntityCoordinates coordinates, EntityUid? user = null) + { + if (!Resolve(entity, ref entity.Comp)) + return true; + + if (TileFree(coordinates, entity.Comp)) + return true; + + _popup.PopupClient(Loc.GetString("anchorable-occupied"), entity, user); + return false; + } + /// /// Returns true if no hard anchored entities exist on the coordinate tile that would collide with the provided physics body. /// diff --git a/Content.Shared/HijackBeacon/ActiveHijackBeaconComponent.cs b/Content.Shared/HijackBeacon/ActiveHijackBeaconComponent.cs new file mode 100644 index 0000000000..b6f1d60c79 --- /dev/null +++ b/Content.Shared/HijackBeacon/ActiveHijackBeaconComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.HijackBeacon; + +/// +/// This is used for tracking a that is currently activated or on cooldown. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ActiveHijackBeaconComponent : Component +{ + /// + /// Remaining time until the hijack is completed. + /// + [DataField, AutoNetworkedField, Access(typeof(HijackBeaconSystem))] + public TimeSpan CompletionTime = TimeSpan.Zero; +} diff --git a/Content.Shared/HijackBeacon/HijackBeaconComponent.cs b/Content.Shared/HijackBeacon/HijackBeaconComponent.cs new file mode 100644 index 0000000000..ed1d3ca74b --- /dev/null +++ b/Content.Shared/HijackBeacon/HijackBeaconComponent.cs @@ -0,0 +1,53 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; +using Robust.Shared.Prototypes; + +namespace Content.Shared.HijackBeacon; + +/// +/// Component for hijack beacons, meant to be planted on the ATS to drain station funds. +/// +/// +/// Status and timer fields are private so the state machine is preserved. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class HijackBeaconComponent : Component +{ + /// + /// Current state of the beacon. + /// + [DataField, AutoNetworkedField, Access(typeof(HijackBeaconSystem))] + public HijackBeaconStatus Status = HijackBeaconStatus.AwaitActivate; + + /// + /// How long it takes to deactivate the beacon. + /// + [DataField, AutoNetworkedField, Access(typeof(HijackBeaconSystem))] + public TimeSpan DeactivationLength = TimeSpan.FromSeconds(5); + + /// + /// Remaining time until the hijack is completed. + /// + [DataField, Access(typeof(HijackBeaconSystem))] + public TimeSpan RemainingTime = TimeSpan.FromSeconds(200); + + /// + /// Default amount of time before the beacon can be re-activated, if it is disarmed. + /// + [DataField, Access(typeof(HijackBeaconSystem))] + public TimeSpan Cooldown = TimeSpan.FromSeconds(20); + + /// + /// Remaining cooldown time before the beacon can be reactivated. + /// + [DataField, AutoNetworkedField, Access(typeof(HijackBeaconSystem))] + public TimeSpan CooldownTime = TimeSpan.Zero; + + /// + /// How much cash should be withdrawn from each department account? + /// + [DataField] + public int Fine = 5000; +} diff --git a/Content.Shared/HijackBeacon/HijackBeaconSystem.cs b/Content.Shared/HijackBeacon/HijackBeaconSystem.cs new file mode 100644 index 0000000000..d3e5810fba --- /dev/null +++ b/Content.Shared/HijackBeacon/HijackBeaconSystem.cs @@ -0,0 +1,353 @@ +using Content.Shared.Cargo.Components; +using Content.Shared.Chat; +using Content.Shared.Construction.Components; +using Content.Shared.Construction.EntitySystems; +using Content.Shared.Database; +using Content.Shared.DoAfter; +using Content.Shared.Examine; +using Content.Shared.Popups; +using Content.Shared.Verbs; +using Robust.Shared.Audio; +using Robust.Shared.Serialization; +using Robust.Shared.Timing; + +namespace Content.Shared.HijackBeacon; + +public sealed class HijackBeaconSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly AnchorableSystem _anchor = default!; + [Dependency] private readonly SharedChatSystem _chat = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + public readonly SoundSpecifier AnnounceSound = new SoundPathSpecifier("/Audio/Misc/notice1.ogg"); + public readonly SoundSpecifier DeactivateSound = new SoundPathSpecifier("/Audio/Misc/notice2.ogg"); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent>(OnGetAltVerbs); + SubscribeLocalEvent(OnUnanchorAttempt); + SubscribeLocalEvent(OnAnchorChanged); + SubscribeLocalEvent(OnDeactivateDoAfter); + SubscribeLocalEvent(OnExaminedEvent); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var active, out var comp)) + { + switch (comp.Status) + { + case HijackBeaconStatus.Armed: + if (_gameTiming.CurTime < active.CompletionTime) + return; + + HijackFinish((uid, comp)); + Dirty(uid, comp); + break; + case HijackBeaconStatus.Cooldown: + if (comp.CooldownTime < _gameTiming.CurTime) + { + comp.Status = HijackBeaconStatus.AwaitActivate; + RemCompDeferred(uid); + Dirty(uid, comp); + } + break; + } + } + } + + #region Event Subs + + /// + /// Deactivate beacon if it gets unanchored(via a bomb or something) + /// + private void OnAnchorChanged(Entity ent, ref AnchorStateChangedEvent args) + { + // Unanchoring the beacon deactivates it. This is to prevent people from bombing the tile the beacon is on and running away with it for a free activation. + if (!args.Anchored && ent.Comp.Status == HijackBeaconStatus.Armed) + DeactivateBeacon(ent); + } + + private void OnUnanchorAttempt(Entity entity, ref UnanchorAttemptEvent args) + { + if (entity.Comp.Status == HijackBeaconStatus.Armed) + args.Cancel(); + } + + /// + /// Get the activation and deactivation verbs. + /// + private void OnGetAltVerbs(Entity ent, ref GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || args.Hands is null) + return; + + if (ent.Comp.Status == HijackBeaconStatus.AwaitActivate) + { + args.Verbs.Add(new() + { + Act = () => + { + ActivateBeacon(ent); + }, + Text = Loc.GetString("hijack-beacon-verb-activate-text"), + Message = Loc.GetString("hijack-beacon-verb-activate-message"), + Disabled = ent.Comp.Status != HijackBeaconStatus.AwaitActivate || !CanActivate(ent), + TextStyleClass = "InteractionVerb", + Impact = LogImpact.High, + }); + } + + if (ent.Comp.Status == HijackBeaconStatus.Armed) + { + var user = args.User; + + args.Verbs.Add(new() + { + Act = () => + { + DeactivateBeaconDoAfter(ent, user); + }, + Text = Loc.GetString("hijack-beacon-verb-deactivate-text"), + Message = Loc.GetString("hijack-beacon-verb-deactivate-message"), + TextStyleClass = "InteractionVerb", + Impact = LogImpact.High, + }); + } + } + + /// + /// When it's examined. + /// + private void OnExaminedEvent(Entity ent, ref ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + switch (ent.Comp.Status) + { + case HijackBeaconStatus.AwaitActivate: + args.PushMarkup(Loc.GetString("hijack-beacon-examine-await-activate")); + break; + case HijackBeaconStatus.Armed: + args.PushMarkup(Loc.GetString("defusable-examine-live", + ("name", ent), + ("time", GetRemainingTime(ent.Owner)))); + break; + case HijackBeaconStatus.Cooldown: + args.PushMarkup(Loc.GetString("hijack-beacon-examine-await-cooldown")); + break; + case HijackBeaconStatus.HijackComplete: + args.PushMarkup(Loc.GetString("hijack-beacon-examine-await-hijack-complete")); + break; + } + } + + /// + /// What happens when you deactivate the beacon. + /// + private void OnDeactivateDoAfter(Entity ent, ref HijackBeaconDeactivateDoAfterEvent args) + { + if (args.Handled || args.Cancelled) + return; + + DeactivateBeacon(ent); + + args.Handled = true; + } + + #endregion + + /// + /// Arming the beacon. Should only occur if on the ATS. + /// + private void ActivateBeacon(Entity ent) + { + if (ent.Comp.Status != HijackBeaconStatus.AwaitActivate || !CanActivate(ent)) + return; + + // Activate and start countdown. + // Remaining time is adjusted by current time to simplify the update loop. + EnsureComp(ent, out var activeComp); + activeComp.CompletionTime = _gameTiming.CurTime + ent.Comp.RemainingTime; + ent.Comp.Status = HijackBeaconStatus.Armed; + + //global announcement + var sender = Loc.GetString("hijack-beacon-announcement-sender"); + var message = Loc.GetString("hijack-beacon-announcement-activated", ("time", GetRemainingTime((ent.Owner, activeComp)))); + _chat.DispatchGlobalAnnouncement(message, sender, true, AnnounceSound, Color.Yellow); + + //Anchor. Anchoring is tied to activation. + Anchor(ent); + + Dirty(ent); + } + + /// + /// Deactivates the beacon. + /// + private void DeactivateBeacon(Entity ent) + { + if (ent.Comp.Status != HijackBeaconStatus.Armed) + return; + + // Put beacon on cooldown + ent.Comp.CooldownTime = ent.Comp.Cooldown + _gameTiming.CurTime; + ent.Comp.Status = HijackBeaconStatus.Cooldown; + + //global announcement + var sender = Loc.GetString("hijack-beacon-announcement-sender"); + var message = Loc.GetString("hijack-beacon-announcement-deactivated"); + _chat.DispatchGlobalAnnouncement(message, sender, true, DeactivateSound, Color.Green); + + // Unanchor. we want anchoring to be tied to activation here so we just call this. + Unanchor(ent); + + Dirty(ent); + } + + /// + /// Complete the hijack. The beacon equivalent to a nuke detonation + /// + private void HijackFinish(Entity ent) + { + if (ent.Comp.Status != HijackBeaconStatus.Armed) + return; + + // Hijack is completed and can't be reattempted + ent.Comp.Status = HijackBeaconStatus.HijackComplete; + RemCompDeferred(ent); + + var beaconXForm = Transform(ent); + + // Ensure we are on the trade station still + if (!TryComp(beaconXForm.GridUid, out var station)) + { + Log.Warning($"Trade station hijack tried to succeed on non-trade station grid {beaconXForm.GridUid}!"); + DeactivateBeacon(ent); + return; + } + + if (station.Hacked) + { + Log.Warning("Hack succeeded on an already hacked trade station!"); + return; + } + + // Mark the station as hacked. + station.Hacked = true; + Dirty(beaconXForm.GridUid.Value, station); + + // Broadcast that the ATS has been hacked. + var ev = new HijackBeaconSuccessEvent(ent.Comp.Fine); + RaiseLocalEvent(ref ev); + + //global announcement + var sender = Loc.GetString("hijack-beacon-announcement-sender"); + var message = Loc.GetString("hijack-beacon-announcement-success", ("fine", ev.Total)); + _chat.DispatchGlobalAnnouncement(message, sender, true, AnnounceSound, Color.Red); + + // Unanchoring must occur after updating the status, or it will disarm the beacon + Unanchor(ent, beaconXForm); + + Dirty(ent); + } + + /// + /// Starts the deactivation doafter. + /// + private void DeactivateBeaconDoAfter(Entity beacon, EntityUid user) + { + var doAfter = new DoAfterArgs(EntityManager, user, beacon.Comp.DeactivationLength, new HijackBeaconDeactivateDoAfterEvent(), beacon) + { + BreakOnDamage = true, + BreakOnMove = true, + NeedHand = true, + }; + + // (try to) start doafter + _doAfter.TryStartDoAfter(doAfter); + } + + #region Helpers + + /// + /// Check if the beacon is on the Trade Station and if the Trade Station has not been hijacked already. + /// + private bool CanActivate(Entity ent) + { + return TryComp(Transform(ent).GridUid, out TradeStationComponent? tradeStation) && !tradeStation.Hacked && _anchor.CanAnchorAt(ent.Owner); + } + + /// + /// Anchoring helper + /// + private void Anchor(Entity ent, TransformComponent? beaconXForm = null) + { + beaconXForm ??= Transform(ent); + + if (beaconXForm.Anchored || beaconXForm.GridUid == null) + return; + + _transform.AnchorEntity(ent, beaconXForm); + _popup.PopupPredicted(Loc.GetString("hijack-beacon-popup-anchor"), ent, null); + } + + /// + /// Unanchoring helper + /// + private void Unanchor(Entity ent, TransformComponent? beaconXForm = null) + { + beaconXForm ??= Transform(ent); + + if (!beaconXForm.Anchored) + return; + + _transform.Unanchor(ent, beaconXForm); + _popup.PopupPredicted(Loc.GetString("hijack-beacon-popup-unanchor"), ent, null); + } + + /// + /// Turns time values into usable values for announcements/examine messages + /// + private int GetRemainingTime(Entity ent) + { + if (!Resolve(ent, ref ent.Comp)) + return 69420; // Mature error code + + return (int) (ent.Comp.CompletionTime - _gameTiming.CurTime).TotalSeconds; + } + + #endregion +} + +public enum HijackBeaconStatus : byte +{ + AwaitActivate, + Armed, + Cooldown, + HijackComplete +} + +/// +/// DoAfter event raised when the hijack beacon is deactivated. +/// +[Serializable, NetSerializable] +public sealed partial class HijackBeaconDeactivateDoAfterEvent : SimpleDoAfterEvent; + +/// +/// Event raised when the hijack beacon succeeds in hijacking the ATS. +/// +[ByRefEvent] +public record struct HijackBeaconSuccessEvent(int Fine) +{ + public int Total = 0; +}; diff --git a/Resources/Locale/en-US/hijack-beacon/hijack-beacon.ftl b/Resources/Locale/en-US/hijack-beacon/hijack-beacon.ftl new file mode 100644 index 0000000000..a98370e891 --- /dev/null +++ b/Resources/Locale/en-US/hijack-beacon/hijack-beacon.ftl @@ -0,0 +1,16 @@ +hijack-beacon-announcement-sender = Automated Trade Station +hijack-beacon-announcement-activated = Attention! An Attempted Breach of the Automated Trade Station's firewall has been detected! Estimated {$time} seconds until firewall breach! +hijack-beacon-announcement-deactivated = Firewall breach failed. Firewall integrity partially restored. Have a nice day! +hijack-beacon-announcement-success = Successfully disengaged Automated Trade Station firewall. {$fine} spessos has been transferred from station funds to [%ERROR%]. Your trade station warranty is now void. This incident has been reported. + +hijack-beacon-examine-await-activate = The beacon is [color=green]ready to activate[/color]. +hijack-beacon-examine-await-cooldown = The beacon is [color=red]on cooldown[/color]. +hijack-beacon-examine-await-hijack-complete = The beacon is [color=red]spent[/color]. + +hijack-beacon-popup-anchor = The beacon anchors itself into the ground! +hijack-beacon-popup-unanchor = The beacon unanchors itself from the ground. + +hijack-beacon-verb-activate-text = Activate +hijack-beacon-verb-activate-message = The beacon can only be armed on the Automated Trade Station, on an unoccupied tile. +hijack-beacon-verb-deactivate-text = Deactivate +hijack-beacon-verb-deactivate-message = The beacon isn't going to deactivate itself, you know. diff --git a/Resources/Locale/en-US/store/categories.ftl b/Resources/Locale/en-US/store/categories.ftl index 4469a576cd..9ecf4f91d2 100644 --- a/Resources/Locale/en-US/store/categories.ftl +++ b/Resources/Locale/en-US/store/categories.ftl @@ -12,6 +12,7 @@ store-category-allies = Allies store-category-job = Job store-category-wearables = Wearables store-category-pointless = Pointless +store-category-objective = Objective store-discounted-items = Discounts # Revenant diff --git a/Resources/Locale/en-US/store/uplink-catalog.ftl b/Resources/Locale/en-US/store/uplink-catalog.ftl index 4f67fae1cd..685a4d83dd 100644 --- a/Resources/Locale/en-US/store/uplink-catalog.ftl +++ b/Resources/Locale/en-US/store/uplink-catalog.ftl @@ -520,3 +520,7 @@ uplink-briefcase-gun-desc = An indistinct briefcase with a highly compact C-20K uplink-energycrossbow-name = Mini Energy Crossbow uplink-energycrossbow-desc = The go-to sidearm of any operative who prefers their victims not to be moving. Fires regenerating toxic arrows that floors victims in an instant. + +#Objective items +uplink-hijack-beacon-name = Hijack Beacon +uplink-hijack-beacon-desc = A syndicate-brand hijack beacon designed to get around the firewalls of Nanotrasen-brand Automated Trade Stations. They take 200 seconds to work and Trade Stations will announce they are being hacked, so prepare accordingly. diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml index e232308d01..98c2206f6a 100644 --- a/Resources/Prototypes/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/Catalog/uplink_catalog.yml @@ -2268,3 +2268,20 @@ - !type:BuyerJobCondition whitelist: - Lawyer + +#Objective items + +- type: listing + id: uplinkHijackBeacon + name: uplink-hijack-beacon-name + description: uplink-hijack-beacon-desc + productEntity: HijackBeacon + categories: + - UplinkObjective + conditions: + - !type:ListingLimitedStockCondition + stock: 1 + - !type:BuyerObjectiveWhitelistCondition + whitelist: + components: + - HijackTradeStationCondition diff --git a/Resources/Prototypes/Entities/Objects/Tools/hijack_beacon.yml b/Resources/Prototypes/Entities/Objects/Tools/hijack_beacon.yml new file mode 100644 index 0000000000..d15b845322 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Tools/hijack_beacon.yml @@ -0,0 +1,34 @@ +- type: entity + parent: [ BaseItem, BaseSyndicateContraband ] + id: HijackBeacon + name: hijack beacon + description: A device that bypasses the firewall on Nanotrasen-brand Automated Trade Stations. + components: + - type: Transform + anchored: false + noRot: true + - type: Physics + bodyType: Dynamic + - type: Anchorable + - type: Item + size: Normal + - type: HijackBeacon + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeAabb + bounds: "-0.3,-0.3,0.3,0.3" + density: 190 + mask: + - MachineMask + layer: + - MachineLayer + - type: Clickable + - type: InteractionOutline + - type: Appearance + - type: Sprite + sprite: Objects/Tools/hijack_beacon.rsi + noRot: true + layers: + - state: extraction_point diff --git a/Resources/Prototypes/Objectives/objectiveGroups.yml b/Resources/Prototypes/Objectives/objectiveGroups.yml index 9aa13fa74a..bbdd6fa236 100644 --- a/Resources/Prototypes/Objectives/objectiveGroups.yml +++ b/Resources/Prototypes/Objectives/objectiveGroups.yml @@ -6,6 +6,7 @@ TraitorObjectiveGroupKill: 1 TraitorObjectiveGroupState: 1 #As in, something about your character. Alive, dead, arrested, gained an ability... TraitorObjectiveGroupSocial: 1 #Involves helping/harming others without killing them or stealing their stuff + TraitorObjectiveGroupOther: 1 - type: weightedRandom id: TraitorObjectiveGroupSteal @@ -43,6 +44,13 @@ # RandomTraitorAliveObjective: 1 - Removed because the objective was boring and didn't progress the round. RandomTraitorProgressObjective: 1 +#misc objectives that don't fall other another category +- type: weightedRandom + id: TraitorObjectiveGroupOther + weights: + HijackTradeStationObjective: 1 + + #Thief groups - type: weightedRandom id: ThiefObjectiveGroups diff --git a/Resources/Prototypes/Objectives/traitor.yml b/Resources/Prototypes/Objectives/traitor.yml index 4825fa4565..9ffc0146a2 100644 --- a/Resources/Prototypes/Objectives/traitor.yml +++ b/Resources/Prototypes/Objectives/traitor.yml @@ -352,3 +352,20 @@ - type: StealCondition stealGroup: NukeDisk owner: objective-condition-steal-station + +# other +- type: entity + parent: BaseTraitorObjective + id: HijackTradeStationObjective + name: Hijack the Automated Trade Station. + description: Your uplink has been authorized one hijack beacon. Deploy it on the Automated Trade Station and defend it whilst it hijacks the trade station. + components: + - type: Objective + difficulty: 3 + icon: + sprite: Objects/Tools/hijack_beacon.rsi + state: extraction_point + - type: HijackTradeStationCondition + - type: ObjectiveLimit + # There is only one trade station so there should never be more than one of these objectives. + limit: 1 diff --git a/Resources/Prototypes/Store/categories.yml b/Resources/Prototypes/Store/categories.yml index ccdb349d9c..a4c339d0e7 100644 --- a/Resources/Prototypes/Store/categories.yml +++ b/Resources/Prototypes/Store/categories.yml @@ -89,6 +89,11 @@ name: store-category-pointless priority: 10 +- type: storeCategory + id: UplinkObjective + name: store-category-objective + priority: 11 + #nukie delivery - type: storeCategory diff --git a/Resources/Prototypes/Store/presets.yml b/Resources/Prototypes/Store/presets.yml index 37329d4096..13823a40f1 100644 --- a/Resources/Prototypes/Store/presets.yml +++ b/Resources/Prototypes/Store/presets.yml @@ -16,6 +16,7 @@ - UplinkWearables - UplinkJob - UplinkPointless + - UplinkObjective currencyWhitelist: - Telecrystal balance: diff --git a/Resources/Textures/Objects/Tools/hijack_beacon.rsi/extraction_point.png b/Resources/Textures/Objects/Tools/hijack_beacon.rsi/extraction_point.png new file mode 100644 index 0000000000000000000000000000000000000000..7b8dd4c3afea906e37a37e17e2dd2abc21bb8125 GIT binary patch literal 1389 zcmeAS@N?(olHy`uVBq!ia0vp^3P9|@!3HF&`%2d_Ffgvk42dX-@b$4u&d=3LOvz75 z)vL%Y0Ln8k*w|MTBqnF4mMA2prf25aD!t#mUr8Y|#a1cY)Yrhbz&SM|)1#^=HMq(z zB)KX(*)m1R-j2(r!U||WZfZ%QLPc&)Ua?h$trFN=DP$9xM zK*2e`C{@8!&rCPj(8NN)+)~fb%*4RhOh>`Uz|d0Pz(U{9Sl7VN%D~LZ)La1ylHCStb$zJpq2r7wn`Z#B?VUc`sL;2dgaD?`9npUS|%T;u8D7?RQWHp1IqI8fwY*33(MwF`xOqnQ_) zHsv-+m>8-w8D2cX$}O%JvHMD|b$!b}p0af&M_Lsboe#Awk(j+;8>@1NP@HVkt%c4j zEz9$~xi6P_ExT(GUsU{j-~Q@zw##kV)&5mS^oTgz5nu9G@1xUwRR;ACr_TMSC7(xd|&qOAC;I+Fy469y01H``<7#ZzyX={zbp1Xi*ys( zP_Xgx+4PG0Jo(&TujHyU3SGDCteW-R_w%+x$8UQ*Xt6iDRQ_pe)qR!x(=~r&dTLs( zvKZ~YYv!P*vU-ifcn?_Q#J;JT3Cs^X0>fW88M) zDvk~Aff2D5caoD^vOH^k{^1PvpZDR;Q3dDr%g(9Y%kfep4+dqS>Ti~-eyOs}c{~5>*Y3TI)z|XA zMC$xlutLo>zu}84UwfqW?fd;sHhdOW?zgf#imO!Hr%zz1V&5p!Yc|EfIsDJsbdKUi z&TqE$+9%dDJos7_x$r)-l4~-HX1vR{_xcr2?%$Z!CndMWn<+eIcf+Rc+IQby8tl3l zX&ozrH8z@7{S&y)b9uCQ<9z@*Wnk9u`NvCLJpCe3?`C tVCkGdbrHYkvwkvy4VZP{4F47OhWc)GBe(dUstKS{($m$?Wt~$(69BAKJ81v_ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Tools/hijack_beacon.rsi/meta.json b/Resources/Textures/Objects/Tools/hijack_beacon.rsi/meta.json new file mode 100644 index 0000000000..5177acf42f --- /dev/null +++ b/Resources/Textures/Objects/Tools/hijack_beacon.rsi/meta.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/austation/austation/commit/e2a4fefd01e702f48d3d4cc8d6a2686d54d104fa and edited by TheShuEd. Recolored by alexalexmax", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "extraction_point", + "delays": [ + [ + 0.5, + 0.5 + ] + ] + } + ] +} From ecf554549ca70db784ea88dc8af0147f0b793caa Mon Sep 17 00:00:00 2001 From: PJBot Date: Fri, 3 Apr 2026 10:37:05 +0000 Subject: [PATCH 033/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index f6d5385c12..c251692f5c 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: TrixxedHeart - changes: - - message: Renamed "trash" reagent to "reprocessed material" - type: Tweak - id: 9090 - time: '2025-10-13T17:18:40.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/39761 - author: ScarKy0 changes: - message: Station AI can no longer control bolts, emergency access and electrify @@ -4030,3 +4023,13 @@ id: 9601 time: '2026-04-03T06:18:14.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/42399 +- author: alexalexmax + changes: + - message: Added a new uplink category for objective items. + type: Add + - message: 'Added a new objective for traitors: Hijack the Automated Trade Station. + Your uplink will have a hijack beacon for you to use if you have this objective' + type: Add + id: 9602 + time: '2026-04-03T10:35:56.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/42135 From f688bfc7713126274df5ac1620de079f2c973946 Mon Sep 17 00:00:00 2001 From: Kyle Tyo Date: Fri, 3 Apr 2026 07:58:47 -0400 Subject: [PATCH 034/126] Remove suitstep1 and suitstep2 ogg files. (#43441) commit --- Resources/Audio/Effects/Footsteps/suitstep1.ogg | Bin 10862 -> 0 bytes Resources/Audio/Effects/Footsteps/suitstep2.ogg | Bin 11896 -> 0 bytes .../Prototypes/SoundCollections/footsteps.yml | 6 ------ 3 files changed, 6 deletions(-) delete mode 100644 Resources/Audio/Effects/Footsteps/suitstep1.ogg delete mode 100644 Resources/Audio/Effects/Footsteps/suitstep2.ogg diff --git a/Resources/Audio/Effects/Footsteps/suitstep1.ogg b/Resources/Audio/Effects/Footsteps/suitstep1.ogg deleted file mode 100644 index 8de74124907dc38bf56017d23145ecd2e4a51df4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10862 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 diff --git a/Resources/Audio/Effects/Footsteps/suitstep2.ogg b/Resources/Audio/Effects/Footsteps/suitstep2.ogg deleted file mode 100644 index 56396fb71eef845fd1105c7dd0bf287fadf6aca9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11896 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 OSJ(8DM}jp90R9g${3?F{ diff --git a/Resources/Prototypes/SoundCollections/footsteps.yml b/Resources/Prototypes/SoundCollections/footsteps.yml index 26ecee5269..8c619226ff 100644 --- a/Resources/Prototypes/SoundCollections/footsteps.yml +++ b/Resources/Prototypes/SoundCollections/footsteps.yml @@ -94,12 +94,6 @@ files: - /Audio/Items/Toys/weh.ogg -- type: soundCollection - id: FootstepHeavy - files: - - /Audio/Effects/Footsteps/suitstep1.ogg - - /Audio/Effects/Footsteps/suitstep2.ogg - - type: soundCollection id: FootstepAsteroid files: From 5e456146e31f4174fc9392cf4bda410606602222 Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:16:06 +0200 Subject: [PATCH 035/126] fix Changeling transformation making items in slime storage inaccessible (#43393) * fix * fix identity update --- .../Changeling/ChangelingTransformEvents.cs | 23 ++++++++ .../Systems/ChangelingTransformSystem.cs | 58 +++++++++++++------ 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/Content.Shared/Changeling/ChangelingTransformEvents.cs b/Content.Shared/Changeling/ChangelingTransformEvents.cs index 9940a60705..9a4ff4454e 100644 --- a/Content.Shared/Changeling/ChangelingTransformEvents.cs +++ b/Content.Shared/Changeling/ChangelingTransformEvents.cs @@ -14,3 +14,26 @@ public sealed partial class ChangelingTransformActionEvent : InstantActionEvent; /// [Serializable, NetSerializable] public sealed partial class ChangelingTransformDoAfterEvent : SimpleDoAfterEvent; + +/// +/// Raised on a changeling before they transform into a stored identity. +/// This is raised after the DoAfter finished. +/// +public readonly record struct BeforeChangelingTransformEvent(EntityUid StoredIdentity) +{ + /// + /// The stored identity the changeling will transform into. + /// + public readonly EntityUid StoredIdentity = StoredIdentity; +}; + +/// +/// Raised on a changeling after they successfully transformed into a stored identity. +/// +public readonly record struct AfterChangelingTransformEvent(EntityUid StoredIdentity) +{ + /// + /// The stored identity the changeling transformed into. + /// + public readonly EntityUid StoredIdentity = StoredIdentity; +}; diff --git a/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs b/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs index 9e4b1d4d03..5f9cc9526b 100644 --- a/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs +++ b/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs @@ -5,10 +5,11 @@ using Content.Shared.Changeling.Components; using Content.Shared.Cloning; using Content.Shared.Database; using Content.Shared.DoAfter; -using Content.Shared.Humanoid; using Content.Shared.IdentityManagement; using Content.Shared.Popups; +using Content.Shared.Storage; using Robust.Shared.Audio.Systems; +using Robust.Shared.Containers; using Robust.Shared.Network; using Robust.Shared.Prototypes; @@ -17,16 +18,18 @@ namespace Content.Shared.Changeling.Systems; public sealed partial class ChangelingTransformSystem : EntitySystem { [Dependency] private readonly INetManager _net = default!; - [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; - [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; - [Dependency] private readonly MetaDataSystem _metaSystem = default!; - [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedUserInterfaceSystem _ui = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedCloningSystem _cloningSystem = default!; + [Dependency] private readonly SharedCloningSystem _cloning = default!; [Dependency] private readonly SharedVisualBodySystem _visualBody = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly IdentitySystem _identity = default!; private const string ChangelingBuiXmlGeneratedName = "ChangelingTransformBoundUserInterface"; public override void Initialize() @@ -38,21 +41,24 @@ public sealed partial class ChangelingTransformSystem : EntitySystem SubscribeLocalEvent(OnSuccessfulTransform); SubscribeLocalEvent(OnTransformSelected); SubscribeLocalEvent(OnShutdown); + + // Components that need special handling outside of cloning. + SubscribeLocalEvent(StorageBeforeTransform); } private void OnMapInit(Entity ent, ref MapInitEvent init) { - _actionsSystem.AddAction(ent, ref ent.Comp.ChangelingTransformActionEntity, ent.Comp.ChangelingTransformAction); + _actions.AddAction(ent, ref ent.Comp.ChangelingTransformActionEntity, ent.Comp.ChangelingTransformAction); var userInterfaceComp = EnsureComp(ent); - _uiSystem.SetUi((ent, userInterfaceComp), ChangelingTransformUiKey.Key, new InterfaceData(ChangelingBuiXmlGeneratedName)); + _ui.SetUi((ent, userInterfaceComp), ChangelingTransformUiKey.Key, new InterfaceData(ChangelingBuiXmlGeneratedName)); } private void OnShutdown(Entity ent, ref ComponentShutdown args) { if (ent.Comp.ChangelingTransformActionEntity != null) { - _actionsSystem.RemoveAction(ent.Owner, ent.Comp.ChangelingTransformActionEntity); + _actions.RemoveAction(ent.Owner, ent.Comp.ChangelingTransformActionEntity); } } @@ -65,9 +71,9 @@ public sealed partial class ChangelingTransformSystem : EntitySystem if (!TryComp(ent, out var userIdentity)) return; - if (!_uiSystem.IsUiOpen((ent, userInterfaceComp), ChangelingTransformUiKey.Key, args.Performer)) + if (!_ui.IsUiOpen((ent, userInterfaceComp), ChangelingTransformUiKey.Key, args.Performer)) { - _uiSystem.OpenUi((ent, userInterfaceComp), ChangelingTransformUiKey.Key, args.Performer); + _ui.OpenUi((ent, userInterfaceComp), ChangelingTransformUiKey.Key, args.Performer); } //TODO: Can add a Else here with TransformInto and CloseUI to make a quick switch, // issue right now is that Radials cover the Action buttons so clicking the action closes the UI (due to clicking off a radial causing it to close, even with UI) // but pressing the number does. @@ -75,7 +81,7 @@ public sealed partial class ChangelingTransformSystem : EntitySystem /// /// Transform the changeling into another identity. - /// This can be any cloneable humanoid and doesn't have to be stored in the ChangelingIdentiyComponent, + /// This can be any cloneable humanoid and doesn't have to be stored in the ChangelingIdentityComponent, /// so make sure to validate the target before. /// public void TransformInto(Entity ent, EntityUid targetIdentity) @@ -85,7 +91,7 @@ public sealed partial class ChangelingTransformSystem : EntitySystem var selfMessage = Loc.GetString("changeling-transform-attempt-self", ("user", Identity.Entity(ent.Owner, EntityManager))); var othersMessage = Loc.GetString("changeling-transform-attempt-others", ("user", Identity.Entity(ent.Owner, EntityManager))); - _popupSystem.PopupPredicted( + _popup.PopupPredicted( selfMessage, othersMessage, ent, @@ -100,7 +106,7 @@ public sealed partial class ChangelingTransformSystem : EntitySystem else _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} begun an attempt to transform into \"{Name(targetIdentity)}\""); - _doAfterSystem.TryStartDoAfter(new DoAfterArgs( + _doAfter.TryStartDoAfter(new DoAfterArgs( EntityManager, ent, ent.Comp.TransformWindup, @@ -119,7 +125,7 @@ public sealed partial class ChangelingTransformSystem : EntitySystem private void OnTransformSelected(Entity ent, ref ChangelingTransformIdentitySelectMessage args) { - _uiSystem.CloseUi(ent.Owner, ChangelingTransformUiKey.Key, ent); + _ui.CloseUi(ent.Owner, ChangelingTransformUiKey.Key, ent); if (!TryGetEntity(args.TargetIdentity, out var targetIdentity)) return; @@ -153,14 +159,18 @@ public sealed partial class ChangelingTransformSystem : EntitySystem if (args.Target is not { } targetIdentity) return; + var beforeTransformEvent = new BeforeChangelingTransformEvent(targetIdentity); + RaiseLocalEvent(args.User, beforeTransformEvent); + _visualBody.CopyAppearanceFrom(targetIdentity, args.User); - _cloningSystem.CloneComponents(targetIdentity, args.User, settings); + _cloning.CloneComponents(targetIdentity, args.User, settings); if (TryComp(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null) _adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent.Owner):player} successfully transformed into \"{Name(targetIdentity)}\" ({storedIdentity.OriginalSession:player})"); else _adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent.Owner):player} successfully transformed into \"{Name(targetIdentity)}\""); - _metaSystem.SetEntityName(ent, Name(targetIdentity), raiseEvents: false); + _metaData.SetEntityName(ent, Name(targetIdentity), raiseEvents: false); // Don't raise events because we don't want to rename the ID card. + _identity.QueueIdentityUpdate(ent); // We have to manually refresh the identity because we did not raise events. Dirty(ent); @@ -169,5 +179,17 @@ public sealed partial class ChangelingTransformSystem : EntitySystem identity.CurrentIdentity = targetIdentity; Dirty(ent.Owner, identity); } + + var afterTransformEvent = new AfterChangelingTransformEvent(targetIdentity); + RaiseLocalEvent(args.User, afterTransformEvent); + } + + private void StorageBeforeTransform(Entity ent, ref BeforeChangelingTransformEvent args) + { + if (HasComp(args.StoredIdentity)) + return; // If we have a storage component and the target has one as well, then do nothing. + + // If the target identity does not have a storage anymore, drop all items inside our storage so that they don't become unreachable. + _container.EmptyContainer(ent.Comp.Container); } } From 7234cec825014418fb5d25b7aec84972d4324ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ph=C3=B6nix?= Date: Fri, 3 Apr 2026 20:26:18 +0300 Subject: [PATCH 036/126] Predict ghost examine (#43150) * never * forgot --- Content.Server/Ghost/GhostSystem.cs | 17 ++--------------- Content.Shared/Ghost/GhostComponent.cs | 2 +- Content.Shared/Ghost/SharedGhostSystem.cs | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs index 964ba99ac3..d986c83a54 100644 --- a/Content.Server/Ghost/GhostSystem.cs +++ b/Content.Server/Ghost/GhostSystem.cs @@ -12,7 +12,6 @@ using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; using Content.Shared.Damage.Systems; using Content.Shared.Database; -using Content.Shared.Examine; using Content.Shared.Eye; using Content.Shared.FixedPoint; using Content.Shared.Follower; @@ -38,7 +37,6 @@ using Robust.Shared.Physics.Systems; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; -using Robust.Shared.Timing; namespace Content.Server.Ghost { @@ -48,7 +46,6 @@ namespace Content.Server.Ghost [Dependency] private readonly IAdminLogManager _adminLog = default!; [Dependency] private readonly SharedEyeSystem _eye = default!; [Dependency] private readonly FollowerSystem _followerSystem = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly JobSystem _jobs = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly MindSystem _minds = default!; @@ -88,8 +85,6 @@ namespace Content.Server.Ghost SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnGhostShutdown); - SubscribeLocalEvent(OnGhostExamine); - SubscribeLocalEvent(OnMindRemovedMessage); SubscribeLocalEvent(OnMindUnvisitedMessage); SubscribeLocalEvent(OnPlayerDetached); @@ -207,6 +202,8 @@ namespace Content.Server.Ghost _eye.RefreshVisibilityMask(uid); var time = _gameTiming.RealTime; component.TimeOfDeath = time; + + Dirty(uid, component); } private void OnGhostShutdown(EntityUid uid, GhostComponent component, ComponentShutdown args) @@ -237,16 +234,6 @@ namespace Content.Server.Ghost _actions.AddAction(uid, ref component.ToggleGhostsActionEntity, component.ToggleGhostsAction); } - private void OnGhostExamine(EntityUid uid, GhostComponent component, ExaminedEvent args) - { - var timeSinceDeath = _gameTiming.RealTime.Subtract(component.TimeOfDeath); - var deathTimeInfo = timeSinceDeath.Minutes > 0 - ? Loc.GetString("comp-ghost-examine-time-minutes", ("minutes", timeSinceDeath.Minutes)) - : Loc.GetString("comp-ghost-examine-time-seconds", ("seconds", timeSinceDeath.Seconds)); - - args.PushMarkup(deathTimeInfo); - } - #region Ghost Deletion private void OnMindRemovedMessage(EntityUid uid, GhostComponent component, MindRemovedMessage args) diff --git a/Content.Shared/Ghost/GhostComponent.cs b/Content.Shared/Ghost/GhostComponent.cs index cd5aa9be22..37f5c9e693 100644 --- a/Content.Shared/Ghost/GhostComponent.cs +++ b/Content.Shared/Ghost/GhostComponent.cs @@ -54,7 +54,7 @@ public sealed partial class GhostComponent : Component /// May not reflect actual time of death if this entity has been paused, /// but will give an accurate length of time since death. /// - [DataField] + [DataField, AutoNetworkedField] public TimeSpan TimeOfDeath = TimeSpan.Zero; /// diff --git a/Content.Shared/Ghost/SharedGhostSystem.cs b/Content.Shared/Ghost/SharedGhostSystem.cs index 7d3561a79f..726c20d31a 100644 --- a/Content.Shared/Ghost/SharedGhostSystem.cs +++ b/Content.Shared/Ghost/SharedGhostSystem.cs @@ -1,9 +1,11 @@ using Content.Shared.Emoting; +using Content.Shared.Examine; using Content.Shared.Hands; using Content.Shared.Interaction.Events; using Content.Shared.Item; using Content.Shared.Popups; using Robust.Shared.Serialization; +using Robust.Shared.Timing; namespace Content.Shared.Ghost { @@ -14,6 +16,7 @@ namespace Content.Shared.Ghost public abstract class SharedGhostSystem : EntitySystem { [Dependency] protected readonly SharedPopupSystem Popup = default!; + [Dependency] protected readonly IGameTiming _gameTiming = default!; public override void Initialize() { @@ -23,6 +26,17 @@ namespace Content.Shared.Ghost SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); + SubscribeLocalEvent(OnGhostExamine); + } + + private void OnGhostExamine(EntityUid uid, GhostComponent component, ExaminedEvent args) + { + var timeSinceDeath = _gameTiming.RealTime.Subtract(component.TimeOfDeath); + var deathTimeInfo = timeSinceDeath.Minutes > 0 + ? Loc.GetString("comp-ghost-examine-time-minutes", ("minutes", timeSinceDeath.Minutes)) + : Loc.GetString("comp-ghost-examine-time-seconds", ("seconds", timeSinceDeath.Seconds)); + + args.PushMarkup(deathTimeInfo); } private void OnAttemptInteract(Entity ent, ref InteractionAttemptEvent args) From 088dbb2d38e7db3f779311f2ca8636440f2e0262 Mon Sep 17 00:00:00 2001 From: Minemoder5000 Date: Fri, 3 Apr 2026 11:46:20 -0600 Subject: [PATCH 037/126] Add blacklist to entitystoragecomponent, add blacklist to genpop lockers (#41633) * cleanup EntityStorageSystem * add blacklist to entitystoragecomponent, add blacklist to genpop lockers * remove test whitelist tag * add documentation * Borger * remove blacklist from genpop closet, fix code comment --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> --- .../Storage/Components/EntityStorageComponent.cs | 16 +++++++++++++++- .../EntitySystems/SharedEntityStorageSystem.cs | 8 ++------ .../Storage/Closets/Lockers/lockers.yml | 1 + 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Content.Shared/Storage/Components/EntityStorageComponent.cs b/Content.Shared/Storage/Components/EntityStorageComponent.cs index ecfcccc45b..ba267aa5bf 100644 --- a/Content.Shared/Storage/Components/EntityStorageComponent.cs +++ b/Content.Shared/Storage/Components/EntityStorageComponent.cs @@ -131,7 +131,21 @@ public sealed partial class EntityStorageComponent : Component, IGasMixtureHolde /// standard requirement that the entity must be an item or mob is waived. /// [DataField] - public EntityWhitelist? Whitelist; + public EntityWhitelist? Whitelist = new() + { + Components = + [ + "MobState", + "Item", + ], + }; + + /// + /// Blacklist for what entities are not allowed to be inserted into this container. + /// Blacklist takes priority over whitelist. + /// + [DataField] + public EntityWhitelist? Blacklist; /// /// The contents of the storage. diff --git a/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs b/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs index 93a534843a..9618ee900b 100644 --- a/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs @@ -377,12 +377,8 @@ public abstract class SharedEntityStorageSystem : EntitySystem if (containerAttemptEvent.Cancelled) return false; - // Consult the whitelist. The whitelist ignores the default assumption about how entity storage works. - if (component.Whitelist != null) - return _whitelistSystem.IsValid(component.Whitelist, toInsert); - - // The inserted entity must be a mob or an item. - return HasComp(toInsert) || HasComp(toInsert); + // Check the whitelist/blacklist. + return _whitelistSystem.CheckBoth(toInsert, component.Blacklist, component.Whitelist); } public bool TryOpenStorage(EntityUid user, EntityUid target, bool silent = false) diff --git a/Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/lockers.yml b/Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/lockers.yml index 8f22f8d68c..1410eb4e1c 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/lockers.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/lockers.yml @@ -454,6 +454,7 @@ hard: True restitution: 0 friction: 0.4 + - type: EntityStorage - type: entity parent: LockerPrisoner From f0cab2d6ed42040f04242e5dde3d02646f70c0ee Mon Sep 17 00:00:00 2001 From: PJBot Date: Fri, 3 Apr 2026 18:02:20 +0000 Subject: [PATCH 038/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index c251692f5c..4b1d22ee97 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: ScarKy0 - changes: - - message: Station AI can no longer control bolts, emergency access and electrify - status on doors it has no access to. - type: Fix - id: 9091 - time: '2025-10-13T20:53:36.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/38444 - author: Myra changes: - message: The built-in soundfont for MIDI's should sound better, especially for @@ -4033,3 +4025,10 @@ id: 9602 time: '2026-04-03T10:35:56.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/42135 +- author: Minemoder + changes: + - message: Humanoids can now break out of locked genpop lockers + type: Tweak + id: 9603 + time: '2026-04-03T18:01:11.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/41633 From 6c9e10e1a9ef8c25904d35ae8e0f92dece76683f Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:47:49 -0700 Subject: [PATCH 039/126] Removed JuicePotato reagent (#43448) No more potatoes Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> --- .../Entities/Objects/Consumable/Food/produce.yml | 5 ----- Resources/Prototypes/Hydroponics/randomChemicals.yml | 1 - Resources/Prototypes/Reagents/Consumable/Drink/juice.yml | 9 --------- 3 files changed, 15 deletions(-) diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml index 2e52646dac..118c008291 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml @@ -883,11 +883,6 @@ heldPrefix: produce - type: Produce seedId: potato - - type: Extractable - juiceSolution: - reagents: - - ReagentId: JuicePotato - Quantity: 10 - type: Tag tags: - Potato diff --git a/Resources/Prototypes/Hydroponics/randomChemicals.yml b/Resources/Prototypes/Hydroponics/randomChemicals.yml index 3b8d225c36..b3468c0848 100644 --- a/Resources/Prototypes/Hydroponics/randomChemicals.yml +++ b/Resources/Prototypes/Hydroponics/randomChemicals.yml @@ -114,7 +114,6 @@ - SodaWater - Ice - JuiceCarrot - - JuicePotato - Protein - Flour - Soysauce diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/juice.yml b/Resources/Prototypes/Reagents/Consumable/Drink/juice.yml index 43bd9efedc..e22993d0a3 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/juice.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/juice.yml @@ -123,15 +123,6 @@ flavor: pineapple color: yellow -- type: reagent - id: JuicePotato - name: reagent-name-juice-potato - parent: BaseJuice - desc: reagent-desc-juice-potato - physicalDesc: reagent-physical-desc-starchy - flavor: potatoes - color: "#302000" - - type: reagent id: JuiceTomato name: reagent-name-juice-tomato From 417f1b7ceb3414580d1f219ecb8663a097d73700 Mon Sep 17 00:00:00 2001 From: Schuyler Duryee Date: Fri, 3 Apr 2026 10:48:07 -0700 Subject: [PATCH 040/126] Add "failed to load crew manifest" message (#43400) --- .../Cartridges/CrewManifestCartridgeSystem.cs | 7 +++++++ Resources/Locale/en-US/cartridge-loader/cartridges.ftl | 1 + 2 files changed, 8 insertions(+) diff --git a/Content.Server/CartridgeLoader/Cartridges/CrewManifestCartridgeSystem.cs b/Content.Server/CartridgeLoader/Cartridges/CrewManifestCartridgeSystem.cs index b1b23b2732..e7687ec3df 100644 --- a/Content.Server/CartridgeLoader/Cartridges/CrewManifestCartridgeSystem.cs +++ b/Content.Server/CartridgeLoader/Cartridges/CrewManifestCartridgeSystem.cs @@ -3,6 +3,7 @@ using Content.Server.Station.Systems; using Content.Shared.CartridgeLoader; using Content.Shared.CartridgeLoader.Cartridges; using Content.Shared.CCVar; +using Content.Shared.CrewManifest; using Robust.Shared.Configuration; using Robust.Shared.Containers; using Robust.Shared.Prototypes; @@ -60,7 +61,13 @@ public sealed class CrewManifestCartridgeSystem : EntitySystem var owningStation = _stationSystem.GetOwningStation(uid); if (owningStation is null) + { + // Display "loading failed" message + var failureMessage = Loc.GetString("crew-manifest-cartridge-loading-failed"); + var failureState = new CrewManifestUiState(failureMessage, new CrewManifestEntries()); + _cartridgeLoader.UpdateCartridgeUiState(loaderUid, failureState); return; + } var (stationName, entries) = _crewManifest.GetCrewManifest(owningStation.Value); diff --git a/Resources/Locale/en-US/cartridge-loader/cartridges.ftl b/Resources/Locale/en-US/cartridge-loader/cartridges.ftl index 11b2fb9402..7dd7c779ac 100644 --- a/Resources/Locale/en-US/cartridge-loader/cartridges.ftl +++ b/Resources/Locale/en-US/cartridge-loader/cartridges.ftl @@ -7,6 +7,7 @@ news-read-program-name = Station news crew-manifest-program-name = Crew manifest crew-manifest-cartridge-loading = Loading ... +crew-manifest-cartridge-loading-failed = Failed to load crew manifest! net-probe-program-name = NetProbe net-probe-scan = Scanned {$device}! From f6fc3edcf7fcc62d6563e7a6f482b5b1e2be96d5 Mon Sep 17 00:00:00 2001 From: PJBot Date: Fri, 3 Apr 2026 18:18:04 +0000 Subject: [PATCH 041/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 4b1d22ee97..a18eb6fa29 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: Myra - changes: - - message: The built-in soundfont for MIDI's should sound better, especially for - songs with percussion. - type: Tweak - id: 9092 - time: '2025-10-13T21:22:53.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40888 - author: KittyCat432 changes: - message: Slime guidebook is more representative of their actual strengths. @@ -4032,3 +4024,10 @@ id: 9603 time: '2026-04-03T18:01:11.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/41633 +- author: sudobeans + changes: + - message: Crew Manifest PDA program now displays a message when it fails to load. + type: Tweak + id: 9604 + time: '2026-04-03T18:16:55.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43400 From 7c60ea97e4a4166159185d845b05abc862881a9a Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:16:17 +0200 Subject: [PATCH 042/126] Fix banana creampie visuals, allow station AI to be pied (#43388) * refactor and fixes * fix ling transformation * small fix * Update Content.Shared/Nutrition/EntitySystems/SharedCreamPieSystem.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --------- Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> --- .../Nutrition/EntitySystems/CreamPieSystem.cs | 66 +++++++ .../EntitySystems/CreamPiedSystem.cs | 10 - .../Systems/AdminVerbSystem.Smites.cs | 2 +- .../Nutrition/EntitySystems/CreamPieSystem.cs | 102 +--------- .../Effects/WashCreamPieEntityEffectSystem.cs | 2 +- .../Nutrition/Components/CreamPieComponent.cs | 44 +++-- .../Components/CreamPiedComponent.cs | 55 ++++-- .../EntitySystems/SharedCreamPieSystem.cs | 184 +++++++++++++----- .../Prototypes/Body/Species/arachnid.yml | 4 + Resources/Prototypes/Body/Species/moth.yml | 4 + .../Prototypes/Body/Species/reptilian.yml | 4 + Resources/Prototypes/Body/Species/vox.yml | 4 + .../Prototypes/Body/Species/vulpkanin.yml | 4 + .../Prototypes/Body/species_appearance.yml | 5 - Resources/Prototypes/Body/species_base.yml | 9 +- .../Prototypes/Entities/Mobs/NPCs/animals.yml | 24 +-- .../Prototypes/Entities/Mobs/Player/clone.yml | 1 + .../Entities/Mobs/Player/silicon.yml | 10 + 18 files changed, 324 insertions(+), 210 deletions(-) create mode 100644 Content.Client/Nutrition/EntitySystems/CreamPieSystem.cs delete mode 100644 Content.Client/Nutrition/EntitySystems/CreamPiedSystem.cs diff --git a/Content.Client/Nutrition/EntitySystems/CreamPieSystem.cs b/Content.Client/Nutrition/EntitySystems/CreamPieSystem.cs new file mode 100644 index 0000000000..96a63cc694 --- /dev/null +++ b/Content.Client/Nutrition/EntitySystems/CreamPieSystem.cs @@ -0,0 +1,66 @@ +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Robust.Client.GameObjects; + +namespace Content.Client.Nutrition.EntitySystems; + +public sealed class CreamPieSystem : SharedCreamPieSystem +{ + [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly AppearanceSystem _appearance = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnComponentShutdown); + SubscribeLocalEvent(OnAppearanceChange); + SubscribeLocalEvent(OnAfterAutoHandleState); + } + + private void OnComponentInit(Entity ent, ref ComponentInit args) + { + UpdateAppearance(ent); + } + + private void OnComponentShutdown(Entity ent, ref ComponentShutdown args) + { + _sprite.RemoveLayer(ent.Owner, CreamPiedVisualLayer.Key); + } + + private void OnAppearanceChange(Entity ent, ref AppearanceChangeEvent args) + { + UpdateAppearance((ent.Owner, ent.Comp, args.Sprite, args.Component)); + } + + private void OnAfterAutoHandleState(Entity ent, ref AfterAutoHandleStateEvent args) + { + // Update when the sprite datafield is changed so that changelings can transform properly. + UpdateAppearance(ent); + } + + private void UpdateAppearance(Entity ent) + { + if (!Resolve(ent, ref ent.Comp2, false) || !Resolve(ent, ref ent.Comp3, false)) + return; + + var creamPied = ent.Comp1; + var sprite = ent.Comp2; + var appearance = ent.Comp3; + + // If there is no sprite to use, remove the layer. Otherwise ensure that it exists and set the visuals accordingly. + int index; + if (creamPied.Sprite == null) + { + _sprite.RemoveLayer((ent.Owner, sprite), CreamPiedVisualLayer.Key); + return; + } + + index = _sprite.LayerMapReserve((ent.Owner, sprite), CreamPiedVisualLayer.Key); + + _appearance.TryGetData(ent.Owner, CreamPiedVisuals.Creamed, out var isCreamPied, appearance); + _sprite.LayerSetSprite((ent.Owner, sprite), index, creamPied.Sprite); + _sprite.LayerSetVisible((ent.Owner, sprite), index, isCreamPied); + } +} diff --git a/Content.Client/Nutrition/EntitySystems/CreamPiedSystem.cs b/Content.Client/Nutrition/EntitySystems/CreamPiedSystem.cs deleted file mode 100644 index 24632b71fa..0000000000 --- a/Content.Client/Nutrition/EntitySystems/CreamPiedSystem.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Content.Shared.Nutrition.EntitySystems; -using JetBrains.Annotations; - -namespace Content.Client.Nutrition.EntitySystems -{ - [UsedImplicitly] - public sealed class CreamPiedSystem : SharedCreamPieSystem - { - } -} diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs index 540bd7d089..63b4b5646b 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs @@ -279,7 +279,7 @@ public sealed partial class AdminVerbSystem Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Consumable/Food/Baked/pie.rsi"), "plain-slice"), Act = () => { - _creamPieSystem.SetCreamPied(args.Target, creamPied, true); + _creamPieSystem.SetCreamPied((args.Target, creamPied), true); }, Impact = LogImpact.Extreme, Message = string.Join(": ", creamPieName, Loc.GetString("admin-smite-creampie-description")) diff --git a/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs b/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs index 33b619732d..202d03c6ea 100644 --- a/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs @@ -1,103 +1,5 @@ -using Content.Server.Fluids.EntitySystems; -using Content.Server.Nutrition.Components; -using Content.Server.Popups; -using Content.Shared.Containers.ItemSlots; -using Content.Shared.IdentityManagement; -using Content.Shared.Nutrition; -using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; -using Content.Shared.Rejuvenate; -using Content.Shared.Throwing; -using Content.Shared.Trigger.Components; -using Content.Shared.Trigger.Systems; -using Content.Shared.Chemistry.EntitySystems; -using JetBrains.Annotations; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Player; -namespace Content.Server.Nutrition.EntitySystems -{ - [UsedImplicitly] - public sealed class CreamPieSystem : SharedCreamPieSystem - { - [Dependency] private readonly IngestionSystem _ingestion = default!; - [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; - [Dependency] private readonly PopupSystem _popup = default!; - [Dependency] private readonly PuddleSystem _puddle = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutions = default!; - [Dependency] private readonly TriggerSystem _trigger = default!; +namespace Content.Server.Nutrition.EntitySystems; - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnSlice); - - SubscribeLocalEvent(OnRejuvenate); - } - - protected override void SplattedCreamPie(Entity entity) - { - // The entity is deleted, so play the sound at its position rather than parenting - var coordinates = Transform(entity).Coordinates; - _audio.PlayPvs(_audio.ResolveSound(entity.Comp1.Sound), coordinates, AudioParams.Default.WithVariation(0.125f)); - - if (Resolve(entity, ref entity.Comp2, false)) - { - if (_solutions.TryGetSolution(entity.Owner, entity.Comp2.Solution, out _, out var solution)) - _puddle.TrySpillAt(entity.Owner, solution, out _, false); - - _ingestion.SpawnTrash((entity, entity.Comp2)); - } - - ActivatePayload(entity); - - QueueDel(entity); - } - - // TODO - // A regression occured here. Previously creampies would activate their hidden payload if you tried to eat them. - // However, the refactor to IngestionSystem caused the event to not be reached, - // because eating is blocked if an item is inside the food. - - private void OnSlice(Entity entity, ref SliceFoodEvent args) - { - ActivatePayload(entity); - } - - private void ActivatePayload(EntityUid uid) - { - if (_itemSlots.TryGetSlot(uid, CreamPieComponent.PayloadSlotName, out var itemSlot)) - { - if (_itemSlots.TryEject(uid, itemSlot, user: null, out var item)) - { - if (TryComp(item.Value, out var timerTrigger)) - { - _trigger.ActivateTimerTrigger((item.Value, timerTrigger)); - } - } - } - } - - protected override void CreamedEntity(EntityUid uid, CreamPiedComponent creamPied, ThrowHitByEvent args) - { - _popup.PopupEntity(Loc.GetString("cream-pied-component-on-hit-by-message", - ("thrown", Identity.Entity(args.Thrown, EntityManager))), - uid, args.Target); - - var otherPlayers = Filter.PvsExcept(uid); - - _popup.PopupEntity(Loc.GetString("cream-pied-component-on-hit-by-message-others", - ("owner", Identity.Entity(uid, EntityManager)), - ("thrown", Identity.Entity(args.Thrown, EntityManager))), - uid, otherPlayers, false); - } - - private void OnRejuvenate(Entity entity, ref RejuvenateEvent args) - { - SetCreamPied(entity, entity.Comp, false); - } - } -} +public sealed class CreamPieSystem : SharedCreamPieSystem; diff --git a/Content.Shared/EntityEffects/Effects/WashCreamPieEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/WashCreamPieEntityEffectSystem.cs index c54912191e..9b3d731fcc 100644 --- a/Content.Shared/EntityEffects/Effects/WashCreamPieEntityEffectSystem.cs +++ b/Content.Shared/EntityEffects/Effects/WashCreamPieEntityEffectSystem.cs @@ -15,7 +15,7 @@ public sealed partial class WashCreamPieEntityEffectSystem : EntityEffectSystem< protected override void Effect(Entity entity, ref EntityEffectEvent args) { - _creamPie.SetCreamPied(entity, entity.Comp, false); + _creamPie.SetCreamPied((entity, entity.Comp), false); } } diff --git a/Content.Shared/Nutrition/Components/CreamPieComponent.cs b/Content.Shared/Nutrition/Components/CreamPieComponent.cs index b6bd124084..91ba3b3d6e 100644 --- a/Content.Shared/Nutrition/Components/CreamPieComponent.cs +++ b/Content.Shared/Nutrition/Components/CreamPieComponent.cs @@ -1,21 +1,39 @@ using Content.Shared.Nutrition.EntitySystems; using Robust.Shared.Audio; +using Robust.Shared.GameStates; -namespace Content.Shared.Nutrition.Components +namespace Content.Shared.Nutrition.Components; + +/// +/// Component used for banana cream pies. +/// These can be thrown at someone to stun them and cream their face. +/// +[Access(typeof(SharedCreamPieSystem))] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class CreamPieComponent : Component { - [Access(typeof(SharedCreamPieSystem))] - [RegisterComponent] - public sealed partial class CreamPieComponent : Component - { - [DataField("paralyzeTime")] - public float ParalyzeTime { get; private set; } = 1f; + /// + /// The time being hit by this entity will stun you. + /// + [DataField, AutoNetworkedField] + public TimeSpan ParalyzeTime = TimeSpan.FromSeconds(1); - [DataField("sound")] - public SoundSpecifier Sound { get; private set; } = new SoundCollectionSpecifier("desecration"); + /// + /// The sound to play when hitting something. + /// + [DataField] + public SoundSpecifier Sound = new SoundCollectionSpecifier("desecration", AudioParams.Default.WithVariation(0.125f)); - [ViewVariables] - public bool Splatted { get; set; } = false; + /// + /// Has this pie been splatted by hitting something? + /// + [DataField, AutoNetworkedField] + public bool Splatted = false; - public const string PayloadSlotName = "payloadSlot"; - } + /// + /// Items in this container will be triggered when the pie hits something. + /// This allows throwable C4 pies or similar. + /// + [ViewVariables] + public const string PayloadSlotName = "payloadSlot"; } diff --git a/Content.Shared/Nutrition/Components/CreamPiedComponent.cs b/Content.Shared/Nutrition/Components/CreamPiedComponent.cs index 6779fe3666..08c583ff7b 100644 --- a/Content.Shared/Nutrition/Components/CreamPiedComponent.cs +++ b/Content.Shared/Nutrition/Components/CreamPiedComponent.cs @@ -1,19 +1,48 @@ using Content.Shared.Nutrition.EntitySystems; +using Robust.Shared.GameStates; using Robust.Shared.Serialization; +using Robust.Shared.Utility; -namespace Content.Shared.Nutrition.Components +namespace Content.Shared.Nutrition.Components; + +/// +/// Allows this entity to be hit by banana cream pies. +/// See . +/// +[Access(typeof(SharedCreamPieSystem))] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)] +public sealed partial class CreamPiedComponent : Component { - [Access(typeof(SharedCreamPieSystem))] - [RegisterComponent] - public sealed partial class CreamPiedComponent : Component - { - [ViewVariables] - public bool CreamPied { get; set; } = false; - } + /// + /// Was this entity hit by a banana cream pie? + /// This is reset if they get splashed with water. + /// + [DataField, AutoNetworkedField] + public bool CreamPied; - [Serializable, NetSerializable] - public enum CreamPiedVisuals - { - Creamed, - } + /// + /// The sprite to draw on someone's face if they were hit by a pie. + /// The layer will be dynamically added with the component. + /// + [DataField, AutoNetworkedField] + public SpriteSpecifier? Sprite; +} + +/// +/// Key to be used with appearance data, indicating if the entity has a banana cream pie in their face. +/// +[Serializable, NetSerializable] +public enum CreamPiedVisuals +{ + Creamed, +} + +/// +/// The visual layer for the creampied face. +/// Will be dynamically added and removed with the component. +/// +[Serializable, NetSerializable] +public enum CreamPiedVisualLayer +{ + Key, } diff --git a/Content.Shared/Nutrition/EntitySystems/SharedCreamPieSystem.cs b/Content.Shared/Nutrition/EntitySystems/SharedCreamPieSystem.cs index a0a82d63ef..84827ed0e3 100644 --- a/Content.Shared/Nutrition/EntitySystems/SharedCreamPieSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/SharedCreamPieSystem.cs @@ -1,72 +1,164 @@ +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Fluids; +using Content.Shared.IdentityManagement; using Content.Shared.Nutrition.Components; +using Content.Shared.Popups; +using Content.Shared.Rejuvenate; using Content.Shared.Stunnable; using Content.Shared.Throwing; -using JetBrains.Annotations; +using Content.Shared.Trigger.Components; +using Content.Shared.Trigger.Systems; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Network; +using Robust.Shared.Player; -namespace Content.Shared.Nutrition.EntitySystems +namespace Content.Shared.Nutrition.EntitySystems; + +public abstract class SharedCreamPieSystem : EntitySystem { - [UsedImplicitly] - public abstract class SharedCreamPieSystem : EntitySystem + [Dependency] private readonly SharedStunSystem _stunSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly IngestionSystem _ingestion = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedPuddleSystem _puddle = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutions = default!; + [Dependency] private readonly TriggerSystem _trigger = default!; + [Dependency] private readonly INetManager _net = default!; + + public override void Initialize() { - [Dependency] private SharedStunSystem _stunSystem = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + base.Initialize(); - public override void Initialize() + SubscribeLocalEvent(OnCreamPieHit); + SubscribeLocalEvent(OnCreamPieLand); + SubscribeLocalEvent(OnCreamPiedHitBy); + SubscribeLocalEvent(OnSlice); + SubscribeLocalEvent(OnRejuvenate); + } + + /// + /// SPLAT! + /// + public void SplatCreamPie(Entity creamPie) + { + // Already splatted! Do nothing. + if (creamPie.Comp.Splatted) + return; + + // The pie will be queued for deletion but there may be multiple collisions in the same tick, so we prevent it from splatting more than once. + creamPie.Comp.Splatted = true; + Dirty(creamPie); + + // The entity is being deleted, so play the sound at its position rather than parenting. + if (_net.IsServer) // we don't have a user to pass in TODO: make the popup API sane and remove this guard { - base.Initialize(); - - SubscribeLocalEvent(OnCreamPieHit); - SubscribeLocalEvent(OnCreamPieLand); - SubscribeLocalEvent(OnCreamPiedHitBy); + var coordinates = Transform(creamPie).Coordinates; + _audio.PlayPvs(creamPie.Comp.Sound, coordinates); } - public void SplatCreamPie(Entity creamPie) + if (TryComp(creamPie, out var edibleComp)) { - // Already splatted! Do nothing. - if (creamPie.Comp.Splatted) - return; + if (_solutions.TryGetSolution(creamPie.Owner, edibleComp.Solution, out _, out var solution)) + _puddle.TrySpillAt(creamPie.Owner, solution, out _, false); - creamPie.Comp.Splatted = true; - - SplattedCreamPie(creamPie); + _ingestion.SpawnTrash((creamPie.Owner, edibleComp)); } - protected virtual void SplattedCreamPie(Entity entity) { } + ActivatePayload(creamPie); + PredictedQueueDel(creamPie); + } - public void SetCreamPied(EntityUid uid, CreamPiedComponent creamPied, bool value) - { - if (value == creamPied.CreamPied) - return; + /// + /// Drop any item hidden in the cream pie and trigger it. + /// + public void ActivatePayload(EntityUid uid) + { + // Keep this server side for now since we don't have a user we can pass in for prediction purposes. + // Ideally the popup and audio API will be reworked so that is not needed anymore. + if (_net.IsClient) + return; - creamPied.CreamPied = value; + if (_itemSlots.TryGetSlot(uid, CreamPieComponent.PayloadSlotName, out var itemSlot) + && _itemSlots.TryEject(uid, itemSlot, user: null, out var item) + && TryComp(item.Value, out var timerTrigger)) + _trigger.ActivateTimerTrigger((item.Value, timerTrigger)); + } - if (TryComp(uid, out AppearanceComponent? appearance)) - { - _appearance.SetData(uid, CreamPiedVisuals.Creamed, value, appearance); - } - } + /// + /// Sets the creampied status of an entity. + /// This toggles the visuals for the pie in their face. + /// + public void SetCreamPied(Entity ent, bool value) + { + if (!Resolve(ent, ref ent.Comp)) + return; - private void OnCreamPieLand(Entity entity, ref LandEvent args) - { - SplatCreamPie(entity); - } + if (value == ent.Comp.CreamPied) + return; - private void OnCreamPieHit(Entity entity, ref ThrowDoHitEvent args) - { - SplatCreamPie(entity); - } + ent.Comp.CreamPied = value; + Dirty(ent); - private void OnCreamPiedHitBy(EntityUid uid, CreamPiedComponent creamPied, ThrowHitByEvent args) - { - if (!Exists(args.Thrown) || !TryComp(args.Thrown, out CreamPieComponent? creamPie)) return; + _appearance.SetData(ent.Owner, CreamPiedVisuals.Creamed, value); + } - SetCreamPied(uid, creamPied, true); + private void OnCreamPieLand(Entity ent, ref LandEvent args) + { + SplatCreamPie(ent); + } - CreamedEntity(uid, creamPied, args); + private void OnCreamPieHit(Entity ent, ref ThrowDoHitEvent args) + { + SplatCreamPie(ent); + } - _stunSystem.TryUpdateParalyzeDuration(uid, TimeSpan.FromSeconds(creamPie.ParalyzeTime)); - } + private void OnCreamPiedHitBy(Entity creamPied, ref ThrowHitByEvent args) + { + if (creamPied.Comp.CreamPied || !Exists(args.Thrown) || !TryComp(args.Thrown, out var creamPie)) + return; - protected virtual void CreamedEntity(EntityUid uid, CreamPiedComponent creamPied, ThrowHitByEvent args) {} + // TODO: Check if they even have a head that can be hit. + SetCreamPied(creamPied.AsNullable(), true); + _stunSystem.TryUpdateParalyzeDuration(creamPied.Owner, creamPie.ParalyzeTime); + + // Throwing is not predicted, so the thrower is not equal to the client predicting the collision, so we cannot pass in a user. + // TODO: Make the popup API sane. + if (_net.IsClient) + return; + + // Shown only to the player that was hit. + _popup.PopupEntity( + Loc.GetString( + "cream-pied-component-on-hit-by-message", + ("thrown", args.Thrown)), + creamPied.Owner, creamPied.Owner); + + var otherPlayers = Filter.PvsExcept(creamPied.Owner); + + // Show to everyone else. + _popup.PopupEntity( + Loc.GetString( + "cream-pied-component-on-hit-by-message-others", + ("owner", Identity.Entity(creamPied.Owner, EntityManager)), + ("thrown", args.Thrown)), + creamPied.Owner, otherPlayers, false); + } + + private void OnRejuvenate(Entity ent, ref RejuvenateEvent args) + { + SetCreamPied(ent.AsNullable(), false); + } + + // TODO + // A regression occured here. Previously creampies would activate their hidden payload if you tried to eat them. + // However, the refactor to IngestionSystem caused the event to not be reached, + // because eating is blocked if an item is inside the food. + + private void OnSlice(Entity ent, ref SliceFoodEvent args) + { + ActivatePayload(ent); } } diff --git a/Resources/Prototypes/Body/Species/arachnid.yml b/Resources/Prototypes/Body/Species/arachnid.yml index 8455f33981..240d486f5a 100644 --- a/Resources/Prototypes/Body/Species/arachnid.yml +++ b/Resources/Prototypes/Body/Species/arachnid.yml @@ -137,6 +137,10 @@ Unsexed: UnisexArachnid - type: TypingIndicator proto: spider + - type: CreamPied + sprite: + sprite: Effects/creampie.rsi + state: creampie_arachnid - type: entity parent: OrganBase diff --git a/Resources/Prototypes/Body/Species/moth.yml b/Resources/Prototypes/Body/Species/moth.yml index af243333a6..b7928db44c 100644 --- a/Resources/Prototypes/Body/Species/moth.yml +++ b/Resources/Prototypes/Body/Species/moth.yml @@ -148,6 +148,10 @@ allowedEmotes: ['Chitter', 'Squeak', 'Flap'] - type: TypingIndicator proto: moth + - type: CreamPied + sprite: + sprite: Effects/creampie.rsi + state: creampie_moth - type: Bloodstream bloodReferenceSolution: reagents: diff --git a/Resources/Prototypes/Body/Species/reptilian.yml b/Resources/Prototypes/Body/Species/reptilian.yml index f3b24081bf..8f28ea0ab4 100644 --- a/Resources/Prototypes/Body/Species/reptilian.yml +++ b/Resources/Prototypes/Body/Species/reptilian.yml @@ -131,6 +131,10 @@ allowedEmotes: ['Thump'] - type: TypingIndicator proto: lizard + - type: CreamPied + sprite: + sprite: Effects/creampie.rsi + state: creampie_lizard - type: Vocal sounds: Male: MaleReptilian diff --git a/Resources/Prototypes/Body/Species/vox.yml b/Resources/Prototypes/Body/Species/vox.yml index 9fab05ea20..111a1f47fa 100644 --- a/Resources/Prototypes/Body/Species/vox.yml +++ b/Resources/Prototypes/Body/Species/vox.yml @@ -170,6 +170,10 @@ allowedEmotes: ['Click', 'Chitter'] - type: TypingIndicator proto: vox + - type: CreamPied + sprite: + sprite: Effects/creampie.rsi + state: creampie_vox - type: Vocal sounds: Male: UnisexVox diff --git a/Resources/Prototypes/Body/Species/vulpkanin.yml b/Resources/Prototypes/Body/Species/vulpkanin.yml index 2ac31f2eec..c4afe1b8e1 100644 --- a/Resources/Prototypes/Body/Species/vulpkanin.yml +++ b/Resources/Prototypes/Body/Species/vulpkanin.yml @@ -204,6 +204,10 @@ reagents: - ReagentId: SulfurBlood Quantity: 300 + - type: CreamPied + sprite: + sprite: Effects/creampie.rsi + state: creampie_vulpkanin - type: entity parent: OrganBase diff --git a/Resources/Prototypes/Body/species_appearance.yml b/Resources/Prototypes/Body/species_appearance.yml index 44369b1548..e70b6abb38 100644 --- a/Resources/Prototypes/Body/species_appearance.yml +++ b/Resources/Prototypes/Body/species_appearance.yml @@ -66,11 +66,6 @@ state: body-overlay-2 visible: false - - map: [ "clownedon" ] # Dynamically generated - sprite: "Effects/creampie.rsi" - state: "creampie_human" - visible: false - - type: entity id: BaseSpeciesAppearance parent: diff --git a/Resources/Prototypes/Body/species_base.yml b/Resources/Prototypes/Body/species_base.yml index 454b376ad3..1f782b0bc3 100644 --- a/Resources/Prototypes/Body/species_base.yml +++ b/Resources/Prototypes/Body/species_base.yml @@ -25,12 +25,6 @@ color: "#FF0000" Burn: sprite: Mobs/Effects/burn_damage.rsi - - type: GenericVisualizer - visuals: - enum.CreamPiedVisuals.Creamed: - clownedon: - True: { visible: true } - False: { visible: false } - type: StatusIcon bounds: -0.5,-0.5,0.5,0.5 - type: JobStatus @@ -126,6 +120,9 @@ factions: - NanoTrasen - type: CreamPied + sprite: + sprite: Effects/creampie.rsi + state: creampie_human - type: ParcelWrapOverride parcelPrototype: WrappedParcelHumanoid wrapDelay: 5 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 0c8e4beedb..2954199554 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -1392,23 +1392,16 @@ - map: [ "outerClothing" ] - map: [ "mask" ] - map: [ "head" ] - - map: [ "clownedon" ] - sprite: "Effects/creampie.rsi" - state: "creampie_human" - visible: false - type: Hands activeHandId: Hand hands: Hand: location: Left - type: ComplexInteraction - - type: GenericVisualizer - visuals: - enum.CreamPiedVisuals.Creamed: - clownedon: - True: {visible: true} - False: {visible: false} - type: CreamPied + sprite: + sprite: Effects/creampie.rsi + state: creampie_monkey - type: FireVisuals sprite: Mobs/Effects/onfire.rsi normalState: Monkey_burning @@ -1505,7 +1498,7 @@ - type: entity name: monkey id: MobBaseSyndicateMonkey - parent: MobBaseAncestor + parent: MobBaseAncestor # TODO: fix copy paste from MobMonkey description: New church of neo-darwinists actually believe that EVERY animal evolved from a monkey. Tastes like pork, and killing them is both fun and relaxing. suffix: syndicate base components: @@ -1629,10 +1622,6 @@ - map: [ "id" ] - map: [ "mask" ] - map: [ "head" ] - - map: [ "clownedon" ] - sprite: "Effects/creampie.rsi" - state: "creampie_human" - visible: false - type: RandomSprite getAllGroups: true available: @@ -2976,6 +2965,11 @@ - type: MobPrice price: 200 - type: MessyDrinker + # TODO: Fix the sprite offset + #- type: CreamPied + # sprite: + # sprite: Effects/creampie.rsi + # state: creampie_corgi - type: entity parent: MobCorgiBase diff --git a/Resources/Prototypes/Entities/Mobs/Player/clone.yml b/Resources/Prototypes/Entities/Mobs/Player/clone.yml index 8aa1c4c7b0..2763afefec 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/clone.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/clone.yml @@ -15,6 +15,7 @@ - Grammar # Pronouns - HumanoidProfile # Age, Sex, Gender - NpcFactionMember + - CreamPied # traits - BlackAndWhiteOverlay - Clumsy diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml index 3dfd48bb99..5e06100098 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -283,6 +283,16 @@ enum.StationAiVisualLayers.Base: False: { state: ai_empty } True: { state: ai_error } + - type: CreamPied + sprite: + sprite: Effects/creampie.rsi + state: creampie_ai + - type: Reactive + reactions: + - reagents: [Water, SpaceCleaner] + methods: [Touch] + effects: + - !type:WashCreamPie - type: InteractionPopup interactSuccessString: petting-success-station-ai interactFailureString: petting-failure-station-ai From b690017d44272233025ba4e32033b27ecf467f9b Mon Sep 17 00:00:00 2001 From: PJBot Date: Fri, 3 Apr 2026 18:36:49 +0000 Subject: [PATCH 043/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index a18eb6fa29..611fde4453 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: KittyCat432 - changes: - - message: Slime guidebook is more representative of their actual strengths. - type: Tweak - id: 9093 - time: '2025-10-14T04:21:07.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40842 - author: MilenVolf changes: - message: Fixed some physics bugs that could cause the grappling hook to teleport @@ -4031,3 +4024,10 @@ id: 9604 time: '2026-04-03T18:16:55.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43400 +- author: slarticodefast + changes: + - message: You can now throw banana cream pies at the station AI. + type: Add + id: 9605 + time: '2026-04-03T18:35:41.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43388 From 9f8ca1262a2cb12ed19893e02908f90ce75ffc9a Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:24:45 -0700 Subject: [PATCH 044/126] Fix Bloodstream Overflow on Rejuvenate (#43447) fix bloodstream overflow Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> --- Content.Shared/Body/Systems/SharedBloodstreamSystem.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs b/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs index ec0521b7b6..a3caad69be 100644 --- a/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs +++ b/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs @@ -278,7 +278,8 @@ public abstract class SharedBloodstreamSystem : EntitySystem if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution)) { SolutionContainer.RemoveAllSolution(ent.Comp.BloodSolution.Value); - TryModifyBloodLevel(ent.AsNullable(), ent.Comp.BloodReferenceSolution.Volume); + // TODO: Use Solutions API for this when it exists + TryRegulateBloodLevel(ent.AsNullable(), ent.Comp.BloodReferenceSolution.Volume); } } @@ -404,11 +405,12 @@ public abstract class SharedBloodstreamSystem : EntitySystem return false; referenceFactor = Math.Clamp(referenceFactor, 0f, ent.Comp.MaxVolumeModifier); + var ratio = amount / ent.Comp.BloodReferenceSolution.Volume; foreach (var (referenceReagent, referenceQuantity) in ent.Comp.BloodReferenceSolution) { var error = referenceQuantity * referenceFactor - bloodSolution.GetTotalPrototypeQuantity(referenceReagent.Prototype); - var adjustedAmount = amount * referenceQuantity / ent.Comp.BloodReferenceSolution.Volume; + var adjustedAmount = referenceQuantity * ratio; if (error > 0) { From be9b2fbb88dd679441ec15390dac295ffdeb3b24 Mon Sep 17 00:00:00 2001 From: Moony Date: Fri, 3 Apr 2026 11:40:21 -0700 Subject: [PATCH 045/126] Remove unused space worldgen. (#43428) * Remove my unmaintained feature. * Cut world loaders. * Cut unused pool. --- Content.Client/Entry/EntryPoint.cs | 1 - Content.IntegrationTests/PoolManager.Cvars.cs | 1 - Content.IntegrationTests/Tests/EntityTest.cs | 3 - Content.Server/IoC/ServerContentIoC.cs | 2 - .../Components/BiomeSelectionComponent.cs | 21 -- .../Carvers/NoiseRangeCarverComponent.cs | 28 -- .../Debris/BlobFloorPlanBuilderComponent.cs | 36 --- .../DebrisFeaturePlacerControllerComponent.cs | 43 --- .../NoiseDrivenDebrisSelectorComponent.cs | 44 --- .../Components/Debris/OwnedDebrisComponent.cs | 24 -- .../Debris/SimpleDebrisSelectorComponent.cs | 34 --- .../SimpleFloorPlanPopulatorComponent.cs | 47 --- .../Components/LoadedChunkComponent.cs | 17 -- .../Components/LocalityLoaderComponent.cs | 19 -- .../Components/NoiseIndexComponent.cs | 20 -- .../Components/WorldChunkComponent.cs | 22 -- .../Components/WorldControllerComponent.cs | 25 -- .../Components/WorldLoaderComponent.cs | 18 -- .../Worldgen/GridPointsNearEnumerator.cs | 59 ---- .../Worldgen/Prototypes/BiomePrototype.cs | 61 ---- .../Prototypes/NoiseChannelPrototype.cs | 170 ----------- .../Prototypes/WorldgenConfigPrototype.cs | 37 --- .../Worldgen/Systems/BaseWorldSystem.cs | 60 ---- .../Systems/Biomes/BiomeSelectionSystem.cs | 75 ----- .../Systems/Carvers/NoiseRangeCarverSystem.cs | 36 --- .../Debris/BlobFloorPlanBuilderSystem.cs | 89 ------ .../Debris/DebrisFeaturePlacerSystem.cs | 278 ------------------ .../Debris/NoiseDrivenDebrisSelectorSystem.cs | 60 ---- .../Debris/SimpleFloorPlanPopulatorSystem.cs | 50 ---- .../Worldgen/Systems/LocalityLoaderSystem.cs | 63 ---- .../Worldgen/Systems/NoiseIndexSystem.cs | 47 --- .../Worldgen/Systems/WorldControllerSystem.cs | 277 ----------------- .../Worldgen/Systems/WorldgenConfigSystem.cs | 85 ------ .../Tools/EntitySpawnCollectionCache.cs | 96 ------ .../Worldgen/Tools/PoissonDiskSampler.cs | 244 --------------- Content.Server/Worldgen/WorldGen.cs | 72 ----- Content.Shared/CCVar/CCVars.Worldgen.cs | 18 -- .../en-US/worldgen/applyworldgenconfig.ftl | 4 - .../Machines/Computers/computers.yml | 4 - .../Entities/Structures/Machines/salvage.yml | 8 - .../Entities/World/Debris/asteroids.yml | 144 --------- .../Entities/World/Debris/base_debris.yml | 6 - .../Entities/World/Debris/wrecks.yml | 80 ----- Resources/Prototypes/Entities/World/chunk.yml | 14 - Resources/Prototypes/World/Biomes/basic.yml | 26 -- .../Prototypes/World/Biomes/failsafes.yml | 21 -- Resources/Prototypes/World/noise_channels.yml | 44 --- .../Prototypes/World/worldgen_default.yml | 9 - 48 files changed, 2642 deletions(-) delete mode 100644 Content.Server/Worldgen/Components/BiomeSelectionComponent.cs delete mode 100644 Content.Server/Worldgen/Components/Carvers/NoiseRangeCarverComponent.cs delete mode 100644 Content.Server/Worldgen/Components/Debris/BlobFloorPlanBuilderComponent.cs delete mode 100644 Content.Server/Worldgen/Components/Debris/DebrisFeaturePlacerControllerComponent.cs delete mode 100644 Content.Server/Worldgen/Components/Debris/NoiseDrivenDebrisSelectorComponent.cs delete mode 100644 Content.Server/Worldgen/Components/Debris/OwnedDebrisComponent.cs delete mode 100644 Content.Server/Worldgen/Components/Debris/SimpleDebrisSelectorComponent.cs delete mode 100644 Content.Server/Worldgen/Components/Debris/SimpleFloorPlanPopulatorComponent.cs delete mode 100644 Content.Server/Worldgen/Components/LoadedChunkComponent.cs delete mode 100644 Content.Server/Worldgen/Components/LocalityLoaderComponent.cs delete mode 100644 Content.Server/Worldgen/Components/NoiseIndexComponent.cs delete mode 100644 Content.Server/Worldgen/Components/WorldChunkComponent.cs delete mode 100644 Content.Server/Worldgen/Components/WorldControllerComponent.cs delete mode 100644 Content.Server/Worldgen/Components/WorldLoaderComponent.cs delete mode 100644 Content.Server/Worldgen/GridPointsNearEnumerator.cs delete mode 100644 Content.Server/Worldgen/Prototypes/BiomePrototype.cs delete mode 100644 Content.Server/Worldgen/Prototypes/NoiseChannelPrototype.cs delete mode 100644 Content.Server/Worldgen/Prototypes/WorldgenConfigPrototype.cs delete mode 100644 Content.Server/Worldgen/Systems/BaseWorldSystem.cs delete mode 100644 Content.Server/Worldgen/Systems/Biomes/BiomeSelectionSystem.cs delete mode 100644 Content.Server/Worldgen/Systems/Carvers/NoiseRangeCarverSystem.cs delete mode 100644 Content.Server/Worldgen/Systems/Debris/BlobFloorPlanBuilderSystem.cs delete mode 100644 Content.Server/Worldgen/Systems/Debris/DebrisFeaturePlacerSystem.cs delete mode 100644 Content.Server/Worldgen/Systems/Debris/NoiseDrivenDebrisSelectorSystem.cs delete mode 100644 Content.Server/Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs delete mode 100644 Content.Server/Worldgen/Systems/LocalityLoaderSystem.cs delete mode 100644 Content.Server/Worldgen/Systems/NoiseIndexSystem.cs delete mode 100644 Content.Server/Worldgen/Systems/WorldControllerSystem.cs delete mode 100644 Content.Server/Worldgen/Systems/WorldgenConfigSystem.cs delete mode 100644 Content.Server/Worldgen/Tools/EntitySpawnCollectionCache.cs delete mode 100644 Content.Server/Worldgen/Tools/PoissonDiskSampler.cs delete mode 100644 Content.Server/Worldgen/WorldGen.cs delete mode 100644 Content.Shared/CCVar/CCVars.Worldgen.cs delete mode 100644 Resources/Locale/en-US/worldgen/applyworldgenconfig.ftl delete mode 100644 Resources/Prototypes/Entities/World/Debris/asteroids.yml delete mode 100644 Resources/Prototypes/Entities/World/Debris/base_debris.yml delete mode 100644 Resources/Prototypes/Entities/World/Debris/wrecks.yml delete mode 100644 Resources/Prototypes/Entities/World/chunk.yml delete mode 100644 Resources/Prototypes/World/Biomes/basic.yml delete mode 100644 Resources/Prototypes/World/Biomes/failsafes.yml delete mode 100644 Resources/Prototypes/World/noise_channels.yml delete mode 100644 Resources/Prototypes/World/worldgen_default.yml diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index e0358d54e7..5f7f827cc2 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -119,7 +119,6 @@ namespace Content.Client.Entry _prototypeManager.RegisterIgnore("noiseChannel"); _prototypeManager.RegisterIgnore("playerConnectionWhitelist"); _prototypeManager.RegisterIgnore("spaceBiome"); - _prototypeManager.RegisterIgnore("worldgenConfig"); _prototypeManager.RegisterIgnore("gameRule"); _prototypeManager.RegisterIgnore("worldSpell"); _prototypeManager.RegisterIgnore("entitySpell"); diff --git a/Content.IntegrationTests/PoolManager.Cvars.cs b/Content.IntegrationTests/PoolManager.Cvars.cs index b457d4a40b..189e37fa39 100644 --- a/Content.IntegrationTests/PoolManager.Cvars.cs +++ b/Content.IntegrationTests/PoolManager.Cvars.cs @@ -25,7 +25,6 @@ public static partial class PoolManager (CCVars.ArrivalsShuttles.Name, "false"), (CCVars.EmergencyShuttleEnabled.Name, "false"), (CCVars.ProcgenPreload.Name, "false"), - (CCVars.WorldgenEnabled.Name, "false"), (CCVars.GatewayGeneratorEnabled.Name, "false"), (CCVars.GameDummyTicker.Name, "true"), (CCVars.GameLobbyEnabled.Name, "false"), diff --git a/Content.IntegrationTests/Tests/EntityTest.cs b/Content.IntegrationTests/Tests/EntityTest.cs index 59dfec1060..13293b7d46 100644 --- a/Content.IntegrationTests/Tests/EntityTest.cs +++ b/Content.IntegrationTests/Tests/EntityTest.cs @@ -385,9 +385,6 @@ namespace Content.IntegrationTests.Tests "StationData", // errors when removed mid-round "StationJobs", "Actor", // We aren't testing actor components, those need their player session set. - "BlobFloorPlanBuilder", // Implodes if unconfigured. - "DebrisFeaturePlacerController", // Above. - "LoadedChunk", // Worldgen chunk loading malding. "BiomeSelection", // Whaddya know, requires config. "ActivatableUI", // Requires enum key }; diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index 1c6d940e20..6a92b08b1e 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -23,7 +23,6 @@ using Content.Server.Preferences.Managers; using Content.Server.ServerInfo; using Content.Server.ServerUpdates; using Content.Server.Voting.Managers; -using Content.Server.Worldgen.Tools; using Content.Shared.Administration.Logs; using Content.Shared.Administration.Managers; using Content.Shared.Chat; @@ -66,7 +65,6 @@ internal static class ServerContentIoC deps.Register(); deps.Register(); deps.Register(); - deps.Register(); deps.Register(); deps.Register(); deps.Register(); diff --git a/Content.Server/Worldgen/Components/BiomeSelectionComponent.cs b/Content.Server/Worldgen/Components/BiomeSelectionComponent.cs deleted file mode 100644 index 08571cd588..0000000000 --- a/Content.Server/Worldgen/Components/BiomeSelectionComponent.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Content.Server.Worldgen.Prototypes; -using Content.Server.Worldgen.Systems.Biomes; -using Robust.Shared.Prototypes; - -namespace Content.Server.Worldgen.Components; - -/// -/// This is used for selecting the biome(s) to be used during world generation. -/// -[RegisterComponent] -[Access(typeof(BiomeSelectionSystem))] -public sealed partial class BiomeSelectionComponent : Component -{ - /// - /// The list of biomes available to this selector. - /// - /// This is always sorted by priority after ComponentStartup. - [DataField(required: true)] - public List> Biomes = new(); -} - diff --git a/Content.Server/Worldgen/Components/Carvers/NoiseRangeCarverComponent.cs b/Content.Server/Worldgen/Components/Carvers/NoiseRangeCarverComponent.cs deleted file mode 100644 index 28724d20a4..0000000000 --- a/Content.Server/Worldgen/Components/Carvers/NoiseRangeCarverComponent.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Numerics; -using Content.Server.Worldgen.Prototypes; -using Content.Server.Worldgen.Systems.Carvers; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Worldgen.Components.Carvers; - -/// -/// This is used for carving out empty space in the game world, providing byways through the debris field. -/// -[RegisterComponent] -[Access(typeof(NoiseRangeCarverSystem))] -public sealed partial class NoiseRangeCarverComponent : Component -{ - /// - /// The noise channel to use as a density controller. - /// - /// This noise channel should be mapped to exactly the range [0, 1] unless you want a lot of warnings in the log. - [DataField("noiseChannel", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string NoiseChannel { get; private set; } = default!; - - /// - /// The index of ranges in which to cut debris generation. - /// - [DataField("ranges", required: true)] - public List Ranges { get; private set; } = default!; -} - diff --git a/Content.Server/Worldgen/Components/Debris/BlobFloorPlanBuilderComponent.cs b/Content.Server/Worldgen/Components/Debris/BlobFloorPlanBuilderComponent.cs deleted file mode 100644 index a1317ae2ed..0000000000 --- a/Content.Server/Worldgen/Components/Debris/BlobFloorPlanBuilderComponent.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Content.Server.Worldgen.Systems.Debris; -using Content.Shared.Maps; -using Robust.Shared.Prototypes; - -namespace Content.Server.Worldgen.Components.Debris; - -/// -/// This is used for constructing asteroid debris. -/// -[RegisterComponent] -[Access(typeof(BlobFloorPlanBuilderSystem))] -public sealed partial class BlobFloorPlanBuilderComponent : Component -{ - /// - /// The probability that placing a floor tile will add up to three-four neighboring tiles as well. - /// - [DataField("blobDrawProb")] public float BlobDrawProb; - - /// - /// The maximum radius for the structure. - /// - [DataField("radius", required: true)] public float Radius; - - /// - /// The tiles to be used for the floor plan. - /// - [DataField(required: true)] - public List> FloorTileset { get; private set; } = default!; - - /// - /// The number of floor tiles to place when drawing the asteroid layout. - /// - [DataField("floorPlacements", required: true)] - public int FloorPlacements { get; private set; } -} - diff --git a/Content.Server/Worldgen/Components/Debris/DebrisFeaturePlacerControllerComponent.cs b/Content.Server/Worldgen/Components/Debris/DebrisFeaturePlacerControllerComponent.cs deleted file mode 100644 index ae61f0581e..0000000000 --- a/Content.Server/Worldgen/Components/Debris/DebrisFeaturePlacerControllerComponent.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Numerics; -using Content.Server.Worldgen.Prototypes; -using Content.Server.Worldgen.Systems.Debris; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Worldgen.Components.Debris; - -/// -/// This is used for controlling the debris feature placer. -/// -[RegisterComponent] -[Access(typeof(DebrisFeaturePlacerSystem))] -public sealed partial class DebrisFeaturePlacerControllerComponent : Component -{ - /// - /// Whether or not to clip debris that would spawn at a location that has a density of zero. - /// - [DataField("densityClip")] public bool DensityClip = true; - - /// - /// Whether or not entities are already spawned. - /// - public bool DoSpawns = true; - - [DataField("ownedDebris")] public Dictionary OwnedDebris = new(); - - /// - /// The chance spawning a piece of debris will just be cancelled randomly. - /// - [DataField("randomCancelChance")] public float RandomCancellationChance = 0.1f; - - /// - /// Radius in which there should be no objects for debris to spawn. - /// - [DataField("safetyZoneRadius")] public float SafetyZoneRadius = 16.0f; - - /// - /// The noise channel to use as a density controller. - /// - [DataField("densityNoiseChannel", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string DensityNoiseChannel { get; private set; } = default!; -} - diff --git a/Content.Server/Worldgen/Components/Debris/NoiseDrivenDebrisSelectorComponent.cs b/Content.Server/Worldgen/Components/Debris/NoiseDrivenDebrisSelectorComponent.cs deleted file mode 100644 index af4ef7f1cf..0000000000 --- a/Content.Server/Worldgen/Components/Debris/NoiseDrivenDebrisSelectorComponent.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Content.Server.Worldgen.Prototypes; -using Content.Server.Worldgen.Systems.Debris; -using Content.Server.Worldgen.Tools; -using Content.Shared.Storage; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Worldgen.Components.Debris; - -/// -/// This is used for selecting debris with a probability determined by a noise channel. -/// Takes priority over SimpleDebrisSelectorComponent and should likely be used in combination. -/// -[RegisterComponent] -[Access(typeof(NoiseDrivenDebrisSelectorSystem))] -public sealed partial class NoiseDrivenDebrisSelectorComponent : Component -{ - private EntitySpawnCollectionCache? _cache; - - /// - /// The prototype-facing debris table entries. - /// - [DataField("debrisTable", required: true)] - private List _entries = default!; - - /// - /// The debris entity spawn collection. - /// - public EntitySpawnCollectionCache CachedDebrisTable - { - get - { - _cache ??= new EntitySpawnCollectionCache(_entries); - return _cache; - } - } - - /// - /// The noise channel to use as a density controller. - /// - /// This noise channel should be mapped to exactly the range [0, 1] unless you want a lot of warnings in the log. - [DataField("noiseChannel", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string NoiseChannel { get; private set; } = default!; -} - diff --git a/Content.Server/Worldgen/Components/Debris/OwnedDebrisComponent.cs b/Content.Server/Worldgen/Components/Debris/OwnedDebrisComponent.cs deleted file mode 100644 index b211277997..0000000000 --- a/Content.Server/Worldgen/Components/Debris/OwnedDebrisComponent.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Numerics; -using Content.Server.Worldgen.Systems.Debris; - -namespace Content.Server.Worldgen.Components.Debris; - -/// -/// This is used for attaching a piece of debris to it's owning controller. -/// Mostly just syncs deletion. -/// -[RegisterComponent] -[Access(typeof(DebrisFeaturePlacerSystem))] -public sealed partial class OwnedDebrisComponent : Component -{ - /// - /// The last location in the controller's internal structure for this debris. - /// - [DataField("lastKey")] public Vector2 LastKey; - - /// - /// The DebrisFeaturePlacerController-having entity that owns this. - /// - [DataField("owningController")] public EntityUid OwningController; -} - diff --git a/Content.Server/Worldgen/Components/Debris/SimpleDebrisSelectorComponent.cs b/Content.Server/Worldgen/Components/Debris/SimpleDebrisSelectorComponent.cs deleted file mode 100644 index 5db9bad925..0000000000 --- a/Content.Server/Worldgen/Components/Debris/SimpleDebrisSelectorComponent.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Content.Server.Worldgen.Systems.Debris; -using Content.Server.Worldgen.Tools; -using Content.Shared.Storage; - -namespace Content.Server.Worldgen.Components.Debris; - -/// -/// This is used for a very simple debris selection for simple biomes. Just uses a spawn table. -/// -[RegisterComponent] -[Access(typeof(DebrisFeaturePlacerSystem))] -public sealed partial class SimpleDebrisSelectorComponent : Component -{ - private EntitySpawnCollectionCache? _cache; - - /// - /// The prototype-facing debris table entries. - /// - [DataField("debrisTable", required: true)] - private List _entries = default!; - - /// - /// The debris entity spawn collection. - /// - public EntitySpawnCollectionCache CachedDebrisTable - { - get - { - _cache ??= new EntitySpawnCollectionCache(_entries); - return _cache; - } - } -} - diff --git a/Content.Server/Worldgen/Components/Debris/SimpleFloorPlanPopulatorComponent.cs b/Content.Server/Worldgen/Components/Debris/SimpleFloorPlanPopulatorComponent.cs deleted file mode 100644 index 4865773bf3..0000000000 --- a/Content.Server/Worldgen/Components/Debris/SimpleFloorPlanPopulatorComponent.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Linq; -using Content.Server.Worldgen.Systems.Debris; -using Content.Server.Worldgen.Tools; -using Content.Shared.Maps; -using Content.Shared.Storage; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; - -namespace Content.Server.Worldgen.Components.Debris; - -/// -/// This is used for populating a grid with random entities automatically. -/// -[RegisterComponent] -[Access(typeof(SimpleFloorPlanPopulatorSystem))] -public sealed partial class SimpleFloorPlanPopulatorComponent : Component -{ - private Dictionary? _caches; - - /// - /// The prototype facing floor plan populator entries. - /// - [DataField("entries", required: true, - customTypeSerializer: typeof(PrototypeIdDictionarySerializer, ContentTileDefinition>))] - private Dictionary> _entries = default!; - - /// - /// The spawn collections used to place entities on different tile types. - /// - [ViewVariables] - public Dictionary Caches - { - get - { - if (_caches is null) - { - _caches = _entries - .Select(x => - new KeyValuePair(x.Key, - new EntitySpawnCollectionCache(x.Value))) - .ToDictionary(x => x.Key, x => x.Value); - } - - return _caches; - } - } -} - diff --git a/Content.Server/Worldgen/Components/LoadedChunkComponent.cs b/Content.Server/Worldgen/Components/LoadedChunkComponent.cs deleted file mode 100644 index d2743ad4ab..0000000000 --- a/Content.Server/Worldgen/Components/LoadedChunkComponent.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Content.Server.Worldgen.Systems; - -namespace Content.Server.Worldgen.Components; - -/// -/// This is used for marking a chunk as loaded. -/// -[RegisterComponent] -[Access(typeof(WorldControllerSystem))] -public sealed partial class LoadedChunkComponent : Component -{ - /// - /// The current list of entities loading this chunk. - /// - [ViewVariables] public List? Loaders = null; -} - diff --git a/Content.Server/Worldgen/Components/LocalityLoaderComponent.cs b/Content.Server/Worldgen/Components/LocalityLoaderComponent.cs deleted file mode 100644 index 1d37ab34c9..0000000000 --- a/Content.Server/Worldgen/Components/LocalityLoaderComponent.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Content.Server.Worldgen.Systems; - -namespace Content.Server.Worldgen.Components; - -/// -/// This is used for sending a signal to the entity it's on to load contents whenever a loader gets close enough. -/// Does not support unloading. -/// -[RegisterComponent] -[Access(typeof(LocalityLoaderSystem))] -public sealed partial class LocalityLoaderComponent : Component -{ - /// - /// The maximum distance an entity can be from the loader for it to not load. - /// Once a loader is closer than this, the event is fired and this component removed. - /// - [DataField("loadingDistance")] public int LoadingDistance = 32; -} - diff --git a/Content.Server/Worldgen/Components/NoiseIndexComponent.cs b/Content.Server/Worldgen/Components/NoiseIndexComponent.cs deleted file mode 100644 index 06d84d2f85..0000000000 --- a/Content.Server/Worldgen/Components/NoiseIndexComponent.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Content.Server.Worldgen.Prototypes; -using Content.Server.Worldgen.Systems; - -namespace Content.Server.Worldgen.Components; - -/// -/// This is used for containing configured noise generators. -/// -[RegisterComponent] -[Access(typeof(NoiseIndexSystem))] -public sealed partial class NoiseIndexComponent : Component -{ - /// - /// An index of generators, to avoid having to recreate them every time a noise channel is used. - /// Keyed by noise generator prototype ID. - /// - [Access(typeof(NoiseIndexSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.None)] - public Dictionary Generators { get; } = new(); -} - diff --git a/Content.Server/Worldgen/Components/WorldChunkComponent.cs b/Content.Server/Worldgen/Components/WorldChunkComponent.cs deleted file mode 100644 index 3a91c00756..0000000000 --- a/Content.Server/Worldgen/Components/WorldChunkComponent.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Content.Server.Worldgen.Systems; - -namespace Content.Server.Worldgen.Components; - -/// -/// This is used for marking an entity as being a world chunk. -/// -[RegisterComponent] -[Access(typeof(WorldControllerSystem))] -public sealed partial class WorldChunkComponent : Component -{ - /// - /// The coordinates of the chunk, in chunk space. - /// - [DataField("coordinates")] public Vector2i Coordinates; - - /// - /// The map this chunk belongs to. - /// - [DataField("map")] public EntityUid Map; -} - diff --git a/Content.Server/Worldgen/Components/WorldControllerComponent.cs b/Content.Server/Worldgen/Components/WorldControllerComponent.cs deleted file mode 100644 index 63580e7541..0000000000 --- a/Content.Server/Worldgen/Components/WorldControllerComponent.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Content.Server.Worldgen.Systems; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Worldgen.Components; - -/// -/// This is used for controlling overall world loading, containing an index of all chunks in the map. -/// -[RegisterComponent] -[Access(typeof(WorldControllerSystem))] -public sealed partial class WorldControllerComponent : Component -{ - /// - /// The prototype to use for chunks on this world map. - /// - [DataField("chunkProto", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string ChunkProto = "WorldChunk"; - - /// - /// An index of chunks owned by the controller. - /// - [DataField("chunks")] public Dictionary Chunks = new(); -} - diff --git a/Content.Server/Worldgen/Components/WorldLoaderComponent.cs b/Content.Server/Worldgen/Components/WorldLoaderComponent.cs deleted file mode 100644 index e6bb7781e9..0000000000 --- a/Content.Server/Worldgen/Components/WorldLoaderComponent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Content.Server.Worldgen.Systems; - -namespace Content.Server.Worldgen.Components; - -/// -/// This is used for allowing some objects to load the game world. -/// -[RegisterComponent] -[Access(typeof(WorldControllerSystem))] -public sealed partial class WorldLoaderComponent : Component -{ - /// - /// The radius in which the loader loads the world. - /// - [ViewVariables(VVAccess.ReadWrite)] [DataField("radius")] - public int Radius = 128; -} - diff --git a/Content.Server/Worldgen/GridPointsNearEnumerator.cs b/Content.Server/Worldgen/GridPointsNearEnumerator.cs deleted file mode 100644 index 24b710626b..0000000000 --- a/Content.Server/Worldgen/GridPointsNearEnumerator.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; - -namespace Content.Server.Worldgen; - -/// -/// A struct enumerator of points on a grid within the given radius. -/// -public struct GridPointsNearEnumerator -{ - private readonly int _radius; - private readonly Vector2i _center; - private int _x; - private int _y; - - /// - /// Initializes a new enumerator with the given center and radius. - /// - public GridPointsNearEnumerator(Vector2i center, int radius) - { - _radius = radius; - _center = center; - _x = -_radius; - _y = -_radius; - } - - /// - /// Gets the next point in the enumeration. - /// - /// The computed point, if any - /// Success - [Pure] - public bool MoveNext([NotNullWhen(true)] out Vector2i? chunk) - { - while (!(_x * _x + _y * _y <= _radius * _radius)) - { - if (_y > _radius) - { - chunk = null; - return false; - } - - if (_x > _radius) - { - _x = -_radius; - _y++; - } - else - { - _x++; - } - } - - chunk = _center + new Vector2i(_x, _y); - _x++; - return true; - } -} - diff --git a/Content.Server/Worldgen/Prototypes/BiomePrototype.cs b/Content.Server/Worldgen/Prototypes/BiomePrototype.cs deleted file mode 100644 index 75e4670e88..0000000000 --- a/Content.Server/Worldgen/Prototypes/BiomePrototype.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Numerics; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.Manager; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; - -namespace Content.Server.Worldgen.Prototypes; - -/// -/// This is a prototype for biome selection, allowing the component list of a chunk to be amended based on the output -/// of noise channels at that location. -/// -[Prototype("spaceBiome")] -public sealed partial class BiomePrototype : IPrototype, IInheritingPrototype -{ - /// - [ParentDataField(typeof(AbstractPrototypeIdArraySerializer))] - public string[]? Parents { get; private set; } - - /// - [NeverPushInheritance] - [AbstractDataField] - public bool Abstract { get; private set; } - - /// - [IdDataField] - public string ID { get; private set; } = default!; - - /// - /// The valid ranges of noise values under which this biome can be picked. - /// - [DataField("noiseRanges", required: true)] - public Dictionary> NoiseRanges = default!; - - /// - /// Higher priority biomes get picked before lower priority ones. - /// - [DataField("priority", required: true)] - public int Priority { get; private set; } - - /// - /// The components that get added to the target map. - /// - [DataField("chunkComponents")] - [AlwaysPushInheritance] - public ComponentRegistry ChunkComponents = new(); - - //TODO: Get someone to make this a method on componentregistry that does it Correctly. - /// - /// Applies the worldgen config to the given target (presumably a map.) - /// - public void Apply(EntityUid target, ISerializationManager serialization, IEntityManager entityManager) - { - // Add all components required by the prototype. Engine update for this whenst. - foreach (var data in ChunkComponents.Values) - { - var comp = (Component) serialization.CreateCopy(data.Component, notNullableOverride: true); - entityManager.AddComponent(target, comp); - } - } -} - diff --git a/Content.Server/Worldgen/Prototypes/NoiseChannelPrototype.cs b/Content.Server/Worldgen/Prototypes/NoiseChannelPrototype.cs deleted file mode 100644 index 41d89cbaa7..0000000000 --- a/Content.Server/Worldgen/Prototypes/NoiseChannelPrototype.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System.Numerics; -using Robust.Shared.Noise; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; - -namespace Content.Server.Worldgen.Prototypes; - -/// -/// This is a config for noise channels, used by worldgen. -/// -[Virtual] -public class NoiseChannelConfig -{ - /// - /// The noise type used by the noise generator. - /// - [DataField("noiseType")] - public FastNoiseLite.NoiseType NoiseType { get; private set; } = FastNoiseLite.NoiseType.Cellular; - - /// - /// The fractal type used by the noise generator. - /// - [DataField("fractalType")] - public FastNoiseLite.FractalType FractalType { get; private set; } = FastNoiseLite.FractalType.FBm; - - /// - /// Multiplied by pi in code when used. - /// - [DataField("fractalLacunarityByPi")] - public float FractalLacunarityByPi { get; private set; } = 2.0f / 3.0f; - - /// - /// Ranges of values that get clamped down to the "clipped" value. - /// - [DataField("clippingRanges")] - public List ClippingRanges { get; private set; } = new(); - - /// - /// The value clipped chunks are set to. - /// - [DataField("clippedValue")] - public float ClippedValue { get; private set; } - - /// - /// A value the output is multiplied by. - /// - [DataField("outputMultiplier")] - public float OutputMultiplier { get; private set; } = 1.0f; - - /// - /// A value the input is multiplied by. - /// - [DataField("inputMultiplier")] - public float InputMultiplier { get; private set; } = 1.0f; - - /// - /// Remaps the output of the noise function from the range (-1, 1) to (0, 1). This is done before all other output - /// transformations. - /// - [DataField("remapTo0Through1")] - public bool RemapTo0Through1 { get; private set; } - - /// - /// For when the transformation you need is too complex to describe in YAML. - /// - [DataField("noisePostProcess")] - public NoisePostProcess? NoisePostProcess { get; private set; } - - /// - /// For when you need a complex transformation of the input coordinates. - /// - [DataField("noiseCoordinateProcess")] - public NoiseCoordinateProcess? NoiseCoordinateProcess { get; private set; } - - /// - /// The "center" of the range of values. Or the minimum if mapped 0 through 1. - /// - [DataField("minimum")] - public float Minimum { get; private set; } -} - -[Prototype] -public sealed partial class NoiseChannelPrototype : NoiseChannelConfig, IPrototype, IInheritingPrototype -{ - /// - [ParentDataField(typeof(AbstractPrototypeIdArraySerializer))] - public string[]? Parents { get; private set; } - - /// - [NeverPushInheritance] - [AbstractDataField] - public bool Abstract { get; private set; } - - /// - [IdDataField] - public string ID { get; private set; } = default!; -} - -/// -/// A wrapper around FastNoise's noise generation, using noise channel configs. -/// -public struct NoiseGenerator -{ - private readonly NoiseChannelConfig _config; - private readonly FastNoiseLite _noise; - - /// - /// Produces a new noise generator from the given channel config and rng seed. - /// - public NoiseGenerator(NoiseChannelConfig config, int seed) - { - _config = config; - _noise = new FastNoiseLite(); - _noise.SetSeed(seed); - _noise.SetNoiseType(_config.NoiseType); - _noise.SetFractalType(_config.FractalType); - _noise.SetFractalLacunarity(_config.FractalLacunarityByPi * MathF.PI); - } - - /// - /// Evaluates the noise generator at the provided coordinates. - /// - /// Coordinates to use as input - /// Computed noise value - public float Evaluate(Vector2 coords) - { - var finCoords = coords * _config.InputMultiplier; - - if (_config.NoiseCoordinateProcess is not null) - finCoords = _config.NoiseCoordinateProcess.Process(finCoords); - - var value = _noise.GetNoise(finCoords.X, finCoords.Y); - - if (_config.RemapTo0Through1) - value = (value + 1.0f) / 2.0f; - - foreach (var range in _config.ClippingRanges) - { - if (range.X < value && value < range.Y) - { - value = _config.ClippedValue; - break; - } - } - - if (_config.NoisePostProcess is not null) - value = _config.NoisePostProcess.Process(value); - value *= _config.OutputMultiplier; - return value + _config.Minimum; - } -} - -/// -/// A processing class that adjusts the input coordinate space to a noise channel. -/// -[ImplicitDataDefinitionForInheritors] -public abstract partial class NoiseCoordinateProcess -{ - public abstract Vector2 Process(Vector2 inp); -} - -/// -/// A processing class that adjusts the final result of the noise channel. -/// -[ImplicitDataDefinitionForInheritors] -public abstract partial class NoisePostProcess -{ - public abstract float Process(float inp); -} - diff --git a/Content.Server/Worldgen/Prototypes/WorldgenConfigPrototype.cs b/Content.Server/Worldgen/Prototypes/WorldgenConfigPrototype.cs deleted file mode 100644 index c9107fa2bd..0000000000 --- a/Content.Server/Worldgen/Prototypes/WorldgenConfigPrototype.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.Manager; - -namespace Content.Server.Worldgen.Prototypes; - -/// -/// This is a prototype for controlling overall world generation. -/// The components included are applied to the map that world generation is configured on. -/// -[Prototype] -public sealed partial class WorldgenConfigPrototype : IPrototype -{ - /// - [IdDataField] - public string ID { get; private set; } = default!; - - /// - /// The components that get added to the target map. - /// - [DataField("components", required: true)] - public ComponentRegistry Components { get; private set; } = default!; - - //TODO: Get someone to make this a method on componentregistry that does it Correctly. - /// - /// Applies the worldgen config to the given target (presumably a map.) - /// - public void Apply(EntityUid target, ISerializationManager serialization, IEntityManager entityManager) - { - // Add all components required by the prototype. Engine update for this whenst. - foreach (var data in Components.Values) - { - var comp = (Component) serialization.CreateCopy(data.Component, notNullableOverride: true); - entityManager.AddComponent(target, comp); - } - } -} - diff --git a/Content.Server/Worldgen/Systems/BaseWorldSystem.cs b/Content.Server/Worldgen/Systems/BaseWorldSystem.cs deleted file mode 100644 index 7a9e74375c..0000000000 --- a/Content.Server/Worldgen/Systems/BaseWorldSystem.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Numerics; -using Content.Server.Worldgen.Components; -using JetBrains.Annotations; - -namespace Content.Server.Worldgen.Systems; - -/// -/// This provides some additional functions for world generation systems. -/// Exists primarily for convenience and to avoid code duplication. -/// -[PublicAPI] -public abstract class BaseWorldSystem : EntitySystem -{ - [Dependency] private readonly WorldControllerSystem _worldController = default!; - [Dependency] private readonly SharedTransformSystem _transformSystem = default!; - - /// - /// Gets a chunk's coordinates in chunk space as an integer value. - /// - /// - /// - /// Chunk space coordinates - [Pure] - public Vector2i GetChunkCoords(EntityUid ent, TransformComponent? xform = null) - { - if (!Resolve(ent, ref xform)) - throw new Exception("Failed to resolve transform, somehow."); - - return WorldGen.WorldToChunkCoords(_transformSystem.GetWorldPosition(xform)).Floored(); - } - - /// - /// Gets a chunk's coordinates in chunk space as a floating point value. - /// - /// - /// - /// Chunk space coordinates - [Pure] - public Vector2 GetFloatingChunkCoords(EntityUid ent, TransformComponent? xform = null) - { - if (!Resolve(ent, ref xform)) - throw new Exception("Failed to resolve transform, somehow."); - - return WorldGen.WorldToChunkCoords(_transformSystem.GetWorldPosition(xform)); - } - - /// - /// Attempts to get a chunk, creating it if it doesn't exist. - /// - /// Chunk coordinates to get the chunk entity for. - /// Map the chunk is in. - /// The controller this chunk belongs to. - /// A chunk, if available. - [Pure] - public EntityUid? GetOrCreateChunk(Vector2i chunk, EntityUid map, WorldControllerComponent? controller = null) - { - return _worldController.GetOrCreateChunk(chunk, map, controller); - } -} - diff --git a/Content.Server/Worldgen/Systems/Biomes/BiomeSelectionSystem.cs b/Content.Server/Worldgen/Systems/Biomes/BiomeSelectionSystem.cs deleted file mode 100644 index 1827f6deed..0000000000 --- a/Content.Server/Worldgen/Systems/Biomes/BiomeSelectionSystem.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Linq; -using Content.Server.Worldgen.Components; -using Content.Server.Worldgen.Prototypes; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.Manager; - -namespace Content.Server.Worldgen.Systems.Biomes; - -/// -/// This handles biome selection, evaluating which biome to apply to a chunk based on noise channels. -/// -public sealed class BiomeSelectionSystem : BaseWorldSystem -{ - [Dependency] private readonly NoiseIndexSystem _noiseIdx = default!; - [Dependency] private readonly IPrototypeManager _proto = default!; - [Dependency] private readonly ISerializationManager _ser = default!; - - /// - public override void Initialize() - { - SubscribeLocalEvent(OnBiomeSelectionStartup); - SubscribeLocalEvent(OnWorldChunkAdded); - } - - private void OnWorldChunkAdded(EntityUid uid, BiomeSelectionComponent component, ref WorldChunkAddedEvent args) - { - var coords = args.Coords; - foreach (var biomeId in component.Biomes) - { - var biome = _proto.Index(biomeId); - if (!CheckBiomeValidity(args.Chunk, biome, coords)) - continue; - - biome.Apply(args.Chunk, _ser, EntityManager); - return; - } - - Log.Error($"Biome selection ran out of biomes to select? See biomes list: {component.Biomes}"); - } - - private void OnBiomeSelectionStartup(EntityUid uid, BiomeSelectionComponent component, ComponentStartup args) - { - // surely this can't be THAAAAAAAAAAAAAAAT bad right???? - var sorted = component.Biomes - .Select(x => (Id: x, _proto.Index(x).Priority)) - .OrderByDescending(x => x.Priority) - .Select(x => x.Id) - .ToList(); - - component.Biomes = sorted; // my hopes and dreams rely on this being pre-sorted by priority. - } - - private bool CheckBiomeValidity(EntityUid chunk, BiomePrototype biome, Vector2i coords) - { - foreach (var (noise, ranges) in biome.NoiseRanges) - { - var value = _noiseIdx.Evaluate(chunk, noise, coords); - var anyValid = false; - foreach (var range in ranges) - { - if (range.X < value && value < range.Y) - { - anyValid = true; - break; - } - } - - if (!anyValid) - return false; - } - - return true; - } -} - diff --git a/Content.Server/Worldgen/Systems/Carvers/NoiseRangeCarverSystem.cs b/Content.Server/Worldgen/Systems/Carvers/NoiseRangeCarverSystem.cs deleted file mode 100644 index 1207d6f157..0000000000 --- a/Content.Server/Worldgen/Systems/Carvers/NoiseRangeCarverSystem.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Content.Server.Worldgen.Components.Carvers; -using Content.Server.Worldgen.Systems.Debris; - -namespace Content.Server.Worldgen.Systems.Carvers; - -/// -/// This handles carving out holes in world generation according to a noise channel. -/// -public sealed class NoiseRangeCarverSystem : EntitySystem -{ - [Dependency] private readonly NoiseIndexSystem _index = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - - /// - public override void Initialize() - { - SubscribeLocalEvent(OnPrePlaceDebris); - } - - private void OnPrePlaceDebris(EntityUid uid, NoiseRangeCarverComponent component, - ref PrePlaceDebrisFeatureEvent args) - { - var coords = WorldGen.WorldToChunkCoords(_transform.ToMapCoordinates(args.Coords).Position); - var val = _index.Evaluate(uid, component.NoiseChannel, coords); - - foreach (var (low, high) in component.Ranges) - { - if (low > val || high < val) - continue; - - args.Handled = true; - return; - } - } -} - diff --git a/Content.Server/Worldgen/Systems/Debris/BlobFloorPlanBuilderSystem.cs b/Content.Server/Worldgen/Systems/Debris/BlobFloorPlanBuilderSystem.cs deleted file mode 100644 index ba0a3a7132..0000000000 --- a/Content.Server/Worldgen/Systems/Debris/BlobFloorPlanBuilderSystem.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System.Linq; -using Content.Server.Worldgen.Components.Debris; -using Content.Shared.Maps; -using Robust.Shared.Map; -using Robust.Shared.Map.Components; -using Robust.Shared.Random; - -namespace Content.Server.Worldgen.Systems.Debris; - -/// -/// This handles building the floor plans for "blobby" debris. -/// -public sealed class BlobFloorPlanBuilderSystem : BaseWorldSystem -{ - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly ITileDefinitionManager _tileDefinition = default!; - [Dependency] private readonly TileSystem _tiles = default!; - [Dependency] private readonly SharedMapSystem _map = default!; - - /// - public override void Initialize() - { - SubscribeLocalEvent(OnBlobFloorPlanBuilderStartup); - } - - private void OnBlobFloorPlanBuilderStartup(EntityUid uid, BlobFloorPlanBuilderComponent component, - ComponentStartup args) - { - PlaceFloorplanTiles(uid, component, Comp(uid)); - } - - private void PlaceFloorplanTiles(EntityUid gridUid, BlobFloorPlanBuilderComponent comp, MapGridComponent grid) - { - // NO MORE THAN TWO ALLOCATIONS THANK YOU VERY MUCH. - // TODO: Just put these on a field instead then? - // Also the end of the method has a big LINQ which is gonna blow this out the water. - var spawnPoints = new HashSet(comp.FloorPlacements * 6); - var taken = new Dictionary(comp.FloorPlacements * 5); - - void PlaceTile(Vector2i point) - { - // Assume we already know that the spawn point is safe. - spawnPoints.Remove(point); - var north = point.Offset(Direction.North); - var south = point.Offset(Direction.South); - var east = point.Offset(Direction.East); - var west = point.Offset(Direction.West); - var radsq = Math.Pow(comp.Radius, - 2); // I'd put this outside but i'm not 100% certain caching it between calls is a gain. - - // The math done is essentially a fancy way of comparing the distance from 0,0 to the radius, - // and skipping the sqrt normally needed for dist. - if (!taken.ContainsKey(north) && Math.Pow(north.X, 2) + Math.Pow(north.Y, 2) <= radsq) - spawnPoints.Add(north); - if (!taken.ContainsKey(south) && Math.Pow(south.X, 2) + Math.Pow(south.Y, 2) <= radsq) - spawnPoints.Add(south); - if (!taken.ContainsKey(east) && Math.Pow(east.X, 2) + Math.Pow(east.Y, 2) <= radsq) - spawnPoints.Add(east); - if (!taken.ContainsKey(west) && Math.Pow(west.X, 2) + Math.Pow(west.Y, 2) <= radsq) - spawnPoints.Add(west); - - var tileDef = _tileDefinition[_random.Pick(comp.FloorTileset)]; - taken.Add(point, new Tile(tileDef.TileId, 0, _tiles.PickVariant((ContentTileDefinition) tileDef))); - } - - PlaceTile(Vector2i.Zero); - - for (var i = 0; i < comp.FloorPlacements; i++) - { - var point = _random.Pick(spawnPoints); - PlaceTile(point); - - if (comp.BlobDrawProb > 0.0f) - { - if (!taken.ContainsKey(point.Offset(Direction.North)) && _random.Prob(comp.BlobDrawProb)) - PlaceTile(point.Offset(Direction.North)); - if (!taken.ContainsKey(point.Offset(Direction.South)) && _random.Prob(comp.BlobDrawProb)) - PlaceTile(point.Offset(Direction.South)); - if (!taken.ContainsKey(point.Offset(Direction.East)) && _random.Prob(comp.BlobDrawProb)) - PlaceTile(point.Offset(Direction.East)); - if (!taken.ContainsKey(point.Offset(Direction.West)) && _random.Prob(comp.BlobDrawProb)) - PlaceTile(point.Offset(Direction.West)); - } - } - - _map.SetTiles(gridUid, grid, taken.Select(x => (x.Key, x.Value)).ToList()); - } -} - diff --git a/Content.Server/Worldgen/Systems/Debris/DebrisFeaturePlacerSystem.cs b/Content.Server/Worldgen/Systems/Debris/DebrisFeaturePlacerSystem.cs deleted file mode 100644 index b609f7e1f7..0000000000 --- a/Content.Server/Worldgen/Systems/Debris/DebrisFeaturePlacerSystem.cs +++ /dev/null @@ -1,278 +0,0 @@ -using System.Linq; -using System.Numerics; -using Content.Server.Worldgen.Components; -using Content.Server.Worldgen.Components.Debris; -using Content.Server.Worldgen.Tools; -using JetBrains.Annotations; -using Robust.Server.GameObjects; -using Robust.Shared.Map; -using Robust.Shared.Map.Components; -using Robust.Shared.Random; -using Robust.Shared.Utility; - -namespace Content.Server.Worldgen.Systems.Debris; - -/// -/// This handles placing debris within the world evenly with rng, primarily for structures like asteroid fields. -/// -public sealed class DebrisFeaturePlacerSystem : BaseWorldSystem -{ - [Dependency] private readonly NoiseIndexSystem _noiseIndex = default!; - [Dependency] private readonly PoissonDiskSampler _sampler = default!; - [Dependency] private readonly TransformSystem _xformSys = default!; - [Dependency] private readonly ILogManager _logManager = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IRobustRandom _random = default!; - - private ISawmill _sawmill = default!; - - private List> _mapGrids = new(); - - /// - public override void Initialize() - { - _sawmill = _logManager.GetSawmill("world.debris.feature_placer"); - SubscribeLocalEvent(OnChunkLoaded); - SubscribeLocalEvent(OnChunkUnloaded); - SubscribeLocalEvent(OnDebrisShutdown); - SubscribeLocalEvent(OnDebrisMove); - SubscribeLocalEvent( - OnTryGetPlacableDebrisEvent); - } - - /// - /// Handles debris moving, and making sure it stays parented to a chunk for loading purposes. - /// - private void OnDebrisMove(EntityUid uid, OwnedDebrisComponent component, ref MoveEvent args) - { - if (!HasComp(component.OwningController)) - return; // Redundant logic, prolly needs it's own handler for your custom system. - - var placer = Comp(component.OwningController); - var xform = args.Component; - var ownerXform = Transform(component.OwningController); - if (xform.MapUid is null || ownerXform.MapUid is null) - return; // not our problem - - if (xform.MapUid != ownerXform.MapUid) - { - _sawmill.Error($"Somehow debris {uid} left it's expected map! Unparenting it to avoid issues."); - RemCompDeferred(uid); - placer.OwnedDebris.Remove(component.LastKey); - return; - } - - placer.OwnedDebris.Remove(component.LastKey); - var newChunk = GetOrCreateChunk(GetChunkCoords(uid), xform.MapUid!.Value); - if (newChunk is null || !TryComp(newChunk, out var newPlacer)) - { - // Whelp. - RemCompDeferred(uid); - return; - } - - newPlacer.OwnedDebris[_xformSys.GetWorldPosition(xform)] = uid; // Change our owner. - component.OwningController = newChunk.Value; - } - - /// - /// Handles debris shutdown/detach. - /// - private void OnDebrisShutdown(EntityUid uid, OwnedDebrisComponent component, ComponentShutdown args) - { - if (!TryComp(component.OwningController, out var placer)) - return; - - placer.OwnedDebris[component.LastKey] = null; - if (Terminating(uid)) - placer.OwnedDebris.Remove(component.LastKey); - } - - /// - /// Queues all debris owned by the placer for garbage collection. - /// - private void OnChunkUnloaded(EntityUid uid, DebrisFeaturePlacerControllerComponent component, - ref WorldChunkUnloadedEvent args) - { - component.DoSpawns = true; - } - - /// - /// Handles providing a debris type to place for SimpleDebrisSelectorComponent. - /// This randomly picks a debris type from the EntitySpawnCollectionCache. - /// - private void OnTryGetPlacableDebrisEvent(EntityUid uid, SimpleDebrisSelectorComponent component, - ref TryGetPlaceableDebrisFeatureEvent args) - { - if (args.DebrisProto is not null) - return; - - var l = new List(1); - component.CachedDebrisTable.GetSpawns(_random, ref l); - - switch (l.Count) - { - case 0: - return; - case > 1: - _sawmill.Warning($"Got more than one possible debris type from {uid}. List: {string.Join(", ", l)}"); - break; - } - - args.DebrisProto = l[0]; - } - - /// - /// Handles loading in debris. This does the following: - /// - Checks if the debris is currently supposed to do spawns, if it isn't, aborts immediately. - /// - Evaluates the density value to be used for placement, if it's zero, aborts. - /// - Generates the points to generate debris at, if and only if they've not been selected already by a prior load. - /// - Does the following in a loop over all generated points: - /// - Raises an event to check if something else wants to intercept debris placement, if the event is handled, - /// continues to the next point without generating anything. - /// - Raises an event to get the debris type that should be used for generation. - /// - Spawns the given debris at the point, adding it to the placer's index. - /// - private void OnChunkLoaded(EntityUid uid, DebrisFeaturePlacerControllerComponent component, - ref WorldChunkLoadedEvent args) - { - if (component.DoSpawns == false) - return; - - component.DoSpawns = false; // Don't repeat yourself if this crashes. - - if (!TryComp(args.Chunk, out var chunk)) - return; - - var chunkMap = chunk.Map; - - if (!TryComp(chunkMap, out var map)) - return; - - var densityChannel = component.DensityNoiseChannel; - var density = _noiseIndex.Evaluate(uid, densityChannel, chunk.Coordinates + new Vector2(0.5f, 0.5f)); - if (density == 0) - return; - - List? points = null; - - // If we've been loaded before, reuse the same coordinates. - if (component.OwnedDebris.Count != 0) - { - //TODO: Remove LINQ. - points = component.OwnedDebris - .Where(x => !Deleted(x.Value)) - .Select(static x => x.Key) - .ToList(); - } - - points ??= GeneratePointsInChunk(args.Chunk, density, chunk.Coordinates, chunkMap); - - var mapId = map.MapId; - - var safetyBounds = Box2.UnitCentered.Enlarged(component.SafetyZoneRadius); - var failures = 0; // Avoid severe log spam. - foreach (var point in points) - { - if (component.OwnedDebris.TryGetValue(point, out var existing)) - { - DebugTools.Assert(Exists(existing)); - continue; - } - - var pointDensity = _noiseIndex.Evaluate(uid, densityChannel, WorldGen.WorldToChunkCoords(point)); - if (pointDensity == 0 && component.DensityClip || _random.Prob(component.RandomCancellationChance)) - continue; - - if (HasCollisions(mapId, safetyBounds.Translated(point))) - continue; - - var coords = new EntityCoordinates(chunkMap, point); - - var preEv = new PrePlaceDebrisFeatureEvent(coords, args.Chunk); - RaiseLocalEvent(uid, ref preEv); - if (uid != args.Chunk) - RaiseLocalEvent(args.Chunk, ref preEv); - - if (preEv.Handled) - continue; - - var debrisFeatureEv = new TryGetPlaceableDebrisFeatureEvent(coords, args.Chunk); - RaiseLocalEvent(uid, ref debrisFeatureEv); - - if (debrisFeatureEv.DebrisProto == null) - { - // Try on the chunk...? - if (uid != args.Chunk) - RaiseLocalEvent(args.Chunk, ref debrisFeatureEv); - - if (debrisFeatureEv.DebrisProto == null) - { - // Nope. - failures++; - continue; - } - } - - var ent = Spawn(debrisFeatureEv.DebrisProto, coords); - component.OwnedDebris.Add(point, ent); - - var owned = EnsureComp(ent); - owned.OwningController = uid; - owned.LastKey = point; - } - - if (failures > 0) - _sawmill.Error($"Failed to place {failures} debris at chunk {args.Chunk}"); - } - - /// - /// Checks to see if the potential spawn point is clear - /// - /// - /// - /// - private bool HasCollisions(MapId mapId, Box2 point) - { - _mapGrids.Clear(); - _mapManager.FindGridsIntersecting(mapId, point, ref _mapGrids); - return _mapGrids.Count > 0; - } - - /// - /// Generates the points to put into a chunk using a poisson disk sampler. - /// - private List GeneratePointsInChunk(EntityUid chunk, float density, Vector2 coords, EntityUid map) - { - var offs = (int) ((WorldGen.ChunkSize - WorldGen.ChunkSize / 8.0f) / 2.0f); - var topLeft = new Vector2(-offs, -offs); - var lowerRight = new Vector2(offs, offs); - var enumerator = _sampler.SampleRectangle(topLeft, lowerRight, density); - var debrisPoints = new List(); - - var realCenter = WorldGen.ChunkToWorldCoordsCentered(coords.Floored()); - - while (enumerator.MoveNext(out var debrisPoint)) - { - debrisPoints.Add(realCenter + debrisPoint.Value); - } - - return debrisPoints; - } -} - -/// -/// Fired directed on the debris feature placer controller and the chunk, ahead of placing a debris piece. -/// -[ByRefEvent] -[PublicAPI] -public record struct PrePlaceDebrisFeatureEvent(EntityCoordinates Coords, EntityUid Chunk, bool Handled = false); - -/// -/// Fired directed on the debris feature placer controller and the chunk, to select which debris piece to place. -/// -[ByRefEvent] -[PublicAPI] -public record struct TryGetPlaceableDebrisFeatureEvent(EntityCoordinates Coords, EntityUid Chunk, - string? DebrisProto = null); - diff --git a/Content.Server/Worldgen/Systems/Debris/NoiseDrivenDebrisSelectorSystem.cs b/Content.Server/Worldgen/Systems/Debris/NoiseDrivenDebrisSelectorSystem.cs deleted file mode 100644 index a02ff81362..0000000000 --- a/Content.Server/Worldgen/Systems/Debris/NoiseDrivenDebrisSelectorSystem.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Content.Server.Worldgen.Components.Debris; -using Robust.Server.GameObjects; -using Robust.Shared.Physics; -using Robust.Shared.Random; - -namespace Content.Server.Worldgen.Systems.Debris; - -/// -/// This handles selecting debris with probability decided by a noise channel. -/// -public sealed class NoiseDrivenDebrisSelectorSystem : BaseWorldSystem -{ - [Dependency] private readonly NoiseIndexSystem _index = default!; - [Dependency] private readonly TransformSystem _xformSys = default!; - [Dependency] private readonly ILogManager _logManager = default!; - [Dependency] private readonly IRobustRandom _random = default!; - - private ISawmill _sawmill = default!; - - /// - public override void Initialize() - { - _sawmill = _logManager.GetSawmill("world.debris.noise_debris_selector"); - // Event is forcibly ordered to always be handled after the simple selector. - SubscribeLocalEvent(OnSelectDebrisKind, - after: new[] {typeof(DebrisFeaturePlacerSystem)}); - } - - private void OnSelectDebrisKind(EntityUid uid, NoiseDrivenDebrisSelectorComponent component, - ref TryGetPlaceableDebrisFeatureEvent args) - { - var coords = WorldGen.WorldToChunkCoords(_xformSys.ToMapCoordinates(args.Coords).Position); - var prob = _index.Evaluate(uid, component.NoiseChannel, coords); - - if (prob is < 0 or > 1) - { - _sawmill.Error( - $"Sampled a probability of {prob}, which is outside the [0, 1] range, at {coords} aka {args.Coords}."); - return; - } - - if (!_random.Prob(prob)) - return; - - var l = new List(1); - component.CachedDebrisTable.GetSpawns(_random, ref l); - - switch (l.Count) - { - case 0: - return; - case > 1: - _sawmill.Warning($"Got more than one possible debris type from {uid}. List: {string.Join(", ", l)}"); - break; - } - - args.DebrisProto = l[0]; - } -} - diff --git a/Content.Server/Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs b/Content.Server/Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs deleted file mode 100644 index dcb7b7fc8f..0000000000 --- a/Content.Server/Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Content.Server.Worldgen.Components.Debris; -using Content.Shared.Maps; -using Robust.Shared.Map; -using Robust.Shared.Map.Components; -using Robust.Shared.Random; - -namespace Content.Server.Worldgen.Systems.Debris; - -/// -/// This handles populating simple structures, simply using a loot table for each tile. -/// -public sealed class SimpleFloorPlanPopulatorSystem : BaseWorldSystem -{ - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly SharedMapSystem _map = default!; - [Dependency] private readonly TurfSystem _turf = default!; - - /// - public override void Initialize() - { - SubscribeLocalEvent(OnFloorPlanBuilt); - } - - private void OnFloorPlanBuilt(EntityUid uid, SimpleFloorPlanPopulatorComponent component, - LocalStructureLoadedEvent args) - { - var placeables = new List(4); - var grid = Comp(uid); - var enumerator = _map.GetAllTilesEnumerator(uid, grid); - while (enumerator.MoveNext(out var tile)) - { - var coords = _map.GridTileToLocal(uid, grid, tile.Value.GridIndices); - var selector = _turf.GetContentTileDefinition(tile.Value).ID; - if (!component.Caches.TryGetValue(selector, out var cache)) - continue; - - placeables.Clear(); - cache.GetSpawns(_random, ref placeables); - - foreach (var proto in placeables) - { - if (proto is null) - continue; - - Spawn(proto, coords); - } - } - } -} - diff --git a/Content.Server/Worldgen/Systems/LocalityLoaderSystem.cs b/Content.Server/Worldgen/Systems/LocalityLoaderSystem.cs deleted file mode 100644 index fb02e8aa0b..0000000000 --- a/Content.Server/Worldgen/Systems/LocalityLoaderSystem.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Content.Server.Worldgen.Components; -using Robust.Server.GameObjects; - -namespace Content.Server.Worldgen.Systems; - -/// -/// This handles loading in objects based on distance from player, using some metadata on chunks. -/// -public sealed class LocalityLoaderSystem : BaseWorldSystem -{ - [Dependency] private readonly TransformSystem _xformSys = default!; - - /// - public override void Update(float frameTime) - { - var e = EntityQueryEnumerator(); - var loadedQuery = GetEntityQuery(); - var xformQuery = GetEntityQuery(); - var controllerQuery = GetEntityQuery(); - - while (e.MoveNext(out var uid, out var loadable, out var xform)) - { - if (!controllerQuery.TryGetComponent(xform.MapUid, out var controller)) - { - RaiseLocalEvent(uid, new LocalStructureLoadedEvent()); - RemCompDeferred(uid); - continue; - } - - var coords = GetChunkCoords(uid, xform); - var done = false; - for (var i = -1; i < 2 && !done; i++) - { - for (var j = -1; j < 2 && !done; j++) - { - var chunk = GetOrCreateChunk(coords + (i, j), xform.MapUid!.Value, controller); - if (!loadedQuery.TryGetComponent(chunk, out var loaded) || loaded.Loaders is null) - continue; - - foreach (var loader in loaded.Loaders) - { - if (!xformQuery.TryGetComponent(loader, out var loaderXform)) - continue; - - if ((_xformSys.GetWorldPosition(loaderXform) - _xformSys.GetWorldPosition(xform)).Length() > loadable.LoadingDistance) - continue; - - RaiseLocalEvent(uid, new LocalStructureLoadedEvent()); - RemCompDeferred(uid); - done = true; - break; - } - } - } - } - } -} - -/// -/// A directed fired on a loadable entity when a local loader enters it's vicinity. -/// -public record struct LocalStructureLoadedEvent; - diff --git a/Content.Server/Worldgen/Systems/NoiseIndexSystem.cs b/Content.Server/Worldgen/Systems/NoiseIndexSystem.cs deleted file mode 100644 index 5a7e02c803..0000000000 --- a/Content.Server/Worldgen/Systems/NoiseIndexSystem.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Numerics; -using Content.Server.Worldgen.Components; -using Content.Server.Worldgen.Prototypes; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Server.Worldgen.Systems; - -/// -/// This handles the noise index. -/// -public sealed class NoiseIndexSystem : EntitySystem -{ - [Dependency] private readonly IPrototypeManager _prototype = default!; - [Dependency] private readonly IRobustRandom _random = default!; - - /// - /// Gets a particular noise channel from the index on the given entity. - /// - /// The holder of the index - /// The channel prototype ID - /// An initialized noise generator - public NoiseGenerator Get(EntityUid holder, string protoId) - { - var idx = EnsureComp(holder); - if (idx.Generators.TryGetValue(protoId, out var generator)) - return generator; - var proto = _prototype.Index(protoId); - var gen = new NoiseGenerator(proto, _random.Next()); - idx.Generators[protoId] = gen; - return gen; - } - - /// - /// Attempts to evaluate the given noise channel using the generator on the given entity. - /// - /// The holder of the index - /// The channel prototype ID - /// The coordinates to evaluate at - /// The result of evaluation - public float Evaluate(EntityUid holder, string protoId, Vector2 coords) - { - var gen = Get(holder, protoId); - return gen.Evaluate(coords); - } -} - diff --git a/Content.Server/Worldgen/Systems/WorldControllerSystem.cs b/Content.Server/Worldgen/Systems/WorldControllerSystem.cs deleted file mode 100644 index 19c777e1ad..0000000000 --- a/Content.Server/Worldgen/Systems/WorldControllerSystem.cs +++ /dev/null @@ -1,277 +0,0 @@ -using System.Linq; -using Content.Server.Worldgen.Components; -using Content.Shared.Ghost; -using Content.Shared.Mind.Components; -using JetBrains.Annotations; -using Robust.Server.GameObjects; -using Robust.Shared.Map; -using Robust.Shared.Timing; - -namespace Content.Server.Worldgen.Systems; - -/// -/// This handles putting together chunk entities and notifying them about important changes. -/// -public sealed class WorldControllerSystem : EntitySystem -{ - [Dependency] private readonly TransformSystem _xformSys = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly ILogManager _logManager = default!; - [Dependency] private readonly MetaDataSystem _metaData = default!; - - private const int PlayerLoadRadius = 2; - - private ISawmill _sawmill = default!; - - /// - public override void Initialize() - { - _sawmill = _logManager.GetSawmill("world"); - SubscribeLocalEvent(OnChunkLoadedCore); - SubscribeLocalEvent(OnChunkUnloadedCore); - SubscribeLocalEvent(OnChunkShutdown); - } - - /// - /// Handles deleting chunks properly. - /// - private void OnChunkShutdown(EntityUid uid, WorldChunkComponent component, ComponentShutdown args) - { - if (!TryComp(component.Map, out var controller)) - return; - - if (HasComp(uid)) - { - var ev = new WorldChunkUnloadedEvent(uid, component.Coordinates); - RaiseLocalEvent(component.Map, ref ev); - RaiseLocalEvent(uid, ref ev, broadcast: true); - } - - controller.Chunks.Remove(component.Coordinates); - } - - /// - /// Handles the inner logic of loading a chunk, i.e. events. - /// - private void OnChunkLoadedCore(EntityUid uid, LoadedChunkComponent component, ComponentStartup args) - { - if (!TryComp(uid, out var chunk)) - return; - - var ev = new WorldChunkLoadedEvent(uid, chunk.Coordinates); - RaiseLocalEvent(chunk.Map, ref ev); - RaiseLocalEvent(uid, ref ev, broadcast: true); - //_sawmill.Debug($"Loaded chunk {ToPrettyString(uid)} at {chunk.Coordinates}"); - } - - /// - /// Handles the inner logic of unloading a chunk, i.e. events. - /// - private void OnChunkUnloadedCore(EntityUid uid, LoadedChunkComponent component, ComponentShutdown args) - { - if (!TryComp(uid, out var chunk)) - return; - - if (Terminating(uid)) - return; // SAFETY: This is in case a loaded chunk gets deleted, to avoid double unload. - - var ev = new WorldChunkUnloadedEvent(uid, chunk.Coordinates); - RaiseLocalEvent(chunk.Map, ref ev); - RaiseLocalEvent(uid, ref ev); - //_sawmill.Debug($"Unloaded chunk {ToPrettyString(uid)} at {coords}"); - } - - /// - public override void Update(float frameTime) - { - //there was a to-do here about every frame alloc but it turns out it's a nothing burger here. - var chunksToLoad = new Dictionary>>(); - - var controllerEnum = EntityQueryEnumerator(); - while (controllerEnum.MoveNext(out var uid, out _)) - { - chunksToLoad[uid] = new Dictionary>(); - } - - if (chunksToLoad.Count == 0) - return; // Just bail early. - - var loaderEnum = EntityQueryEnumerator(); - - while (loaderEnum.MoveNext(out var uid, out var worldLoader, out var xform)) - { - var mapOrNull = xform.MapUid; - if (mapOrNull is null) - continue; - var map = mapOrNull.Value; - if (!chunksToLoad.ContainsKey(map)) - continue; - - var wc = _xformSys.GetWorldPosition(xform); - var coords = WorldGen.WorldToChunkCoords(wc); - var chunks = new GridPointsNearEnumerator(coords.Floored(), - (int) Math.Ceiling(worldLoader.Radius / (float) WorldGen.ChunkSize) + 1); - - var set = chunksToLoad[map]; - - while (chunks.MoveNext(out var chunk)) - { - if (!set.TryGetValue(chunk.Value, out _)) - set[chunk.Value] = new List(4); - set[chunk.Value].Add(uid); - } - } - - var mindEnum = EntityQueryEnumerator(); - var ghostQuery = GetEntityQuery(); - - // Mindful entities get special privilege as they're always a player and we don't want the illusion being broken around them. - while (mindEnum.MoveNext(out var uid, out var mind, out var xform)) - { - if (!mind.HasMind) - continue; - if (ghostQuery.HasComponent(uid)) - continue; - var mapOrNull = xform.MapUid; - if (mapOrNull is null) - continue; - var map = mapOrNull.Value; - if (!chunksToLoad.ContainsKey(map)) - continue; - - var wc = _xformSys.GetWorldPosition(xform); - var coords = WorldGen.WorldToChunkCoords(wc); - var chunks = new GridPointsNearEnumerator(coords.Floored(), PlayerLoadRadius); - - var set = chunksToLoad[map]; - - while (chunks.MoveNext(out var chunk)) - { - if (!set.TryGetValue(chunk.Value, out _)) - set[chunk.Value] = new List(4); - set[chunk.Value].Add(uid); - } - } - - var loadedEnum = EntityQueryEnumerator(); - var chunksUnloaded = 0; - - // Make sure these chunks get unloaded at the end of the tick. - while (loadedEnum.MoveNext(out var uid, out var _, out var chunk)) - { - var coords = chunk.Coordinates; - - if (!chunksToLoad[chunk.Map].ContainsKey(coords)) - { - RemCompDeferred(uid); - chunksUnloaded++; - } - } - - if (chunksUnloaded > 0) - _sawmill.Debug($"Queued {chunksUnloaded} chunks for unload."); - - if (chunksToLoad.All(x => x.Value.Count == 0)) - return; - - var startTime = _gameTiming.RealTime; - var count = 0; - var loadedQuery = GetEntityQuery(); - var controllerQuery = GetEntityQuery(); - foreach (var (map, chunks) in chunksToLoad) - { - var controller = controllerQuery.GetComponent(map); - foreach (var (chunk, loaders) in chunks) - { - var ent = GetOrCreateChunk(chunk, map, controller); // Ensure everything loads. - LoadedChunkComponent? c = null; - if (ent is not null && !loadedQuery.TryGetComponent(ent.Value, out c)) - { - c = AddComp(ent.Value); - count += 1; - } - - if (c is not null) - c.Loaders = loaders; - } - } - - if (count > 0) - { - var timeSpan = _gameTiming.RealTime - startTime; - _sawmill.Debug($"Loaded {count} chunks in {timeSpan.TotalMilliseconds:N2}ms."); - } - } - - /// - /// Attempts to get a chunk, creating it if it doesn't exist. - /// - /// Chunk coordinates to get the chunk entity for. - /// Map the chunk is in. - /// The controller this chunk belongs to. - /// A chunk, if available. - [Pure] - public EntityUid? GetOrCreateChunk(Vector2i chunk, EntityUid map, WorldControllerComponent? controller = null) - { - if (!Resolve(map, ref controller)) - throw new Exception($"Tried to use {ToPrettyString(map)} as a world map, without actually being one."); - - if (controller.Chunks.TryGetValue(chunk, out var ent)) - return ent; - return CreateChunkEntity(chunk, map, controller); - } - - /// - /// Constructs a new chunk entity, attaching it to the map. - /// - /// The coordinates the new chunk should be initialized for. - /// - /// - /// - private EntityUid CreateChunkEntity(Vector2i chunkCoords, EntityUid map, WorldControllerComponent controller) - { - var chunk = Spawn(controller.ChunkProto, MapCoordinates.Nullspace); - StartupChunkEntity(chunk, chunkCoords, map, controller); - _metaData.SetEntityName(chunk, $"Chunk {chunkCoords.X}/{chunkCoords.Y}"); - return chunk; - } - - private void StartupChunkEntity(EntityUid chunk, Vector2i coords, EntityUid map, - WorldControllerComponent controller) - { - if (!TryComp(chunk, out var chunkComponent)) - { - _sawmill.Error($"Chunk {ToPrettyString(chunk)} is missing WorldChunkComponent."); - return; - } - - ref var chunks = ref controller.Chunks; - - chunks[coords] = chunk; // Add this entity to chunk index. - chunkComponent.Coordinates = coords; - chunkComponent.Map = map; - var ev = new WorldChunkAddedEvent(chunk, coords); - RaiseLocalEvent(map, ref ev, broadcast: true); - } -} - -/// -/// A directed event fired when a chunk is initially set up in the world. The chunk is not loaded at this point. -/// -[ByRefEvent] -[PublicAPI] -public readonly record struct WorldChunkAddedEvent(EntityUid Chunk, Vector2i Coords); - -/// -/// A directed event fired when a chunk is loaded into the world, i.e. a player or other world loader has entered vicinity. -/// -[ByRefEvent] -[PublicAPI] -public readonly record struct WorldChunkLoadedEvent(EntityUid Chunk, Vector2i Coords); - -/// -/// A directed event fired when a chunk is unloaded from the world, i.e. no world loaders remain nearby. -/// -[ByRefEvent] -[PublicAPI] -public readonly record struct WorldChunkUnloadedEvent(EntityUid Chunk, Vector2i Coords); diff --git a/Content.Server/Worldgen/Systems/WorldgenConfigSystem.cs b/Content.Server/Worldgen/Systems/WorldgenConfigSystem.cs deleted file mode 100644 index cc0ec62733..0000000000 --- a/Content.Server/Worldgen/Systems/WorldgenConfigSystem.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Content.Server.Administration; -using Content.Server.GameTicking; -using Content.Server.GameTicking.Events; -using Content.Server.Worldgen.Components; -using Content.Server.Worldgen.Prototypes; -using Content.Shared.Administration; -using Content.Shared.CCVar; -using Robust.Shared.Configuration; -using Robust.Shared.Console; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.Manager; -using Robust.Shared.Utility; - -namespace Content.Server.Worldgen.Systems; - -/// -/// This handles configuring world generation during round start. -/// -public sealed class WorldgenConfigSystem : EntitySystem -{ - [Dependency] private readonly GameTicker _gameTicker = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly IConsoleHost _conHost = default!; - [Dependency] private readonly SharedMapSystem _map = default!; - [Dependency] private readonly IPrototypeManager _proto = default!; - [Dependency] private readonly ISerializationManager _ser = default!; - - private bool _enabled; - private string _worldgenConfig = default!; - - /// - public override void Initialize() - { - SubscribeLocalEvent(OnLoadingMaps); - _conHost.RegisterCommand("applyworldgenconfig", Loc.GetString("cmd-applyworldgenconfig-description"), Loc.GetString("cmd-applyworldgenconfig-help"), ApplyWorldgenConfigCommand); - Subs.CVar(_cfg, CCVars.WorldgenEnabled, b => _enabled = b, true); - Subs.CVar(_cfg, CCVars.WorldgenConfig, s => _worldgenConfig = s, true); - } - - [AdminCommand(AdminFlags.Mapping)] - private void ApplyWorldgenConfigCommand(IConsoleShell shell, string argstr, string[] args) - { - if (args.Length != 2) - { - shell.WriteError(Loc.GetString("shell-wrong-arguments-number-need-specific", ("properAmount", 2), ("currentAmount", args.Length))); - return; - } - - if (!int.TryParse(args[0], out var mapInt) || !_map.MapExists(new MapId(mapInt))) - { - shell.WriteError(Loc.GetString("shell-invalid-map-id")); - return; - } - - var map = _map.GetMapOrInvalid(new MapId(mapInt)); - - if (!_proto.TryIndex(args[1], out var proto)) - { - shell.WriteError(Loc.GetString("shell-argument-must-be-prototype", ("index", 2), ("prototypeName", "cmd-applyworldgenconfig-prototype"))); - return; - } - - proto.Apply(map, _ser, EntityManager); - shell.WriteLine(Loc.GetString("cmd-applyworldgenconfig-success")); - } - - /// - /// Applies the world config to the default map if enabled. - /// - private void OnLoadingMaps(RoundStartingEvent ev) - { - if (_enabled == false) - return; - - var target = _map.GetMapOrInvalid(_gameTicker.DefaultMap); - Log.Debug($"Trying to configure {_gameTicker.DefaultMap}, aka {ToPrettyString(target)} aka {target}"); - var cfg = _proto.Index(_worldgenConfig); - - cfg.Apply(target, _ser, EntityManager); // Apply the config to the map. - - DebugTools.Assert(HasComp(target)); - } -} - diff --git a/Content.Server/Worldgen/Tools/EntitySpawnCollectionCache.cs b/Content.Server/Worldgen/Tools/EntitySpawnCollectionCache.cs deleted file mode 100644 index 5480575427..0000000000 --- a/Content.Server/Worldgen/Tools/EntitySpawnCollectionCache.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System.Linq; -using Content.Shared.Storage; -using Robust.Shared.Random; - -namespace Content.Server.Worldgen.Tools; - -/// -/// A faster version of EntitySpawnCollection that requires caching to work. -/// -public sealed class EntitySpawnCollectionCache -{ - [ViewVariables] private readonly Dictionary _orGroups = new(); - - public EntitySpawnCollectionCache(IEnumerable entries) - { - // collect groups together, create singular items that pass probability - foreach (var entry in entries) - { - if (!_orGroups.TryGetValue(entry.GroupId ?? string.Empty, out var orGroup)) - { - orGroup = new OrGroup(); - _orGroups.Add(entry.GroupId ?? string.Empty, orGroup); - } - - orGroup.Entries.Add(entry); - orGroup.CumulativeProbability += entry.SpawnProbability; - } - } - - /// - /// Using a collection of entity spawn entries, picks a random list of entity prototypes to spawn from that collection. - /// - /// - /// This does not spawn the entities. The caller is responsible for doing so, since it may want to do something - /// special to those entities (offset them, insert them into storage, etc) - /// - /// Resolve param. - /// List that spawned entities are inserted into. - /// A list of entity prototypes that should be spawned. - /// This is primarily useful if you're calling it many times over, as it lets you reuse the list repeatedly. - public void GetSpawns(IRobustRandom random, ref List spawned) - { - // handle orgroup spawns - foreach (var spawnValue in _orGroups.Values) - { - //HACK: This doesn't seem to work without this if there's only a single orgroup entry. Not sure how to fix the original math properly, but it works in every other case. - if (spawnValue.Entries.Count == 1) - { - var entry = spawnValue.Entries.First(); - var amount = entry.Amount; - - if (entry.MaxAmount > amount) - amount = random.Next(amount, entry.MaxAmount); - - for (var index = 0; index < amount; index++) - { - spawned.Add(entry.PrototypeId); - } - - continue; - } - - // For each group use the added cumulative probability to roll a double in that range - var diceRoll = random.NextDouble() * spawnValue.CumulativeProbability; - // Add the entry's spawn probability to this value, if equals or lower, spawn item, otherwise continue to next item. - var cumulative = 0.0; - foreach (var entry in spawnValue.Entries) - { - cumulative += entry.SpawnProbability; - if (diceRoll > cumulative) - continue; - // Dice roll succeeded, add item and break loop - - var amount = entry.Amount; - - if (entry.MaxAmount > amount) - amount = random.Next(amount, entry.MaxAmount); - - for (var index = 0; index < amount; index++) - { - spawned.Add(entry.PrototypeId); - } - - break; - } - } - } - - private sealed class OrGroup - { - [ViewVariables] public List Entries { get; } = new(); - - [ViewVariables] public float CumulativeProbability { get; set; } - } -} - diff --git a/Content.Server/Worldgen/Tools/PoissonDiskSampler.cs b/Content.Server/Worldgen/Tools/PoissonDiskSampler.cs deleted file mode 100644 index cb7c5a1411..0000000000 --- a/Content.Server/Worldgen/Tools/PoissonDiskSampler.cs +++ /dev/null @@ -1,244 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Numerics; -using Robust.Shared.Random; -using Robust.Shared.Utility; - -namespace Content.Server.Worldgen.Tools; - -/// -/// An implementation of Poisson Disk Sampling, for evenly spreading points across a given area. -/// -public sealed class PoissonDiskSampler -{ - public const int DefaultPointsPerIteration = 30; - [Dependency] private readonly IRobustRandom _random = default!; - - /// - /// Samples for points within the given circle. - /// - /// Center of the sample - /// Radius of the sample - /// Minimum distance between points. Must be above 0! - /// The number of points placed per iteration of the algorithm - /// An enumerator of points - public SampleEnumerator SampleCircle(Vector2 center, float radius, float minimumDistance, - int pointsPerIteration = DefaultPointsPerIteration) - { - return Sample(center - new Vector2(radius, radius), center + new Vector2(radius, radius), radius, - minimumDistance, pointsPerIteration); - } - - /// - /// Samples for points within the given rectangle. - /// - /// The top left of the rectangle - /// The bottom right of the rectangle - /// Minimum distance between points. Must be above 0! - /// The number of points placed per iteration of the algorithm - /// An enumerator of points - public SampleEnumerator SampleRectangle(Vector2 topLeft, Vector2 lowerRight, float minimumDistance, - int pointsPerIteration = DefaultPointsPerIteration) - { - return Sample(topLeft, lowerRight, null, minimumDistance, pointsPerIteration); - } - - /// - /// Samples for points within the given rectangle, with an optional rejection distance. - /// - /// The top left of the rectangle - /// The bottom right of the rectangle - /// The distance at which points will be discarded, if any - /// Minimum distance between points. Must be above 0! - /// The number of points placed per iteration of the algorithm - /// An enumerator of points - public SampleEnumerator Sample(Vector2 topLeft, Vector2 lowerRight, float? rejectionDistance, - float minimumDistance, int pointsPerIteration) - { - // This still doesn't guard against dangerously low but non-zero distances, but this will do for now. - DebugTools.Assert(minimumDistance > 0, "Minimum distance must be above 0, or else an infinite number of points would be generated."); - - var settings = new SampleSettings - { - TopLeft = topLeft, LowerRight = lowerRight, - Dimensions = lowerRight - topLeft, - Center = (topLeft + lowerRight) / 2, - CellSize = minimumDistance / (float) Math.Sqrt(2), - MinimumDistance = minimumDistance, - RejectionSqDistance = rejectionDistance * rejectionDistance - }; - - settings.GridWidth = (int) (settings.Dimensions.X / settings.CellSize) + 1; - settings.GridHeight = (int) (settings.Dimensions.Y / settings.CellSize) + 1; - - var state = new State - { - Grid = new Vector2?[settings.GridWidth, settings.GridHeight], - ActivePoints = new List() - }; - - return new SampleEnumerator(this, state, settings, pointsPerIteration); - } - - private Vector2 AddFirstPoint(ref SampleSettings settings, ref State state) - { - while (true) - { - var d = _random.NextDouble(); - var xr = settings.TopLeft.X + settings.Dimensions.X * d; - - d = _random.NextDouble(); - var yr = settings.TopLeft.Y + settings.Dimensions.Y * d; - - var p = new Vector2((float) xr, (float) yr); - if (settings.RejectionSqDistance != null && - (settings.Center - p).LengthSquared() > settings.RejectionSqDistance) - continue; - - var index = Denormalize(p, settings.TopLeft, settings.CellSize); - - state.Grid[(int) index.X, (int) index.Y] = p; - - state.ActivePoints.Add(p); - return p; - } - } - - private Vector2? AddNextPoint(Vector2 point, ref SampleSettings settings, ref State state) - { - var q = GenerateRandomAround(point, settings.MinimumDistance); - - if (q.X >= settings.TopLeft.X && q.X < settings.LowerRight.X && - q.Y > settings.TopLeft.Y && q.Y < settings.LowerRight.Y && - (settings.RejectionSqDistance == null || - (settings.Center - q).LengthSquared() <= settings.RejectionSqDistance)) - { - var qIndex = Denormalize(q, settings.TopLeft, settings.CellSize); - var tooClose = false; - - for (var i = (int) Math.Max(0, qIndex.X - 2); - i < Math.Min(settings.GridWidth, qIndex.X + 3) && !tooClose; - i++) - for (var j = (int) Math.Max(0, qIndex.Y - 2); - j < Math.Min(settings.GridHeight, qIndex.Y + 3) && !tooClose; - j++) - { - if (state.Grid[i, j].HasValue && (state.Grid[i, j]!.Value - q).Length() < settings.MinimumDistance) - tooClose = true; - } - - if (!tooClose) - { - state.ActivePoints.Add(q); - state.Grid[(int) qIndex.X, (int) qIndex.Y] = q; - return q; - } - } - - return null; - } - - private Vector2 GenerateRandomAround(Vector2 center, float minimumDistance) - { - var d = _random.NextDouble(); - var radius = minimumDistance + minimumDistance * d; - - d = _random.NextDouble(); - var angle = Math.PI * 2 * d; - - var newX = radius * Math.Sin(angle); - var newY = radius * Math.Cos(angle); - - return new Vector2((float) (center.X + newX), (float) (center.Y + newY)); - } - - private static Vector2 Denormalize(Vector2 point, Vector2 origin, double cellSize) - { - return new Vector2((int) ((point.X - origin.X) / cellSize), (int) ((point.Y - origin.Y) / cellSize)); - } - - public struct SampleEnumerator - { - private PoissonDiskSampler _pds; - private State _state; - private SampleSettings _settings; - // These variables make up the state machine. - private bool _returnedFirstPoint; - private int _pointsPerIteration; - private int _iterationListIndex; - private bool _iterationFound; - private int _iterationPosition; - - // This has internal access because C# nested type access is being weird. - internal SampleEnumerator(PoissonDiskSampler pds, State state, SampleSettings settings, int ppi) - { - _pds = pds; - _state = state; - _settings = settings; - _pointsPerIteration = ppi; - } - - public bool MoveNext([NotNullWhen(true)] out Vector2? point) - { - // First point is chosen via a very particular method. - if (!_returnedFirstPoint) - { - _returnedFirstPoint = true; - point = _pds.AddFirstPoint(ref _settings, ref _state); - return true; - } - - // Remaining points have to be fed out carefully. - // We can be interrupted (by a successful point) mid-stream. - while (_state.ActivePoints.Count != 0) - { - if (_iterationPosition == 0) - { - // First point of iteration. - _iterationListIndex = _pds._random.Next(_state.ActivePoints.Count); - _iterationFound = false; - } - - var basePoint = _state.ActivePoints[_iterationListIndex]; - - point = _pds.AddNextPoint(basePoint, ref _settings, ref _state); - - // Set this now, return later after processing is complete. - _iterationFound |= point != null; - - // Iteration loop advance. - _iterationPosition++; - if (_iterationPosition == _pointsPerIteration) - { - // Reached end of this iteration. - _iterationPosition = 0; - if (!_iterationFound) - _state.ActivePoints.RemoveAt(_iterationListIndex); - } - - if (point != null) - return true; - } - point = null; - return false; - } - } - - internal struct State - { - public Vector2?[,] Grid; - public List ActivePoints; - } - - internal struct SampleSettings - { - public Vector2 TopLeft, LowerRight, Center; - public Vector2 Dimensions; - public float? RejectionSqDistance; - public float MinimumDistance; - public float CellSize; - public int GridWidth, GridHeight; - } -} - - - diff --git a/Content.Server/Worldgen/WorldGen.cs b/Content.Server/Worldgen/WorldGen.cs deleted file mode 100644 index 1ed20b9f1f..0000000000 --- a/Content.Server/Worldgen/WorldGen.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Diagnostics.Contracts; -using System.Numerics; - -namespace Content.Server.Worldgen; - -/// -/// Contains a few world-generation related constants and static functions. -/// -public static class WorldGen -{ - /// - /// The size of each chunk (isn't that self-explanatory.) - /// Be careful about how small you make this. - /// - public const int ChunkSize = 128; - - /// - /// Converts world coordinates to chunk coordinates. - /// - /// World coordinates - /// Chunk coordinates - [Pure] - public static Vector2i WorldToChunkCoords(Vector2i inp) - { - return (inp * new Vector2(1.0f / ChunkSize, 1.0f / ChunkSize)).Floored(); - } - - /// - /// Converts world coordinates to chunk coordinates. - /// - /// World coordinates - /// Chunk coordinates - [Pure] - public static Vector2 WorldToChunkCoords(Vector2 inp) - { - return inp * new Vector2(1.0f / ChunkSize, 1.0f / ChunkSize); - } - - /// - /// Converts chunk coordinates to world coordinates. - /// - /// Chunk coordinates - /// World coordinates - [Pure] - public static Vector2 ChunkToWorldCoords(Vector2i inp) - { - return inp * ChunkSize; - } - - /// - /// Converts chunk coordinates to world coordinates. - /// - /// Chunk coordinates - /// World coordinates - [Pure] - public static Vector2 ChunkToWorldCoords(Vector2 inp) - { - return inp * ChunkSize; - } - - /// - /// Converts chunk coordinates to world coordinates, getting the center of the chunk. - /// - /// Chunk coordinates - /// World coordinates - [Pure] - public static Vector2 ChunkToWorldCoordsCentered(Vector2i inp) - { - return inp * ChunkSize + Vector2i.One * (ChunkSize / 2); - } -} - diff --git a/Content.Shared/CCVar/CCVars.Worldgen.cs b/Content.Shared/CCVar/CCVars.Worldgen.cs deleted file mode 100644 index da165ce74a..0000000000 --- a/Content.Shared/CCVar/CCVars.Worldgen.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Robust.Shared.Configuration; - -namespace Content.Shared.CCVar; - -public sealed partial class CCVars -{ - /// - /// Whether or not world generation is enabled. - /// - public static readonly CVarDef WorldgenEnabled = - CVarDef.Create("worldgen.enabled", false, CVar.SERVERONLY); - - /// - /// The worldgen config to use. - /// - public static readonly CVarDef WorldgenConfig = - CVarDef.Create("worldgen.worldgen_config", "Default", CVar.SERVERONLY); -} diff --git a/Resources/Locale/en-US/worldgen/applyworldgenconfig.ftl b/Resources/Locale/en-US/worldgen/applyworldgenconfig.ftl deleted file mode 100644 index a2144d08b2..0000000000 --- a/Resources/Locale/en-US/worldgen/applyworldgenconfig.ftl +++ /dev/null @@ -1,4 +0,0 @@ -cmd-applyworldgenconfig-description = Applies the given worldgen configuration to a map, setting it up for chunk loading/etc. -cmd-applyworldgenconfig-help = applyworldgenconfig -cmd-applyworldgenconfig-prototype = worldgen config prototype -cmd-applyworldgenconfig-success = Config applied successfully. Do not rerun this command on this map. diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml index 96e30a5aa7..5a96e342fb 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml @@ -130,8 +130,6 @@ enum.WiresUiKey.Key: type: WiresBoundUserInterface - type: RadarConsole - - type: WorldLoader - radius: 256 - type: PointLight radius: 1.5 energy: 4.0 @@ -200,8 +198,6 @@ - Syndicate - type: RadarConsole maxRange: 384 - - type: WorldLoader - radius: 1536 - type: PointLight radius: 1.5 energy: 4.0 diff --git a/Resources/Prototypes/Entities/Structures/Machines/salvage.yml b/Resources/Prototypes/Entities/Structures/Machines/salvage.yml index cafdc7cef2..dad4176617 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/salvage.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/salvage.yml @@ -54,11 +54,3 @@ - type: SalvageMagnet - type: ApcPowerReceiver powerLoad: 1000 - -- type: weightedRandomEntity - id: RandomAsteroidPool - weights: - AsteroidSalvageSmall: 3 - AsteroidSalvageMedium: 7 - AsteroidSalvageLarge: 5 - AsteroidSalvageHuge: 3 diff --git a/Resources/Prototypes/Entities/World/Debris/asteroids.yml b/Resources/Prototypes/Entities/World/Debris/asteroids.yml deleted file mode 100644 index bb33801e2d..0000000000 --- a/Resources/Prototypes/Entities/World/Debris/asteroids.yml +++ /dev/null @@ -1,144 +0,0 @@ -- type: entity - id: BaseAsteroidDebris - parent: BaseDebris - name: asteroid debris - abstract: true - components: - - type: MapGrid - - type: BlobFloorPlanBuilder - floorTileset: - - FloorAsteroidSand - blobDrawProb: 0.5 - radius: 6 - floorPlacements: 16 - - type: SimpleFloorPlanPopulator - entries: - FloorAsteroidSand: - - id: WallRock - prob: 0.5 - orGroup: rock - - id: WallRockCoal - prob: 0.15 - orGroup: rock - - id: WallRockTin - prob: 0.15 - orGroup: rock - - id: WallRockQuartz - prob: 0.15 - orGroup: rock - - id: WallRockSalt - prob: 0.15 - orGroup: rock - - id: WallRockGold - prob: 0.05 - orGroup: rock - - id: WallRockDiamond - prob: 0.005 - orGroup: rock - - id: WallRockSilver - prob: 0.05 - orGroup: rock - - id: WallRockPlasma - prob: 0.05 - orGroup: rock - - id: WallRockUranium - prob: 0.02 - orGroup: rock - - id: WallRockBananium - prob: 0.02 - orGroup: rock - - id: WallRockArtifactFragment - prob: 0.01 - orGroup: rock - - type: IFF - flags: HideLabel - color: "#d67e27" - -- type: entity - id: AsteroidDebrisSmall - parent: BaseAsteroidDebris - name: asteroid debris small - categories: [ HideSpawnMenu ] - components: - - type: MapGrid - - type: BlobFloorPlanBuilder - floorPlacements: 8 - -- type: entity - id: AsteroidDebrisMedium - parent: BaseAsteroidDebris - name: asteroid debris medium - categories: [ HideSpawnMenu ] - components: - - type: MapGrid - - type: BlobFloorPlanBuilder - floorPlacements: 16 - -- type: entity - id: AsteroidDebrisLarge - parent: BaseAsteroidDebris - name: asteroid debris large - categories: [ HideSpawnMenu ] - components: - - type: MapGrid - - type: BlobFloorPlanBuilder - floorPlacements: 24 - -- type: entity - id: AsteroidDebrisLarger - parent: BaseAsteroidDebris - name: asteroid debris larger - categories: [ HideSpawnMenu ] - components: - - type: MapGrid - - type: BlobFloorPlanBuilder - radius: 12 - floorPlacements: 36 - -- type: entity - id: AsteroidSalvageSmall - parent: BaseAsteroidDebris - name: salvage asteroid small - categories: [ HideSpawnMenu ] - components: - - type: MapGrid - - type: BlobFloorPlanBuilder - blobDrawProb: 0.66 - radius: 15 - floorPlacements: 100 - -- type: entity - id: AsteroidSalvageMedium - parent: BaseAsteroidDebris - name: salvage asteroid medium - categories: [ HideSpawnMenu ] - components: - - type: MapGrid - - type: BlobFloorPlanBuilder - blobDrawProb: 0.66 - radius: 17 - floorPlacements: 150 - -- type: entity - id: AsteroidSalvageLarge - parent: BaseAsteroidDebris - name: salvage asteroid large - categories: [ HideSpawnMenu ] - components: - - type: MapGrid - - type: BlobFloorPlanBuilder - blobDrawProb: 0.66 - radius: 20 - floorPlacements: 200 - -- type: entity - id: AsteroidSalvageHuge - parent: BaseAsteroidDebris - name: salvage asteroid huge - categories: [ HideSpawnMenu ] - components: - - type: MapGrid - - type: BlobFloorPlanBuilder - blobDrawProb: 0.66 - radius: 23 - floorPlacements: 250 diff --git a/Resources/Prototypes/Entities/World/Debris/base_debris.yml b/Resources/Prototypes/Entities/World/Debris/base_debris.yml deleted file mode 100644 index c125d991fd..0000000000 --- a/Resources/Prototypes/Entities/World/Debris/base_debris.yml +++ /dev/null @@ -1,6 +0,0 @@ -- type: entity - id: BaseDebris - abstract: true - components: - - type: OwnedDebris - - type: LocalityLoader diff --git a/Resources/Prototypes/Entities/World/Debris/wrecks.yml b/Resources/Prototypes/Entities/World/Debris/wrecks.yml deleted file mode 100644 index 7bbeadeb5b..0000000000 --- a/Resources/Prototypes/Entities/World/Debris/wrecks.yml +++ /dev/null @@ -1,80 +0,0 @@ -- type: entity - id: BaseScrapDebris - parent: BaseDebris - name: scrap debris - abstract: true - components: - - type: MapGrid - - type: BlobFloorPlanBuilder - floorTileset: - - Plating - - Plating - - Plating - - FloorSteel - - Lattice - blobDrawProb: 0.5 - radius: 6 - floorPlacements: 16 - - type: SimpleFloorPlanPopulator - entries: - Plating: - - prob: 3 # Intentional blank. - - id: SalvageMaterialCrateSpawner - prob: 1 - - id: SalvageCanisterSpawner - prob: 0.2 - - id: SalvageMobSpawner - prob: 0.7 - - id: WallSolid - prob: 1 - - id: Grille - prob: 0.5 - Lattice: - - prob: 2 - - id: Grille - prob: 0.2 - - id: SalvageMaterialCrateSpawner - prob: 0.3 - - id: SalvageCanisterSpawner - prob: 0.2 - FloorSteel: - - prob: 3 # Intentional blank. - - id: SalvageMaterialCrateSpawner - prob: 1 - - id: SalvageCanisterSpawner - prob: 0.2 - - id: SalvageMobSpawner - prob: 0.7 - - type: IFF - flags: HideLabel - color: "#88b0d1" - -- type: entity - id: ScrapDebrisSmall - parent: BaseScrapDebris - name: scrap debris small - categories: [ HideSpawnMenu ] - components: - - type: MapGrid - - type: BlobFloorPlanBuilder - floorPlacements: 8 - -- type: entity - id: ScrapDebrisMedium - parent: BaseScrapDebris - name: scrap debris medium - categories: [ HideSpawnMenu ] - components: - - type: MapGrid - - type: BlobFloorPlanBuilder - floorPlacements: 16 - -- type: entity - id: ScrapDebrisLarge - parent: BaseScrapDebris - name: scrap debris large - categories: [ HideSpawnMenu ] - components: - - type: MapGrid - - type: BlobFloorPlanBuilder - floorPlacements: 24 diff --git a/Resources/Prototypes/Entities/World/chunk.yml b/Resources/Prototypes/Entities/World/chunk.yml deleted file mode 100644 index c7cb09c2a4..0000000000 --- a/Resources/Prototypes/Entities/World/chunk.yml +++ /dev/null @@ -1,14 +0,0 @@ -- type: entity - id: WorldChunk - parent: MarkerBase - name: world chunk - description: | - It's rude to stare. - It's also a bit odd you're looking at the abstract representation of the grid of reality. - categories: [ HideSpawnMenu ] - components: - - type: WorldChunk - - type: Sprite - sprite: Markers/cross.rsi - layers: - - state: blue diff --git a/Resources/Prototypes/World/Biomes/basic.yml b/Resources/Prototypes/World/Biomes/basic.yml deleted file mode 100644 index 5ecd85bbc4..0000000000 --- a/Resources/Prototypes/World/Biomes/basic.yml +++ /dev/null @@ -1,26 +0,0 @@ -- type: spaceBiome - id: AsteroidsStandard - priority: 0 # This probably shouldn't get selected. - noiseRanges: {} - chunkComponents: - - type: DebrisFeaturePlacerController - densityNoiseChannel: Density - - type: SimpleDebrisSelector - debrisTable: - - id: AsteroidDebrisSmall - - id: AsteroidDebrisMedium - - id: AsteroidDebrisLarge - prob: 0.7 - - id: AsteroidDebrisLarger - prob: 0.4 - - type: NoiseDrivenDebrisSelector - noiseChannel: Wreck - debrisTable: - - id: ScrapDebrisSmall - - id: ScrapDebrisMedium - - id: ScrapDebrisLarge - prob: 0.5 - - type: NoiseRangeCarver - ranges: - - 0.4, 0.6 - noiseChannel: Carver diff --git a/Resources/Prototypes/World/Biomes/failsafes.yml b/Resources/Prototypes/World/Biomes/failsafes.yml deleted file mode 100644 index 5e3c50b44c..0000000000 --- a/Resources/Prototypes/World/Biomes/failsafes.yml +++ /dev/null @@ -1,21 +0,0 @@ -- type: spaceBiome - id: Failsafe - priority: -999999 # This DEFINITELY shouldn't get selected! - noiseRanges: {} - -- type: spaceBiome - id: AsteroidsFallback - priority: -999998 # This probably shouldn't get selected. - noiseRanges: {} - chunkComponents: - - type: DebrisFeaturePlacerController - densityNoiseChannel: Density - - type: SimpleDebrisSelector - debrisTable: - - id: AsteroidDebrisSmall - - id: AsteroidDebrisMedium - - id: AsteroidDebrisLarge - prob: 0.7 - - id: AsteroidDebrisLarger - prob: 0.4 - diff --git a/Resources/Prototypes/World/noise_channels.yml b/Resources/Prototypes/World/noise_channels.yml deleted file mode 100644 index 668b338dd3..0000000000 --- a/Resources/Prototypes/World/noise_channels.yml +++ /dev/null @@ -1,44 +0,0 @@ -- type: noiseChannel - id: Density - noiseType: Perlin - fractalLacunarityByPi: 0.666666666 - remapTo0Through1: true - clippingRanges: - - 0.4, 0.6 - clippedValue: 1.658 # magic number for chunk size. - inputMultiplier: 6 # Makes density hopefully low noise in the local area while still being interesting at scale. - outputMultiplier: 50.0 # We scale density up significantly for more human-friendly numbers. - minimum: 45.0 - -- type: noiseChannel - id: DensityUnclipped - noiseType: Perlin - fractalLacunarityByPi: 0.666666666 - remapTo0Through1: true - inputMultiplier: 6 # Makes density hopefully low noise in the local area while still being interesting at scale. - outputMultiplier: 50.0 # We scale density up significantly for more human-friendly numbers. - minimum: 45.0 - -- type: noiseChannel - id: Carver - noiseType: Perlin - fractalLacunarityByPi: 0.666666666 - remapTo0Through1: true - inputMultiplier: 6 - -- type: noiseChannel - id: Wreck - noiseType: Perlin - fractalLacunarityByPi: 0.666666666 - clippingRanges: - - 0.0, 0.4 - clippedValue: 0 - remapTo0Through1: true - inputMultiplier: 16 # Makes wreck concentration very low noise at scale. - -- type: noiseChannel - id: Temperature - noiseType: Perlin - fractalLacunarityByPi: 0.666666666 - remapTo0Through1: true - inputMultiplier: 6 # Makes wreck concentration very low noise at scale. diff --git a/Resources/Prototypes/World/worldgen_default.yml b/Resources/Prototypes/World/worldgen_default.yml deleted file mode 100644 index af52c30cf1..0000000000 --- a/Resources/Prototypes/World/worldgen_default.yml +++ /dev/null @@ -1,9 +0,0 @@ -- type: worldgenConfig - id: Default - components: - - type: WorldController - - type: BiomeSelection - biomes: - - AsteroidsFallback - - Failsafe - - AsteroidsStandard From ee618e3037132019d1b6c8111e9ced9b5fdfe52e Mon Sep 17 00:00:00 2001 From: Samuka <47865393+Samuka-C@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:30:12 -0300 Subject: [PATCH 046/126] Add tile gun module (#41503) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add tile gun module * fix stacks being loaded all at once in guns * fix tile gun module * fixed indentation * remove not necessary stuff Co-authored-by: āda * Use alias Co-authored-by: āda * fix OnBallisticInteractUsing * forgot one Co-authored-by: āda * fix FillAmmo * add another todo * pass user instead of event args Co-authored-by: āda * fix last commit * update todo Co-authored-by: āda * wrong place * move gun to first slot * custom sprite * call a event * important detail in the commentary * remove recipe * not used * also not used anymore * when did i remove this? * fix sprite * shoot faster * move to shared * use API method * remove useless stuff * Revert "remove useless stuff" This reverts commit b198303aed7eb50aafb60e5a91b7af535219006b. * fix code * shot sound * change spread * fix attribution * load sound * empty sound * commentary * more commentary fix * rename event * remove duplicate * who knew * turn to mono * yet another beep * increase volume * make it a crowbar * make it able to self load clicking tiles * make it able to pry tiles * who needs a crowbar anymore * make it networked * no more hands * move BeforeAmmoLoadedEvent call to TryBallisticInsert * before now * change sprite * slight update to sprite * mag visuals * move split back to server * get one method * remove subscription * use API method in the gun system * move API call up Co-authored-by: āda * remove unused stuff * fix get one method * comment change * half the firerate --------- Co-authored-by: āda Co-authored-by: beck-thompson Co-authored-by: iaada --- Content.Server/Stack/StackSystem.cs | 4 +- .../Stacks/SharedStackSystem.API.cs | 23 +++++++ .../BallisticAmmoInteractLoaderComponent.cs | 11 ++++ .../Ranged/Events/BeforeAmmoLoadedEvent.cs | 21 +++++++ .../Systems/SharedGunSystem.Ballistic.cs | 34 +++++++++- .../Audio/Weapons/Guns/Empty/attributions.yml | 4 ++ .../Audio/Weapons/Guns/Empty/empty_beep.ogg | Bin 0 -> 4175 bytes .../Weapons/Guns/Gunshots/attributions.yml | 5 ++ .../Weapons/Guns/Gunshots/magnetic_shot.ogg | Bin 0 -> 9951 bytes .../Audio/Weapons/Guns/MagIn/attributions.yml | 4 ++ .../Audio/Weapons/Guns/MagIn/tile_load.ogg | Bin 0 -> 6374 bytes .../Specific/Robotics/borg_modules.yml | 16 +++++ .../Objects/Weapons/Guns/tile_gun.yml | 58 ++++++++++++++++++ .../Actions/actions_borg.rsi/meta.json | 3 + .../actions_borg.rsi/xenoborg-tile-module.png | Bin 0 -> 275 bytes .../borgmodule.rsi/icon-xenoborg-tile.png | Bin 0 -> 179 bytes .../Robotics/borgmodule.rsi/meta.json | 3 + .../Weapons/Guns/Basic/tilegun.rsi/icon.png | Bin 0 -> 410 bytes .../Weapons/Guns/Basic/tilegun.rsi/mag-1.png | Bin 0 -> 191 bytes .../Weapons/Guns/Basic/tilegun.rsi/mag-2.png | Bin 0 -> 192 bytes .../Weapons/Guns/Basic/tilegun.rsi/mag-3.png | Bin 0 -> 193 bytes .../Weapons/Guns/Basic/tilegun.rsi/mag-4.png | Bin 0 -> 194 bytes .../Weapons/Guns/Basic/tilegun.rsi/mag-5.png | Bin 0 -> 195 bytes .../Weapons/Guns/Basic/tilegun.rsi/mag-6.png | Bin 0 -> 193 bytes .../Weapons/Guns/Basic/tilegun.rsi/meta.json | 40 ++++++++++++ 25 files changed, 222 insertions(+), 4 deletions(-) create mode 100644 Content.Shared/Weapons/Ranged/Components/BallisticAmmoInteractLoaderComponent.cs create mode 100644 Content.Shared/Weapons/Ranged/Events/BeforeAmmoLoadedEvent.cs create mode 100644 Resources/Audio/Weapons/Guns/Empty/attributions.yml create mode 100644 Resources/Audio/Weapons/Guns/Empty/empty_beep.ogg create mode 100644 Resources/Audio/Weapons/Guns/Gunshots/magnetic_shot.ogg create mode 100644 Resources/Audio/Weapons/Guns/MagIn/attributions.yml create mode 100644 Resources/Audio/Weapons/Guns/MagIn/tile_load.ogg create mode 100644 Resources/Prototypes/Entities/Objects/Weapons/Guns/tile_gun.yml create mode 100644 Resources/Textures/Interface/Actions/actions_borg.rsi/xenoborg-tile-module.png create mode 100644 Resources/Textures/Objects/Specific/Robotics/borgmodule.rsi/icon-xenoborg-tile.png create mode 100644 Resources/Textures/Objects/Weapons/Guns/Basic/tilegun.rsi/icon.png create mode 100644 Resources/Textures/Objects/Weapons/Guns/Basic/tilegun.rsi/mag-1.png create mode 100644 Resources/Textures/Objects/Weapons/Guns/Basic/tilegun.rsi/mag-2.png create mode 100644 Resources/Textures/Objects/Weapons/Guns/Basic/tilegun.rsi/mag-3.png create mode 100644 Resources/Textures/Objects/Weapons/Guns/Basic/tilegun.rsi/mag-4.png create mode 100644 Resources/Textures/Objects/Weapons/Guns/Basic/tilegun.rsi/mag-5.png create mode 100644 Resources/Textures/Objects/Weapons/Guns/Basic/tilegun.rsi/mag-6.png create mode 100644 Resources/Textures/Objects/Weapons/Guns/Basic/tilegun.rsi/meta.json diff --git a/Content.Server/Stack/StackSystem.cs b/Content.Server/Stack/StackSystem.cs index a0d923dd1e..a6b75f953c 100644 --- a/Content.Server/Stack/StackSystem.cs +++ b/Content.Server/Stack/StackSystem.cs @@ -21,7 +21,9 @@ namespace Content.Server.Stack /// Spawns a new entity and moves an amount to it from the stack. /// Moves nothing if amount is greater than ent's stack count. /// - /// How much to move to the new entity. + /// Entity to split in a new stack. + /// How much to move to the new entity. + /// Where to spawn the new stack /// Null if StackComponent doesn't resolve, or amount to move is greater than ent has available. [PublicAPI] public EntityUid? Split(Entity ent, int amount, EntityCoordinates spawnPosition) diff --git a/Content.Shared/Stacks/SharedStackSystem.API.cs b/Content.Shared/Stacks/SharedStackSystem.API.cs index 1356c8ecda..372255701c 100644 --- a/Content.Shared/Stacks/SharedStackSystem.API.cs +++ b/Content.Shared/Stacks/SharedStackSystem.API.cs @@ -7,6 +7,29 @@ namespace Content.Shared.Stacks; // Partial for public API functions. public abstract partial class SharedStackSystem { + #region Spawning + // Interactions with spawned entities can not currently be predicted. + // This means that when spawning a stack it should not be given directly to the player, but have some intermediary. + + /// + /// Gets or spawns an entity with a stack count of 1. + /// Useful when you don't know if something is a stack, and want to make sure you just have a single entity. + /// + /// An entity to pop one count off the stack. + /// An entity with a stack count of 1, or a non-stack. + [PublicAPI] + public EntityUid GetOne(Entity stackEnt) + { + if (!Resolve(stackEnt.Owner, ref stackEnt.Comp, logMissing: false) // If it's not a stack, you already have the one + || stackEnt.Comp.Count == 1) // If it's at one, just use this + return stackEnt.Owner; + + ReduceCount(stackEnt, 1); + var stackId = _prototype.Index(stackEnt.Comp.StackTypeId); + return PredictedSpawnNextToOrDrop(stackId.Spawn, stackEnt.Owner); + } + + #endregion #region Merge Stacks /// diff --git a/Content.Shared/Weapons/Ranged/Components/BallisticAmmoInteractLoaderComponent.cs b/Content.Shared/Weapons/Ranged/Components/BallisticAmmoInteractLoaderComponent.cs new file mode 100644 index 0000000000..04d4a80774 --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Components/BallisticAmmoInteractLoaderComponent.cs @@ -0,0 +1,11 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Weapons.Ranged.Components; + +/// +/// If an entity with has this component, it can be used to interact +/// with the ammo entity to load it into the gun (or magazine). +/// Basically the reverse order (used vs target) to achieve the same result (loading the gun) +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class BallisticAmmoInteractLoaderComponent : Component; diff --git a/Content.Shared/Weapons/Ranged/Events/BeforeAmmoLoadedEvent.cs b/Content.Shared/Weapons/Ranged/Events/BeforeAmmoLoadedEvent.cs new file mode 100644 index 0000000000..896bfbcb6d --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Events/BeforeAmmoLoadedEvent.cs @@ -0,0 +1,21 @@ +using Content.Shared.Weapons.Ranged.Components; + +namespace Content.Shared.Weapons.Ranged.Events; + +/// +/// Raised on the ammo before it is loaded into a gun (or a magazine) +/// +[ByRefEvent] +public struct BeforeAmmoLoadedEvent() +{ + /// + /// if the entity can be used to load the gun or magazine + /// + public bool CanLoad = true; + + /// + /// If null the entity itself is used to load a gun or magazine, + /// if not null, the entity provided is used to load the gun or magazine + /// + public EntityUid? AmmoOverride; +} diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs index 831db83c3b..eb80b26b58 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs @@ -3,6 +3,7 @@ using Content.Shared.Emp; using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; +using Content.Shared.Stacks; using Content.Shared.Verbs; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Events; @@ -16,6 +17,7 @@ public abstract partial class SharedGunSystem { [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly SharedStackSystem _stack = null!; [MustCallBase] protected virtual void InitializeBallistic() @@ -34,6 +36,8 @@ public abstract partial class SharedGunSystem SubscribeLocalEvent(OnBallisticRefillerMapInit); SubscribeLocalEvent(OnRefillerEmpPulsed); + + SubscribeLocalEvent(OnBallisticAmmoLoad); } private void OnBallisticRefillerMapInit(Entity entity, ref MapInitEvent _) @@ -324,11 +328,20 @@ public abstract partial class SharedGunSystem bool suppressInsertionSound = false ) { - if (!CanInsertBallistic(entity, inserted)) + inserted = _stack.GetOne(inserted); + var ammoEv = new BeforeAmmoLoadedEvent(); + RaiseLocalEvent(inserted, ref ammoEv); + + if (!ammoEv.CanLoad) return false; - entity.Comp.Entities.Add(inserted); - Containers.Insert(inserted, entity.Comp.Container); + var ammo = ammoEv.AmmoOverride ?? inserted; + + if (!CanInsertBallistic(entity, ammo)) + return false; + + entity.Comp.Entities.Add(ammo); + Containers.Insert(ammo, entity.Comp.Container); if (!suppressInsertionSound) { Audio.PlayPredicted(entity.Comp.SoundInsert, entity, user); @@ -369,6 +382,21 @@ public abstract partial class SharedGunSystem PauseSelfRefill(entity, args.Duration); } + private void OnBallisticAmmoLoad(Entity ent, ref AfterInteractEvent args) + { + if (args.Handled || args.Target == null) + return; + + if (!TryComp(ent, out var ballisticAmmoProviderComp)) + return; + + if (TryBallisticInsert( + (ent, ballisticAmmoProviderComp), + args.Target.Value, + args.User)) + args.Handled = true; + } + private void UpdateBallistic(float frameTime) { var query = EntityQueryEnumerator(); diff --git a/Resources/Audio/Weapons/Guns/Empty/attributions.yml b/Resources/Audio/Weapons/Guns/Empty/attributions.yml new file mode 100644 index 0000000000..2789237e39 --- /dev/null +++ b/Resources/Audio/Weapons/Guns/Empty/attributions.yml @@ -0,0 +1,4 @@ +- files: ["empty_beep.ogg"] + license: "CC-BY-4.0" + copyright: "Created by altemark" + source: "https://freesound.org/people/altemark/sounds/39747" diff --git a/Resources/Audio/Weapons/Guns/Empty/empty_beep.ogg b/Resources/Audio/Weapons/Guns/Empty/empty_beep.ogg new file mode 100644 index 0000000000000000000000000000000000000000..512cd8c12fe3c3dd68bf8c155835de56d82ea1a1 GIT binary patch literal 4175 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 BjjI3v literal 0 HcmV?d00001 diff --git a/Resources/Audio/Weapons/Guns/Gunshots/attributions.yml b/Resources/Audio/Weapons/Guns/Gunshots/attributions.yml index 1a0136111c..167935d540 100644 --- a/Resources/Audio/Weapons/Guns/Gunshots/attributions.yml +++ b/Resources/Audio/Weapons/Guns/Gunshots/attributions.yml @@ -42,3 +42,8 @@ license: "CC-BY-4.0" copyright: "Created by UnderlinedDesigns and DrinkingWindGames (https://freesound.org/people/DrinkingWindGames/sounds/789647), modified by Halycon" source: "https://freesound.org/people/UnderlinedDesigns/sounds/188499/" + +- files: ["magnetic_shot.ogg"] + license: "CC-BY-4.0" + copyright: "Created by simeonradivoev" + source: "https://freesound.org/people/simeonradivoev/sounds/740218" diff --git a/Resources/Audio/Weapons/Guns/Gunshots/magnetic_shot.ogg b/Resources/Audio/Weapons/Guns/Gunshots/magnetic_shot.ogg new file mode 100644 index 0000000000000000000000000000000000000000..9f77a610bbe756d133ffc81fb28e689deb70aa72 GIT binary patch literal 9951 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 x+m&toS-Y&i(cwy!7_}6B11s24^H$p4D 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 VdX>2qIe->3c)I$ztaD0e0sw_kHMRf% literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Specific/Robotics/borgmodule.rsi/meta.json b/Resources/Textures/Objects/Specific/Robotics/borgmodule.rsi/meta.json index 345a622f3f..df98b24960 100644 --- a/Resources/Textures/Objects/Specific/Robotics/borgmodule.rsi/meta.json +++ b/Resources/Textures/Objects/Specific/Robotics/borgmodule.rsi/meta.json @@ -154,6 +154,9 @@ { "name": "icon-xenoborg-sword2" }, + { + "name": "icon-xenoborg-tile" + }, { "name": "icon-xenoborg-tools" }, diff --git a/Resources/Textures/Objects/Weapons/Guns/Basic/tilegun.rsi/icon.png b/Resources/Textures/Objects/Weapons/Guns/Basic/tilegun.rsi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5e345c10e74dda39c5979e3fac7bc44263b7176c GIT binary patch literal 410 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!7>k44ofy`glX(f`SO)lnxB_V* zAqjDDd0AOCWkq!zO#@>?Q#WV#h_HyPjI8?ldM++kLB32O?tVqyo36UQ&K^GrVRPyy zg#$GUmjw9*GyF#b)28jqyJ`=V{^0527!uL?cA7ii5d|Js-;Hyp?);zYuF*T~K+onA z9yLc^)i!GAF$AAK>7Hrf8f*P)&SE{;<~?N}EZm(AU3xh2_)f#N37oY`Q3Z3FH@98u z?K^1q^x-spf$)S=ZIaJD7#cyHa*Lx&N9ny6AC*&cf6aS6 z`PG#x-X_1Br+;C;VOh{m$U&>gTe~DWM4fq_?q@ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Weapons/Guns/Basic/tilegun.rsi/mag-1.png b/Resources/Textures/Objects/Weapons/Guns/Basic/tilegun.rsi/mag-1.png new file mode 100644 index 0000000000000000000000000000000000000000..171ad181625a3e7903d1d77c06965167cdc88102 GIT binary patch literal 191 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|ECYN(T!FNZ zkc7CnysWI6vZA_@hFJ6_CrC_enANwl{|w+{^TN% O3I Date: Fri, 3 Apr 2026 21:46:08 +0000 Subject: [PATCH 047/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 611fde4453..9219d5e6a5 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: MilenVolf - changes: - - message: Fixed some physics bugs that could cause the grappling hook to teleport - entities. - type: Fix - id: 9094 - time: '2025-10-14T10:35:38.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40899 - author: SirWarock changes: - message: Made Bodybags slightly harder to drag, and rollerbeds slightly easier. @@ -4031,3 +4023,11 @@ id: 9605 time: '2026-04-03T18:35:41.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43388 +- author: Samuka + changes: + - message: When reloading guns or magazines with stack items, it only reloads one + at the time, instead of inserting the whole stack. + type: Fix + id: 9606 + time: '2026-04-03T21:44:59.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/41503 From 8d052e7f8510bf169acc7b7ffeb5e8018451257f Mon Sep 17 00:00:00 2001 From: baynarikattu <189896646+baynarikattu@users.noreply.github.com> Date: Sat, 4 Apr 2026 01:29:08 +0300 Subject: [PATCH 048/126] fix: small typo in the restart alert V2 (#43454) fix: round end typo Co-authored-by: baynarikattu <> --- Content.Server/RoundEnd/RoundEndSystem.cs | 2 +- Resources/Locale/en-US/round-end/round-end-system.ftl | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Content.Server/RoundEnd/RoundEndSystem.cs b/Content.Server/RoundEnd/RoundEndSystem.cs index ed1ea90a16..57fabd7787 100644 --- a/Content.Server/RoundEnd/RoundEndSystem.cs +++ b/Content.Server/RoundEnd/RoundEndSystem.cs @@ -313,7 +313,7 @@ namespace Content.Server.RoundEnd Loc.GetString( "round-end-system-round-restart-eta-announcement", ("time", time), - ("units", Loc.GetString(unitsLocString)))); + ("units", Loc.GetString(unitsLocString, ("amount", time))))); Timer.Spawn(countdownTime.Value, AfterEndRoundRestart, _countdownTokenSource.Token); } diff --git a/Resources/Locale/en-US/round-end/round-end-system.ftl b/Resources/Locale/en-US/round-end/round-end-system.ftl index 30069f7171..6068e4385e 100644 --- a/Resources/Locale/en-US/round-end/round-end-system.ftl +++ b/Resources/Locale/en-US/round-end/round-end-system.ftl @@ -7,5 +7,11 @@ round-end-system-shuttle-recalled-announcement = The emergency shuttle has been round-end-system-shuttle-sender-announcement = Station round-end-system-round-restart-eta-announcement = Restarting the round in {$time} {$units}... -eta-units-minutes = minutes -eta-units-seconds = seconds +eta-units-minutes = {$amount -> + [one] minute + *[other] minutes +} +eta-units-seconds = {$amount -> + [one] second + *[other] seconds +} From 4fb78b777c498b18337c88b9e4303b2f705d30c8 Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Sat, 4 Apr 2026 01:22:43 +0200 Subject: [PATCH 049/126] fix tailwag action for changelings (#43452) --- .../Cloning/CloningSystem.Subscriptions.cs | 4 ++-- Content.Server/Wagging/WaggingSystem.cs | 8 +++++++- .../Movement/Systems/SharedJumpAbilitySystem.cs | 8 +++++--- Content.Shared/Rootable/RootableSystem.cs | 7 +++++-- Content.Shared/Sericulture/SericultureSystem.cs | 16 +++++++++------- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/Content.Server/Cloning/CloningSystem.Subscriptions.cs b/Content.Server/Cloning/CloningSystem.Subscriptions.cs index a05c7069f0..10ac8bc9ed 100644 --- a/Content.Server/Cloning/CloningSystem.Subscriptions.cs +++ b/Content.Server/Cloning/CloningSystem.Subscriptions.cs @@ -53,7 +53,7 @@ public sealed partial class CloningSystem SubscribeLocalEvent(OnCloneVocal); SubscribeLocalEvent(OnCloneStorage); SubscribeLocalEvent(OnCloneInventory); - SubscribeLocalEvent(OnCloneInventory); + SubscribeLocalEvent(OnCloneMovementSpeedModifier); } private void OnCloneItemStack(Entity ent, ref CloningItemEvent args) @@ -120,7 +120,7 @@ public sealed partial class CloningSystem _inventory.CopyComponent(ent.AsNullable(), args.CloneUid); } - private void OnCloneInventory(Entity ent, ref CloningEvent args) + private void OnCloneMovementSpeedModifier(Entity ent, ref CloningEvent args) { if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name)) return; diff --git a/Content.Server/Wagging/WaggingSystem.cs b/Content.Server/Wagging/WaggingSystem.cs index 6ece6f0d95..d369ea8689 100644 --- a/Content.Server/Wagging/WaggingSystem.cs +++ b/Content.Server/Wagging/WaggingSystem.cs @@ -35,7 +35,13 @@ public sealed class WaggingSystem : EntitySystem if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name)) return; - EnsureComp(args.CloneUid); + // Make sure to set the datafields before adding the component so that the correct action gets spawned on map init. + var cloneComp = Factory.GetComponent(); + cloneComp.Action = ent.Comp.Action; + cloneComp.Layer = ent.Comp.Layer; + cloneComp.Organ = ent.Comp.Organ; + cloneComp.Suffix = ent.Comp.Suffix; + AddComp(args.CloneUid, cloneComp, true); } private void OnWaggingMapInit(Entity ent, ref MapInitEvent args) diff --git a/Content.Shared/Movement/Systems/SharedJumpAbilitySystem.cs b/Content.Shared/Movement/Systems/SharedJumpAbilitySystem.cs index 598e4b564a..d55596ad7d 100644 --- a/Content.Shared/Movement/Systems/SharedJumpAbilitySystem.cs +++ b/Content.Shared/Movement/Systems/SharedJumpAbilitySystem.cs @@ -99,13 +99,15 @@ public sealed partial class SharedJumpAbilitySystem : EntitySystem if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name)) return; + // Make sure to set the datafields before adding the component so that the correct action gets spawned on map init. var targetComp = Factory.GetComponent(); targetComp.Action = ent.Comp.Action; - targetComp.CanCollide = ent.Comp.CanCollide; - targetComp.JumpSound = ent.Comp.JumpSound; - targetComp.CollideKnockdown = ent.Comp.CollideKnockdown; targetComp.JumpDistance = ent.Comp.JumpDistance; targetComp.JumpThrowSpeed = ent.Comp.JumpThrowSpeed; + targetComp.CanCollide = ent.Comp.CanCollide; + targetComp.CollideKnockdown = ent.Comp.CollideKnockdown; + targetComp.JumpSound = ent.Comp.JumpSound; + targetComp.JumpFailedPopup = ent.Comp.JumpFailedPopup; AddComp(args.CloneUid, targetComp, true); } } diff --git a/Content.Shared/Rootable/RootableSystem.cs b/Content.Shared/Rootable/RootableSystem.cs index 1d718dbdf3..1b9be3537b 100644 --- a/Content.Shared/Rootable/RootableSystem.cs +++ b/Content.Shared/Rootable/RootableSystem.cs @@ -121,12 +121,15 @@ public sealed class RootableSystem : EntitySystem if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name)) return; - var cloneComp = EnsureComp(args.CloneUid); + // Make sure to set the datafields before adding the component so that the correct action gets spawned on map init. + var cloneComp = Factory.GetComponent(); + cloneComp.Action = ent.Comp.Action; + cloneComp.RootedAlert = ent.Comp.RootedAlert; cloneComp.TransferRate = ent.Comp.TransferRate; cloneComp.TransferFrequency = ent.Comp.TransferFrequency; cloneComp.SpeedModifier = ent.Comp.SpeedModifier; cloneComp.RootSound = ent.Comp.RootSound; - Dirty(args.CloneUid, cloneComp); + AddComp(args.CloneUid, cloneComp, true); } private void OnRootableMapInit(Entity ent, ref MapInitEvent args) diff --git a/Content.Shared/Sericulture/SericultureSystem.cs b/Content.Shared/Sericulture/SericultureSystem.cs index fb87c907f4..e733b93483 100644 --- a/Content.Shared/Sericulture/SericultureSystem.cs +++ b/Content.Shared/Sericulture/SericultureSystem.cs @@ -41,13 +41,15 @@ public abstract partial class SharedSericultureSystem : EntitySystem if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name)) return; - var comp = EnsureComp(args.CloneUid); - comp.PopupText = ent.Comp.PopupText; - comp.ProductionLength = ent.Comp.ProductionLength; - comp.HungerCost = ent.Comp.HungerCost; - comp.EntityProduced = ent.Comp.EntityProduced; - comp.MinHungerThreshold = ent.Comp.MinHungerThreshold; - Dirty(args.CloneUid, comp); + // Make sure to set the datafields before adding the component so that the correct action gets spawned on map init. + var cloneComp = Factory.GetComponent(); + cloneComp.PopupText = ent.Comp.PopupText; + cloneComp.EntityProduced = ent.Comp.EntityProduced; + cloneComp.Action = ent.Comp.Action; + cloneComp.ProductionLength = ent.Comp.ProductionLength; + cloneComp.HungerCost = ent.Comp.HungerCost; + cloneComp.MinHungerThreshold = ent.Comp.MinHungerThreshold; + AddComp(args.CloneUid, cloneComp, true); } /// From 85b4a5d0f604ee68e7337f682de4a3d9efc92e55 Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Sat, 4 Apr 2026 02:29:43 +0200 Subject: [PATCH 050/126] changeling devour cleanup (#43451) * devour cleanup * fix audio cancel * fix for transform as well --- .../Components/ChangelingDevourComponent.cs | 57 +++++------ .../Systems/ChangelingDevourSystem.cs | 99 ++++++++----------- .../Systems/ChangelingTransformSystem.cs | 7 +- .../Locale/en-US/changeling/changeling.ftl | 3 +- 4 files changed, 75 insertions(+), 91 deletions(-) diff --git a/Content.Shared/Changeling/Components/ChangelingDevourComponent.cs b/Content.Shared/Changeling/Components/ChangelingDevourComponent.cs index f2c5c82ca9..f0a7bb44ba 100644 --- a/Content.Shared/Changeling/Components/ChangelingDevourComponent.cs +++ b/Content.Shared/Changeling/Components/ChangelingDevourComponent.cs @@ -1,12 +1,10 @@ using Content.Shared.Changeling.Systems; using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; -using Content.Shared.FixedPoint; using Content.Shared.Whitelist; using Robust.Shared.Audio; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared.Changeling.Components; @@ -14,24 +12,24 @@ namespace Content.Shared.Changeling.Components; /// Component responsible for Changelings Devour attack. Including the amount of damage /// and how long it takes to devour someone /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] [Access(typeof(ChangelingDevourSystem))] public sealed partial class ChangelingDevourComponent : Component { /// - /// The Action for devouring + /// The action for devouring. /// [DataField] public EntProtoId? ChangelingDevourAction = "ActionChangelingDevour"; /// - /// The action entity associated with devouring + /// The action entity associated with devouring. /// [DataField, AutoNetworkedField] public EntityUid? ChangelingDevourActionEntity; /// - /// The whitelist of targets for devouring + /// The whitelist of targets for devouring. /// [DataField, AutoNetworkedField] public EntityWhitelist? Whitelist = new() @@ -44,7 +42,7 @@ public sealed partial class ChangelingDevourComponent : Component }; /// - /// The Sound to use during consumption of a victim + /// The sound to use during consumption of a victim. /// /// /// 6 distance due to the default 15 being hearable all the way across PVS. Changeling is meant to be stealthy. @@ -54,7 +52,7 @@ public sealed partial class ChangelingDevourComponent : Component public SoundSpecifier? ConsumeNoise = new SoundCollectionSpecifier("ChangelingDevourConsume", AudioParams.Default.WithMaxDistance(6)); /// - /// The Sound to use during the windup before consuming a victim + /// The sound to use during the windup before consuming a victim. /// /// /// 6 distance due to the default 15 being hearable all the way across PVS. Changeling is meant to be stealthy. @@ -64,36 +62,31 @@ public sealed partial class ChangelingDevourComponent : Component public SoundSpecifier? DevourWindupNoise = new SoundCollectionSpecifier("ChangelingDevourWindup", AudioParams.Default.WithMaxDistance(6)); /// - /// The time between damage ticks - /// - [DataField, AutoNetworkedField] - public TimeSpan DamageTimeBetweenTicks = TimeSpan.FromSeconds(1); - - /// - /// The windup time before the changeling begins to engage in devouring the identity of a target + /// The windup time before the changeling begins to engage in devouring the identity of a target. /// [DataField, AutoNetworkedField] public TimeSpan DevourWindupTime = TimeSpan.FromSeconds(2); /// - /// The time it takes to FULLY consume someones identity. + /// The time it takes to consume someones identity. + /// Starts after the windup. /// [DataField, AutoNetworkedField] public TimeSpan DevourConsumeTime = TimeSpan.FromSeconds(10); /// - /// The Currently active devour sound in the world + /// The currently active devour sound in the world. /// [DataField] public EntityUid? CurrentDevourSound; /// - /// The damage profile for a single tick of devour damage + /// The damage dealt after the windup finished and devouring started. /// [DataField, AutoNetworkedField] - public DamageSpecifier DamagePerTick = new() + public DamageSpecifier WindupDamage = new() { - DamageDict = new () + DamageDict = new() { { "Slash", 10}, { "Piercing", 10 }, @@ -102,7 +95,21 @@ public sealed partial class ChangelingDevourComponent : Component }; /// - /// The list of protective damage types capable of preventing a devour if over the threshold + /// The damage dealt after the devouring is fully finished. + /// + [DataField, AutoNetworkedField] + public DamageSpecifier DevourDamage = new() + { + DamageDict = new() + { + { "Slash", 20}, + { "Piercing", 20 }, + { "Blunt", 10 }, + }, + }; + + /// + /// The list of protective damage types capable of preventing a devour if over the threshold. /// [DataField, AutoNetworkedField] public List> ProtectiveDamageTypes = new() @@ -113,13 +120,7 @@ public sealed partial class ChangelingDevourComponent : Component }; /// - /// The next Tick to deal damage on (utilized during the consumption "do-during" (a do after with an attempt event)) - /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField] - public TimeSpan NextTick = TimeSpan.Zero; - - /// - /// The percentage of ANY brute damage resistance that will prevent devouring + /// The percentage of ANY brute damage resistance that will prevent devouring. /// [DataField, AutoNetworkedField] public float DevourPreventionPercentageThreshold = 0.1f; diff --git a/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs b/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs index 3406038e9c..d08e6315ce 100644 --- a/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs +++ b/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs @@ -2,9 +2,7 @@ using Content.Shared.Actions; using Content.Shared.Administration.Logs; using Content.Shared.Armor; using Content.Shared.Atmos.Rotting; -using Content.Shared.Body; using Content.Shared.Changeling.Components; -using Content.Shared.Damage.Components; using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.DoAfter; @@ -19,13 +17,11 @@ using Content.Shared.Whitelist; using Robust.Shared.Audio.Systems; using Robust.Shared.Network; using Robust.Shared.Random; -using Robust.Shared.Timing; namespace Content.Shared.Changeling.Systems; public sealed class ChangelingDevourSystem : EntitySystem { - [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly INetManager _net = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; @@ -47,7 +43,6 @@ public sealed class ChangelingDevourSystem : EntitySystem SubscribeLocalEvent(OnDevourAction); SubscribeLocalEvent(OnDevourWindup); SubscribeLocalEvent(OnDevourConsume); - SubscribeLocalEvent>(OnConsumeAttemptTick); SubscribeLocalEvent(OnShutdown); } @@ -64,29 +59,6 @@ public sealed class ChangelingDevourSystem : EntitySystem } } - //TODO: Allow doafters to have proper update loop support. Attempt events should not be doing state changes. - private void OnConsumeAttemptTick(Entity ent, - ref DoAfterAttemptEvent eventData) - { - - var curTime = _timing.CurTime; - - if (curTime < ent.Comp.NextTick) - return; - - ConsumeDamageTick(eventData.Event.Target, ent.Comp, eventData.Event.User); - ent.Comp.NextTick += ent.Comp.DamageTimeBetweenTicks; - Dirty(ent, ent.Comp); - } - - private void ConsumeDamageTick(EntityUid? target, ChangelingDevourComponent comp, EntityUid? user) - { - if (target == null) - return; - - _damageable.ChangeDamage(target.Value, comp.DamagePerTick, true, true, user); - } - /// /// Checkes if the targets outerclothing is beyond a DamageCoefficientThreshold to protect them from being devoured. /// @@ -110,10 +82,13 @@ public sealed class ChangelingDevourSystem : EntitySystem return false; } + // The action was used. + // Start the first doafter for the windup. private void OnDevourAction(Entity ent, ref ChangelingDevourActionEvent args) { - if (args.Handled || _whitelistSystem.IsWhitelistFailOrNull(ent.Comp.Whitelist, args.Target) - || !HasComp(ent)) + if (args.Handled + || _whitelistSystem.IsWhitelistFailOrNull(ent.Comp.Whitelist, args.Target) + || !HasComp(ent)) return; args.Handled = true; @@ -122,6 +97,12 @@ public sealed class ChangelingDevourSystem : EntitySystem if (target == ent.Owner) return; // don't eat yourself + if (!_mobState.IsDead(target)) + { + _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-not-dead"), args.Performer, args.Performer, PopupType.Medium); + return; + } + if (HasComp(target)) { _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-rotting"), args.Performer, args.Performer, PopupType.Medium); @@ -136,6 +117,7 @@ public sealed class ChangelingDevourSystem : EntitySystem if (_net.IsServer) { + ent.Comp.CurrentDevourSound = _audio.Stop(ent.Comp.CurrentDevourSound); var pvsSound = _audio.PlayPvs(ent.Comp.DevourWindupNoise, ent); if (pvsSound != null) ent.Comp.CurrentDevourSound = pvsSound.Value.Entity; @@ -146,7 +128,7 @@ public sealed class ChangelingDevourSystem : EntitySystem _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, ent, ent.Comp.DevourWindupTime, new ChangelingDevourWindupDoAfterEvent(), ent, target: target, used: ent) { BreakOnMove = true, - BlockDuplicate = true, + CancelDuplicate = true, DuplicateCondition = DuplicateConditions.None, }); @@ -160,17 +142,21 @@ public sealed class ChangelingDevourSystem : EntitySystem PopupType.MediumCaution); } + // First doafter finished. + // Start the second doafter for the actual consumption and deal a small amount of damage. private void OnDevourWindup(Entity ent, ref ChangelingDevourWindupDoAfterEvent args) { - var curTime = _timing.CurTime; args.Handled = true; - - if (!Exists(ent.Comp.CurrentDevourSound)) - _audio.Stop(ent.Comp.CurrentDevourSound!); + ent.Comp.CurrentDevourSound = _audio.Stop(ent.Comp.CurrentDevourSound); if (args.Cancelled) return; + if (args.Target is not { } target) + return; + + _damageable.ChangeDamage(target, ent.Comp.WindupDamage, true, true, ent.Owner); + var selfMessage = Loc.GetString("changeling-devour-begin-consume-self", ("user", Identity.Entity(ent.Owner, EntityManager))); var othersMessage = Loc.GetString("changeling-devour-begin-consume-others", ("user", Identity.Entity(ent.Owner, EntityManager))); _popupSystem.PopupPredicted( @@ -188,44 +174,41 @@ public sealed class ChangelingDevourSystem : EntitySystem ent.Comp.CurrentDevourSound = pvsSound.Value.Entity; } - - ent.Comp.NextTick = curTime + ent.Comp.DamageTimeBetweenTicks; - - _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} began to devour {ToPrettyString(args.Target):player} identity"); + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} began to devour {ToPrettyString(target):player}'s identity"); _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, ent, ent.Comp.DevourConsumeTime, new ChangelingDevourConsumeDoAfterEvent(), ent, - target: args.Target, + target: target, used: ent) { - AttemptFrequency = AttemptFrequency.EveryTick, BreakOnMove = true, - BlockDuplicate = true, + CancelDuplicate = true, DuplicateCondition = DuplicateConditions.None, }); } + // Second doafter finished. + // Save the identity and deal more damage. private void OnDevourConsume(Entity ent, ref ChangelingDevourConsumeDoAfterEvent args) { args.Handled = true; - var target = args.Target; - - if (target == null) - return; - - if (Exists(ent.Comp.CurrentDevourSound)) - _audio.Stop(ent.Comp.CurrentDevourSound!); + ent.Comp.CurrentDevourSound = _audio.Stop(ent.Comp.CurrentDevourSound); if (args.Cancelled) return; - if (!_mobState.IsDead((EntityUid)target)) + if (args.Target is not { } target) + return; + + _damageable.ChangeDamage(target, ent.Comp.DevourDamage, true, true, ent.Owner); + + if (!_mobState.IsDead(target)) { - _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} unsuccessfully devoured {ToPrettyString(args.Target):player}'s identity"); - _popupSystem.PopupClient(Loc.GetString("changeling-devour-consume-failed-not-dead"), args.User, args.User, PopupType.Medium); + _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-not-dead"), args.User, args.User, PopupType.Medium); + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} unsuccessfully devoured {ToPrettyString(args.Target):player}'s identity"); return; } @@ -238,22 +221,22 @@ public sealed class ChangelingDevourSystem : EntitySystem args.User, PopupType.LargeCaution); - if (_mobState.IsDead(target.Value) - && TryComp(target, out var body) + if (_mobState.IsDead(target) && HasComp(target) && TryComp(args.User, out var identityStorage)) { - _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} successfully devoured {ToPrettyString(args.Target):player}'s identity"); - _changelingIdentitySystem.CloneToPausedMap((ent, identityStorage), target.Value); + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} successfully devoured {ToPrettyString(target):player}'s identity"); + _changelingIdentitySystem.CloneToPausedMap((ent, identityStorage), target); - if (_inventorySystem.TryGetSlotEntity(target.Value, "jumpsuit", out var item) + if (_inventorySystem.TryGetSlotEntity(target, "jumpsuit", out var item) && TryComp(item, out var butcherable)) - RipClothing(target.Value, (item.Value, butcherable)); + RipClothing(target, (item.Value, butcherable)); } Dirty(ent); } + // TODO: This should just be an API method in the butcher system private void RipClothing(EntityUid victim, Entity item) { var spawnEntities = EntitySpawnCollection.GetSpawns(item.Comp.SpawnedEntities, _robustRandom); diff --git a/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs b/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs index 5f9cc9526b..94d2cd77a8 100644 --- a/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs +++ b/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs @@ -99,7 +99,10 @@ public sealed partial class ChangelingTransformSystem : EntitySystem PopupType.MediumCaution); if (_net.IsServer) + { + ent.Comp.CurrentTransformSound = _audio.Stop(ent.Comp.CurrentTransformSound); // cancel any previous sounds first ent.Comp.CurrentTransformSound = _audio.PlayPvs(ent.Comp.TransformAttemptNoise, ent)?.Entity; + } if (TryComp(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null) _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} begun an attempt to transform into \"{Name(targetIdentity)}\" ({storedIdentity.OriginalSession:player}) "); @@ -146,9 +149,7 @@ public sealed partial class ChangelingTransformSystem : EntitySystem ref ChangelingTransformDoAfterEvent args) { args.Handled = true; - - if (Exists(ent.Comp.CurrentTransformSound)) - _audio.Stop(ent.Comp.CurrentTransformSound); + ent.Comp.CurrentTransformSound = _audio.Stop(ent.Comp.CurrentTransformSound); if (args.Cancelled) return; diff --git a/Resources/Locale/en-US/changeling/changeling.ftl b/Resources/Locale/en-US/changeling/changeling.ftl index 57ad3550bf..e33e5bc715 100644 --- a/Resources/Locale/en-US/changeling/changeling.ftl +++ b/Resources/Locale/en-US/changeling/changeling.ftl @@ -1,6 +1,7 @@ roles-antag-changeling-name = Changeling roles-antag-changeling-objective = A intelligent predator that assumes the identities of its victims. +changeling-devour-attempt-failed-not-dead = This body yet lives! We cannot consume it alive! changeling-devour-attempt-failed-rotting = This corpse has only rotted biomass. changeling-devour-attempt-failed-protected = This victim's biomass is protected by armor! @@ -8,8 +9,6 @@ changeling-devour-begin-windup-self = Our uncanny mouth reveals itself with othe changeling-devour-begin-windup-others = { CAPITALIZE(POSS-ADJ($user)) } uncanny mouth reveals itself with otherworldly hunger. changeling-devour-begin-consume-self = The uncanny mouth digs deep into its victim. changeling-devour-begin-consume-others = { CAPITALIZE(POSS-ADJ($user)) } uncanny mouth digs deep into { POSS-ADJ($user) } victim. - -changeling-devour-consume-failed-not-dead = This body yet lives! We cannot consume it alive! changeling-devour-consume-complete-self = Our uncanny mouth retreats, biomass consumed. changeling-devour-consume-complete-others = { CAPITALIZE(POSS-ADJ($user)) } uncanny mouth retreats. From af5819e30e7a42aea135fec90a55d8835c12eafa Mon Sep 17 00:00:00 2001 From: 0-Anon Date: Fri, 3 Apr 2026 23:51:43 -0400 Subject: [PATCH 051/126] Traitor Syndicate Reinforcement rebalance (#43057) * Traitor Reinforcement Rebalance As per discussion with traitor workgroup, there was consensus that the reinforcements felt extremely underwhelming to use. As a result I took a stab at reworking their kits to make them all feel more unique, specialized into their respective niche, and FUNCTIONAL in that niche. Medic is now a dedicated medic with a flimsy but existent cover story, Spy is now a great low-investment / immediate-value pick, and Thief is now a bit better at breakins and other aggressive tactics while leaning into being a very obvious foreign invader. * camera bug instead of operative ID card I may have gotten too silly, so I'm drawing this back a bit. * Backpack Space Errors Put chameleon in hand, changed backpack to duffel to fit in the survival box, * Remove Necrosol, Add Viper When you think about it lead is the best medicine because your patient never gets sick again --- Resources/Prototypes/Roles/Antags/traitor.yml | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/Resources/Prototypes/Roles/Antags/traitor.yml b/Resources/Prototypes/Roles/Antags/traitor.yml index edc130ef8b..2920b2c56f 100644 --- a/Resources/Prototypes/Roles/Antags/traitor.yml +++ b/Resources/Prototypes/Roles/Antags/traitor.yml @@ -52,30 +52,53 @@ - type: startingGear id: SyndicateReinforcementMedic - parent: SyndicateOperativeClothing equipment: - pocket1: WeaponPistolViper + jumpsuit: ClothingUniformJumpsuitOperative + shoes: ClothingShoesColorRed + gloves: ClothingHandsGlovesLatex + head: ClothingHeadHatSurgcapPurple + id: VisitorPDA + belt: ClothingBeltMedicalFilled + back: ClothingBackpackDuffelMedical + ears: ClothingHeadsetMedical + eyes: ClothingEyesHudMedical + pocket1: HandheldHealthAnalyzer + pocket2: PillCanisterBicaridine inhand: - MedkitCombatFilled - + storage: + back: + - Defibrillator + - CigPackSyndicate + - UniformScrubsColorPurple + - ChemistryBottleToxin + - WeaponPistolViper + - Syringe + - Syringe + - type: startingGear id: SyndicateReinforcementSpy - parent: SyndicateOperativeClothing equipment: - id: AgentIDCard - mask: ClothingMaskGasVoiceChameleon + jumpsuit: ClothingUniformJumpsuitOperative + back: ClothingBackpack + shoes: ClothingShoesColorBlack + gloves: ClothingHandsGlovesColorBlack pocket1: WeaponPistolViper - + pocket2: VoiceMaskImplanter + inhand: + - ClothingBackpackChameleonFillAgent - type: startingGear id: SyndicateReinforcementThief parent: SyndicateOperativeClothing equipment: pocket1: WeaponPistolViper + pocket2: MagazinePistolHighCapacity inhand: - ToolboxSyndicateFilled storage: back: - SyndicateJawsOfLife + - CameraBug #Syndicate Operative Outfit - Basic - type: startingGear From cfb6e2c376776eac8d960e86b6de8b809144cc15 Mon Sep 17 00:00:00 2001 From: PJBot Date: Sat, 4 Apr 2026 04:07:39 +0000 Subject: [PATCH 052/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 9219d5e6a5..ff82a9595f 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: SirWarock - changes: - - message: Made Bodybags slightly harder to drag, and rollerbeds slightly easier. - They have wheels. - type: Tweak - id: 9095 - time: '2025-10-14T11:41:30.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40880 - author: aada changes: - message: Geiger counters can now be placed on a utility belt. @@ -4031,3 +4023,11 @@ id: 9606 time: '2026-04-03T21:44:59.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/41503 +- author: 0-Anon + changes: + - message: Rebalanced the syndicate reinforcement specializations to be more effective + at their roles. + type: Tweak + id: 9607 + time: '2026-04-04T04:06:27.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43057 From 8fca1e064effacef4f1b3bab8b376be7b200d009 Mon Sep 17 00:00:00 2001 From: B_Kirill <153602297+B-Kirill@users.noreply.github.com> Date: Sat, 4 Apr 2026 16:43:03 +1000 Subject: [PATCH 053/126] Fix accessing deleted entities in SharedMeleeWeaponSystem (#42340) * Fix accessing deleted entities in SharedMeleeWeaponSystem * retry * Review --- .../Weapons/Melee/SharedMeleeWeaponSystem.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs index f827249dd3..02d561d942 100644 --- a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs +++ b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs @@ -574,7 +574,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem _meleeSound.PlayHitSound(target.Value, user, GetHighestDamageSound(modifiedDamage, _protoManager), hitEvent.HitSoundOverride, component); - if (damageResult.GetTotal() > FixedPoint2.Zero) + if (damageResult.GetTotal() > FixedPoint2.Zero && !TerminatingOrDeleted(target.Value)) { DoDamageEffect(targets, user, targetXform); } @@ -633,7 +633,15 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem // Validate client for (var i = entities.Count - 1; i >= 0; i--) { - if (ArcRaySuccessful(entities[i], + var entity = entities[i]; + + if (TerminatingOrDeleted(entity)) + { + entities.RemoveAt(i); + continue; + } + + if (!ArcRaySuccessful(entity, userPos, direction.ToWorldAngle(), component.Angle, @@ -642,11 +650,9 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem user, session)) { - continue; + // Bad input + entities.RemoveAt(i); } - - // Bad input - entities.RemoveAt(i); } var targets = new List(); @@ -728,6 +734,9 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem $"{ToPrettyString(user):actor} melee attacked (heavy) {ToPrettyString(entity):subject} using {ToPrettyString(meleeUid):tool} and dealt {damageResult.GetTotal():damage} damage"); } } + + if (TerminatingOrDeleted(entity)) + targets.RemoveAt(i); } if (entities.Count != 0) @@ -736,7 +745,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem _meleeSound.PlayHitSound(target, user, GetHighestDamageSound(appliedDamage, _protoManager), hitEvent.HitSoundOverride, component); } - if (appliedDamage.GetTotal() > FixedPoint2.Zero) + if (appliedDamage.GetTotal() > FixedPoint2.Zero && targets.Count > 0) { DoDamageEffect(targets, user, Transform(targets[0])); } From 85550217dcd2d8c221c7cc35172864b4af51a7a4 Mon Sep 17 00:00:00 2001 From: Spanky <180730777+spanky-spanky@users.noreply.github.com> Date: Sat, 4 Apr 2026 03:00:50 -0400 Subject: [PATCH 054/126] Make Chargers Items (Pickupable) (#40645) * Itemize and add in-hands for tabletop chargers. * Tweak inhands to be vox friendly * Update copyright * Parent chargers off of BaseItem * Unparent chargers off BaseItem since it doesn't work, for now --- .../Entities/Structures/Power/chargers.yml | 9 +++++++++ .../Power/cell_recharger.rsi/inhand-left.png | Bin 0 -> 788 bytes .../Power/cell_recharger.rsi/inhand-right.png | Bin 0 -> 793 bytes .../Structures/Power/cell_recharger.rsi/meta.json | 10 +++++++++- .../Power/recharger.rsi/inhand-left.png | Bin 0 -> 751 bytes .../Power/recharger.rsi/inhand-right.png | Bin 0 -> 751 bytes .../Structures/Power/recharger.rsi/meta.json | 10 +++++++++- .../Power/turbo_recharger.rsi/inhand-left.png | Bin 0 -> 862 bytes .../Power/turbo_recharger.rsi/inhand-right.png | Bin 0 -> 862 bytes .../Power/turbo_recharger.rsi/meta.json | 10 +++++++++- 10 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 Resources/Textures/Structures/Power/cell_recharger.rsi/inhand-left.png create mode 100644 Resources/Textures/Structures/Power/cell_recharger.rsi/inhand-right.png create mode 100644 Resources/Textures/Structures/Power/recharger.rsi/inhand-left.png create mode 100644 Resources/Textures/Structures/Power/recharger.rsi/inhand-right.png create mode 100644 Resources/Textures/Structures/Power/turbo_recharger.rsi/inhand-left.png create mode 100644 Resources/Textures/Structures/Power/turbo_recharger.rsi/inhand-right.png diff --git a/Resources/Prototypes/Entities/Structures/Power/chargers.yml b/Resources/Prototypes/Entities/Structures/Power/chargers.yml index e80bb90941..469de4bad3 100644 --- a/Resources/Prototypes/Entities/Structures/Power/chargers.yml +++ b/Resources/Prototypes/Entities/Structures/Power/chargers.yml @@ -111,6 +111,9 @@ blacklist: tags: - PotatoBattery + - type: Item + size: Ginormous + - type: MultiHandedItem - type: entity parent: [ BaseItemRecharger, ConstructibleMachine ] @@ -175,6 +178,9 @@ blacklist: tags: - PotatoBattery + - type: Item + size: Ginormous + - type: MultiHandedItem - type: entity parent: BaseItemRecharger @@ -200,6 +206,9 @@ blacklist: tags: - PotatoBattery + - type: Item + size: Ginormous + - type: MultiHandedItem - type: entity parent: [ BaseItemRecharger, BaseWallmount ] diff --git a/Resources/Textures/Structures/Power/cell_recharger.rsi/inhand-left.png b/Resources/Textures/Structures/Power/cell_recharger.rsi/inhand-left.png new file mode 100644 index 0000000000000000000000000000000000000000..12d689adf9f9a16da40824dacc760142e1f35ed2 GIT binary patch literal 788 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 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Power/turbo_recharger.rsi/inhand-right.png b/Resources/Textures/Structures/Power/turbo_recharger.rsi/inhand-right.png new file mode 100644 index 0000000000000000000000000000000000000000..40ab6caf127f4211e8c0d2f077f281ae8ca358a8 GIT binary patch literal 862 zcmV-k1EKthP)?9?qJcNAO+DF2Dw;+8mP7C^ie!q9UVVprTTsDtiChr>V`) zwbI=|aJ|^}$29csL!)NfKPR@`I0ff3T$MWhavc`Gz**~P-ed6Wz{Pb(OCP}HE{s3v zDI?XCoPa~q;Qbs%5-@rPLmREG+V`A2LTnqtw{Um_Ga7g{e*u+*XW2_XP7?qC0kcU& zK~#90?b<<00znuC;Md565Gqfd{RN(bAof&Oe}GWo#e+x)K|iB=1>GVOA&P>KPMwV~ zJcSgg2bm@hd(gvnh!v-FS)Ey>cpng!nc1C>&)vQ33lI?z5fKrQKbdsS=?L3-t2dQj zN7zm#opWYg-#F$cIjPZT;^MqCa_%pgr~%P%0r`VF06;d?1ppkLE&u?w)}_h%0vGbT zFq6(XJGOYj}FB0sy4lLTq&r#bQxelT0Qd z?Uwl}1LV5_(r)4MwxEnzmIWaM03a5NnXhgN#(D=xDV5`Lxvb1vmSwK8De>KacDw!S zIFU#wYlINWIwB$>A|fIpA|fIpqJLvlPQYYVAm}|N`aJIP$_cFf%7w}rU z?q>sn$O?F65wfW+qG4O>`dZ)d2Qve%tCp}9y;c?s3uUl5PSwTc=?rBczl!?YTJADIEKG1GgPqTK*D oA>taABhbOT0TB@q5m6w%0EEN+Y!G@fIRF3v07*qoM6N<$g2>B!B>(^b literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Power/turbo_recharger.rsi/meta.json b/Resources/Textures/Structures/Power/turbo_recharger.rsi/meta.json index 50271d1e33..0483760670 100644 --- a/Resources/Textures/Structures/Power/turbo_recharger.rsi/meta.json +++ b/Resources/Textures/Structures/Power/turbo_recharger.rsi/meta.json @@ -5,7 +5,7 @@ "y": 32 }, "license": "CC-BY-SA-3.0", - "copyright": "adapted from https://github.com/discordia-space/CEV-Eris/raw/9ea3eccbe22e18d24653949067f3d7dd12194ea9/icons/obj/stationobjs.dmi by EmoGarbage404 (github)", + "copyright": "adapted from https://github.com/discordia-space/CEV-Eris/raw/9ea3eccbe22e18d24653949067f3d7dd12194ea9/icons/obj/stationobjs.dmi by EmoGarbage404 (github), in-hands by spanky-spanky (GitHub)", "states": [ { "name": "empty" @@ -57,6 +57,14 @@ 0.05 ] ] + }, + { + "name": "inhand-right", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 } ] } From b365636086a3786f7424ba1d2064262e97a304fa Mon Sep 17 00:00:00 2001 From: PJBot Date: Sat, 4 Apr 2026 07:16:32 +0000 Subject: [PATCH 055/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index ff82a9595f..9f6b2cac26 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: aada - changes: - - message: Geiger counters can now be placed on a utility belt. - type: Fix - id: 9096 - time: '2025-10-14T12:12:06.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40898 - author: kontakt changes: - message: Singulo now consumes carpet tiles. @@ -4031,3 +4024,11 @@ id: 9607 time: '2026-04-04T04:06:27.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43057 +- author: spanky-spanky + changes: + - message: Cell rechargers, weapon rechargers and turbo rechargers can now be picked + up to transport them when unanchored. + type: Tweak + id: 9608 + time: '2026-04-04T07:15:23.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/40645 From 6afc7919e3f1a22c9c700ffe81af582a2ae4116a Mon Sep 17 00:00:00 2001 From: ThatGuyUSA Date: Sat, 4 Apr 2026 08:36:41 -0700 Subject: [PATCH 056/126] [BUGFIX] Midround Wizards now show up on the end results screen (#42766) very shrimple --- Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml | 1 + Resources/Prototypes/GameRules/events.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml index e620f376c5..1ac053f872 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml @@ -349,6 +349,7 @@ - type: GhostRole name: ghost-role-information-wizard-name description: ghost-role-information-wizard-desc + rules: ghost-role-information-antagonist-rules mindRoles: - MindRoleWizard raffle: diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index 8c46bae789..6368d2e23e 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -323,7 +323,7 @@ components: - type: StationEvent weight: 1 # rare - duration: 1 + duration: null earliestStart: 30 reoccurrenceDelay: 60 minimumPlayers: 30 From d521606348620687d1442c8ddbb3515a0e3acf19 Mon Sep 17 00:00:00 2001 From: PJBot Date: Sat, 4 Apr 2026 15:52:22 +0000 Subject: [PATCH 057/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 9f6b2cac26..fc9d8f8224 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: kontakt - changes: - - message: Singulo now consumes carpet tiles. - type: Fix - id: 9097 - time: '2025-10-14T18:27:58.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40896 - author: B_Kirill changes: - message: Fixed blocked rotation of zombie NPCs. @@ -4032,3 +4025,10 @@ id: 9608 time: '2026-04-04T07:15:23.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/40645 +- author: ThatGuyUSA + changes: + - message: Midround Wizards now show up on the end results screen. + type: Fix + id: 9609 + time: '2026-04-04T15:51:14.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/42766 From 92fa40b79dceeb51b950b35738c4e45233e4f639 Mon Sep 17 00:00:00 2001 From: Zekins <136648667+Zekins3366@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:32:42 +0300 Subject: [PATCH 058/126] Fix Human HeadTop (#43465) forgottop --- Resources/Prototypes/Body/Species/human.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Resources/Prototypes/Body/Species/human.yml b/Resources/Prototypes/Body/Species/human.yml index b569527a61..d6c76015e5 100644 --- a/Resources/Prototypes/Body/Species/human.yml +++ b/Resources/Prototypes/Body/Species/human.yml @@ -2,6 +2,9 @@ parent: Undergarments id: Human limits: + enum.HumanoidVisualLayers.HeadTop: + limit: 1 + required: false enum.HumanoidVisualLayers.Hair: limit: 1 required: false From f41670c77028176926f78328285da2087e4b120f Mon Sep 17 00:00:00 2001 From: PJBot Date: Sat, 4 Apr 2026 16:49:12 +0000 Subject: [PATCH 059/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index fc9d8f8224..2ec57487ce 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: B_Kirill - changes: - - message: Fixed blocked rotation of zombie NPCs. - type: Fix - id: 9098 - time: '2025-10-14T19:12:36.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40812 - author: temm1ie, aksosotl changes: - message: Added new botany-themed poster. @@ -4032,3 +4025,11 @@ id: 9609 time: '2026-04-04T15:51:14.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/42766 +- author: Zekins3366 + changes: + - message: Fixed being able to set multiple conflicting head-top visuals on humans + at the same time. + type: Fix + id: 9610 + time: '2026-04-04T16:48:03.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43465 From 17490ca615a8a51c36684c3813750af5ce301e14 Mon Sep 17 00:00:00 2001 From: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:40:51 +0200 Subject: [PATCH 060/126] Add random sell value to player mobs (#43445) * Initial commit * Review fixes --- .../Cargo/Components/RandomPriceComponent.cs | 38 +++++++++++++++++++ Content.Server/Cargo/Systems/PricingSystem.cs | 37 +++++++++++++++++- Resources/Prototypes/Body/species_base.yml | 2 + .../Prototypes/Entities/Mobs/Player/clone.yml | 1 + 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 Content.Server/Cargo/Components/RandomPriceComponent.cs diff --git a/Content.Server/Cargo/Components/RandomPriceComponent.cs b/Content.Server/Cargo/Components/RandomPriceComponent.cs new file mode 100644 index 0000000000..3ece8334e6 --- /dev/null +++ b/Content.Server/Cargo/Components/RandomPriceComponent.cs @@ -0,0 +1,38 @@ +using Content.Server.Cargo.Systems; + +namespace Content.Server.Cargo.Components; + +/// +/// Adds a random value between 0 and X to an entity's sell value. +/// +[RegisterComponent, Access(typeof(PricingSystem))] +public sealed partial class RandomPriceComponent : Component +{ + /// + /// The max random price the entity may be priced at. Non-inclusive. + /// + [DataField(required: true)] + public double MaxRandomPrice; + + /// + /// How the random pricing modifier (0.0 - 1.0) should be distributed. + /// + [DataField] + public RandomPricingCurve PricingCurve = RandomPricingCurve.Cubed; + + /// + /// The generated price for the specific entity. + /// + [DataField] + public double? RandomPrice = null; +} + +/// +/// The random distribution used when generating a random price. +/// +public enum RandomPricingCurve +{ + Linear, + Squared, + Cubed, +} diff --git a/Content.Server/Cargo/Systems/PricingSystem.cs b/Content.Server/Cargo/Systems/PricingSystem.cs index 0101b913d6..bbf8176035 100644 --- a/Content.Server/Cargo/Systems/PricingSystem.cs +++ b/Content.Server/Cargo/Systems/PricingSystem.cs @@ -8,13 +8,14 @@ using Content.Shared.Chemistry.Reagent; using Content.Shared.Materials; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; +using Content.Shared.Research.Prototypes; using Content.Shared.Stacks; using Robust.Shared.Console; using Robust.Shared.Containers; using Robust.Shared.Map.Components; using Robust.Shared.Prototypes; +using Robust.Shared.Random; using Robust.Shared.Utility; -using Content.Shared.Research.Prototypes; namespace Content.Server.Cargo.Systems; @@ -24,6 +25,7 @@ namespace Content.Server.Cargo.Systems; public sealed class PricingSystem : EntitySystem { [Dependency] private readonly IConsoleHost _consoleHost = default!; + [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; @@ -32,6 +34,8 @@ public sealed class PricingSystem : EntitySystem public override void Initialize() { SubscribeLocalEvent(CalculateMobPrice); + SubscribeLocalEvent(SetRandomPrice); + SubscribeLocalEvent(CalculateRandomPrice); _consoleHost.RegisterCommand("appraisegrid", "Calculates the total value of the given grids.", @@ -95,6 +99,37 @@ public sealed class PricingSystem : EntitySystem args.Price += component.Price * (_mobStateSystem.IsAlive(uid, state) ? 1.0 : component.DeathPenalty); } + private void SetRandomPrice(Entity entity, ref MapInitEvent args) + { + if (entity.Comp.RandomPrice == null) + { + var modifier = _random.NextDouble(); + switch (entity.Comp.PricingCurve) + { + default: + case RandomPricingCurve.Linear: + break; + case RandomPricingCurve.Squared: + modifier = modifier * modifier; + break; + case RandomPricingCurve.Cubed: + modifier = modifier * modifier * modifier; + break; + } + + entity.Comp.RandomPrice = modifier * entity.Comp.MaxRandomPrice; + } + } + + private void CalculateRandomPrice(Entity entity, ref PriceCalculationEvent args) + { + // TODO: Estimated pricing. + if (args.Handled) + return; + + args.Price += entity.Comp.RandomPrice ?? 0; + } + private double GetSolutionPrice(Entity entity) { if (Comp(entity).EntityLifeStage < EntityLifeStage.MapInitialized) diff --git a/Resources/Prototypes/Body/species_base.yml b/Resources/Prototypes/Body/species_base.yml index 1f782b0bc3..f5c428fe1e 100644 --- a/Resources/Prototypes/Body/species_base.yml +++ b/Resources/Prototypes/Body/species_base.yml @@ -151,6 +151,8 @@ - type: MobPrice price: 1500 # Kidnapping a living person and selling them for cred is a good move. deathPenalty: 0.01 # However they really ought to be living and intact, otherwise they're worth 100x less. + - type: RandomPrice + maxRandomPrice: 20000 - type: Tag tags: - CanPilot diff --git a/Resources/Prototypes/Entities/Mobs/Player/clone.yml b/Resources/Prototypes/Entities/Mobs/Player/clone.yml index 2763afefec..6942263c65 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/clone.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/clone.yml @@ -15,6 +15,7 @@ - Grammar # Pronouns - HumanoidProfile # Age, Sex, Gender - NpcFactionMember + - RandomPrice - CreamPied # traits - BlackAndWhiteOverlay From d1f913c5fe1a0e9b49fefe37c38f011393588fa6 Mon Sep 17 00:00:00 2001 From: PJBot Date: Sat, 4 Apr 2026 17:05:35 +0000 Subject: [PATCH 061/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 2ec57487ce..1ffc17eec7 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: temm1ie, aksosotl - changes: - - message: Added new botany-themed poster. - type: Add - id: 9099 - time: '2025-10-14T19:42:59.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40908 - author: ArtisticRoomba changes: - message: Atmospherics Delta-Pressure now properly computes delta pressure for @@ -4033,3 +4026,10 @@ id: 9610 time: '2026-04-04T16:48:03.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43465 +- author: SlamBamActionman + changes: + - message: Player sell costs are now randomized when scanned with an appraisal tool. + type: Tweak + id: 9611 + time: '2026-04-04T17:04:27.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43445 From 515f686950ce59504aa90e6d5314a6c8f4147355 Mon Sep 17 00:00:00 2001 From: Benas Kiburtas <26272940+eveloop@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:53:05 +0300 Subject: [PATCH 062/126] Hide humanoid appearance entities from Entity Spawn menu (#43467) * Hide humanoid appearance entities from Entity Spawn menu * Update species appearance entities to follow YAML convention --- Resources/Prototypes/Body/Species/arachnid.yml | 1 + Resources/Prototypes/Body/Species/diona.yml | 1 + Resources/Prototypes/Body/Species/dwarf.yml | 1 + Resources/Prototypes/Body/Species/gingerbread.yml | 1 + Resources/Prototypes/Body/Species/human.yml | 1 + Resources/Prototypes/Body/Species/moth.yml | 1 + Resources/Prototypes/Body/Species/reptilian.yml | 1 + Resources/Prototypes/Body/Species/skeleton.yml | 1 + Resources/Prototypes/Body/Species/slime.yml | 1 + Resources/Prototypes/Body/Species/vox.yml | 1 + Resources/Prototypes/Body/Species/vulpkanin.yml | 1 + 11 files changed, 11 insertions(+) diff --git a/Resources/Prototypes/Body/Species/arachnid.yml b/Resources/Prototypes/Body/Species/arachnid.yml index 240d486f5a..e1b8e5dcdd 100644 --- a/Resources/Prototypes/Body/Species/arachnid.yml +++ b/Resources/Prototypes/Body/Species/arachnid.yml @@ -36,6 +36,7 @@ - type: entity parent: BaseSpeciesAppearance id: AppearanceArachnid + categories: [ HideSpawnMenu ] name: arachnid appearance components: - type: Inventory diff --git a/Resources/Prototypes/Body/Species/diona.yml b/Resources/Prototypes/Body/Species/diona.yml index 7790c67ecf..bab251032b 100644 --- a/Resources/Prototypes/Body/Species/diona.yml +++ b/Resources/Prototypes/Body/Species/diona.yml @@ -35,6 +35,7 @@ - type: entity parent: BaseSpeciesAppearance id: AppearanceDiona + categories: [ HideSpawnMenu ] name: diona appearance components: - type: Inventory diff --git a/Resources/Prototypes/Body/Species/dwarf.yml b/Resources/Prototypes/Body/Species/dwarf.yml index 1d64142ca9..5b053f0735 100644 --- a/Resources/Prototypes/Body/Species/dwarf.yml +++ b/Resources/Prototypes/Body/Species/dwarf.yml @@ -1,6 +1,7 @@ - type: entity parent: AppearanceHuman id: AppearanceDwarf + categories: [ HideSpawnMenu ] name: dwarf appearance components: - type: Inventory diff --git a/Resources/Prototypes/Body/Species/gingerbread.yml b/Resources/Prototypes/Body/Species/gingerbread.yml index b32cbac91d..5c429af01c 100644 --- a/Resources/Prototypes/Body/Species/gingerbread.yml +++ b/Resources/Prototypes/Body/Species/gingerbread.yml @@ -1,6 +1,7 @@ - type: entity parent: BaseSpeciesAppearance id: AppearanceGingerbread + categories: [ HideSpawnMenu ] name: gingerbread appearance components: - type: Inventory diff --git a/Resources/Prototypes/Body/Species/human.yml b/Resources/Prototypes/Body/Species/human.yml index d6c76015e5..9b50ba8c53 100644 --- a/Resources/Prototypes/Body/Species/human.yml +++ b/Resources/Prototypes/Body/Species/human.yml @@ -51,6 +51,7 @@ - type: entity parent: BaseSpeciesAppearance id: AppearanceHuman + categories: [ HideSpawnMenu ] name: human appearance components: - type: Inventory diff --git a/Resources/Prototypes/Body/Species/moth.yml b/Resources/Prototypes/Body/Species/moth.yml index b7928db44c..4b46381c3c 100644 --- a/Resources/Prototypes/Body/Species/moth.yml +++ b/Resources/Prototypes/Body/Species/moth.yml @@ -51,6 +51,7 @@ - type: entity parent: BaseSpeciesAppearance id: AppearanceMoth + categories: [ HideSpawnMenu ] name: moth appearance components: - type: Inventory diff --git a/Resources/Prototypes/Body/Species/reptilian.yml b/Resources/Prototypes/Body/Species/reptilian.yml index 8f28ea0ab4..ea2336c16d 100644 --- a/Resources/Prototypes/Body/Species/reptilian.yml +++ b/Resources/Prototypes/Body/Species/reptilian.yml @@ -54,6 +54,7 @@ - type: entity parent: BaseSpeciesAppearance id: AppearanceReptilian + categories: [ HideSpawnMenu ] name: reptilian appearance components: - type: Inventory diff --git a/Resources/Prototypes/Body/Species/skeleton.yml b/Resources/Prototypes/Body/Species/skeleton.yml index 8372e4923d..953a8c7bae 100644 --- a/Resources/Prototypes/Body/Species/skeleton.yml +++ b/Resources/Prototypes/Body/Species/skeleton.yml @@ -42,6 +42,7 @@ - type: entity parent: BaseSpeciesAppearance id: AppearanceSkeletonPerson + categories: [ HideSpawnMenu ] name: skeletonperson appearance components: - type: Inventory diff --git a/Resources/Prototypes/Body/Species/slime.yml b/Resources/Prototypes/Body/Species/slime.yml index 82c8b30ca6..f0ba746a6f 100644 --- a/Resources/Prototypes/Body/Species/slime.yml +++ b/Resources/Prototypes/Body/Species/slime.yml @@ -46,6 +46,7 @@ - type: entity parent: BaseSpeciesAppearance id: AppearanceSlimePerson + categories: [ HideSpawnMenu ] name: SlimePerson appearance components: - type: Inventory diff --git a/Resources/Prototypes/Body/Species/vox.yml b/Resources/Prototypes/Body/Species/vox.yml index 111a1f47fa..b947b3e15f 100644 --- a/Resources/Prototypes/Body/Species/vox.yml +++ b/Resources/Prototypes/Body/Species/vox.yml @@ -74,6 +74,7 @@ - type: entity parent: BaseSpeciesAppearance id: AppearanceVox + categories: [ HideSpawnMenu ] name: vox appearance components: - type: Inventory diff --git a/Resources/Prototypes/Body/Species/vulpkanin.yml b/Resources/Prototypes/Body/Species/vulpkanin.yml index c4afe1b8e1..c4e1577149 100644 --- a/Resources/Prototypes/Body/Species/vulpkanin.yml +++ b/Resources/Prototypes/Body/Species/vulpkanin.yml @@ -63,6 +63,7 @@ - type: entity parent: BaseSpeciesAppearance id: AppearanceVulpkanin + categories: [ HideSpawnMenu ] name: vulpkanin appearance components: - type: InitialBody From e150b9cbcc44d3025beda93fda851a9604e88076 Mon Sep 17 00:00:00 2001 From: B_Kirill <153602297+B-Kirill@users.noreply.github.com> Date: Sun, 5 Apr 2026 03:13:25 +1000 Subject: [PATCH 063/126] A small cleanup in meteor rules (#43402) --- .../Prototypes/GameRules/meteorswarms.yml | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/Resources/Prototypes/GameRules/meteorswarms.yml b/Resources/Prototypes/GameRules/meteorswarms.yml index ed3a510d9a..134995b801 100644 --- a/Resources/Prototypes/GameRules/meteorswarms.yml +++ b/Resources/Prototypes/GameRules/meteorswarms.yml @@ -4,14 +4,14 @@ id: MeteorSwarmDustEventsTable table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp children: - - id: GameRuleSpaceDustMinor - - id: GameRuleSpaceDustMajor + - id: SpaceDustMinor + - id: SpaceDustMajor - type: entityTable id: MeteorSwarmSmallChanceEventsTable table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp children: - - id: GameRuleMeteorSwarmSmall + - id: MeteorSwarmSmall prob: 0.15 - type: entityTable @@ -29,14 +29,14 @@ children: - !type:NestedSelector tableId: MeteorSwarmDustEventsTable - - id: GameRuleMeteorSwarmSmall - - id: GameRuleMeteorSwarmMedium - - id: GameRuleMeteorSwarmLarge - - id: GameRuleUristSwarm - - id: GameRuleClownSwarm - - id: GameRuleCowSwarm - - id: GameRulePotatoSwarm - - id: GameRuleFunSwarm + - id: MeteorSwarmSmall + - id: MeteorSwarmMedium + - id: MeteorSwarmLarge + - id: UristSwarm + - id: ClownSwarm + - id: CowSwarm + - id: PotatoSwarm + - id: FunSwarm - id: ImmovableRodSpawn - type: weightedRandomEntity @@ -94,7 +94,7 @@ - type: entity parent: BaseGameRule - id: GameRuleMeteorSwarm + id: MeteorSwarm abstract: true components: - type: GameRule @@ -105,8 +105,8 @@ - type: MeteorSwarm - type: entity - parent: GameRuleMeteorSwarm - id: GameRuleSpaceDustMinor + parent: MeteorSwarm + id: SpaceDustMinor components: - type: StationEvent weight: 44 @@ -126,8 +126,8 @@ max: 5 - type: entity - parent: GameRuleMeteorSwarm - id: GameRuleSpaceDustMajor + parent: MeteorSwarm + id: SpaceDustMajor components: - type: StationEvent weight: 22 @@ -146,8 +146,8 @@ max: 12 - type: entity - parent: GameRuleMeteorSwarm - id: GameRuleMeteorSwarmSmall + parent: MeteorSwarm + id: MeteorSwarmSmall components: - type: StationEvent weight: 18 @@ -158,8 +158,8 @@ MeteorMedium: 3 - type: entity - parent: GameRuleMeteorSwarm - id: GameRuleMeteorSwarmMedium + parent: MeteorSwarm + id: MeteorSwarmMedium components: - type: StationEvent weight: 10 @@ -170,8 +170,8 @@ MeteorLarge: 1 - type: entity - parent: GameRuleMeteorSwarm - id: GameRuleMeteorSwarmLarge + parent: MeteorSwarm + id: MeteorSwarmLarge components: - type: StationEvent weight: 5 @@ -182,8 +182,8 @@ MeteorLarge: 4 - type: entity - parent: GameRuleMeteorSwarm - id: GameRuleUristSwarm + parent: MeteorSwarm + id: UristSwarm components: - type: StationEvent weight: 0.05 @@ -243,8 +243,8 @@ orGroup: rodProto - type: entity - parent: GameRuleMeteorSwarm - id: GameRuleCowSwarm + parent: MeteorSwarm + id: CowSwarm components: - type: StationEvent weight: 0.05 @@ -261,8 +261,8 @@ max: 10 - type: entity - parent: GameRuleMeteorSwarm - id: GameRuleClownSwarm + parent: MeteorSwarm + id: ClownSwarm components: - type: StationEvent weight: 0.05 @@ -279,8 +279,8 @@ max: 10 - type: entity - parent: GameRuleMeteorSwarm - id: GameRulePotatoSwarm + parent: MeteorSwarm + id: PotatoSwarm components: - type: StationEvent weight: 0.05 @@ -297,8 +297,8 @@ max: 10 - type: entity - parent: GameRuleMeteorSwarm - id: GameRuleFunSwarm + parent: MeteorSwarm + id: FunSwarm components: - type: StationEvent weight: 0.03 From 968862db28baa8b25fa0af6cc941b1192e804916 Mon Sep 17 00:00:00 2001 From: salarua Date: Sat, 4 Apr 2026 10:44:58 -0700 Subject: [PATCH 064/126] Fix typography on loading screen tips (#43348) Also a couple of minor copyedits. --- Resources/Locale/en-US/tips.ftl | 34 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Resources/Locale/en-US/tips.ftl b/Resources/Locale/en-US/tips.ftl index c02e72da19..37a52a8d4d 100644 --- a/Resources/Locale/en-US/tips.ftl +++ b/Resources/Locale/en-US/tips.ftl @@ -6,9 +6,9 @@ tips-dataset-5 = Artifacts have the ability to gain permanent effects for some t tips-dataset-6 = You can avoid slipping on most puddles by walking. However, some strong chemicals like space lube will slip people anyway. tips-dataset-7 = Some plants, such as galaxy thistle, can be ground up into extremely useful and potent medicines. tips-dataset-8 = Mopping up puddles and draining them into other containers conserves the reagents found in the puddle. -tips-dataset-9 = Floor drains, usually found in the chef's freezer or janitor's office, rapidly consume reagent found in puddles around them--including blood. +tips-dataset-9 = Floor drains, usually found in the chef's freezer or janitor's office, rapidly consume reagent found in puddles around them — including blood. tips-dataset-10 = Cognizine, a hard to manufacture chemical, makes animals sentient when they are injected with it. -tips-dataset-11 = Loaded mousetraps are incredibly effective at dealing with all manner of low-mass mobs--including Rat Servants. +tips-dataset-11 = Loaded mousetraps are incredibly effective at dealing with all manner of low-mass mobs — including Rat Servants. tips-dataset-12 = Fire extinguishers can be loaded with any reagent in the game. tips-dataset-13 = Some reagents, like chlorine trifluoride, have unique effects when applied by touch, such as through a spray bottle or foam. tips-dataset-14 = Remember to touch grass in between playing Space Station 14 every once in a while. @@ -19,7 +19,7 @@ tips-dataset-18 = When running the Singularity, make sure to check on it periodi tips-dataset-19 = If the Singularity is up, make sure to refuel the radiation collectors once in a while. tips-dataset-20 = Chemicals don't react while inside the ChemMaster's buffer. tips-dataset-21 = Don't anger the bartender by throwing their glasses! Politely place them on the table by tapping Q. -tips-dataset-22 = You can hold SPACE by default to slow the movement of the shuttle when piloting, to allow for precise movements--or even coming to a complete stop. +tips-dataset-22 = You can hold SPACE by default to slow the movement of the shuttle when piloting, to allow for precise movements — or even coming to a complete stop. tips-dataset-23 = Dexalin, Dexalin Plus, and Epinephrine will all purge heartbreaker toxin from your bloodstream while metabolizing. tips-dataset-24 = Every crewmember comes with an emergency medipen in their survival box containing epinephrine and tranexamic acid. tips-dataset-25 = The AME is a high-priority target and is easily sabotaged. Make sure to set up the Singularity or Solars so that you don't run out of power if it blows. @@ -29,15 +29,15 @@ tips-dataset-28 = Riot armor is significantly more powerful against opponents th tips-dataset-29 = As a ghost, you can use the Verb Menu to orbit around and follow any entity in game automatically. tips-dataset-30 = As a Traitor, you may sometimes be assigned to hunt other traitors, and in turn be hunted by others. tips-dataset-31 = As a Traitor, the syndicate encryption key can be used to communicate through a secure channel with any other traitors who have purchased it. -tips-dataset-32 = As a Traitor, compromising important communications channels like security or engineering can give valuable intelligence. Be aware that this goes in both ways - security can compromise syndicate communications as well! +tips-dataset-32 = As a Traitor, compromising important communications channels like security or engineering can give valuable intelligence. Be aware that this goes in both ways — security can compromise syndicate communications as well! tips-dataset-33 = As a Traitor, the syndicate toolbox is extremely versatile. For only 2 telecrystals, you can get a full set of tools to help you in an emergency, insulated combat gloves and a syndicate gas mask. tips-dataset-34 = As a Traitor, never underestimate the web vest. It may not provide space protection, but its cheap cost and robust protection makes it handy for protecting against trigger-happy foes. tips-dataset-35 = As a Traitor, any purchased grenade penguins won't attack you, and will explode if killed. tips-dataset-36 = As a Traitor, be careful when using vestine from the chemical synthesis kit. If someone checks your station, they could easily out you. tips-dataset-37 = As a Traitor, remember that power sinks will create a loud sound and alert the crew after running for long enough. Try to hide them in a tricky-to-find spot, or reinforce the area around them so that they're harder to reach. -tips-dataset-38 = As a Traitor, plasma gas is an excellent way to create chaos. It can be ignited to make an area extra-uninhabitable, and can cause toxin damage to those that inhale it. +tips-dataset-38 = As a Traitor, plasma gas is an excellent way to create chaos. It can be ignited to make an area extra uninhabitable, and can cause toxin damage to those that inhale it. tips-dataset-39 = As a Traitor, dehydrated carps are useful for killing a large hoard of people. As long as you pat it before rehydrating it, it can be used as a great distraction. -tips-dataset-40 = As a Traitor, have you tried injecting plasma into batteries? In the case of a defibrillator, it explodes on use; hurting the user and the patient! +tips-dataset-40 = As a Traitor, have you tried injecting plasma into batteries? In the case of a defibrillator, it explodes on use, hurting the user and the patient! tips-dataset-41 = As a Nuclear Operative, stick together! While your equipment is robust, your fellow operatives are much better at saving your life: they can drag you away from danger while stunned and provide cover fire. tips-dataset-42 = As a Nuclear Operative, communication is key! Use your radio to speak to your fellow operatives and coordinate an attack plan. tips-dataset-43 = As a Nuclear Operative, remember that stealth is an option. It'll be hard for the captain to fight back if he gets caught off guard by what he thinks is just a regular passenger! @@ -49,7 +49,7 @@ tips-dataset-48 = As a Salvage Specialist, never forget to mine ore! Ore can be tips-dataset-49 = As a Salvage Specialist, try asking science for a tethergun. It can be used to grab items off of salvage wrecks extremely efficiently! tips-dataset-50 = As a Salvage Specialist, try asking science for a grappling hook. It can be used to propel yourself onto wrecks, or if stuck in space you don't have to rely on the proto-kinetic accelerator. tips-dataset-51 = Tip #51 does not exist and has never existed. Ignore any rumors to the contrary. -tips-dataset-52 = As a Salvage Specialist, consider cooperating with the Cargo Technicians. They can order you a wide variety of useful items, including ones that may be hard to get otherwise, such laser guns and shuttle building materials. +tips-dataset-52 = As a Salvage Specialist, consider cooperating with the Cargo Technicians. They can order you a wide variety of useful items, including ones that may be hard to get otherwise, such as laser guns and shuttle building materials. tips-dataset-53 = As a Cargo Technician, consider asking science for a Ripley APLU. When paired with a hydraulic clamp, you can grab valuable maintenance objects like fuel tanks much more easily, and make deliveries in a swift manner. tips-dataset-54 = As a Cargo Technician, try to maintain a surplus of materials. They are extremely useful for Scientists and Station Engineers to have immediate access to. tips-dataset-55 = As a Cargo Technician, if you have a surplus of cash try gambling! Sometimes you gain more money than you begin with. @@ -70,28 +70,28 @@ tips-dataset-69 = As an Atmospheric Technician, your ATMOS holofan projector blo tips-dataset-70 = As an Atmospheric Technician, try to resist the temptation of making canister bombs for Nuclear Operatives, unless you're in a last-ditch scenario. They often lead to large amounts of unnecessary friendly fire! tips-dataset-71 = As an Engineer, you can repair cracked windows by using a lit welding tool on them while not in combat mode. tips-dataset-72 = As an Engineer, you can electrify grilles by placing powered cables beneath them. -tips-dataset-73 = As an Engineer, always double check when you're setting up the singularity. It is easier than you think to loose it! +tips-dataset-73 = As an Engineer, always double-check when you're setting up the singularity. It is easier than you think to loose it! tips-dataset-74 = As an Engineer, you can use plasma glass to reinforce an area and prevent radiation. Uranium glass can also be used to prevent radiation. -tips-dataset-75 = As the Captain, you are one of the highest priority targets on the station. Everything from revolutions, to nuclear operatives, to traitors that need to rob you of your unique laser pistol or your life are things to worry about. +tips-dataset-75 = As the Captain, you are one of the highest-priority targets on the station. Everything from revolutions, to Nuclear Operatives, to traitors that need to rob you of your unique laser pistol or your life are things to worry about. tips-dataset-76 = As the Captain, always take the nuclear disk and pinpointer with you every shift. It's a good idea to give one of these to another head you can trust with keeping it safe. tips-dataset-77 = As the Captain, you have absolute access and control over the station, but this does not mean that being a horrible person won't result in mutiny. tips-dataset-78 = As the Captain, try to be active and patrol the station. Staying in the bridge might be tempting, but you'll just end up putting a bigger target on your back! tips-dataset-79 = As a Scientist, you can use the node scanner to see when an artifact is able to react to triggers, and which triggers it is currently reacting to; each number it shows corresponds to a different trigger! tips-dataset-80 = As a Scientist, you can utilize upgraded versions of machines to increase its effectiveness. This can make certain machines significantly better; salvage will love you if you upgrade their ore processor! -tips-dataset-81 = As a Scientist, you can build cyborgs using positronic brains and a chassis, they are just as useful as a new crew member. +tips-dataset-81 = As a Scientist, you can build cyborgs using positronic brains and a chassis; they are just as useful as a new crew member. tips-dataset-82 = As a Medical Doctor, try to be wary of overdosing your patients, especially if someone else has already been on the scene. Overdoses are often lethal to patients in crit! tips-dataset-83 = As a Medical Doctor, don't underestimate your cryo pods! They heal almost every type of damage, making them very useful when you are overloaded or need to heal someone in a pinch. tips-dataset-84 = As a Medical Doctor, exercise caution when putting reptilians in cryopods. They will take a lot of extra cold damage, but you can mitigate this with some burn medicine or leporazine. tips-dataset-85 = As a Medical Doctor, remember that the health analyzer can be used if you lose your PDA. tips-dataset-86 = As a Chemist, once you've made everything you've needed to, don't be afraid to make more silly reagents. Have you tried desoxyephedrine or licoxide? -tips-dataset-87 = As a Medical Doctor, Chemist, or Chief Medical Officer, you can use chloral hydrate to non-lethally sedate unruly patients. +tips-dataset-87 = As a Medical Doctor, Chemist, or Chief Medical Officer, you can use chloral hydrate to nonlethally sedate unruly patients. tips-dataset-88 = Don't be afraid to ask for help, whether from your peers in character or through LOOC, or from admins! -tips-dataset-89 = You'll quickly lose your interest in the game if you play to win and kill. If you find yourself doing this, take a step back and talk to people--it's a much better experience! +tips-dataset-89 = You'll quickly lose your interest in the game if you play to win and kill. If you find yourself doing this, take a step back and talk to people — it's a much better experience! tips-dataset-90 = If there's something you need from another department, try asking! This game isn't singleplayer and you'd be surprised what you can get accomplished together! tips-dataset-91 = The station's nuke is invincible. Go find the disk instead of trying to destroy it. tips-dataset-92 = Maintenance is full of equipment that is randomized every round. Look around and see if anything is worth using. -tips-dataset-93 = We were all new once, be patient and guide new players, especially those playing intern roles, in the right direction. -tips-dataset-94 = Firesuits, winter coats and emergency EVA suits offer mild protection from the cold, allowing you to spend longer periods of time near breaches and space than if wearing nothing at all. +tips-dataset-93 = We were all new once. Be patient and guide new players, especially those playing intern roles, in the right direction. +tips-dataset-94 = Firesuits, winter coats, and emergency EVA suits offer mild protection from the cold, allowing you to spend longer periods of time near breaches and space than if wearing nothing at all. tips-dataset-95 = In an emergency, you can always rely on firesuits and emergency EVA suits; they will always spawn in their respective lockers. They might be awkward to move around in, but can easily save your life in a dangerous situation. tips-dataset-96 = In an emergency, remember that you can craft improvised weapons! A baseball bat or spear could easily mean the difference between deterring an attacker or perishing from the hands of one. tips-dataset-97 = Spears can be tipped with chemicals, and will inject a few units every time you hit someone with them directly. @@ -112,7 +112,7 @@ tips-dataset-111 = You can move an item out of the way by dragging it, and then tips-dataset-112 = When dealing with security, you can often get your sentence negated entirely through cooperation and deception. tips-dataset-113 = Fire can spread to other players through touch! Be careful around flaming bodies or large crowds with people on fire in them. tips-dataset-114 = Hull breaches take a few seconds to fully space an area. You can use this time to patch up the hole if you're confident enough, or just run away. -tips-dataset-115 = Burn damage, such as that from a welding tool or lightbulb, can be used to cauterize wounds and stop bleeding. +tips-dataset-115 = Burn damage, such as from a welding tool or lightbulb, can be used to cauterize wounds and stop bleeding. tips-dataset-116 = Bleeding is no joke! If you've been shot or acquired any other major injury, make sure to treat it quickly. tips-dataset-117 = In an emergency, you can butcher a jumpsuit with a sharp object to get cloth, which can be crafted into gauze. tips-dataset-118 = You can use sharp objects to butcher clothes or animals in the right click context menu. This includes glass shards. @@ -121,7 +121,7 @@ tips-dataset-120 = You can stun grenade penguins, which can bide valuable time f tips-dataset-121 = You can click on the names of items to pick them up in the right click menu, instead of hovering over the item and then selecting pick up. tips-dataset-122 = Space Station 14 is open source! If there's a change you want to make, or a simple item you want to add, then try contributing to the game. It's not as hard as you'd think it is. tips-dataset-123 = In a pinch, you can throw drinks or other reagent containers behind you to create a spill that can slip people chasing you. -tips-dataset-124 = Some weapons, such as knives & shivs, have a fast attack speed. +tips-dataset-124 = Some weapons, such as knives and shivs, have a fast attack speed. tips-dataset-125 = The jaws of life can be used to open powered doors. tips-dataset-126 = If you're not a human, you can drink blood to heal back some of your blood volume, albeit very inefficiently. tips-dataset-127 = If you're a human, don't drink blood! It makes you sick and you'll begin to take damage. @@ -135,5 +135,5 @@ tips-dataset-134 = You can tell if an area with firelocks up is spaced by lookin tips-dataset-135 = Instead of picking it up, you can alt-click food to eat it. This also works for mice and other creatures without hands. tips-dataset-136 = If you're trapped behind an electrified door, disable the APC or throw your ID at the door to avoid getting shocked! tips-dataset-137 = If the AI electrifies a door and you have insulated gloves, snip and mend the power wire to reset their electrification! -tips-dataset-138 = If you want to stop your prisoner from escaping from the cell right after being uncuffed, turn on combat mode while uncuffing - this will shove the prisoner down. +tips-dataset-138 = If you want to stop your prisoner from escaping from the cell right after being uncuffed, turn on combat mode while uncuffing — this will shove the prisoner down. tips-dataset-139 = Make sure to clean your illegal implanters with a soap after you use them! Detectives can scan used implanters for incriminating DNA evidence, but not if they've been wiped clean. From d595590f39da8f224852962f2b5b72126e3baead Mon Sep 17 00:00:00 2001 From: themias <89101928+themias@users.noreply.github.com> Date: Sat, 4 Apr 2026 14:20:46 -0400 Subject: [PATCH 065/126] Replace space dragon timer deletion with gibbing (#43296) * save * fix test failure --- .../Dragon/Components/DragonComponent.cs | 13 ++++++++++ Content.Server/Dragon/DragonSystem.cs | 24 +++++++++++++++---- .../Entities/Mobs/Player/dragon.yml | 13 ++++++++++ 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/Content.Server/Dragon/Components/DragonComponent.cs b/Content.Server/Dragon/Components/DragonComponent.cs index 80461e156a..c634836b5a 100644 --- a/Content.Server/Dragon/Components/DragonComponent.cs +++ b/Content.Server/Dragon/Components/DragonComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Chemistry.Components; using Content.Shared.NPC.Prototypes; using Robust.Shared.Audio; using Robust.Shared.Prototypes; @@ -65,5 +66,17 @@ namespace Content.Server.Dragon /// [DataField] public ProtoId Faction = "Dragon"; + + /// + /// The smoke to spawn upon rift timeout death. + /// + [DataField] + public EntProtoId SmokePrototype = "BloodSmoke"; + + /// + /// The solution to place into the smoke (mostly just needed for color) + /// + [DataField] + public Solution SmokeSolution = new ([new("Blood", 1)]); } } diff --git a/Content.Server/Dragon/DragonSystem.cs b/Content.Server/Dragon/DragonSystem.cs index 1c838939ec..8bc5fd83b1 100644 --- a/Content.Server/Dragon/DragonSystem.cs +++ b/Content.Server/Dragon/DragonSystem.cs @@ -1,8 +1,11 @@ +using Content.Server.Fluids.EntitySystems; using Content.Server.Objectives.Components; using Content.Server.Objectives.Systems; using Content.Server.Popups; using Content.Shared.Actions; +using Content.Shared.Chemistry.Components; using Content.Shared.Dragon; +using Content.Shared.Gibbing; using Content.Shared.Maps; using Content.Shared.Mind; using Content.Shared.Mind.Components; @@ -12,6 +15,7 @@ using Content.Shared.Movement.Systems; using Content.Shared.NPC.Systems; using Content.Shared.Zombies; using Robust.Shared.Audio.Systems; +using Robust.Shared.Map; using Robust.Shared.Map.Components; namespace Content.Server.Dragon; @@ -29,6 +33,8 @@ public sealed partial class DragonSystem : EntitySystem [Dependency] private readonly SharedMapSystem _map = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly TurfSystem _turf = default!; + [Dependency] private readonly GibbingSystem _gibbing = default!; + [Dependency] private readonly SmokeSystem _smoke = default!; private EntityQuery _objQuery; @@ -96,11 +102,14 @@ public sealed partial class DragonSystem : EntitySystem if (!_mobState.IsDead(uid)) comp.RiftAccumulator += frameTime; - // Delete it, naughty dragon! + // Gib it, naughty dragon! if (comp.RiftAccumulator >= comp.RiftMaxAccumulator) { - Roar(uid, comp); - QueueDel(uid); + Roar(uid, comp, Transform(uid).Coordinates); + var smoke = Spawn(comp.SmokePrototype, Transform(uid).Coordinates); + if (TryComp(smoke, out var smokeComp)) + _smoke.StartSmoke(smoke, comp.SmokeSolution, smokeComp.Duration, smokeComp.SpreadAmount, smokeComp); + _gibbing.Gib(uid); } } } @@ -200,10 +209,15 @@ public sealed partial class DragonSystem : EntitySystem _faction.AddFaction(ent.Owner, ent.Comp.Faction); } - private void Roar(EntityUid uid, DragonComponent comp) + private void Roar(EntityUid uid, DragonComponent comp, EntityCoordinates? coords = null) { if (comp.SoundRoar != null) - _audio.PlayPvs(comp.SoundRoar, uid); + { + if (coords != null) + _audio.PlayPvs(comp.SoundRoar, coords.Value); + else + _audio.PlayPvs(comp.SoundRoar, uid); + } } /// diff --git a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml index 57b682caea..a1e2e962df 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml @@ -285,3 +285,16 @@ range: 0 - type: WorldTargetAction event: !type:ActionGunShootEvent + +- type: entity + parent: Smoke + id: BloodSmoke + name: smoke + categories: [ HideSpawnMenu ] + components: + - type: Smoke + spreadAmount: 3 + duration: 2 + - type: Sprite + sprite: Effects/chemsmoke.rsi + state: chemsmoke_white From fb940e46696aa643066131152b1065f806833599 Mon Sep 17 00:00:00 2001 From: PJBot Date: Sat, 4 Apr 2026 18:36:52 +0000 Subject: [PATCH 066/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 1ffc17eec7..e6b75e0bb0 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,13 +1,4 @@ Entries: -- author: ArtisticRoomba - changes: - - message: Atmospherics Delta-Pressure now properly computes delta pressure for - structures that can hold air in their tile while still being airtight (ex. directional - windows, diagonal windows). - type: Fix - id: 9100 - time: '2025-10-14T22:46:46.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40435 - author: SlamBamActionman changes: - message: Lying trait now features more grammatically correct lying. @@ -4033,3 +4024,11 @@ id: 9611 time: '2026-04-04T17:04:27.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43445 +- author: themias + changes: + - message: Instead of disappearing, space dragons now burst into giblets if they + go too long without summoning a rift. + type: Tweak + id: 9612 + time: '2026-04-04T18:35:44.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43296 From bcb84b52cd7ed59b51dd241f68854d8985bb4dea Mon Sep 17 00:00:00 2001 From: August Sun <45527070+august-sun@users.noreply.github.com> Date: Sat, 4 Apr 2026 13:10:02 -0600 Subject: [PATCH 067/126] Adds the scout xenoborg jump module (#43243) * Added prototype and sprites for borg jump module * Made new action, switched sprites/inheritance * cleanup * Update Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml * improved icon --------- Co-authored-by: august-sun <45527070+august.sun@users.noreply.github.com> Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --- Resources/Prototypes/Actions/types.yml | 11 +++++++++++ .../Objects/Specific/Robotics/borg_modules.yml | 17 +++++++++++++++++ .../Actions/actions_borg.rsi/meta.json | 3 +++ .../actions_borg.rsi/xenoborg-jump-module.png | Bin 0 -> 365 bytes .../borgmodule.rsi/icon-xenoborg-jump.png | Bin 0 -> 183 bytes .../Specific/Robotics/borgmodule.rsi/meta.json | 3 +++ 6 files changed, 34 insertions(+) create mode 100644 Resources/Textures/Interface/Actions/actions_borg.rsi/xenoborg-jump-module.png create mode 100644 Resources/Textures/Objects/Specific/Robotics/borgmodule.rsi/icon-xenoborg-jump.png diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index e0956a76f7..c07584e737 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -439,6 +439,17 @@ - type: InstantAction event: !type:GravityJumpEvent {} +- type: entity + parent: ActionGravityJump + id: ActionJumpBoost + name: Jump boost + components: + - type: Action + useDelay: 16 + icon: + sprite: Interface/Actions/actions_borg.rsi + state: xenoborg-jump-module + - type: entity parent: BaseAction id: ActionVulpkaninGravityJump diff --git a/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml b/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml index fa45506e33..a10437d5b7 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml @@ -1625,6 +1625,23 @@ - type: BorgModuleIcon icon: { sprite: Interface/Actions/actions_borg.rsi, state: xenoborg-space-movement-module } +- type: entity + parent: [ BaseXenoborgModuleScout, BaseXenoborgContraband ] + id: XenoborgModuleJump + name: xenoborg jump module + description: Module that allows a xenoborg to jump forward. + components: + - type: ComponentBorgModule + components: + - type: JumpAbility + action: ActionJumpBoost + jumpDistance: 4 + jumpSound: /Audio/Effects/stealthoff.ogg + - type: Sprite + layers: + - state: xenoborg_scout + - state: icon-xenoborg-jump + - type: entity parent: [ BaseXenoborgModuleScout, BaseProviderBorgModule, BaseXenoborgContraband ] id: XenoborgModuleSword diff --git a/Resources/Textures/Interface/Actions/actions_borg.rsi/meta.json b/Resources/Textures/Interface/Actions/actions_borg.rsi/meta.json index 8538e24fb9..e61a7ab6eb 100644 --- a/Resources/Textures/Interface/Actions/actions_borg.rsi/meta.json +++ b/Resources/Textures/Interface/Actions/actions_borg.rsi/meta.json @@ -133,6 +133,9 @@ { "name":"xenoborg-basic-module" }, + { + "name":"xenoborg-jump-module" + }, { "name":"xenoborg-camera-computer" }, diff --git a/Resources/Textures/Interface/Actions/actions_borg.rsi/xenoborg-jump-module.png b/Resources/Textures/Interface/Actions/actions_borg.rsi/xenoborg-jump-module.png new file mode 100644 index 0000000000000000000000000000000000000000..bb09b413daa9186553a3c3bfd5f67991da7f5ffa GIT binary patch literal 365 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|v;urWT!Azg zh%+!~3knv*#r@aR{I9HhMqIohAmFB)+~%&X{|ycQot!rJc=WypD&;E)@(X78j|{@S zZi@hAu6w#ThFJ8joqSN}hysUmw6sOQ=l}b6GhewGRa$l9tm(tfrrY266IVWa&mGJ8 zCUxOn*1pznng`Fv2{X)?ndn#`zD%h}lkuI_kycK|G%nG`j#WttJLZ245TD0#+tb|^n}KMt7T5*+`BgQ`aDU#$1&wphBEWC$2$aF zUY)!okt-F?v~=F{nLI7`&nq!Z?D#IRd?tf`%!KE5TO8ItDOqe3u-Zf6*xWC2cf>wD v=n0VUcACa_;jGg9qK#Mc<)43j{dWbwSp%bP0l+XkKZ6cQ~ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Specific/Robotics/borgmodule.rsi/icon-xenoborg-jump.png b/Resources/Textures/Objects/Specific/Robotics/borgmodule.rsi/icon-xenoborg-jump.png new file mode 100644 index 0000000000000000000000000000000000000000..e66775448107f48ab1628887f453a5ef3a9b0102 GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}g`O^sArY;~ z2@g6&|Nc&( Date: Sat, 4 Apr 2026 15:23:39 -0400 Subject: [PATCH 068/126] Add lobby screen invisiblewall (#42947) * add lobby screen: invisiblewall * Update lobby-state-background.ftl * Update attributions.yml had two lines swapped. fixed. --- .../en-US/lobby/lobby-state-background.ftl | 3 +++ Resources/Prototypes/lobbyscreens.yml | 6 ++++++ .../Textures/LobbyScreens/attributions.yml | 7 ++++++- .../Textures/LobbyScreens/invisiblewall.webp | Bin 0 -> 108088 bytes .../LobbyScreens/invisiblewall.webp.yml | 2 ++ 5 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 Resources/Textures/LobbyScreens/invisiblewall.webp create mode 100644 Resources/Textures/LobbyScreens/invisiblewall.webp.yml diff --git a/Resources/Locale/en-US/lobby/lobby-state-background.ftl b/Resources/Locale/en-US/lobby/lobby-state-background.ftl index c0046ca693..7191ebbe12 100644 --- a/Resources/Locale/en-US/lobby/lobby-state-background.ftl +++ b/Resources/Locale/en-US/lobby/lobby-state-background.ftl @@ -1,6 +1,9 @@ lobby-state-background-warden-title = Warden lobby-state-background-warden-artist = Solbusaur +lobby-state-background-invisiblewall-title = Invisible Wall +lobby-state-background-invisiblewall-artist = Vandersloot + lobby-state-background-pharmacy-title = Pharmacy lobby-state-background-pharmacy-artist = Solbusaur diff --git a/Resources/Prototypes/lobbyscreens.yml b/Resources/Prototypes/lobbyscreens.yml index d0f6da99d5..94fc8bd3ba 100644 --- a/Resources/Prototypes/lobbyscreens.yml +++ b/Resources/Prototypes/lobbyscreens.yml @@ -4,6 +4,12 @@ title: lobby-state-background-warden-title artist: lobby-state-background-warden-artist +- type: lobbyBackground + id: InvisibleWall + background: /Textures/LobbyScreens/invisiblewall.webp + title: lobby-state-background-invisiblewall-title + artist: lobby-state-background-invisiblewall-artist + - type: lobbyBackground id: Pharmacy background: /Textures/LobbyScreens/pharmacy.webp diff --git a/Resources/Textures/LobbyScreens/attributions.yml b/Resources/Textures/LobbyScreens/attributions.yml index 1df5da9a2b..dead0a28fe 100644 --- a/Resources/Textures/LobbyScreens/attributions.yml +++ b/Resources/Textures/LobbyScreens/attributions.yml @@ -48,6 +48,11 @@ copyright: "plantyfern on discord" source: "https://github.com/space-wizards/space-station-14" +- files: ["invisiblewall.webp"] + license: "CC-BY-SA-3.0" + copyright: "vanderslootassgiraffe on discord" + source: "https://github.com/space-wizards/space-station-14" + - files: ["janishootout.webp"] license: "CC0-1.0" copyright: "psychpsyo on Github/Twitter" @@ -56,4 +61,4 @@ - files: ["reclaimer-nuke.webp"] license: "CC-BY-NC-SA-3.0" copyright: "GetOutMarutak,aka.Snicket on Discord/github/Cara/Ko-fi" - source: "https://github.com/space-wizards/space-station-14" \ No newline at end of file + source: "https://github.com/space-wizards/space-station-14" diff --git a/Resources/Textures/LobbyScreens/invisiblewall.webp b/Resources/Textures/LobbyScreens/invisiblewall.webp new file mode 100644 index 0000000000000000000000000000000000000000..ac38cdbac02cb34a2cf073a8545e6aae01fdf4c8 GIT binary patch literal 108088 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�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

EJu5VGc?n%Uuz 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 Date: Sat, 4 Apr 2026 19:39:20 +0000 Subject: [PATCH 069/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index e6b75e0bb0..1f585a949d 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: SlamBamActionman - changes: - - message: Lying trait now features more grammatically correct lying. - type: Fix - id: 9101 - time: '2025-10-14T23:13:02.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/39370 - author: Winkarst-cpu changes: - message: Now doors play an animation while being emagged. @@ -4032,3 +4025,11 @@ id: 9612 time: '2026-04-04T18:35:44.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43296 +- author: VanderslootAssgiraffe + changes: + - message: Added new lobby art depicting a mime mocking nukies trapped behind an + invisible wall as the mime defuses the nuke + type: Add + id: 9613 + time: '2026-04-04T19:38:12.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/42947 From 094736c510baddb3aaea2b881dc211c6b7ade795 Mon Sep 17 00:00:00 2001 From: 0-Anon Date: Sat, 4 Apr 2026 15:28:25 -0400 Subject: [PATCH 070/126] Add Tail Drag to Rat King (#42701) Add pulling to Rat King Taildrag power --- Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml index ce31df386d..2c2e2a9cf8 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml @@ -33,6 +33,8 @@ proto: regal - type: Physics bodyType: KinematicController + - type: Puller + needsHands: false - type: Fixtures fixtures: fix1: From ef3929be8645bbc0b8ab9dd3e6d5113dac77e4a1 Mon Sep 17 00:00:00 2001 From: PJBot Date: Sat, 4 Apr 2026 19:59:43 +0000 Subject: [PATCH 071/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 1f585a949d..7b35a1062c 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Winkarst-cpu - changes: - - message: Now doors play an animation while being emagged. - type: Fix - id: 9102 - time: '2025-10-14T23:31:16.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40350 - author: slarticodefast changes: - message: Zombified arachnids can no longer spam infinite silk due to having no @@ -4033,3 +4026,10 @@ id: 9613 time: '2026-04-04T19:38:12.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/42947 +- author: 0-Anon + changes: + - message: Rat Kings can now pull objects. + type: Tweak + id: 9614 + time: '2026-04-04T19:58:35.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/42701 From 0d616cf6df20b2d1f6c875fb5dbde347eb5fedec Mon Sep 17 00:00:00 2001 From: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> Date: Sat, 4 Apr 2026 21:46:43 +0200 Subject: [PATCH 072/126] Universal uplink codes (#38712) * Halfway commit * Finishing commit, maybe? * Fix test, update ringtone UI to look nicer * Fix command, add failsafe * Documentation * Can we just mark ValidatePrototypeId as obsolete please * I'm too tired and my bones hurt * Change uplink code generation method * Move RingerAccessUplinkComponent to Server, because cheat clients could make use of it * Fix uplink implant changes, review changes, repair broken serialization. * cleanup and master merge * forgot the Linq * Linqd * Our store system is pretty goddamn awful wow --------- Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> --- Content.Client/Store/StoreSystem.cs | 5 + .../Store/Ui/StoreBoundUserInterface.cs | 11 +- Content.IntegrationTests/Tests/StoreTests.cs | 13 +- .../GameTicking/Rules/TraitorRuleSystem.cs | 30 +-- Content.Server/PDA/PdaSystem.cs | 33 ++- .../PDA/Ringer/RingerAccessUplinkComponent.cs | 24 ++ Content.Server/PDA/Ringer/RingerSystem.cs | 224 +++++++++++++++--- .../Store/Systems/StoreSystem.Ui.cs | 48 +++- Content.Server/Store/Systems/StoreSystem.cs | 20 +- .../Uplink/Commands/AddUplinkCommand.cs | 8 +- Content.Server/Traitor/Uplink/UplinkSystem.cs | 111 +++++++-- .../PDA/Ringer/RingerUplinkComponent.cs | 15 +- Content.Shared/PDA/SharedRingerSystem.cs | 4 + .../Store/Components/RemoteStoreComponent.cs | 17 ++ Content.Shared/Store/SharedStoreSystem.cs | 59 +++++ Content.Shared/Store/StoreUi.cs | 3 +- .../commands/add-uplink-command.ftl | 4 +- .../Entities/Objects/Devices/pda.yml | 3 +- Resources/Prototypes/Store/presets.yml | 7 + 19 files changed, 525 insertions(+), 114 deletions(-) create mode 100644 Content.Client/Store/StoreSystem.cs create mode 100644 Content.Server/PDA/Ringer/RingerAccessUplinkComponent.cs create mode 100644 Content.Shared/Store/Components/RemoteStoreComponent.cs create mode 100644 Content.Shared/Store/SharedStoreSystem.cs diff --git a/Content.Client/Store/StoreSystem.cs b/Content.Client/Store/StoreSystem.cs new file mode 100644 index 0000000000..cb6e42a0d7 --- /dev/null +++ b/Content.Client/Store/StoreSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Store; + +namespace Content.Client.Store; + +public sealed class StoreSystem : SharedStoreSystem; diff --git a/Content.Client/Store/Ui/StoreBoundUserInterface.cs b/Content.Client/Store/Ui/StoreBoundUserInterface.cs index d8236604bf..36eec4a671 100644 --- a/Content.Client/Store/Ui/StoreBoundUserInterface.cs +++ b/Content.Client/Store/Ui/StoreBoundUserInterface.cs @@ -1,7 +1,6 @@ +using System.Linq; using Content.Shared.Store; using JetBrains.Annotations; -using System.Linq; -using Content.Shared.Store.Components; using Robust.Client.UserInterface; using Robust.Shared.Prototypes; @@ -11,6 +10,7 @@ namespace Content.Client.Store.Ui; public sealed class StoreBoundUserInterface : BoundUserInterface { private IPrototypeManager _prototypeManager = default!; + private readonly StoreSystem _storeSystem = default!; [ViewVariables] private StoreMenu? _menu; @@ -23,6 +23,7 @@ public sealed class StoreBoundUserInterface : BoundUserInterface public StoreBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { + _storeSystem = EntMan.System(); } protected override void Open() @@ -30,12 +31,12 @@ public sealed class StoreBoundUserInterface : BoundUserInterface base.Open(); _menu = this.CreateWindow(); - if (EntMan.TryGetComponent(Owner, out var store)) - _menu.Title = Loc.GetString(store.Name); + if (_storeSystem.TryGetStore(Owner, out var store)) + _menu.Title = Loc.GetString(store.Value.Comp.Name); _menu.OnListingButtonPressed += (_, listing) => { - SendMessage(new StoreBuyListingMessage(listing.ID)); + SendMessage(new StoreBuyListingMessage(listing.ID, EntMan.GetNetEntity(Owner))); }; _menu.OnCategoryButtonPressed += (_, category) => diff --git a/Content.IntegrationTests/Tests/StoreTests.cs b/Content.IntegrationTests/Tests/StoreTests.cs index 811bf40548..411f4762dd 100644 --- a/Content.IntegrationTests/Tests/StoreTests.cs +++ b/Content.IntegrationTests/Tests/StoreTests.cs @@ -22,7 +22,7 @@ public sealed class StoreTests : GameTest - type: entity name: InventoryPdaDummy id: InventoryPdaDummy - parent: BasePDA + parent: [BasePDA, StorePresetUplink] components: - type: Clothing QuickEquip: false @@ -89,10 +89,13 @@ public sealed class StoreTests : GameTest mindSystem.TransferTo(mind, human, mind: mind); FixedPoint2 originalBalance = 20; - uplinkSystem.AddUplink(human, originalBalance, null, true); + uplinkSystem.AddUplink(human, originalBalance, out var notes, pda, null, true); - var storeComponent = entManager.GetComponent(pda); - var discountComponent = entManager.GetComponent(pda); + var remote = entManager.GetComponent(pda); + var storeEnt = remote.Store; + Assert.That(storeEnt.HasValue); + var storeComponent = entManager.GetComponent(storeEnt.Value); + var discountComponent = entManager.GetComponent(storeEnt.Value); Assert.That( discountComponent.Discounts, Has.Exactly(6).Items, @@ -138,7 +141,7 @@ public sealed class StoreTests : GameTest Assert.That(plainDiscountedCost.Value, Is.LessThan(prototypeCost.Value), "Expected discounted cost to be lower then prototype cost."); - var buyMsg = new StoreBuyListingMessage(discountedListingItem.ID){Actor = human}; + var buyMsg = new StoreBuyListingMessage(discountedListingItem.ID, null){Actor = human}; server.EntMan.EventBus.RaiseLocalEvent(pda, buyMsg); var newBalance = storeComponent.Balance[UplinkSystem.TelecrystalCurrencyPrototype]; diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs index 3568f17306..bc053c80ba 100644 --- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs @@ -18,6 +18,7 @@ using Robust.Shared.Random; using System.Linq; using System.Text; using Content.Server.Codewords; +using Robust.Shared.Map; namespace Content.Server.GameTicking.Rules; @@ -148,32 +149,22 @@ public sealed class TraitorRuleSystem : GameRuleSystem private (Note[]?, string) RequestUplink(EntityUid traitor, FixedPoint2 startingBalance, string briefing) { var pda = _uplink.FindUplinkTarget(traitor); - Note[]? code = null; Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink add"); - var uplinked = _uplink.AddUplink(traitor, startingBalance, pda, true); + var uplinked = _uplink.AddUplink(traitor, startingBalance, out var code, pda, giveDiscounts: true, bindToPda: false); - if (pda is not null && uplinked) + if (code != null && uplinked == AddUplinkResult.Pda) { Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink is PDA"); - // Codes are only generated if the uplink is a PDA - var ev = new GenerateUplinkCodeEvent(); - RaiseLocalEvent(pda.Value, ref ev); - if (ev.Code is { } generatedCode) - { - code = generatedCode; - - // If giveUplink is false the uplink code part is omitted - briefing = string.Format("{0}\n{1}", - briefing, - Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#")))); - return (code, briefing); - } - - Log.Error($"MakeTraitor {ToPrettyString(traitor)} failed to generate an uplink code on {ToPrettyString(pda)}."); + // If giveUplink is false the uplink code part is omitted + briefing = string.Format("{0}\n{1}", + briefing, + Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#")))); + return (code, briefing); } - else if (pda is null && uplinked) + + if (uplinked == AddUplinkResult.Implant) { Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink is implant"); briefing += "\n" + Loc.GetString("traitor-role-uplink-implant-short"); @@ -183,6 +174,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem Log.Error($"MakeTraitor failed on {ToPrettyString(traitor)} - No uplink could be added"); } + return (null, briefing); } diff --git a/Content.Server/PDA/PdaSystem.cs b/Content.Server/PDA/PdaSystem.cs index 9122c96964..afc5876760 100644 --- a/Content.Server/PDA/PdaSystem.cs +++ b/Content.Server/PDA/PdaSystem.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Content.Server.Access.Systems; using Content.Server.AlertLevel; using Content.Server.CartridgeLoader; @@ -17,6 +18,7 @@ using Content.Shared.Light; using Content.Shared.Light.EntitySystems; using Content.Shared.PDA; using Content.Shared.PDA.Ringer; +using Content.Shared.Store.Components; using Content.Shared.VoiceMask; using Robust.Server.Containers; using Robust.Server.GameObjects; @@ -189,7 +191,7 @@ namespace Content.Server.PDA var address = GetDeviceNetAddress(uid); var hasInstrument = HasComp(uid); - var showUplink = HasComp(uid) && IsUnlocked(uid); + var showUplink = TryGetUnlockedStore(uid, out _); UpdateStationName(uid, pda); UpdateAlertLevel(uid, pda); @@ -274,8 +276,19 @@ namespace Content.Server.PDA return; // check if its locked again to prevent malicious clients opening locked uplinks - if (HasComp(uid) && IsUnlocked(uid)) - _store.ToggleUi(msg.Actor, uid); + if (TryGetUnlockedStore(uid, out var store)) + { + if (store != uid) + { + if (TryComp(uid, out var remoteStore)) + remoteStore.Store = store; + _store.ToggleUi(msg.Actor, store.Value, remoteAccess: uid); + } + else + { + _store.ToggleUi(msg.Actor, store.Value); + } + } } private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaLockUplinkMessage msg) @@ -285,14 +298,24 @@ namespace Content.Server.PDA if (TryComp(uid, out var uplink)) { + if (TryComp(uid, out var remoteStore)) + remoteStore.Store = null; _ringer.LockUplink((uid, uplink)); UpdatePdaUi(uid, pda); } } - private bool IsUnlocked(EntityUid uid) + ///

+ /// Returns the currently unlocked store, if there is one. + /// + private bool TryGetUnlockedStore(EntityUid uid, [NotNullWhen(true)] out EntityUid? store) { - return !TryComp(uid, out var uplink) || uplink.Unlocked; + store = null; + if (!TryComp(uid, out var uplink) || !uplink.Unlocked || uplink.TargetStore == null) + return false; + + store = uplink.TargetStore; + return true; } private void UpdateStationName(EntityUid uid, PdaComponent pda) diff --git a/Content.Server/PDA/Ringer/RingerAccessUplinkComponent.cs b/Content.Server/PDA/Ringer/RingerAccessUplinkComponent.cs new file mode 100644 index 0000000000..f7d124adef --- /dev/null +++ b/Content.Server/PDA/Ringer/RingerAccessUplinkComponent.cs @@ -0,0 +1,24 @@ +using Content.Shared.PDA; + +namespace Content.Server.PDA.Ringer; + +/// +/// Opens the store UI when a PDA's ringtone is set to the secret code. +/// Traitors are told the code when greeted. +/// +[RegisterComponent, Access(typeof(RingerSystem))] +public sealed partial class RingerAccessUplinkComponent : Component +{ + /// + /// Notes to set ringtone to in order to lock or unlock the uplink. + /// Set via GenerateUplinkCodeEvent. + /// + [DataField] + public Note[]? Code; + + /// + /// If set, the uplink store can only be opened with the given entity. + /// + [DataField] + public EntityUid? BoundEntity; +} diff --git a/Content.Server/PDA/Ringer/RingerSystem.cs b/Content.Server/PDA/Ringer/RingerSystem.cs index b47ca0fde3..06df92bb68 100644 --- a/Content.Server/PDA/Ringer/RingerSystem.cs +++ b/Content.Server/PDA/Ringer/RingerSystem.cs @@ -1,9 +1,11 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Store.Systems; +using Content.Shared.GameTicking; using Content.Shared.PDA; using Content.Shared.PDA.Ringer; -using Content.Shared.Store.Components; using Robust.Shared.Random; +using Robust.Shared.Utility; namespace Content.Server.PDA.Ringer; @@ -14,15 +16,35 @@ public sealed class RingerSystem : SharedRingerSystem { [Dependency] private readonly IRobustRandom _random = default!; + public static Note[] AllowedNotes = + { + Note.C, + Note.D, + Note.E, + Note.F, + Note.G, + Note.A, + Note.B + }; + + /// + /// Stores the serialized version of any ringtone that can be excluded from new ringtone generations. + /// + [ViewVariables] + public readonly HashSet ReservedSerializedRingtones = new(); + /// public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnCurrencyInsert); - SubscribeLocalEvent(OnGenerateUplinkCode); + SubscribeLocalEvent(OnGenerateUplinkCode); + + SubscribeLocalEvent(CleanupReserved); + + InitialSetup(); } /// @@ -30,7 +52,11 @@ public sealed class RingerSystem : SharedRingerSystem /// private void OnMapInit(Entity ent, ref MapInitEvent args) { - UpdateRingerRingtone(ent, GenerateRingtone()); + var ringtone = GenerateRingtone(); + + ringtone ??= new Note[RingtoneLength] { Note.A, Note.A, Note.A, Note.A, Note.A, Note.A }; // Fallback + + UpdateRingerRingtone(ent, ringtone); } /// @@ -53,71 +79,209 @@ public sealed class RingerSystem : SharedRingerSystem /// /// Handles the for generating an uplink code. /// - private void OnGenerateUplinkCode(Entity ent, ref GenerateUplinkCodeEvent ev) + private void OnGenerateUplinkCode(Entity ent, ref GenerateUplinkCodeEvent ev) { - var code = GenerateRingtone(); + var code = GenerateRingtone(true, true); // Set the code on the component ent.Comp.Code = code; - // Return the code via the event ev.Code = code; } + private void InitialSetup() + { + ReservedSerializedRingtones.Clear(); + } + /// public override bool TryToggleUplink(EntityUid uid, Note[] ringtone, EntityUid? user = null) { if (!TryComp(uid, out var uplink)) return false; - if (!HasComp(uid)) - return false; - - // Wasn't generated yet - if (uplink.Code is null) - return false; - // On the server, we always check if the code matches - if (!uplink.Code.SequenceEqual(ringtone)) + if (!TryMatchRingtoneToStore(ringtone, out var store, uid)) return false; + uplink.TargetStore = store; + return ToggleUplinkInternal((uid, uplink)); } /// - /// Generates a random ringtone using the C pentatonic scale. + /// Generates a random ringtone using the C major scale. /// + /// Exclude any ringtone registered to ReservedSerializedRingtones. + /// Add the generated ringtone to ReservedSerializedRingtones. Requires ExcludeReserved to be true. /// An array of Notes representing the ringtone. /// The logic for this is on the Server so that we don't get a different result on the Client every time. - private Note[] GenerateRingtone() + private Note[]? GenerateRingtone(bool excludeReserved = false, bool reserveRingtone = false) { - // Default to using C pentatonic so it at least sounds not terrible. - return GenerateRingtone(new[] - { - Note.C, - Note.D, - Note.E, - Note.G, - Note.A - }); + // Default to using C major so it at least sounds not terrible. + return GenerateRingtone(AllowedNotes, excludeReserved, reserveRingtone); } /// /// Generates a random ringtone using the specified notes. /// /// The notes to choose from when generating the ringtone. + /// Exclude any ringtone registered to ReservedSerializedRingtones. + /// Add the generated ringtone to ReservedSerializedRingtones. Requires ExcludeReserved to be true. /// An array of Notes representing the ringtone. /// The logic for this is on the Server so that we don't get a different result on the Client every time. - private Note[] GenerateRingtone(Note[] notes) + private Note[]? GenerateRingtone(Note[] notes, bool excludeReserved = false, bool reserveRingtone = false) { - var ringtone = new Note[RingtoneLength]; + var excludedRingtones = excludeReserved ? ReservedSerializedRingtones.ToArray() : null; + + var maxPow = Math.Pow(notes.Length, RingtoneLength); + if (maxPow > int.MaxValue) + { + return null; + } + + var generatedRingtone = NextIntInRangeButExclude(0, Convert.ToInt32(maxPow) - 1, excludedRingtones); + + if (!TryDeserializeRingtone(notes, generatedRingtone, out var ringtone)) + return null; + + if (excludeReserved && reserveRingtone) + ReservedSerializedRingtones.Add(generatedRingtone); + + return ringtone; + } + + /// + /// Serialize a ringtone, representing it as an Int32. + /// + /// The array of notes used to generate the ringtone. + /// The ringtone which needs to be serialized. + /// The ringtone in a serialized format. + /// Whether the ringtone could be serialized or not. + private bool TrySerializeRingtone(Note[] allowedNotes, Note[] ringtone, [NotNullWhen(true)] out int? serializedRingtone) + { + var noteLength = allowedNotes.Length; + + // The serialization stores as an Int32, and therefore using Pow risks overshooting the max value, so we check for if that's a risk. + // If using 12 possible notes, you can have a ringtone sequence of 7 notes safely without overshooting. + var maxPow = Math.Pow(noteLength, ringtone.Length); + if (maxPow > int.MaxValue) + { + serializedRingtone = null; + return false; + } + + var serializationValue = 0; + + for (var i = 0; i < ringtone.Length; i++) + { + var pow = Math.Pow(noteLength, i); + var index = Array.IndexOf(allowedNotes, ringtone[i]); + if (index == -1) + { + serializedRingtone = null; + return false; + } + + serializationValue += Convert.ToInt32(pow) * index; + } + + serializedRingtone = serializationValue; + return true; + } + + /// + /// Deserialize a serialized ringtone into a Note array. + /// + /// The array of notes used to generate the ringtone. + /// The ringtone in a serialized format. + /// The ringtone resulting from the deserialization. + /// Whether the ringtone could be deserialized or not. + private bool TryDeserializeRingtone(Note[] allowedNotes, int serializedRingtone, [NotNullWhen(true)] out Note[]? ringtone) + { + var noteLength = allowedNotes.Length; + ringtone = new Note[RingtoneLength]; + + // The serialization stores as an Int32, and therefore using Pow risks overshooting the max value, so we check for if that's a risk. + // If using 12 possible notes, you can have a ringtone sequence of 7 notes safely without overshooting. + var maxPow = Math.Pow(noteLength, RingtoneLength); + if (maxPow > int.MaxValue) + { + ringtone = null; + return false; + } for (var i = 0; i < RingtoneLength; i++) { - ringtone[i] = _random.Pick(notes); + var pow = Math.Pow(noteLength, RingtoneLength - 1 - i); + var powInt = Convert.ToInt32(pow); + var val = serializedRingtone / powInt; + if (!AllowedNotes.TryGetValue(val, out var note)) + { + ringtone = null; + return false; + } + + ringtone[RingtoneLength - 1 - i] = note; + serializedRingtone -= val * powInt; } - return ringtone; + return true; + } + + /// + /// Try to get the store entity that has the matching ringer access. + /// + /// Notes from the ringer. + /// The store entity, if there is one. + /// The entity providing the code. + public bool TryMatchRingtoneToStore(Note[] notes, [NotNullWhen(true)] out EntityUid? store, EntityUid? ringer = null) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (comp.Code != null && notes.SequenceEqual(comp.Code)) + { + if (comp.BoundEntity != null && comp.BoundEntity != ringer) + break; + + store = uid; + return true; + } + } + + store = null; + return false; + } + + private void CleanupReserved(RoundRestartCleanupEvent ev) + { + ReservedSerializedRingtones.Clear(); + } + + private int NextIntInRangeButExclude(int start, int end, int[]? excludes) + { + excludes ??= new int[0]; + Array.Sort(excludes); + var rangeLength = end - start - excludes.Length; + var randomInt = _random.Next(rangeLength) + start; + + for (var i = 0; i < excludes.Length; i++) + { + if (excludes[i] > randomInt) + { + return randomInt; + } + + randomInt++; + } + + return randomInt; + } + + public void SetBoundUplinkEntity(Entity entity, EntityUid? targetEntity) + { + entity.Comp.BoundEntity = targetEntity; } } diff --git a/Content.Server/Store/Systems/StoreSystem.Ui.cs b/Content.Server/Store/Systems/StoreSystem.Ui.cs index 6b67ff5d60..ccf7c3f36c 100644 --- a/Content.Server/Store/Systems/StoreSystem.Ui.cs +++ b/Content.Server/Store/Systems/StoreSystem.Ui.cs @@ -41,6 +41,16 @@ public sealed partial class StoreSystem SubscribeLocalEvent(OnRequestWithdraw); SubscribeLocalEvent(OnRequestRefund); SubscribeLocalEvent(OnRefundEntityDeleted); + SubscribeLocalEvent((e, c, ev) => + RemoteStoreRelay((e, c), ev)); + SubscribeLocalEvent((e, c, ev) => + RemoteStoreRelay((e, c), ev)); + SubscribeLocalEvent((e, c, ev) => + RemoteStoreRelay((e, c), ev)); + SubscribeLocalEvent((e, c, ev) => + RemoteStoreRelay((e, c), ev)); + SubscribeLocalEvent((e, c, ev) => + RemoteStoreRelay((e, c), ev)); } private void OnRefundEntityDeleted(Entity ent, ref RefundEntityDeletedEvent args) @@ -48,21 +58,34 @@ public sealed partial class StoreSystem ent.Comp.BoughtEntities.Remove(args.Uid); } + private void RemoteStoreRelay(Entity entity, object ev) + { + if (entity.Comp.Store == null || !TryComp(entity.Comp.Store, out var store)) + return; + + RaiseLocalEvent(entity.Comp.Store.Value, ev); + } + /// /// Toggles the store Ui open and closed /// /// the person doing the toggling /// the store being toggled /// - public void ToggleUi(EntityUid user, EntityUid storeEnt, StoreComponent? component = null) + /// The entity remotely accessing the store, if any. + /// The remote access component, if any. + public void ToggleUi(EntityUid user, EntityUid storeEnt, StoreComponent? component = null, EntityUid? remoteAccess = null, RemoteStoreComponent? remoteComponent = null) { if (!Resolve(storeEnt, ref component)) return; + if (remoteAccess != null && !Resolve(remoteAccess.Value, ref remoteComponent) && remoteComponent!.Store != storeEnt) + return; + if (!TryComp(user, out var actor)) return; - if (!_ui.TryToggleUi(storeEnt, StoreUiKey.Key, actor.PlayerSession)) + if (!_ui.TryToggleUi(remoteAccess != null ? remoteAccess.Value : storeEnt, StoreUiKey.Key, actor.PlayerSession)) return; UpdateUserInterface(user, storeEnt, component); @@ -114,9 +137,27 @@ public sealed partial class StoreSystem var showFooter = HasComp(store); var state = new StoreUpdateState(component.LastAvailableListings, allCurrency, showFooter, component.RefundAllowed); + UpdateRemoteStores(store, state); _ui.SetUiState(store, StoreUiKey.Key, state); } + /// + /// Updates any remote store connections to a specific store. + /// + /// The store being updated. + /// The state being applied. + public void UpdateRemoteStores(EntityUid store, StoreUpdateState state) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var remote, out var ui)) + { + if (remote.Store != store) + continue; + + _ui.SetUiState((uid, ui), StoreUiKey.Key, state); + } + } + private void OnRequestUpdate(EntityUid uid, StoreComponent component, StoreRequestUpdateInterfaceMessage args) { UpdateUserInterface(args.Actor, GetEntity(args.Entity), component); @@ -284,7 +325,8 @@ public sealed partial class StoreSystem $"{ToPrettyString(buyer):player} purchased listing \"{ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, _proto)}\" from {ToPrettyString(uid)}{logExtraInfo}."); listing.PurchaseAmount++; //track how many times something has been purchased - _audio.PlayEntity(component.BuySuccessSound, msg.Actor, uid); //cha-ching! + if (msg.SoundSource != null && GetEntity(msg.SoundSource) != null) + _audio.PlayEntity(component.BuySuccessSound, msg.Actor, GetEntity(msg.SoundSource.Value)); //cha-ching! var buyFinished = new StoreBuyFinishedEvent { diff --git a/Content.Server/Store/Systems/StoreSystem.cs b/Content.Server/Store/Systems/StoreSystem.cs index 3806842507..0fcabba30f 100644 --- a/Content.Server/Store/Systems/StoreSystem.cs +++ b/Content.Server/Store/Systems/StoreSystem.cs @@ -15,11 +15,7 @@ using Robust.Shared.Utility; namespace Content.Server.Store.Systems; -/// -/// Manages general interactions with a store and different entities, -/// getting listings for stores, and interfacing with the store UI. -/// -public sealed partial class StoreSystem : EntitySystem +public sealed partial class StoreSystem : SharedStoreSystem { [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; @@ -95,23 +91,23 @@ public sealed partial class StoreSystem : EntitySystem private void OnAfterInteract(EntityUid uid, CurrencyComponent component, AfterInteractEvent args) { - if (args.Handled || !args.CanReach) + if (args.Handled || !args.CanReach || args.Target is not { } target) return; - if (!TryComp(args.Target, out var store)) + if (!TryGetStore(target, out var store)) return; - var ev = new CurrencyInsertAttemptEvent(args.User, args.Target.Value, args.Used, store); - RaiseLocalEvent(args.Target.Value, ev); + var ev = new CurrencyInsertAttemptEvent(args.User, target, args.Used, store.Value.Comp); + RaiseLocalEvent(target, ev); if (ev.Cancelled) return; - if (!TryAddCurrency((uid, component), (args.Target.Value, store))) + if (!TryAddCurrency((uid, component), (store.Value, store.Value.Comp))) return; args.Handled = true; - var msg = Loc.GetString("store-currency-inserted", ("used", args.Used), ("target", args.Target)); - _popup.PopupEntity(msg, args.Target.Value, args.User); + var msg = Loc.GetString("store-currency-inserted", ("used", args.Used), ("target", target)); + _popup.PopupEntity(msg, target, args.User); } private void OnImplantActivate(EntityUid uid, StoreComponent component, OpenUplinkImplantEvent args) diff --git a/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs b/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs index 8350c01f7a..bf00f5ebd8 100644 --- a/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs +++ b/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs @@ -73,7 +73,13 @@ public sealed class AddUplinkCommand : LocalizedEntityCommands } // Finally add uplink - if (!_uplinkSystem.AddUplink(user, 20, uplinkEntity: uplinkEntity, giveDiscounts: isDiscounted)) + var result = _uplinkSystem.AddUplink(user, 20, out var code, uplinkEntity: uplinkEntity, giveDiscounts: isDiscounted); + + if (code != null && result == AddUplinkResult.Pda) + shell.WriteLine(Loc.GetString("add-uplink-command-success-pda", ("code", string.Join("-", code).Replace("sharp", "#")))); + else if (result == AddUplinkResult.Implant) + shell.WriteLine(Loc.GetString("add-uplink-command-success-implant")); + else if (result == AddUplinkResult.Failure) shell.WriteLine(Loc.GetString("add-uplink-command-error-2")); } diff --git a/Content.Server/Traitor/Uplink/UplinkSystem.cs b/Content.Server/Traitor/Uplink/UplinkSystem.cs index e8ed868dfb..a8c1ab3da5 100644 --- a/Content.Server/Traitor/Uplink/UplinkSystem.cs +++ b/Content.Server/Traitor/Uplink/UplinkSystem.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Server.PDA.Ringer; using Content.Server.Store.Systems; using Content.Server.StoreDiscount.Systems; using Content.Shared.FixedPoint; @@ -9,6 +10,7 @@ using Content.Shared.Mind; using Content.Shared.PDA; using Content.Shared.Store; using Content.Shared.Store.Components; +using Robust.Shared.Map; using Robust.Shared.Prototypes; namespace Content.Server.Traitor.Uplink; @@ -21,7 +23,9 @@ public sealed class UplinkSystem : EntitySystem [Dependency] private readonly StoreSystem _store = default!; [Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!; [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly RingerSystem _ringer = default!; + public static readonly EntProtoId TraitorUplinkStore = "StorePresetRemoteUplink"; public static readonly ProtoId TelecrystalCurrencyPrototype = "Telecrystal"; private static readonly EntProtoId FallbackUplinkImplant = "UplinkImplant"; private static readonly ProtoId FallbackUplinkCatalog = "UplinkUplinkImplanter"; @@ -31,28 +35,72 @@ public sealed class UplinkSystem : EntitySystem /// /// The person who is getting the uplink /// The amount of currency on the uplink. If null, will just use the amount specified in the preset. + /// The code which was generated, if any. /// The entity that will actually have the uplink functionality. Defaults to the PDA if null. + /// The entity that will have the store in it. /// Marker that enables discounts for uplink items. - /// Whether or not the uplink was added successfully - public bool AddUplink( + /// Binds the uplink to the specific uplink entity. + /// Whether the uplink was added successfully to a PDA, implant or not at all. + public AddUplinkResult AddUplink( EntityUid user, FixedPoint2 balance, + out Note[]? code, EntityUid? uplinkEntity = null, - bool giveDiscounts = false) + EntityUid? storeEntity = null, + bool giveDiscounts = false, + bool bindToPda = false) { - // Try to find target item if none passed + code = null; + if (TryAddEntityUplink(user, balance, out var generatedCode, uplinkEntity, storeEntity, giveDiscounts, bindToPda)) + { + code = generatedCode; + return AddUplinkResult.Pda; + } + + if (TryImplantUplink(user, balance, giveDiscounts)) + { + return AddUplinkResult.Implant; + } + + return AddUplinkResult.Failure; + } + + public bool TryAddEntityUplink( + EntityUid user, + FixedPoint2 balance, + out Note[]? code, + EntityUid? uplinkEntity, + EntityUid? storeEntity, + bool giveDiscounts = false, + bool bindToPda = false) + { + code = null; + + storeEntity ??= Spawn(TraitorUplinkStore, MapCoordinates.Nullspace); uplinkEntity ??= FindUplinkTarget(user); if (uplinkEntity == null) - return ImplantUplink(user, balance, giveDiscounts); + return false; - EnsureComp(uplinkEntity.Value); + var ev = new GenerateUplinkCodeEvent(); + RaiseLocalEvent(storeEntity.Value, ref ev); - SetUplink(user, uplinkEntity.Value, balance, giveDiscounts); + if (ev.Code == null) + { + QueueDel(storeEntity); + return false; + } - // TODO add BUI. Currently can't be done outside of yaml -_- - // ^ What does this even mean? + code = ev.Code; + + if (bindToPda) + { + var accessComp = EnsureComp(storeEntity.Value); + _ringer.SetBoundUplinkEntity((storeEntity.Value, accessComp), uplinkEntity.Value); + } + + SetUplink(user, storeEntity.Value, uplinkEntity.Value, balance, giveDiscounts); return true; } @@ -60,25 +108,35 @@ public sealed class UplinkSystem : EntitySystem /// /// Configure TC for the uplink /// - private void SetUplink(EntityUid user, EntityUid uplink, FixedPoint2 balance, bool giveDiscounts) + private void SetUplink(EntityUid user, EntityUid store, EntityUid uplink, FixedPoint2 balance, bool giveDiscounts) + { + SetUplink(user, store, balance, giveDiscounts); + var remote = EnsureComp(uplink); + remote.Store = store; + } + + /// + /// Configure TC for the uplink + /// + private void SetUplink(EntityUid user, EntityUid store, FixedPoint2 balance, bool giveDiscounts) { if (!_mind.TryGetMind(user, out var mind, out _)) return; - var store = EnsureComp(uplink); + var storeComp = EnsureComp(store); - store.AccountOwner = mind; + storeComp.AccountOwner = mind; - store.Balance.Clear(); + storeComp.Balance.Clear(); _store.TryAddCurrency(new Dictionary { { TelecrystalCurrencyPrototype, balance } }, - uplink, - store); + store, + storeComp); var uplinkInitializedEvent = new StoreInitializedEvent( TargetUser: mind, - Store: uplink, + Store: store, UseDiscounts: giveDiscounts, - Listings: _store.GetAvailableListings(mind, uplink, store) + Listings: _store.GetAvailableListings(mind, store, storeComp) .ToArray()); RaiseLocalEvent(ref uplinkInitializedEvent); } @@ -86,7 +144,7 @@ public sealed class UplinkSystem : EntitySystem /// /// Implant an uplink as a fallback measure if the traitor had no PDA /// - private bool ImplantUplink(EntityUid user, FixedPoint2 balance, bool giveDiscounts) + public bool TryImplantUplink(EntityUid user, FixedPoint2 balance, bool giveDiscounts) { if (!_proto.Resolve(FallbackUplinkCatalog, out var catalog)) return false; @@ -115,7 +173,7 @@ public sealed class UplinkSystem : EntitySystem /// Finds the entity that can hold an uplink for a user. /// Usually this is a pda in their pda slot, but can also be in their hands. (but not pockets or inside bag, etc.) ///
- public EntityUid? FindUplinkTarget(EntityUid user) + public Entity? FindUplinkTarget(EntityUid user) { // Try to find PDA in inventory if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator)) @@ -124,18 +182,25 @@ public sealed class UplinkSystem : EntitySystem { var pdaUid = containerSlot.ContainedEntity; - if (HasComp(pdaUid) && HasComp(pdaUid)) - return pdaUid; + if (HasComp(pdaUid) && TryComp(pdaUid, out var remote)) + return (pdaUid.Value, remote); } } // Also check hands foreach (var item in _handsSystem.EnumerateHeld(user)) { - if (HasComp(item) && HasComp(item)) - return item; + if (HasComp(item) && TryComp(item, out var remote)) + return (item, remote); } return null; } } + +public enum AddUplinkResult +{ + Pda, + Implant, + Failure, +} diff --git a/Content.Shared/PDA/Ringer/RingerUplinkComponent.cs b/Content.Shared/PDA/Ringer/RingerUplinkComponent.cs index 2dbbfb5efc..dd0de10493 100644 --- a/Content.Shared/PDA/Ringer/RingerUplinkComponent.cs +++ b/Content.Shared/PDA/Ringer/RingerUplinkComponent.cs @@ -3,22 +3,21 @@ using Robust.Shared.GameStates; namespace Content.Shared.PDA.Ringer; /// -/// Opens the store UI when the ringstone is set to the secret code. +/// Makes a PDA able to open store UIs when the ringtone is set to a secret code. /// Traitors are told the code when greeted. /// [RegisterComponent, NetworkedComponent, Access(typeof(SharedRingerSystem))] public sealed partial class RingerUplinkComponent : Component { - /// - /// Notes to set ringtone to in order to lock or unlock the uplink. - /// Set via GenerateUplinkCodeEvent. - /// - [DataField] - public Note[]? Code; - /// /// Whether to show the toggle uplink button in PDA settings. /// [DataField] public bool Unlocked; + + /// + /// The store which the ringer is targetting. + /// + [DataField] + public EntityUid? TargetStore; } diff --git a/Content.Shared/PDA/SharedRingerSystem.cs b/Content.Shared/PDA/SharedRingerSystem.cs index 392cd46e72..3c1e392f13 100644 --- a/Content.Shared/PDA/SharedRingerSystem.cs +++ b/Content.Shared/PDA/SharedRingerSystem.cs @@ -137,6 +137,7 @@ public abstract class SharedRingerSystem : EntitySystem return; ent.Comp.Unlocked = false; + ent.Comp.TargetStore = null; UI.CloseUi(ent.Owner, StoreUiKey.Key); } @@ -249,7 +250,10 @@ public abstract class SharedRingerSystem : EntitySystem // Close store UI if we're locking if (!ent.Comp.Unlocked) + { + ent.Comp.TargetStore = null; UI.CloseUi(ent.Owner, StoreUiKey.Key); + } return true; } diff --git a/Content.Shared/Store/Components/RemoteStoreComponent.cs b/Content.Shared/Store/Components/RemoteStoreComponent.cs new file mode 100644 index 0000000000..3b07e03e09 --- /dev/null +++ b/Content.Shared/Store/Components/RemoteStoreComponent.cs @@ -0,0 +1,17 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Store.Components; + +/// +/// This component manages a store which players can use to purchase different listings +/// through the ui. The currency, listings, and categories are defined in yaml. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class RemoteStoreComponent : Component +{ + /// + /// The store which is currently being targetted for remote opening + /// + [DataField] + public EntityUid? Store; +} diff --git a/Content.Shared/Store/SharedStoreSystem.cs b/Content.Shared/Store/SharedStoreSystem.cs new file mode 100644 index 0000000000..0abd12742e --- /dev/null +++ b/Content.Shared/Store/SharedStoreSystem.cs @@ -0,0 +1,59 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Store.Components; + +namespace Content.Shared.Store; + +/// +/// Manages general interactions with a store and different entities, +/// getting listings for stores, and interfacing with the store UI. +/// +public abstract partial class SharedStoreSystem : EntitySystem +{ + [Dependency] protected readonly EntityQuery StoreQuery = default!; + [Dependency] protected readonly EntityQuery RemoteStoreQuery = default!; + + /// + /// Attempts to find a store connected to this entity. + /// First checking for a on this entity, + /// then checking for a to find a remotely connected store. + /// + /// Entity we're checking for an attached store on + /// Store entity we're returning. + /// True if a store was found. + public bool TryGetStore(Entity entity, [NotNullWhen(true)] out Entity? store) + { + store = GetStore(entity); + return store != null; + } + + /// + /// Attempts to find a store connected to this entity. + /// First checking for a on this entity, + /// then checking for a to find a remotely connected store. + /// + /// Entity we're checking for an attached store on + /// The store entity and component if found. + public Entity? GetStore(Entity entity) + { + if (StoreQuery.TryComp(entity, out var storeComp)) + return (entity, storeComp); + + return GetRemoteStore(entity); + } + + /// + /// Attempts to find a remote store connected to this entity. + /// Checking for a with an attached store entity. + /// + /// Entity we're checking for an attached store on + /// The store entity and component if found. + public Entity? GetRemoteStore(Entity entity) + { + if (RemoteStoreQuery.Resolve(entity, ref entity.Comp, false) + && entity.Comp.Store != null + && StoreQuery.TryComp(entity.Comp.Store, out var storeComp)) + return (entity.Comp.Store.Value, storeComp); + + return null; + } +} diff --git a/Content.Shared/Store/StoreUi.cs b/Content.Shared/Store/StoreUi.cs index d8cb9e6ca8..531788df1f 100644 --- a/Content.Shared/Store/StoreUi.cs +++ b/Content.Shared/Store/StoreUi.cs @@ -37,9 +37,10 @@ public sealed class StoreRequestUpdateInterfaceMessage : BoundUserInterfaceMessa } [Serializable, NetSerializable] -public sealed class StoreBuyListingMessage(ProtoId listing) : BoundUserInterfaceMessage +public sealed class StoreBuyListingMessage(ProtoId listing, NetEntity? soundSource) : BoundUserInterfaceMessage { public ProtoId Listing = listing; + public NetEntity? SoundSource = soundSource; } [Serializable, NetSerializable] diff --git a/Resources/Locale/en-US/administration/commands/add-uplink-command.ftl b/Resources/Locale/en-US/administration/commands/add-uplink-command.ftl index 40d0c5fa1b..a0cee58050 100644 --- a/Resources/Locale/en-US/administration/commands/add-uplink-command.ftl +++ b/Resources/Locale/en-US/administration/commands/add-uplink-command.ftl @@ -5,4 +5,6 @@ add-uplink-command-completion-1 = Username (defaults to self) add-uplink-command-completion-2 = Uplink uid (default to PDA) add-uplink-command-completion-3 = Is uplink discount enabled add-uplink-command-error-1 = Selected player doesn't control any entity -add-uplink-command-error-2 = Failed to add uplink to the player \ No newline at end of file +add-uplink-command-error-2 = Failed to add uplink to the player +add-uplink-command-success-pda = Uplink added to player PDA with code {$code} +add-uplink-command-success-implant = Uplink added to player as an implant diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml index 79beb70551..ad3b59b0ea 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml @@ -1,6 +1,6 @@ - type: entity abstract: true - parent: [ BaseItem, StorePresetUplink ] #PDA's have uplinks so they have to inherit the data. + parent: [ BaseItem ] id: BasePDA name: PDA description: Personal Data Assistant. @@ -107,6 +107,7 @@ type: InstrumentBoundUserInterface enum.HealthAnalyzerUiKey.Key: type: HealthAnalyzerBoundUserInterface + - type: RemoteStore # PDAs can access uplinks so they need this component. - type: Tag tags: - DoorBumpOpener diff --git a/Resources/Prototypes/Store/presets.yml b/Resources/Prototypes/Store/presets.yml index 13823a40f1..d4c1245c55 100644 --- a/Resources/Prototypes/Store/presets.yml +++ b/Resources/Prototypes/Store/presets.yml @@ -51,3 +51,10 @@ - ChangelingAbilities currencyWhitelist: - ChangelingDNA + +- type: entity + parent: StorePresetUplink + id: StorePresetRemoteUplink + categories: [ HideSpawnMenu ] + components: + - type: RingerAccessUplink From 1a8b1c48b936735635d2472afd48038f57d65f7e Mon Sep 17 00:00:00 2001 From: PJBot Date: Sat, 4 Apr 2026 20:15:23 +0000 Subject: [PATCH 073/126] Automatic changelog update --- Resources/Changelog/Admin.yml | 8 ++++++++ Resources/Changelog/Changelog.yml | 18 ++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Admin.yml b/Resources/Changelog/Admin.yml index dcaae63db7..cf6842025e 100644 --- a/Resources/Changelog/Admin.yml +++ b/Resources/Changelog/Admin.yml @@ -1691,5 +1691,13 @@ Entries: id: 206 time: '2026-03-08T23:41:39.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/41427 +- author: SlamBamActionman + changes: + - message: adduplink command now works properly, gives an uplink implant if there's + no PDA, and returns the uplink code in the command terminal. + type: Fix + id: 207 + time: '2026-04-04T20:14:15.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/38712 Name: Admin Order: 3 diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 7b35a1062c..769227c0d6 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: slarticodefast - changes: - - message: Zombified arachnids can no longer spam infinite silk due to having no - hunger. - type: Fix - id: 9103 - time: '2025-10-14T23:31:16.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40279 - author: telavivgamers changes: - message: You may put ashes and matchsticks into ashtrays. @@ -4033,3 +4025,13 @@ id: 9614 time: '2026-04-04T19:58:35.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/42701 +- author: SlamBamActionman + changes: + - message: Uplink PDA codes are now universal. This means any PDA can open an uplink + store, as long as they have the correct code. + type: Tweak + - message: PDA ringtones now feature more notes. + type: Tweak + id: 9615 + time: '2026-04-04T20:14:15.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/38712 From fd9212c73a68ec23f6668ed1cb8d35b434941de8 Mon Sep 17 00:00:00 2001 From: Kyle Tyo Date: Sat, 4 Apr 2026 17:09:24 -0400 Subject: [PATCH 074/126] Remove delta_alt.ogg (#43468) Delete delta_alt.ogg --- Resources/Audio/Misc/delta_alt.ogg | Bin 226574 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Resources/Audio/Misc/delta_alt.ogg diff --git a/Resources/Audio/Misc/delta_alt.ogg b/Resources/Audio/Misc/delta_alt.ogg deleted file mode 100644 index b389910f136b91b55a7aad5975ca23318488e55e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 226574 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 z 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

)84o4 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�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

SU0I<`OOb3e4aLM%K+7bMF<)9G%3(^dldH1 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 iy&Plo*UQB@;(lqdgwjzfED_ Date: Sat, 4 Apr 2026 17:29:38 -0400 Subject: [PATCH 075/126] Update DungeonSystem.Rooms to use IRobustRandom instead of Random (#43457) * Use IRobustRandom instead of System.Random * Use IRobustRandom instead of System.Random * Add Robust.Shared.Random directive --- Content.Server/Procedural/DungeonSystem.Rooms.cs | 9 +++++---- Content.Server/Procedural/RoomFillSystem.cs | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Content.Server/Procedural/DungeonSystem.Rooms.cs b/Content.Server/Procedural/DungeonSystem.Rooms.cs index e5b0981b3d..0854fbb172 100644 --- a/Content.Server/Procedural/DungeonSystem.Rooms.cs +++ b/Content.Server/Procedural/DungeonSystem.Rooms.cs @@ -7,6 +7,7 @@ using Content.Shared.Whitelist; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Utility; +using Robust.Shared.Random; namespace Content.Server.Procedural; @@ -19,7 +20,7 @@ public sealed partial class DungeonSystem ///

/// Gets a random dungeon room matching the specified area, whitelist and size. /// - public DungeonRoomPrototype? GetRoomPrototype(Vector2i size, Random random, EntityWhitelist? whitelist = null) + public DungeonRoomPrototype? GetRoomPrototype(Vector2i size, IRobustRandom random, EntityWhitelist? whitelist = null) { return GetRoomPrototype(random, whitelist, minSize: size, maxSize: size); } @@ -27,7 +28,7 @@ public sealed partial class DungeonSystem /// /// Gets a random dungeon room matching the specified area and whitelist and size range /// - public DungeonRoomPrototype? GetRoomPrototype(Random random, + public DungeonRoomPrototype? GetRoomPrototype(IRobustRandom random, EntityWhitelist? whitelist = null, Vector2i? minSize = null, Vector2i? maxSize = null) @@ -77,7 +78,7 @@ public sealed partial class DungeonSystem MapGridComponent grid, Vector2i origin, DungeonRoomPrototype room, - Random random, + IRobustRandom random, HashSet? reservedTiles, bool clearExisting = false, bool rotation = false) @@ -96,7 +97,7 @@ public sealed partial class DungeonSystem SpawnRoom(gridUid, grid, finalTransform, room, reservedTiles, clearExisting); } - public Angle GetRoomRotation(DungeonRoomPrototype room, Random random) + public Angle GetRoomRotation(DungeonRoomPrototype room, IRobustRandom random) { var roomRotation = Angle.Zero; diff --git a/Content.Server/Procedural/RoomFillSystem.cs b/Content.Server/Procedural/RoomFillSystem.cs index f4ccab2367..96adb8dec3 100644 --- a/Content.Server/Procedural/RoomFillSystem.cs +++ b/Content.Server/Procedural/RoomFillSystem.cs @@ -1,4 +1,5 @@ using Robust.Shared.Map.Components; +using Robust.Shared.Random; namespace Content.Server.Procedural; @@ -6,6 +7,7 @@ public sealed class RoomFillSystem : EntitySystem { [Dependency] private readonly DungeonSystem _dungeon = default!; [Dependency] private readonly SharedMapSystem _maps = default!; + [Dependency] private readonly IRobustRandom _random = default!; public override void Initialize() { @@ -19,8 +21,7 @@ public sealed class RoomFillSystem : EntitySystem if (xform.GridUid != null) { - var random = new Random(); - var room = _dungeon.GetRoomPrototype(random, component.RoomWhitelist, component.MinSize, component.MaxSize); + var room = _dungeon.GetRoomPrototype(_random, component.RoomWhitelist, component.MinSize, component.MaxSize); if (room != null) { @@ -30,7 +31,7 @@ public sealed class RoomFillSystem : EntitySystem mapGrid, _maps.LocalToTile(xform.GridUid.Value, mapGrid, xform.Coordinates) - new Vector2i(room.Size.X/2,room.Size.Y/2), room, - random, + _random, null, clearExisting: component.ClearExisting, rotation: component.Rotation); From 9d9294cdfdce8aeb78f3e409377de4542c9a150e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 5 Apr 2026 03:04:39 +0200 Subject: [PATCH 076/126] Update Credits (#43473) Co-authored-by: PJBot --- Resources/Credits/GitHub.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Credits/GitHub.txt b/Resources/Credits/GitHub.txt index 221a6124d5..64ab3e20ae 100644 --- a/Resources/Credits/GitHub.txt +++ b/Resources/Credits/GitHub.txt @@ -1 +1 @@ -0-Anon, 0leshe, 0tito, 0x6273, 11BelowStudio, 12rabbits, 1337dakota, 13spacemen, 154942, 2013HORSEMEATSCANDAL, 20kdc, 21Melkuu, 27alaing, 2DSiggy, 3nderall, 4310v343k, 4dplanner, 5tickman, 612git, 778b, 96flo, aaron, abadaba695, Ablankmann, abregado, Absolute-Potato, Absotively, achookh, Acruid, ActiveMammmoth, actually-reb, ada-please, adamsong, Adeinitas, adm2play, Admiral-Obvious-001, adrian, Adrian16199, Ady4ik, Aearo-Deepwater, Aerocrux, Aeshus, Aexolott, Aexxie, AffleWaffle, africalimedrop, afrokada, AftrLite, AgentSmithRadio, Agoichi, ahandleman, Ahion, aiden, Aidenkrz, aidenkrz, Aisu9, ajcm, AJCM-git, AjexRose, Alekshhh, alexalexmax, alexkar598, AlexMorgan3817, alexum418, alexumandxgabriel08x, Alice4267, Alithsko, Alkheemist, alliephante, ALMv1, Alpaccalypse, AlphaQwerty, Altoids1, amatwiedle, amylizzle, Andre19926, Andrew-Fall, AndrewEyeke, AndrewFenriz, AndreyCamper, anri, Anzarot121, ApolloVector, Appiah, april-gras, ar4ill, Arcane-Waffle, arcanevaliance, archee1, ArchPigeon, ArchRBX, areitpog, Arendian, areyouconfused, arimah, Arkanic, ArkiveDev, armoks, Arteben, arthropodia, ArthurMousatov, ArtisticRoomba, artur, Artxmisery, ArZarLordOfMango, as334, AshBats, AsikKEsel, AsnDen, asperger-sind, aspiringLich, astriloqua, Atakku, Ataman, august-sun, AutoOtter, AverageNotDoingAnythingEnjoyer, avghdev, AwareFoxy, Awlod, Axionyxx, azloserbits, AzzyIsNotHere, azzyisnothere, B-Kirill, B3CKDOOR, baa14453, BackeTako, BadaBoomie, Bakke, BananaFlambe, Baptr0b0t, BarryNorfolk, BasedUser, bea, bebr3ght, beck-thompson, beesterman, bellwetherlogic, ben, benbryant0, benev0, benjamin-burges, BGare, bhespiritu, bibbly, BigfootBravo, BIGZi0348, bingojohnson, BismarckShuffle, Bixkitts, Blackern5000, Blazeror, blitzthesquishy, Blobadoodle, bloodrizer, Bloody2372, blueDev2, Boaz1111, BobdaBiscuit, BobTheSleder, boiled-water-tsar, Bokser815, bolantej, BombasterDS, Booblesnoot42, Boolean-Buckeye, botanySupremist, brainfood1183, BramvanZijp, Brandon-Huu, breeplayx3, BriBrooo, BRINGit34, brndd, bryce0110, BubblegumBlue, buletsponge, buntobaggins, buunie099, bvelliquette, BWTCK, byondfuckery, c0rigin, c4llv07e, CaasGit, Caconym27, Calecute, Callmore, Camdot, cammusubi, capnsockless, CaptainMaru, captainsqrbeard, Carbonhell, Carolyn3114, Carou02, carteblanche4me, catdotjs, catlord, Catofquestionableethics, CatTheSystem, CawsForConcern, CDWimmer, Centronias, Chaboricks, chairbender, chaisftw, Chaoticaa, Charlese2, charlie, chartman, ChaseFlorom, chavonadelal, Cheackraze, CheddaCheez, cheesePizza2, CheesePlated, Chief-Engineer, chillyconmor, christhirtle, chromiumboy, Chronophylos, Chubbicous, Chubbygummibear, Ciac32, ciaran, citrea, civilCornball, claustro305, Clement-O, cloudyias, clyf, Clyybber, CMDR-Piboy314, cnv41, coco, cohanna, Cohnway, Cojoke-dot, ColdAutumnRain, Colin-Tel, collinlunn, ComicIronic, Compilatron144, CookieMasterT, coolboy911, CoolioDudio, coolmankid12345, Coolsurf6, cooperwallace, corentt, CormosLemming, CrafterKolyan, CraftyRenter, crazybrain23, Crazydave91920, CrazyPhantom779, creadth, CrigCrag, CroilBird, Crotalus, CrudeWax, cryals, CrzyPotato, cubixthree, cutemoongod, Cyberboss, d34d10cc, DaCookieCakes, DadeKuma, Daemon, daerSeebaer, dahnte, dakamakat, DamianX, dan, dangerrevolution, daniel-cr, DanSAussieITS, Daracke, Darkie, DaturoDewitt, david, DawBla, Daxxi3, dch-GH, ddeegan, de0rix, Deahaka, dean, DEATHB4DEFEAT, Deatherd, deathride58, debugok, Decappi, Decortex, Deeeeja, deepdarkdepths, DeepwaterCreations, Deerstop, degradka, Delete69, deltanedas, DenisShvalov, DerbyX, derek, dersheppard, Deserty0, Detintinto, DevilishMilk, devinschubert14, dexlerxd, dffdff2423, DieselMohawk, DieselMohawkTheSequel, digitalic, Dimastra, dimmoon1, DinnerCalzone, DinoWattz, Disp-Dev, DisposableCrewmember42, dissidentbullet, DjfjdfofdjfjD, doc-michael, docnite, Doctor-Cpu, DogZeroX, dolgovmi, dontbetank, Doomsdrayk, Doru991, DoubleRiceEddiedd, DoutorWhite, DR-DOCTOR-EVIL-EVIL, Dragonjspider, dragonryan06, drakewill-CRL, Drayff, dreamlyjack, DrEnzyme, dribblydrone, DrMelon, drongood12, DrSingh, DrSmugleaf, drteaspoon420, DTanxxx, DubiousDoggo, DuckManZach, Duddino, dukevanity, duskyjay, Dutch-VanDerLinde, dvir001, dylanstrategie, dylanwhittingham, Dynexust, Easypoller, echo, EchoOfNothing, eclips_e, eden077, EEASAS, Efruit, efzapa, Ekkosangen, ElectroSR, elsie, elthundercloud, Elysium206, emberwinters, Emisse, emmafornash, EmoGarbage404, Endecc, EnrichedCaramel, Entvari, eoineoineoin, ephememory, eris, erohrs2, erorr404v1, Errant-4, ertanic, esguard, estacaoespacialpirata, eternally-confused, eugene, ewokswagger, exincore, exp111, f0x-n3rd, F1restar4, FacePluslll, Fahasor, FairlySadPanda, farrellka-dev, FATFSAAM2, Feluk6174, ficcialfaint, Fiftyllama, Fildrance, fillervk, FinnishPaladin, firenamefn, Firewars763, FirinMaLazors, Fishfish458, fl-oz, Flareguy, flashgnash, FlipBrooke, FluffiestFloof, FluffMe, FluidRock, flymo5678, foboscheshir, FoLoKe, fooberticus, ForestNoises, forgotmyotheraccount, forkeyboards, forthbridge, Fortune117, foxhorn, freeman2651, freeze2222, frobnic8, Froffy025, Fromoriss, froozigiusz, FrostMando, FrostRibbon, Fruitsalad, Funce, FungiFellow, FunkySphere, FunTust, Futuristic-OK, GalacticChimp, gamer3107, Gamewar360, gansulalan, GaussiArson, Gaxeer, gbasood, gcoremans, Geekyhobo, genderGeometries, GeneralGaws, Genkail, Gentleman-Bird, geraeumig, Ghagliiarghii, Git-Nivrak, githubuser508, GitHubUser53123, gituhabu, GlassEclipse, GnarpGnarp, GNF54, godisdeadLOL, goet, GoldenCan, Goldminermac, Golinth, golubgik, GoodWheatley, Gorox221, GR1231, gradientvera, graevy, GraniteSidewalk, GreaseMonk, greenrock64, GreyMario, GrownSamoyedDog, GTRsound, gusxyz, Gyrandola, h3half, hamurlik, Hanzdegloker, HappyRoach, happyrobot33, Hardly3D, harikattar, Hayden, he1acdvv, Hebi, Helix-ctrl, helm4142, Henry, HerCoyote23, Hi-Im-Shot, HighTechPuddle, Hitlinemoss, hiucko, hivehum, Hmeister-fake, Hmeister-real, Hobbitmax, hobnob, HoidC, Holinka4ever, holyssss, HoofedEar, Hoolny, hord-brayden, hoshizora-sayo, Hreno, Hrosts, htmlsystem, Huaqas, hubismal, Hugal31, Hyenh, hyperb1, hyperDelegate, hyphenationc, i-justuser-i, iaada, iacore, IamVelcroboy, Ian321, icekot8, icesickleone, iczero, iglov, IgorAnt028, igorsaux, ike709, illersaver, Illiux, Ilushkins33, Ilya246, IlyaElDunaev, imatsoup, IMCB, impubbi, imrenq, imweax, indeano, Injazz, Insineer, insoPL, IntegerTempest, Interrobang01, Intoxicating-Innocence, IProduceWidgets, itsmethom, Itzbenz, iztokbajcar, Jackal298, Jackrost, JackRyd3r, jacksonzck, JackspajfMain, Jacktastic09, Jackw2As, jacob, jamessimo, janekvap-havok, Jark255, Jarmer123, Jaskanbe, JasperJRoth, jbox144, JCGWE30, jerryimmouse, JerryImMouse, Jessetriesagain, jessicamaybe, JesterX666, Jewelots, Jezithyr, jicksaw, JiimBob, JimGamemaster, jimmy12or, JIPDawg, jjtParadox, jkwookee, jmcb, JohnGinnane, JohnJJohn, johnjjohn, johnku1, Jophire, Jopogrechkin, joshepvodka, JpegOfAFrog, jproads, JrInventor05, Jrpl, jukereise, juliangiebel, JustArt1m, JustCone14, justdie12, justin, justintether, JustinTrotter, JustinWinningham, justtne, K-Dynamic, k3yw, Kadeo64, Kaga-404, kaiserbirch, KaiShibaa, kalane15, kalanosh, KamTheSythe, Kanashi-Panda, katzenminer, kbailey-git, Keelin, Keer-Sar, KEEYNy, keikiru, Kelrak, kerisargit, keronshb, KeTuFaisPiKiNut, KIBORG04, KieueCaprie, Kimpes, kin98, KingFroozy, kipdotnet, kira-er, kiri-yoshikage, Kirillcas, Kirus59, Kistras, Kit, Kit0vras, KittenColony, Kittygyat, klaypexx, kleinerstation13, Kmc2000, Ko4ergaPunk, kognise, kokoc9n, komunre, KonstantinAngelov, kontakt, korczoczek, koteq, kotobdev, Kowlin, KrasnoshchekovPavel, Krosus777, Krunklehorn, Kryyto, Kupie, kxvvv, Kyoth25f, kyupolaris, kzhanik, LaCumbiaDelCoronavirus, lajolico, Lamrr, lanedon, LankLTE, laok233, lapatison, larryrussian, lawdog4817, Lazzi0706, Le-Arctic-Fox, leahcat, leander-0, leonardo-dabepis, leonidussaks, leonsfriedrich, LeoSantich, lettern, LetterN, Level10Cybermancer, LEVELcat, lever1209, LevitatingTree, Lgibb18, lgruthes, liem161, LightVillet, lilazero, liltenhead, linkbro1, linkuyx, Litraxx, little-meow-meow, LittleBuilderJane, LittleNorthStar, LittleNyanCat, lizelive, ljm862, lmsnoise, localcc, lokachop, lolman360, Lomcastar, Lordbrandon12, LordCarve, LordEclipse, lucas, LucasTheDrgn, luckyshotpictures, LudwigVonChesterfield, luegamer, luizwritescode, LukaSlade, luminight, lunarcomets, Lusatia, Luxeator, lvvova1, Lyndomen, lyroth001, lyxcaster, lzimann, lzk228, M1tht1c, M3739, M4rchy-S, M87S, mac6na6na, MACMAN2003, Macoron, magicalus, magmodius, magnnusson, magnuscrowe, maland1, malchanceux, MaloTV, manelnavola, ManelNavola, Mangohydra, marboww, Markek1, MarkerWicker, marlyn, mastermiller01, matt, Matz05, max, MaxNox7, maylokana, mdrkrg, MDuch369, meara1179, meganerobot, MehimoNemo, Mehnix, MeltedPixel, memeproof, MendaxxDev, Menshin, Mephisto72, MerrytheManokit, Mervill, metalgearsloth, MetalSage, MFMessage, mhamsterr, michaelcu, micheel665, mifia, mikeysaurus, MilenVolf, MilonPL, Minemoder5000, Minty642, minus1over12, Mirino97, mirrorcult, misandrie, MishaUnity, MissKay1994, MisterImp, MisterMecky, Mith-randalf, Mixelz, mjarduk, MjrLandWhale, mkanke-real, MLGTASTICa, mnva0, moderatelyaware, modern-nm, mohamedwidar, mokiros, momo, Moneyl, monotheonist, Moomoobeef, moony, Morb0, MossyGreySlope, mqole, mr-bo-jangles, Mr0maks, MrFippik, MrPersival, mrrobdemo, mtrs163, muburu, MureixloI, murolem, murphyneko, musicmanvr, MWKane, Myakot, Myctai, N3X15, nabegator, nails-n-tape, Nairodian, Naive817, NakataRin, namespace-Memory, Nannek, NazrinNya, neborsh, nekokiwa, neomoth, neutrino-laser, NickPowers43, nikitosych, nikthechampiongr, Nimfar11, ninruB, Nirnael, NIXC, nkokic, NkoKirkto, nmajask, noctyrnal, noelkathegod, noirogen, nok-ko, NonchalantNoob, NoobyLegion, Nopey, NoreUhh, Not-A-Chair, not-gavnaed, notafet, notquitehadouken, notsodana, noudoit, noverd, Nox38, NuclearWinter, Nuggets219, nukashimika, nuke-haus, NULL882, nullarmo, nyeogmi, Nylux, Nyranu, Nyxilath, och-och, OctoRocket, Ohelig, OldDanceJacket, OliverOtter, onesch, OneZerooo0, OnsenCapy, OnyxTheBrave, opl-, Orange-Winds, OrangeMoronage9622, OrbitSystem07, Orsoniks, osjarw, Ostaf, othymer, OttoMaticode, Owai-Seek, packmore, PAFFhassoocks, paige404, paigemaeforrest, pali6, Palladinium, Pangogie, panzer-iv1, partyaddict, patrikturi, PaulRitter, pavlockblaine03, peccneck, Peptide90, peptron1, perryprog, PeterFuto, PetMudstone, pewter-wiz, PGrayCS, pgraycs, Pgriha, phantom-lily, Pharaz4, pheenty, philingham, Phill101, Phooooooooooooooooooooooooooooooosphate, phunnyguy, PicklOH, PilgrimViis, Pill-U, pinkbat5, Piras314, Pireax, Pissachu, pissdemon, Pixel8-dev, PixeltheAertistContrib, PixelTheKermit, PJB3005, Plasmaguy, plinyvic, Plykiya, poeMota, pofitlo, pointer-to-null, Pok27, poklj, PolterTzi, PoorMansDreams, PopGamer45, portfiend, potato1234x, PotentiallyTom, PotRoastPiggy, Princess-Cheeseballs, ProfanedBane, Prole0, ProPandaBear, ProPeperos, PrPleGoo, ps3moira, Pspritechologist, Psychpsyo, psykana, psykzz, PuceTint, pumkin69, PuroSlavKing, PursuitInAshes, Putnam3145, py01, Pyrovi, qrtDaniil, qrwas, Quantum-cross, quasr-9, quatre, QueerNB, QuietlyWhisper, qwerltaz, Radezolid, RadioMull, Radosvik, Radrark, Rainbeon, Rainfey, Raitononai, Ramlik, RamZ, randy10122, Rane, Ranger6012, Rapidgame7, ravage123321, rbertoche, RedBookcase, Redfire1331, Redict, RedlineTriad, redmushie, RednoWCirabrab, Redrover1760, redspyy, ReeZer2, RemberBM, RemieRichards, RemTim, rene-descartes2021, Renlou, retequizzle, rewafflution, rhailrake, rhsvenson, rich-dunne, RieBi, riggleprime, RIKELOLDABOSS, rinary1, Rinkashikachi, riolume, rlebell33, RobbyTheFish, robinthedragon, robinthegirlthing, Rockdtben, Rohesie, rok-povsic, rokudara-sen, rolfero, RomanNovo, roryflowers, rosieposieeee, Roudenn, router, ruddygreat, rumaks-xyz, RumiTiger, Ruzihm, rwrv, S1rFl0, S1ss3l, Saakra, SabreML, Sadie-silly, saga3152, saintmuntzer, salarua, Salex08, sam, samgithubaccount, Samuka-C, SaphireLattice, SapphicOverload, sarahon, sativaleanne, SaveliyM360, sBasalto, ScalyChimp, ScarKy0, ScholarNZL, schrodinger71, scrato, Scribbles0, scrivoy, scruq445, scuffedjays, ScumbagDog, SeamLesss, Segonist, semensponge, sephtasm, ser1-1y, Serkket, sewerpig, SG6732, sh18rw, Shaddap1, ShadeAware, ShadowCommander, shadowtheprotogen546, shaeone, shampunj, shariathotpatrol, SharkSnake98, Shegare, shepardtothestars, shibechef, Siginanto, signalsender, SignalWalker, siigiil, silicon14wastaken, Silverfur-underscore, Simyon264, sirdragooon, Sirionaut, SirWarock, Sk1tch, SkaldetSkaeg, Skarletto, skeeka-dev, skrybl, Skybailey-dev, skye, Skyedra, SlamBamActionman, slarticodefast, Slava0135, sleepyyapril, slimmslamm, Slyfox333, Smugman, SnappingOpossum, snebl, snicket, sniperchance, Snowni, snowsignal, SolidSyn, SolidusSnek, solstar2, SomegnihT, SonarZeBat, SonicHDC, SoulFN, SoulSloth, Soundwavesghost, soupkilove, southbridge-fur, sowelipililimute, Soydium, SpaceLizard24, SpaceLizardSky, SpaceManiac, SpaceRox1244, SpaceyLady, Spangs04, spanky-spanky, Sparlight, spartak, SpartanKadence, spderman3333, SpeltIncorrectyl, Spessmann, SphiraI, SplinterGP, spoogemonster, sporekto, sporkyz, ssdaniel24, stalengd, stanberytrask, Stanislav4ix, StanTheCarpenter, starbuckss14, Stealthbomber16, steel, Steffo99, stellar-novas, stewie523, stomf, Stop-Signs, stopbreaking, stopka-html, StrawberryMoses, Stray-Pyramid, strO0pwafel, Strol20, StStevens, Subversionary, sunbear-dev, SuperGDPWYL, superjj18, Supernorn, SurrealShibe, SweetAplle, SweptWasTaken, SyaoranFox, Sybil, SYNCHRONIC, Synthestra, Szunti, t, Tainakov, takemysoult, taonewt, tap, TaralGit, Taran, taserthefox, taurie, Tayrtahn, tday93, teamaki, TeenSarlacc, TekuNut, telavivgamers, telyonok, temm1ie, TemporalOroboros, tentekal, terezi4real, Terraspark4941, texcruize, Tezzaide, TGODiamond, TGRCdev, tgrkzus, thanosdegraf, ThatGuyUSA, ThatOneGoblin25, thatrandomcanadianguy, TheArturZh, TheBlueYowie, thecopbennet, TheCze, TheDarkElites, thedraccx, TheEmber, theexetron, TheFlyingSentry, thefoty, TheGrimbeeper, TheIntoxicatedCat, thekilk, themias, theomund, TheProNoob678, TherapyGoth, ThereDrD0, TheSecondLord, TheShuEd, thetolbean, thevinter, TheWaffleJesus, Thinbug0, ThunderBear2006, timothyteakettle, TimrodDX, timurjavid, tin-man-tim, TiniestShark, Titian3, tk-a369, tkdrg, tmtmtl30, ToastEnjoyer, Toby222, TokenStyle, Tollhouse, Toly65, tom-leys, tomasalves8, Tomeno, Tonydatguy, topy, tornado-technology, TornadoTechnology, tosatur, TotallyLemon, ToxicSonicFan04, Tr1bute, travis-g-reid, treytipton, TriviaSolari, trixxedbit, TrixxedHeart, tropicalhibi, truepaintgit, Truoizys, Tryded, TsjipTsjip, tuchila-adi-bogdan, Tunguso4ka, TurboTrackerss14, TVK-04, tyashley, Tyler-IN, TytosB, Tyzemol, UbaserB, Uberration, ubis1, UBlueberry, uhbg, UKNOWH, UltimateJester, Unbelievable-Salmon, underscorex5, UnicornOnLSD, Unisol, Unkn0wnGh0st333, unusualcrow, UpAndLeaves, Uriende, UristMcDorf, user424242420, Utmanarn, Vaaankas, valentfingerov, valquaint, Varen, Vasilis, VasilisThePikachu, veliebm, Velken, VelonacepsCalyxEggs, veprolet, VerinSenpai, veritable-calamity, Veritius, Vermidia, vero5123, verslebas, vexerot, vgskye, viceemargo, VigersRay, violet754, Visne, vitopigno, vitusveit, vlad, vlados1408, VMSolidus, vmzd, VoidMeticulous, voidnull000, volotomite, volundr-, Voomra, Vordenburg, vorkathbruh, Vortebo, vulppine, wachte1, wafehling, walksanatora, Warentan, WarMechanic, Watermelon914, weaversam8, wertanchik, whateverusername0, whatston3, widgetbeck, Will-Oliver-Br, Willhelm53, WilliamECrew, willicassi, Winkarst-cpu, wirdal, wixoaGit, WlarusFromDaSpace, Wolfkey-SomeoneElseTookMyUsername, Worldwaker, wrexbe, wtcwr68, xeri7, xkreksx, xprospero, xRiriq, xsainteer, YanehCheck, yathxyz, Ygg01, YotaXP, youarereadingthis, YoungThugSS14, Yousifb26, youtissoum, yunii, YuriyKiss, yuriykiss, zach-hill, Zadeon, Zalycon, zamp, Zandario, Zap527, Zealith-Gamer, ZelteHonor, zero, ZeroDiamond, ZeWaka, zHonys, zionnBE, ZNixian, Zokkie, ZoldorfTheWizard, zonespace27, Zylofan, Zymem, zzylex +0-Anon, 0leshe, 0tito, 0x6273, 11BelowStudio, 12rabbits, 1337dakota, 13spacemen, 154942, 2013HORSEMEATSCANDAL, 20kdc, 21Melkuu, 27alaing, 2DSiggy, 3nderall, 4310v343k, 4dplanner, 5tickman, 612git, 778b, 96flo, aaron, abadaba695, Ablankmann, abregado, Absolute-Potato, Absotively, achookh, Acruid, ActiveMammmoth, actually-reb, ada-please, adamsong, Adeinitas, adm2play, Admiral-Obvious-001, adrian, Adrian16199, Ady4ik, Aearo-Deepwater, Aerocrux, Aeshus, Aexolott, Aexxie, AffleWaffle, africalimedrop, afrokada, AftrLite, AgentSmithRadio, Agoichi, ahandleman, Ahion, aiden, Aidenkrz, aidenkrz, Aisu9, ajcm, AJCM-git, AjexRose, Alekshhh, alexalexmax, alexkar598, AlexMorgan3817, alexum418, alexumandxgabriel08x, Alice4267, Alithsko, Alkheemist, alliephante, ALMv1, Alpaccalypse, AlphaQwerty, Altoids1, amatwiedle, amylizzle, Andre19926, Andrew-Fall, AndrewEyeke, AndrewFenriz, AndreyCamper, anri, Anzarot121, ApolloVector, Appiah, april-gras, ar4ill, Arcane-Waffle, arcanevaliance, archee1, ArchPigeon, ArchRBX, areitpog, Arendian, areyouconfused, arimah, Arkanic, ArkiveDev, armoks, Arteben, arthropodia, ArthurMousatov, ArtisticRoomba, artur, Artxmisery, ArZarLordOfMango, as334, AshBats, AsikKEsel, AsnDen, asperger-sind, aspiringLich, astriloqua, Atakku, Ataman, august-sun, AutoOtter, AverageNotDoingAnythingEnjoyer, avghdev, AwareFoxy, Awlod, Axionyxx, azloserbits, AzzyIsNotHere, azzyisnothere, B-Kirill, B3CKDOOR, baa14453, BackeTako, BadaBoomie, Bakke, BananaFlambe, Baptr0b0t, BarryNorfolk, BasedUser, baynarikattu, bea, bebr3ght, beck-thompson, beesterman, bellwetherlogic, ben, benbryant0, benev0, benjamin-burges, BGare, bhespiritu, bibbly, BigfootBravo, BIGZi0348, bingojohnson, BismarckShuffle, Bixkitts, Blackern5000, Blazeror, blitzthesquishy, Blobadoodle, bloodrizer, Bloody2372, blueDev2, Boaz1111, BobdaBiscuit, BobTheSleder, boiled-water-tsar, Bokser815, bolantej, BombasterDS, Booblesnoot42, Boolean-Buckeye, botanySupremist, brainfood1183, BramvanZijp, Brandon-Huu, breeplayx3, BriBrooo, BRINGit34, brndd, bryce0110, BubblegumBlue, buletsponge, buntobaggins, buunie099, bvelliquette, BWTCK, byondfuckery, c0rigin, c4llv07e, CaasGit, Caconym27, Calecute, Callmore, Camdot, cammusubi, capnsockless, CaptainMaru, captainsqrbeard, Carbonhell, Carolyn3114, Carou02, carteblanche4me, catdotjs, catlord, Catofquestionableethics, CatTheSystem, CawsForConcern, CDWimmer, Centronias, Chaboricks, chairbender, chaisftw, Chaoticaa, Charlese2, charlie, chartman, ChaseFlorom, chavonadelal, Cheackraze, CheddaCheez, cheesePizza2, CheesePlated, Chief-Engineer, chillyconmor, christhirtle, chromiumboy, Chronophylos, Chubbicous, Chubbygummibear, Ciac32, ciaran, citrea, civilCornball, claustro305, Clement-O, cloudyias, clyf, Clyybber, CMDR-Piboy314, cnv41, coco, cohanna, Cohnway, Cojoke-dot, ColdAutumnRain, Colin-Tel, collinlunn, ComicIronic, Compilatron144, CookieMasterT, coolboy911, CoolioDudio, coolmankid12345, Coolsurf6, cooperwallace, corentt, CormosLemming, CrafterKolyan, CraftyRenter, crazybrain23, Crazydave91920, CrazyPhantom779, creadth, CrigCrag, CroilBird, Crotalus, CrudeWax, cryals, CrzyPotato, cubixthree, cutemoongod, Cyberboss, d34d10cc, DaCookieCakes, DadeKuma, Daemon, daerSeebaer, dahnte, dakamakat, DamianX, dan, dangerrevolution, daniel-cr, DanSAussieITS, Daracke, Darkie, DaturoDewitt, david, DawBla, Daxxi3, dch-GH, ddeegan, de0rix, Deahaka, dean, DEATHB4DEFEAT, Deatherd, deathride58, debugok, Decappi, Decortex, Deeeeja, deepdarkdepths, DeepwaterCreations, Deerstop, degradka, Delete69, deltanedas, DenisShvalov, DerbyX, derek, dersheppard, Deserty0, Detintinto, DevilishMilk, devinschubert14, dexlerxd, dffdff2423, DieselMohawk, DieselMohawkTheSequel, digitalic, Dimastra, dimmoon1, DinnerCalzone, DinoWattz, Disp-Dev, DisposableCrewmember42, dissidentbullet, DjfjdfofdjfjD, doc-michael, docnite, Doctor-Cpu, DogZeroX, dolgovmi, dontbetank, Doomsdrayk, Doru991, DoubleRiceEddiedd, DoutorWhite, DR-DOCTOR-EVIL-EVIL, Dragonjspider, dragonryan06, drakewill-CRL, Drayff, dreamlyjack, DrEnzyme, dribblydrone, DrMelon, drongood12, DrSingh, DrSmugleaf, drteaspoon420, DTanxxx, DubiousDoggo, DuckManZach, Duddino, dukevanity, duskyjay, Dutch-VanDerLinde, dvir001, dylanstrategie, dylanwhittingham, Dynexust, Easypoller, echo, EchoOfNothing, eclips_e, eden077, EEASAS, Efruit, efzapa, Ekkosangen, ElectroSR, elsie, elthundercloud, Elysium206, emberwinters, Emisse, emmafornash, EmoGarbage404, Endecc, EnrichedCaramel, Entvari, eoineoineoin, ephememory, eris, erohrs2, erorr404v1, Errant-4, ertanic, esguard, estacaoespacialpirata, eternally-confused, eugene, eveloop, ewokswagger, exincore, exp111, f0x-n3rd, F1restar4, FacePluslll, Fahasor, FairlySadPanda, farrellka-dev, FATFSAAM2, Feluk6174, ficcialfaint, Fiftyllama, Fildrance, fillervk, FinnishPaladin, firenamefn, Firewars763, FirinMaLazors, Fishfish458, fl-oz, Flareguy, flashgnash, FlipBrooke, FluffiestFloof, FluffMe, FluidRock, flymo5678, foboscheshir, FoLoKe, fooberticus, ForestNoises, forgotmyotheraccount, forkeyboards, forthbridge, Fortune117, foxhorn, freeman2651, freeze2222, frobnic8, Froffy025, Fromoriss, froozigiusz, FrostMando, FrostRibbon, Fruitsalad, Funce, FungiFellow, FunkySphere, FunTust, Futuristic-OK, GalacticChimp, gamer3107, Gamewar360, gansulalan, GaussiArson, Gaxeer, gbasood, gcoremans, Geekyhobo, genderGeometries, GeneralGaws, Genkail, Gentleman-Bird, geraeumig, Ghagliiarghii, Git-Nivrak, githubuser508, GitHubUser53123, gituhabu, GlassEclipse, GnarpGnarp, GNF54, godisdeadLOL, goet, GoldenCan, Goldminermac, Golinth, golubgik, GoodWheatley, Gorox221, GR1231, gradientvera, graevy, GraniteSidewalk, GreaseMonk, greenrock64, GreyMario, GrownSamoyedDog, GTRsound, gusxyz, Gyrandola, h3half, hamurlik, Hanzdegloker, HappyRoach, happyrobot33, Hardly3D, harikattar, Hayden, he1acdvv, Hebi, Helix-ctrl, helm4142, Henry, HerCoyote23, Hi-Im-Shot, HighTechPuddle, Hitlinemoss, hiucko, hivehum, Hmeister-fake, Hmeister-real, Hobbitmax, hobnob, HoidC, Holinka4ever, holyssss, HoofedEar, Hoolny, hord-brayden, hoshizora-sayo, Hreno, Hrosts, htmlsystem, Huaqas, hubismal, Hugal31, Hyenh, hyperb1, hyperDelegate, hyphenationc, i-justuser-i, iaada, iacore, IamVelcroboy, Ian321, icekot8, icesickleone, iczero, iglov, IgorAnt028, igorsaux, ike709, illersaver, Illiux, Ilushkins33, Ilya246, IlyaElDunaev, imatsoup, IMCB, impubbi, imrenq, imweax, indeano, Injazz, Insineer, insoPL, IntegerTempest, Interrobang01, Intoxicating-Innocence, IProduceWidgets, itsmethom, Itzbenz, iztokbajcar, Jackal298, Jackrost, JackRyd3r, jacksonzck, JackspajfMain, Jacktastic09, Jackw2As, jacob, jamessimo, janekvap-havok, Jark255, Jarmer123, Jaskanbe, JasperJRoth, jbox144, JCGWE30, jerryimmouse, JerryImMouse, Jessetriesagain, jessicamaybe, JesterX666, Jewelots, Jezithyr, jicksaw, JiimBob, JimGamemaster, jimmy12or, JIPDawg, jjtParadox, jkwookee, jmcb, JohnGinnane, JohnJJohn, johnjjohn, johnku1, Jophire, Jopogrechkin, joshepvodka, JpegOfAFrog, jproads, JrInventor05, Jrpl, jukereise, juliangiebel, JustArt1m, JustCone14, justdie12, justin, justintether, JustinTrotter, JustinWinningham, justtne, K-Dynamic, k3yw, Kadeo64, Kaga-404, kaiserbirch, KaiShibaa, kalane15, kalanosh, KamTheSythe, Kanashi-Panda, katzenminer, kbailey-git, Keelin, Keer-Sar, KEEYNy, keikiru, Kelrak, kerisargit, keronshb, KeTuFaisPiKiNut, KIBORG04, KieueCaprie, Kimpes, kin98, KingFroozy, kipdotnet, kira-er, kiri-yoshikage, Kirillcas, Kirus59, Kistras, Kit, Kit0vras, KittenColony, Kittygyat, klaypexx, kleinerstation13, Kmc2000, Ko4ergaPunk, kognise, kokoc9n, komunre, KonstantinAngelov, kontakt, korczoczek, koteq, kotobdev, Kowlin, KrasnoshchekovPavel, Krosus777, Krunklehorn, Kryyto, Kupie, kxvvv, Kyoth25f, kyupolaris, kzhanik, LaCumbiaDelCoronavirus, lajolico, Lamrr, lanedon, LankLTE, laok233, lapatison, larryrussian, lawdog4817, Lazzi0706, Le-Arctic-Fox, leahcat, leander-0, leonardo-dabepis, leonidussaks, leonsfriedrich, LeoSantich, LetterN, lettern, Level10Cybermancer, LEVELcat, lever1209, LevitatingTree, Lgibb18, lgruthes, liem161, LightVillet, lilazero, liltenhead, linkbro1, linkuyx, Litraxx, little-meow-meow, LittleBuilderJane, LittleNorthStar, LittleNyanCat, lizelive, ljm862, lmsnoise, localcc, lokachop, lolman360, Lomcastar, Lordbrandon12, LordCarve, LordEclipse, lucas, LucasTheDrgn, luckyshotpictures, LudwigVonChesterfield, luegamer, luizwritescode, LukaSlade, luminight, lunarcomets, Lusatia, Luxeator, lvvova1, Lyndomen, lyroth001, lyxcaster, lzimann, lzk228, M1tht1c, M3739, M4rchy-S, M87S, mac6na6na, MACMAN2003, Macoron, magicalus, magmodius, magnnusson, magnuscrowe, maland1, malchanceux, MaloTV, ManelNavola, manelnavola, Mangohydra, marboww, Markek1, MarkerWicker, marlyn, mastermiller01, matt, Matz05, max, MaxNox7, maylokana, mdrkrg, MDuch369, meara1179, meganerobot, MehimoNemo, Mehnix, MeltedPixel, memeproof, MendaxxDev, Menshin, Mephisto72, MerrytheManokit, Mervill, metalgearsloth, MetalSage, MFMessage, mhamsterr, michaelcu, micheel665, mifia, mikeysaurus, MilenVolf, MilonPL, Minemoder5000, Minty642, minus1over12, Mirino97, mirrorcult, misandrie, MishaUnity, MissKay1994, MisterImp, MisterMecky, Mith-randalf, Mixelz, mjarduk, MjrLandWhale, mkanke-real, MLGTASTICa, mnva0, moderatelyaware, modern-nm, mohamedwidar, mokiros, momo, Moneyl, monotheonist, Moomoobeef, moony, Morb0, MossyGreySlope, mqole, mr-bo-jangles, Mr0maks, MrFippik, MrPersival, mrrobdemo, mtrs163, muburu, MureixloI, murolem, murphyneko, musicmanvr, MWKane, Myakot, Myctai, N3X15, nabegator, nails-n-tape, Nairodian, Naive817, NakataRin, namespace-Memory, Nannek, NazrinNya, neborsh, nekokiwa, neomoth, neutrino-laser, NickPowers43, nikitosych, nikthechampiongr, Nimfar11, ninruB, Nirnael, NIXC, nkokic, NkoKirkto, nmajask, noctyrnal, noelkathegod, noirogen, nok-ko, NonchalantNoob, NoobyLegion, Nopey, NoreUhh, Not-A-Chair, not-gavnaed, NotActuallyMarty, notafet, notquitehadouken, notsodana, noudoit, noverd, Nox38, NuclearWinter, Nuggets219, nukashimika, nuke-haus, NULL882, nullarmo, nyeogmi, Nylux, Nyranu, Nyxilath, och-och, OctoRocket, Ohelig, OldDanceJacket, OliverOtter, onesch, OneZerooo0, OnsenCapy, OnyxTheBrave, opl-, Orange-Winds, OrangeMoronage9622, OrbitSystem07, Orsoniks, osjarw, Ostaf, othymer, OttoMaticode, Owai-Seek, packmore, PAFFhassoocks, paige404, paigemaeforrest, pali6, Palladinium, Pangogie, panzer-iv1, partyaddict, patrikturi, PaulRitter, pavlockblaine03, peccneck, Peptide90, peptron1, perryprog, PeterFuto, PetMudstone, pewter-wiz, pgraycs, PGrayCS, Pgriha, phantom-lily, Pharaz4, philingham, Phill101, Phonix, Phooooooooooooooooooooooooooooooosphate, phunnyguy, PicklOH, PilgrimViis, Pill-U, pinkbat5, Piras314, Pireax, Pissachu, pissdemon, Pixel8-dev, PixeltheAertistContrib, PixelTheKermit, PJB3005, Plasmaguy, plinyvic, Plykiya, poeMota, pofitlo, pointer-to-null, Pok27, poklj, PolterTzi, PoorMansDreams, PopGamer45, portfiend, potato1234x, PotentiallyTom, PotRoastPiggy, Princess-Cheeseballs, ProfanedBane, Prole0, ProPandaBear, ProPeperos, PrPleGoo, ps3moira, Pspritechologist, Psychpsyo, psykana, psykzz, PuceTint, pumkin69, PuroSlavKing, PursuitInAshes, Putnam3145, py01, Pyrovi, qrtDaniil, qrwas, Quantum-cross, quasr-9, quatre, QueerNB, QuietlyWhisper, qwerltaz, Radezolid, RadioMull, Radosvik, Radrark, Rainbeon, Rainfey, RainyGale, Raitononai, Ramlik, RamZ, randy10122, Rane, Ranger6012, Rapidgame7, ravage123321, rbertoche, RedBookcase, Redfire1331, Redict, RedlineTriad, redmushie, RednoWCirabrab, Redrover1760, redspyy, ReeZer2, RemberBM, RemieRichards, RemTim, rene-descartes2021, Renlou, retequizzle, rewafflution, rhailrake, rhsvenson, rich-dunne, RieBi, riggleprime, RIKELOLDABOSS, rinary1, Rinkashikachi, riolume, rlebell33, RobbyTheFish, robinthedragon, robinthegirlthing, Rockdtben, Rohesie, rok-povsic, rokudara-sen, rolfero, RomanNovo, roryflowers, rosieposieeee, Roudenn, router, ruddygreat, rumaks-xyz, RumiTiger, Ruzihm, rwrv, S1rFl0, S1ss3l, Saakra, SabreML, Sadie-silly, saga3152, saintmuntzer, salarua, Salex08, sam, samgithubaccount, Samuka-C, SaphireLattice, SapphicOverload, sarahon, sativaleanne, SaveliyM360, sBasalto, ScalyChimp, ScarKy0, ScholarNZL, schrodinger71, scrato, Scribbles0, scrivoy, scruq445, scuffedjays, ScumbagDog, SeamLesss, Segonist, semensponge, sephtasm, ser1-1y, Serkket, sewerpig, SG6732, sh18rw, Shaddap1, ShadeAware, ShadowCommander, shadowtheprotogen546, shaeone, shampunj, shariathotpatrol, SharkSnake98, Shegare, shepardtothestars, shibechef, Siginanto, signalsender, SignalWalker, siigiil, silicon14wastaken, Silverfur-underscore, Simyon264, sirdragooon, Sirionaut, SirWarock, Sk1tch, SkaldetSkaeg, Skarletto, skeeka-dev, skrybl, Skybailey-dev, skye, Skyedra, SlamBamActionman, slarticodefast, Slava0135, sleepyyapril, slimmslamm, Slyfox333, Smugman, SnappingOpossum, snebl, snicket, sniperchance, Snowni, snowsignal, SolidSyn, SolidusSnek, solstar2, SomegnihT, SonarZeBat, SonicHDC, SoulFN, SoulSloth, Soundwavesghost, soupkilove, southbridge-fur, sowelipililimute, Soydium, SpaceLizard24, SpaceLizardSky, SpaceManiac, SpaceRox1244, SpaceyLady, Spangs04, spanky-spanky, Sparlight, spartak, SpartanKadence, spderman3333, SpeltIncorrectyl, Spessmann, SphiraI, SplinterGP, spoogemonster, sporekto, sporkyz, ssdaniel24, stalengd, stanberytrask, Stanislav4ix, StanTheCarpenter, starbuckss14, Stealthbomber16, steel, Steffo99, stellar-novas, stewie523, stomf, Stop-Signs, stopbreaking, stopka-html, StrawberryMoses, Stray-Pyramid, strO0pwafel, Strol20, StStevens, Subversionary, sunbear-dev, SuperGDPWYL, superjj18, Supernorn, SurrealShibe, SweetAplle, SweptWasTaken, SyaoranFox, Sybil, SYNCHRONIC, Synthestra, Szunti, t, Tainakov, takemysoult, taonewt, tap, TaralGit, Taran, taserthefox, taurie, Tayrtahn, tday93, teamaki, TeenSarlacc, TekuNut, telavivgamers, telyonok, temm1ie, TemporalOroboros, tentekal, terezi4real, Terraspark4941, texcruize, Tezzaide, TGODiamond, TGRCdev, tgrkzus, thanosdegraf, ThatGuyUSA, ThatOneGoblin25, thatrandomcanadianguy, TheArturZh, TheBlueYowie, thecopbennet, TheCze, TheDarkElites, thedraccx, TheEmber, theexetron, TheFlyingSentry, thefoty, TheGrimbeeper, TheIntoxicatedCat, thekilk, themias, theomund, TheProNoob678, TherapyGoth, ThereDrD0, TheSecondLord, TheShuEd, thetolbean, thevinter, TheWaffleJesus, Thinbug0, ThunderBear2006, timothyteakettle, TimrodDX, timurjavid, tin-man-tim, TiniestShark, Titian3, tk-a369, tkdrg, tmtmtl30, ToastEnjoyer, Toby222, TokenStyle, Tollhouse, Toly65, tom-leys, tomasalves8, Tomeno, Tonydatguy, topy, tornado-technology, TornadoTechnology, tosatur, TotallyLemon, ToxicSonicFan04, Tr1bute, travis-g-reid, treytipton, TriviaSolari, trixxedbit, TrixxedHeart, tropicalhibi, truepaintgit, Truoizys, Tryded, TsjipTsjip, tuchila-adi-bogdan, Tunguso4ka, TurboTrackerss14, TVK-04, tyashley, Tyler-IN, TytosB, Tyzemol, UbaserB, Uberration, ubis1, UBlueberry, uhbg, UKNOWH, UltimateJester, Unbelievable-Salmon, underscorex5, UnicornOnLSD, Unisol, Unkn0wnGh0st333, unusualcrow, UpAndLeaves, Uriende, UristMcDorf, user424242420, Utmanarn, Vaaankas, valentfingerov, valquaint, VanderslootAssgiraffe, Varen, Vasilis, VasilisThePikachu, veliebm, Velken, VelonacepsCalyxEggs, veprolet, VerinSenpai, veritable-calamity, Veritius, Vermidia, vero5123, verslebas, vexerot, vgskye, viceemargo, VigersRay, violet754, Visne, vitopigno, vitusveit, vlad, vlados1408, VMSolidus, vmzd, VoidMeticulous, voidnull000, volotomite, volundr-, Voomra, Vordenburg, vorkathbruh, Vortebo, vulppine, wachte1, wafehling, walksanatora, Warentan, WarMechanic, Watermelon914, weaversam8, wertanchik, whateverusername0, whatston3, widgetbeck, Will-Oliver-Br, Willhelm53, WilliamECrew, willicassi, Winkarst-cpu, wirdal, wixoaGit, WlarusFromDaSpace, Wolfkey-SomeoneElseTookMyUsername, Worldwaker, wrexbe, wtcwr68, xeri7, xkreksx, xprospero, xRiriq, xsainteer, YanehCheck, yathxyz, Ygg01, YotaXP, youarereadingthis, YoungThugSS14, Yousifb26, youtissoum, yunii, YuriyKiss, yuriykiss, zach-hill, Zadeon, Zalycon, zamp, Zandario, Zap527, Zealith-Gamer, zekins3366, ZelteHonor, zero, ZeroDiamond, ZeWaka, zHonys, zionnBE, ZNixian, Zokkie, ZoldorfTheWizard, zonespace27, Zylofan, Zymem, zzylex From ca95da6daaf595c475a144c82c8e44de31d90c5c Mon Sep 17 00:00:00 2001 From: Naxel <46362288+Naxel11@users.noreply.github.com> Date: Sun, 5 Apr 2026 07:39:00 +0300 Subject: [PATCH 077/126] Add parallelization to RadiationSystem.GridCast (#39173) * add parallelization to RadiationSystem.GridCast * change the preparation of sources receivers data * failed integrationtest fix * Update Content.Server/Radiation/Systems/RadiationSystem.GridCast.cs Co-authored-by: Southbridge <7013162+southbridge-fur@users.noreply.github.com> * Change namespace to file-scoped * Method signature changed * null check changed * null check changed. exactly * Use TryComp instead of Resolve in UpdateSource * replace TryComp with xform in UpdateSource * replace DynamicTree with B2DynamicTree * return comment * Remove magic number * Optimize radiation memory usage * replace ThreadLocal to ArrayPool * fix "false sharing" issue * Revert "fix "false sharing" issue" This reverts commit c0729f024f8e2df9d15f1cc9cc9bc1be4d4be312. * cleanup * inject entquery --------- Co-authored-by: xNaxelx <46362288+xNaxelx@users.noreply.github.com> Co-authored-by: Southbridge <7013162+southbridge-fur@users.noreply.github.com> Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> --- .../Systems/RadiationSystem.GridCast.cs | 294 +++++++++--------- .../Radiation/Systems/RadiationSystem.cs | 111 ++++++- .../Components/RadiationSourceComponent.cs | 5 + 3 files changed, 251 insertions(+), 159 deletions(-) diff --git a/Content.Server/Radiation/Systems/RadiationSystem.GridCast.cs b/Content.Server/Radiation/Systems/RadiationSystem.GridCast.cs index 796c1e1fce..8bbfa7ad41 100644 --- a/Content.Server/Radiation/Systems/RadiationSystem.GridCast.cs +++ b/Content.Server/Radiation/Systems/RadiationSystem.GridCast.cs @@ -1,11 +1,15 @@ +using System.Buffers; +using System.Collections.Concurrent; +using System.Linq; using System.Numerics; using Content.Server.Radiation.Components; using Content.Server.Radiation.Events; using Content.Shared.Radiation.Components; using Content.Shared.Radiation.Systems; -using Robust.Shared.Collections; +using JetBrains.Annotations; using Robust.Shared.Map.Components; -using Robust.Shared.Timing; +using Robust.Shared.Physics; +using Robust.Shared.Threading; using Robust.Shared.Utility; namespace Content.Server.Radiation.Systems; @@ -13,113 +17,62 @@ namespace Content.Server.Radiation.Systems; // main algorithm that fire radiation rays to target public partial class RadiationSystem { - private List> _grids = new(); - private readonly record struct SourceData( float Intensity, + float Slope, + float MaxRange, Entity Entity, Vector2 WorldPosition) { - public EntityUid? GridUid => Entity.Comp2.GridUid; - public float Slope => Entity.Comp1.Slope; + public EntityUid Uid => Entity.Owner; public TransformComponent Transform => Entity.Comp2; } private void UpdateGridcast() { - // should we save debug information into rays? - // if there is no debug sessions connected - just ignore it var debug = _debugSessions.Count > 0; - - var stopwatch = new Stopwatch(); + var stopwatch = new Robust.Shared.Timing.Stopwatch(); stopwatch.Start(); - _sources.Clear(); - _sources.EnsureCapacity(Count()); - - var sources = EntityQueryEnumerator(); - var destinations = EntityQueryEnumerator(); - - while (sources.MoveNext(out var uid, out var source, out var xform)) + var sourcesCount = _sourceDataMap.Count; + if (_activeReceivers.Count == 0 || sourcesCount == 0) { - if (!source.Enabled) - continue; - - var worldPos = _transform.GetWorldPosition(xform); - - // Intensity is scaled by stack size. - var intensity = source.Intensity * _stack.GetCount(uid); - - // Apply rad modifier if the source is enclosed within a radiation blocking container - // Note that this also applies to receivers, and it doesn't bother to check if the container sits between them. - // I.e., a source & receiver in the same blocking container will get double-blocked, when no blocking should be applied. - intensity = GetAdjustedRadiationIntensity(uid, intensity); - - _sources.Add(new(intensity, (uid, source, xform), worldPos)); + RaiseLocalEvent(new RadiationSystemUpdatedEvent()); + return; } - var debugRays = debug ? new List() : null; - var receiversTotalRads = new ValueList<(Entity, float)>(); + var results = new float[_activeReceivers.Count]; + var debugRays = debug ? new ConcurrentBag() : null; - // TODO RADIATION Parallelize - // Would need to give receiversTotalRads a fixed size. - // Also the _grids list needs to be local to a job. (or better yet cached in SourceData) - // And I guess disable parallelization when debugging to make populating the debug List easier. - // Or just make it threadsafe? - while (destinations.MoveNext(out var destUid, out var dest, out var destTrs)) + var job = new RadiationJob { - var destWorld = _transform.GetWorldPosition(destTrs); + System = this, + SourceTree = _sourceTree, + SourceDataMap = _sourceDataMap, + Destinations = _activeReceivers, + Results = results, + DebugRays = debugRays, + Debug = debug + }; - var rads = 0f; - foreach (var source in _sources) + _parallel.ProcessNow(job, _activeReceivers.Count); + + for (var i = 0; i < _activeReceivers.Count; i++) + { + var uid = _activeReceivers[i]; + var rads = results[i]; + + if (_receiverQuery.TryComp(uid, out var receiver)) { - // send ray towards destination entity - if (Irradiate(source, destUid, destTrs, destWorld, debug) is not { } ray) - continue; - - // add rads to total rad exposure - if (ray.ReachedDestination) - rads += ray.Rads; - - if (!debug) - continue; - - debugRays!.Add(new DebugRadiationRay( - ray.MapId, - GetNetEntity(ray.SourceUid), - ray.Source, - GetNetEntity(ray.DestinationUid), - ray.Destination, - ray.Rads, - ray.Blockers ?? new()) - ); + receiver.CurrentRadiation = rads; + if (rads > 0) + IrradiateEntity(uid, rads, GridcastUpdateRate); } - - // Apply modifier if the destination entity is hidden within a radiation blocking container - rads = GetAdjustedRadiationIntensity(destUid, rads); - - receiversTotalRads.Add(((destUid, dest), rads)); } - // update information for debug overlay - var elapsedTime = stopwatch.Elapsed.TotalMilliseconds; - var totalSources = _sources.Count; - var totalReceivers = receiversTotalRads.Count; - UpdateGridcastDebugOverlay(elapsedTime, totalSources, totalReceivers, debugRays); + if (debugRays is not null) + UpdateGridcastDebugOverlay(stopwatch.Elapsed.TotalMilliseconds, sourcesCount, _activeReceivers.Count, debugRays.ToList()); - // send rads to each entity - foreach (var (receiver, rads) in receiversTotalRads) - { - // update radiation value of receiver - // if no radiation rays reached target, that will set it to 0 - receiver.Comp.CurrentRadiation = rads; - - // also send an event with combination of total rad - if (rads > 0) - IrradiateEntity(receiver, rads, GridcastUpdateRate); - } - - // raise broadcast event that radiation system has updated RaiseLocalEvent(new RadiationSystemUpdatedEvent()); } @@ -127,68 +80,28 @@ public partial class RadiationSystem EntityUid destUid, TransformComponent destTrs, Vector2 destWorld, - bool saveVisitedTiles) + bool saveVisitedTiles, + List> gridList) { - // lets first check that source and destination on the same map - if (source.Transform.MapID != destTrs.MapID) - return null; - var mapId = destTrs.MapID; - - // get direction from rad source to destination and its distance - var dir = destWorld - source.WorldPosition; - var dist = dir.Length(); - - // check if receiver is too far away - if (dist > GridcastMaxDistance) - return null; - - // will it even reach destination considering distance penalty + var dist = (destWorld - source.WorldPosition).Length(); var rads = source.Intensity - source.Slope * dist; if (rads < MinIntensity) return null; - // create a new radiation ray from source to destination - // at first we assume that it doesn't hit any radiation blockers - // and has only distance penalty var ray = new RadiationRay(mapId, source.Entity, source.WorldPosition, destUid, destWorld, rads); - // if source and destination on the same grid it's possible that - // between them can be another grid (ie. shuttle in center of donut station) - // however we can do simplification and ignore that case - if (GridcastSimplifiedSameGrid && destTrs.GridUid is { } gridUid && source.GridUid == gridUid) - { - if (!_gridQuery.TryGetComponent(gridUid, out var gridComponent)) - return ray; - return Gridcast((gridUid, gridComponent, Transform(gridUid)), ref ray, saveVisitedTiles, source.Transform, destTrs); - } - - // lets check how many grids are between source and destination - // do a box intersection test between target and destination - // it's not very precise, but really cheap - - // TODO RADIATION - // Consider caching this in SourceData? - // I.e., make the lookup for grids as large as the sources's max distance and store the result in SourceData. - // Avoids having to do a lookup per source*receiver. var box = Box2.FromTwoPoints(source.WorldPosition, destWorld); - _grids.Clear(); - _mapManager.FindGridsIntersecting(mapId, box, ref _grids, true); + gridList.Clear(); + _mapManager.FindGridsIntersecting(mapId, box, ref gridList, true); - // gridcast through each grid and try to hit some radiation blockers - // the ray will be updated with each grid that has some blockers - foreach (var grid in _grids) + foreach (var grid in gridList) { ray = Gridcast((grid.Owner, grid.Comp, Transform(grid)), ref ray, saveVisitedTiles, source.Transform, destTrs); - - // looks like last grid blocked all radiation - // we can return right now if (ray.Rads <= 0) return ray; } - _grids.Clear(); - return ray; } @@ -200,19 +113,12 @@ public partial class RadiationSystem TransformComponent destTrs) { var blockers = saveVisitedTiles ? new List<(Vector2i, float)>() : null; - - // if grid doesn't have resistance map just apply distance penalty var gridUid = grid.Owner; if (!_resistanceQuery.TryGetComponent(gridUid, out var resistance)) return ray; + var resistanceMap = resistance.ResistancePerTile; - // get coordinate of source and destination in grid coordinates - - // TODO Grid overlap. This currently assumes the grid is always parented directly to the map (local matrix == world matrix). - // If ever grids are allowed to overlap, this might no longer be true. In that case, this should precompute and cache - // inverse world matrices. - Vector2 srcLocal = sourceTrs.ParentUid == grid.Owner ? sourceTrs.LocalPosition : Vector2.Transform(ray.Source, grid.Comp2.InvLocalMatrix); @@ -221,28 +127,20 @@ public partial class RadiationSystem ? destTrs.LocalPosition : Vector2.Transform(ray.Destination, grid.Comp2.InvLocalMatrix); - Vector2i sourceGrid = new( - (int)Math.Floor(srcLocal.X / grid.Comp1.TileSize), - (int)Math.Floor(srcLocal.Y / grid.Comp1.TileSize)); + Vector2i sourceGrid = new((int)Math.Floor(srcLocal.X / grid.Comp1.TileSize), (int)Math.Floor(srcLocal.Y / grid.Comp1.TileSize)); + Vector2i destGrid = new((int)Math.Floor(dstLocal.X / grid.Comp1.TileSize), (int)Math.Floor(dstLocal.Y / grid.Comp1.TileSize)); - Vector2i destGrid = new( - (int)Math.Floor(dstLocal.X / grid.Comp1.TileSize), - (int)Math.Floor(dstLocal.Y / grid.Comp1.TileSize)); - - // iterate tiles in grid line from source to destination var line = new GridLineEnumerator(sourceGrid, destGrid); while (line.MoveNext()) { var point = line.Current; if (!resistanceMap.TryGetValue(point, out var resData)) continue; + ray.Rads -= resData; + if (saveVisitedTiles && blockers is not null) + blockers.Add((point, ray.Rads)); - // save data for debug - if (saveVisitedTiles) - blockers!.Add((point, ray.Rads)); - - // no intensity left after blocker if (ray.Rads <= MinIntensity) { ray.Rads = 0; @@ -250,13 +148,11 @@ public partial class RadiationSystem } } - if (!saveVisitedTiles || blockers!.Count <= 0) + if (blockers is null || blockers.Count == 0) return ray; - // save data for debug if needed ray.Blockers ??= new(); ray.Blockers.Add(GetNetEntity(gridUid), blockers); - return ray; } @@ -291,4 +187,92 @@ public partial class RadiationSystem return rads; } + + [UsedImplicitly] + private readonly record struct RadiationJob : IParallelRobustJob + { + public int BatchSize => 5; + public required RadiationSystem System { get; init; } + public required B2DynamicTree SourceTree { get; init; } + public required Dictionary SourceDataMap { get; init; } + public required List Destinations { get; init; } + public required float[] Results { get; init; } + public required ConcurrentBag? DebugRays { get; init; } + public required bool Debug { get; init; } + + public void Execute(int index) + { + var destUid = Destinations[index]; + if (System.Deleted(destUid) || !System.TryComp(destUid, out TransformComponent? destTrs)) + { + Results[index] = 0; + return; + } + + var nearbySourcesArray = ArrayPool.Shared.Rent(256); + + var gridList = new List>(8); + + try + { + var destWorld = System._transform.GetWorldPosition(destTrs); + var rads = 0f; + var destMapId = destTrs.MapID; + + var queryAabb = new Box2(destWorld, destWorld); + + var state = (nearbySourcesArray, 0, SourceTree); + SourceTree.Query(ref state, + static (ref (EntityUid[] arr, int count, B2DynamicTree tree) tuple, + DynamicTree.Proxy proxy) => + { + if (tuple.count >= tuple.arr.Length) + return true; + + var uid = tuple.tree.GetUserData(proxy); + tuple.arr[tuple.count++] = uid; + return true; + }, + in queryAabb); + + var nearbySourcesSpan = nearbySourcesArray.AsSpan(0, state.Item2); + + foreach (var sourceUid in nearbySourcesSpan) + { + if (!SourceDataMap.TryGetValue(sourceUid, out var source) + || source.Transform.MapID != destMapId) + continue; + var delta = source.WorldPosition - destWorld; + if (delta.LengthSquared() > source.MaxRange * source.MaxRange) + continue; + var dist = delta.Length(); + var radsAfterDist = source.Intensity - source.Slope * dist; + if (radsAfterDist < System.MinIntensity) + continue; + if (System.Irradiate(source, destUid, destTrs, destWorld, Debug, gridList) is not { } ray) + continue; + + if (ray.ReachedDestination) + rads += ray.Rads; + + DebugRays?.Add(new DebugRadiationRay( + ray.MapId, + System.GetNetEntity(ray.SourceUid), + ray.Source, + System.GetNetEntity(ray.DestinationUid), + ray.Destination, + ray.Rads, + ray.Blockers ?? new Dictionary>()) + ); + } + + rads = System.GetAdjustedRadiationIntensity(destUid, rads); + Results[index] = rads; + } + finally + { + ArrayPool.Shared.Return(nearbySourcesArray); + } + } + } } diff --git a/Content.Server/Radiation/Systems/RadiationSystem.cs b/Content.Server/Radiation/Systems/RadiationSystem.cs index 4a4d0f8930..39d8d6cd33 100644 --- a/Content.Server/Radiation/Systems/RadiationSystem.cs +++ b/Content.Server/Radiation/Systems/RadiationSystem.cs @@ -4,7 +4,9 @@ using Content.Shared.Radiation.Events; using Content.Shared.Stacks; using Robust.Shared.Configuration; using Robust.Shared.Map; -using Robust.Shared.Map.Components; +using Robust.Shared.Physics; +using Robust.Shared.Threading; +using System.Numerics; namespace Content.Server.Radiation.Systems; @@ -15,14 +17,18 @@ public sealed partial class RadiationSystem : EntitySystem [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedStackSystem _stack = default!; [Dependency] private readonly SharedMapSystem _maps = default!; + [Dependency] private readonly IParallelManager _parallel = default!; + [Dependency] private readonly EntityQuery _receiverQuery = default!; private EntityQuery _blockerQuery; private EntityQuery _resistanceQuery; - private EntityQuery _gridQuery; private EntityQuery _stackQuery; + private readonly B2DynamicTree _sourceTree = new(); + private readonly Dictionary _sourceDataMap = new(); + private readonly List _activeReceivers = new(); + private float _accumulator; - private List _sources = new(); public override void Initialize() { @@ -32,8 +38,102 @@ public sealed partial class RadiationSystem : EntitySystem _blockerQuery = GetEntityQuery(); _resistanceQuery = GetEntityQuery(); - _gridQuery = GetEntityQuery(); _stackQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnSourceInit); + SubscribeLocalEvent(OnSourceShutdown); + SubscribeLocalEvent(OnSourceMove); + SubscribeLocalEvent(OnSourceStackChanged); + + SubscribeLocalEvent(OnReceiverInit); + SubscribeLocalEvent(OnReceiverShutdown); + } + + private void OnSourceInit(Entity entity, ref ComponentInit args) + { + UpdateSource(entity); + } + + private void OnSourceShutdown(EntityUid uid, RadiationSourceComponent component, ComponentShutdown args) + { + if (component.Proxy != DynamicTree.Proxy.Free) + { + _sourceTree.DestroyProxy(component.Proxy); + component.Proxy = DynamicTree.Proxy.Free; + } + _sourceDataMap.Remove(uid); + } + + private void OnSourceMove(Entity entity, ref MoveEvent args) + { + if (args.NewPosition.EntityId == args.OldPosition.EntityId && + args.NewPosition.Position.EqualsApprox(args.OldPosition.Position)) + return; + + UpdateSource(entity); + } + + private void OnSourceStackChanged(Entity entity, ref StackCountChangedEvent args) + { + UpdateSource(entity); + } + + private void OnReceiverInit(EntityUid uid, RadiationReceiverComponent component, ComponentInit args) + { + _activeReceivers.Add(uid); + } + + private void OnReceiverShutdown(EntityUid uid, RadiationReceiverComponent component, ComponentShutdown args) + { + _activeReceivers.Remove(uid); + } + + private void UpdateSource(Entity entity) + { + var (uid, component) = entity; + var xform = Transform(uid); + + if (!component.Enabled || Terminating(uid)) + { + if (component.Proxy != DynamicTree.Proxy.Free) + { + _sourceTree.DestroyProxy(component.Proxy); + component.Proxy = DynamicTree.Proxy.Free; + } + _sourceDataMap.Remove(uid); + return; + } + + var worldPos = _transform.GetWorldPosition(xform); + var intensity = component.Intensity * _stack.GetCount(uid); + intensity = GetAdjustedRadiationIntensity(uid, intensity); + + if (intensity <= 0) + { + if (component.Proxy != DynamicTree.Proxy.Free) + { + _sourceTree.DestroyProxy(component.Proxy); + component.Proxy = DynamicTree.Proxy.Free; + } + _sourceDataMap.Remove(uid); + return; + } + + // Avoid division by 0 + var maxRange = component.Slope >= float.Epsilon ? intensity / component.Slope : GridcastMaxDistance; + maxRange = Math.Min(maxRange, GridcastMaxDistance); + + _sourceDataMap[uid] = new SourceData(intensity, component.Slope, maxRange, (uid, component, xform), worldPos); + var aabb = Box2.CenteredAround(worldPos, new Vector2(maxRange * 2, maxRange * 2)); + + if (component.Proxy != DynamicTree.Proxy.Free) + { + _sourceTree.MoveProxy(component.Proxy, in aabb); + } + else + { + component.Proxy = _sourceTree.CreateProxy(in aabb, uint.MaxValue, uid); + } } public override void Update(float frameTime) @@ -59,8 +159,11 @@ public sealed partial class RadiationSystem : EntitySystem { if (!Resolve(entity, ref entity.Comp, false)) return; + if (entity.Comp.Enabled == val) + return; entity.Comp.Enabled = val; + UpdateSource((entity.Owner, entity.Comp)); } /// diff --git a/Content.Shared/Radiation/Components/RadiationSourceComponent.cs b/Content.Shared/Radiation/Components/RadiationSourceComponent.cs index 874024a905..337e9c648e 100644 --- a/Content.Shared/Radiation/Components/RadiationSourceComponent.cs +++ b/Content.Shared/Radiation/Components/RadiationSourceComponent.cs @@ -1,3 +1,5 @@ +using Robust.Shared.Physics; + namespace Content.Shared.Radiation.Components; /// @@ -26,4 +28,7 @@ public sealed partial class RadiationSourceComponent : Component [DataField, ViewVariables(VVAccess.ReadWrite)] public bool Enabled = true; + + [ViewVariables] + public DynamicTree.Proxy Proxy = DynamicTree.Proxy.Free; } From 209c7f169f0aa969d15ec3cf41ca0335f234049d Mon Sep 17 00:00:00 2001 From: PJBot Date: Sun, 5 Apr 2026 04:54:59 +0000 Subject: [PATCH 078/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 769227c0d6..53ac6ff490 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: telavivgamers - changes: - - message: You may put ashes and matchsticks into ashtrays. - type: Tweak - id: 9104 - time: '2025-10-15T12:38:14.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40926 - author: slarticodefast, Davyei changes: - message: You can now use parcelwrap on humanoids. @@ -4035,3 +4028,11 @@ id: 9615 time: '2026-04-04T20:14:15.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/38712 +- author: Naxel + changes: + - message: Heavily optimized the radiation system to prevent severe server lag when + a large number of radiation sources are active. + type: Fix + id: 9616 + time: '2026-04-05T04:53:47.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/39173 From c5c7549dc23c8ca2882c33f8ffd7a68365e3dd0d Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com> Date: Sat, 4 Apr 2026 22:21:48 -0700 Subject: [PATCH 079/126] Remove ambuzol and ambuzol plus from pill canister random (#43458) * remove ambuzol and ambuzol plus from pill canister random * cleanup * review --------- Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> --- .../Objects/Specific/Medical/healing.yml | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml index c09bef4a3a..7555e88861 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml @@ -834,40 +834,33 @@ storagebase: !type:GroupSelector children: - id: PillDexalin - amount: &Range1to6 !type:RangeNumberSelector - range: 1, 6 + amount: 1, 6 - id: PillDylovene - amount: *Range1to6 + amount: 1, 6 - id: PillHyronalin - amount: *Range1to6 + amount: 1, 6 - id: PillPotassiumIodide - amount: *Range1to6 + amount: 1, 6 - id: PillIron - amount: *Range1to6 + amount: 1, 6 - id: PillCopper - amount: *Range1to6 + amount: 1, 6 - id: PillKelotane - amount: *Range1to6 + amount: 1, 6 - id: PillDermaline - amount: *Range1to6 + amount: 1, 6 - id: PillTricordrazine - amount: *Range1to6 + amount: 1, 6 - id: PillBicaridine - amount: *Range1to6 + amount: 1, 6 - id: PillCharcoal - amount: *Range1to6 - - id: PillAmbuzol - weight: 0.75 - amount: *Range1to6 - - id: PillAmbuzolPlus - weight: 0.75 - amount: *Range1to6 + amount: 1, 6 - id: PillSpaceDrugs weight: 0.75 - amount: *Range1to6 + amount: 1, 6 - id: StrangePill weight: 0.75 - amount: *Range1to6 + amount: 1, 6 # Syringes - type: entity From 9d51bad8be41af9a98f3dbac137604d3feeca8cb Mon Sep 17 00:00:00 2001 From: PJBot Date: Sun, 5 Apr 2026 05:37:38 +0000 Subject: [PATCH 080/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 53ac6ff490..f493a253d5 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: slarticodefast, Davyei - changes: - - message: You can now use parcelwrap on humanoids. - type: Add - id: 9105 - time: '2025-10-15T20:30:45.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40911 - author: aada changes: - message: Grenade penguins have new AI. They won't bite your hand while holding @@ -4036,3 +4029,10 @@ id: 9616 time: '2026-04-05T04:53:47.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/39173 +- author: Princess-Cheeseballs + changes: + - message: Random pill bottles can no longer spawn with ambuzol or ambuzol+ + type: Remove + id: 9617 + time: '2026-04-05T05:36:30.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43458 From 5b53d2489166063c8657e3699f954d078be36206 Mon Sep 17 00:00:00 2001 From: ThatGuyUSA Date: Sat, 4 Apr 2026 23:10:57 -0700 Subject: [PATCH 081/126] Wire interface syntax correction (#43475) * save station * Revert "Merge branch 'wiz-guardian-deck'" This reverts commit 78fa318583b6c93110c47e3b9e23f7222747f89a, reversing changes made to 4d5dab1098bcfdbce14906d9c77dbc669e295760. * syntax --- Content.Shared/Wires/SharedWiresComponent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Shared/Wires/SharedWiresComponent.cs b/Content.Shared/Wires/SharedWiresComponent.cs index d691e854ea..8fdb3de0b1 100644 --- a/Content.Shared/Wires/SharedWiresComponent.cs +++ b/Content.Shared/Wires/SharedWiresComponent.cs @@ -242,7 +242,7 @@ namespace Content.Shared.Wires WireLetter.γ => "wire-letter-name-gamma", WireLetter.δ => "wire-letter-name-delta", WireLetter.ε => "wire-letter-name-epsilon", - WireLetter.ζ => "wire-letter-name-zeta ", + WireLetter.ζ => "wire-letter-name-zeta", WireLetter.η => "wire-letter-name-eta", WireLetter.θ => "wire-letter-name-theta", WireLetter.ι => "wire-letter-name-iota", From 8280ec0bbb7b20b3b076f541cfce016372a70dc9 Mon Sep 17 00:00:00 2001 From: K-Dynamic <20566341+K-Dynamic@users.noreply.github.com> Date: Sun, 5 Apr 2026 22:02:58 +1200 Subject: [PATCH 082/126] Ports honkmother mitre from /tg/station 13, renames honkmother coat (#35237) * initial assets and attribution * 4 space meta.json * remove all ported assets except clown mitre * change honkmother mitre description * add mitre to loadout and theater vendor * icon outline change Co-authored-by: TiniestShark --------- Co-authored-by: TiniestShark --- .../VendingMachines/Inventories/theater.yml | 1 + .../Entities/Clothing/Head/hats.yml | 11 ++++ .../Entities/Clothing/OuterClothing/coats.yml | 2 +- .../Loadouts/Jobs/Civilian/clown.yml | 5 ++ .../Loadouts/LoadoutGroups/loadout_groups.yml | 1 + .../Hats/mitre_clown.rsi/equipped-HELMET.png | Bin 0 -> 617 bytes .../Head/Hats/mitre_clown.rsi/icon.png | Bin 0 -> 511 bytes .../Head/Hats/mitre_clown.rsi/inhand-left.png | Bin 0 -> 585 bytes .../Hats/mitre_clown.rsi/inhand-right.png | Bin 0 -> 574 bytes .../Head/Hats/mitre_clown.rsi/meta.json | 26 ++++++++++ .../Coats/clownpriest.rsi/inhand-left.png | Bin 692 -> 665 bytes .../Coats/clownpriest.rsi/meta.json | 48 +++++++++--------- 12 files changed, 69 insertions(+), 25 deletions(-) create mode 100644 Resources/Textures/Clothing/Head/Hats/mitre_clown.rsi/equipped-HELMET.png create mode 100644 Resources/Textures/Clothing/Head/Hats/mitre_clown.rsi/icon.png create mode 100644 Resources/Textures/Clothing/Head/Hats/mitre_clown.rsi/inhand-left.png create mode 100644 Resources/Textures/Clothing/Head/Hats/mitre_clown.rsi/inhand-right.png create mode 100644 Resources/Textures/Clothing/Head/Hats/mitre_clown.rsi/meta.json diff --git a/Resources/Prototypes/Catalog/VendingMachines/Inventories/theater.yml b/Resources/Prototypes/Catalog/VendingMachines/Inventories/theater.yml index 7e7bb37162..4ceaeef80c 100644 --- a/Resources/Prototypes/Catalog/VendingMachines/Inventories/theater.yml +++ b/Resources/Prototypes/Catalog/VendingMachines/Inventories/theater.yml @@ -40,6 +40,7 @@ ClothingUniformJumpsuitDameDane: 2 ClothingShoesDameDane: 2 ClothingOuterDameDane: 2 + ClothingHeadHatMitreClown: 1 ClothingOuterClownPriest: 1 ClothingMaskSadMime: 1 ClothingMaskScaredMime: 1 diff --git a/Resources/Prototypes/Entities/Clothing/Head/hats.yml b/Resources/Prototypes/Entities/Clothing/Head/hats.yml index 44e4983fd8..af513936da 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/hats.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/hats.yml @@ -1535,3 +1535,14 @@ tags: - PetWearable - CorgiWearable + +- type: entity + parent: ClothingHeadBase + id: ClothingHeadHatMitreClown + name: honkmother mitre + description: It's hard for parishoners to see a banana peel on the floor when they're looking up at your glorious chapeau. + components: + - type: Sprite + sprite: Clothing/Head/Hats/mitre_clown.rsi + - type: Clothing + sprite: Clothing/Head/Hats/mitre_clown.rsi diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/coats.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/coats.yml index 22ad9096f4..a05cf9baa4 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/coats.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/coats.yml @@ -348,7 +348,7 @@ - type: entity parent: ClothingOuterStorageBase id: ClothingOuterClownPriest - name: robes of the honkmother + name: honkmother coat description: Meant for a clown of the cloth. components: - type: Sprite diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml index 3e4823d449..b9c87343b2 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml @@ -9,6 +9,11 @@ equipment: head: ClothingHeadHatJesterAlt +- type: loadout + id: ClownMitre + equipment: + head: ClothingHeadHatMitreClown + # Jumpsuit - type: loadout id: ClownSuit diff --git a/Resources/Prototypes/Loadouts/LoadoutGroups/loadout_groups.yml b/Resources/Prototypes/Loadouts/LoadoutGroups/loadout_groups.yml index 52106d2d6f..34a26f28ec 100644 --- a/Resources/Prototypes/Loadouts/LoadoutGroups/loadout_groups.yml +++ b/Resources/Prototypes/Loadouts/LoadoutGroups/loadout_groups.yml @@ -512,6 +512,7 @@ loadouts: - JesterHat - JesterAltHat + - ClownMitre - type: loadoutGroup id: ClownJumpsuit diff --git a/Resources/Textures/Clothing/Head/Hats/mitre_clown.rsi/equipped-HELMET.png b/Resources/Textures/Clothing/Head/Hats/mitre_clown.rsi/equipped-HELMET.png new file mode 100644 index 0000000000000000000000000000000000000000..5647e824379ce4a83942e3eec105ddca2681d6d7 GIT binary patch literal 617 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=oCO|{#S9F5M?jcysy3fA0|S$^ zr;B4q#hkaZY`qT$h_u~rSREg2zEI@Pfwf=6MPiOTc=T$S-dFVwi(Tb~Aq7RNOWx>j z;A@J=pV-x1BIqbMr^nJTeATwb$XrpfoME4jBW|5Ukvt<3JXtV5#7P1DV-Cw~XeNHfm*FLqzu zUPp(qy^i~H#?iG^U-k;sbG(W?ka18*!0+78XuHn!<_qlaaHrTa)pOo$&{ptx^}NqA zh^b>u_`?&NC!|Cf9@&Dcsre8Cc6_?c1R5WVa9h>KO=J_PA12Hk(%cE=^Z->0NQ@XtQ z`dhVtxvkubHlEE&vYURT-$6uz?e=}KWS*Fd*54M?=sE9Yu4?WVd;Rer!~ICkyWjt= zu<}v;^!4=9g=%?;+!7&+zt?5HZ&=I6-u+ene9bIgsT{`oy`G0AO)dB<-Gb>>Y~I+z ZymapAxJN3F=K|9XgQu&X%Q~loCIFkL2}l3{ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Clothing/Head/Hats/mitre_clown.rsi/icon.png b/Resources/Textures/Clothing/Head/Hats/mitre_clown.rsi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..abf8dbbcdffa7114d01d0e1666dbb2bb09344d44 GIT binary patch literal 511 zcmVtI1pbg@|0j~sLd$HOLZcZqIh;zh`F88UhEzR$dwOdu!cE#vx*svk{g%UXOF)c)LY zCJCeBUw)6NudCzA^&PbfN`W$w(C&ciJ8Hn~r>(sJz=w~5Dc@laFzJMrKz;E-;b}gL zL~4QNl{eGV*J&hSya``R!fF+8<%wF7+k4nNqBJ^~7`O8G-_wOB*z?X(Nxvu%7q8=& zz0f#ehvW9QhUxd7$S%-KYNiND|x`b#D1Q_domtEjFq_nswpCYAJ0~1M=!S3#eSJzuC0+K#4f_jkj7Hcsl zVVo|>AN4c1zN6Ns|6ngIQ7M(;F=A?M5`gP!!2N&8!Q64n{6?=JskkZH0zVIa#1i$) zZhSWqFxo|tgnFnQ6V*T(6-Rdutb1%jJ^`I#N6;r)pe`LzU=K@BC$--Tmz z3RHn}U^+$qSN$19oNL5tb%3x7tzy!wJSQh7;t_VmurIE?(!>A&002ovPDHLkV1mH3 B?)d-! literal 0 HcmV?d00001 diff --git a/Resources/Textures/Clothing/Head/Hats/mitre_clown.rsi/inhand-left.png b/Resources/Textures/Clothing/Head/Hats/mitre_clown.rsi/inhand-left.png new file mode 100644 index 0000000000000000000000000000000000000000..1329de8c115122908f3336022f7c35127d185e46 GIT binary patch literal 585 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`0Z1^9%xUSnYR&%j{1Ni+6}_ohvoUOant^XAR}4931+eIMrV0ww?dXZTbv z0aPCM@iGUH;wuUA3kHfJ0E6p+J12lLoCO|{#XtoIK$tP>S|=w^P@=>&q9iy!t)x7$ zD3u`~F*C13&(AeP!Bo#s&*=9a=50VV+fpMu(>y)37(nK;GDtD9GB5&JUO+4jWrKXD z!N?32X9BVf8JQRafOHfPXSTC|#j}8H5ZGVF$nXLf2rwGWQU;*Z1a<}%ph^QHV*|zo z5K}=mvMzv_GzG{80VbfiOkkBkmKH!3RF|QF0Z5k5w)9dGoF|r!uG432`U{C^R_y^gmc6S!3s!oIlSY zpyS6Wm!qFDs?@!W7dBkKed>1Lha8==F69BQ>+WZ7UD9*xkdH#fTEopYy8ETQjW2RW zX-8ZB`Jbh8#-&L_z+r&{Ln9L-%PD`>ItvcTMtMFbzYn%DEmvfJJe(l>~~iGacXaZ>m4zbE{)PKgj)_u6{1-oD!M<0}rlN literal 0 HcmV?d00001 diff --git a/Resources/Textures/Clothing/Head/Hats/mitre_clown.rsi/inhand-right.png b/Resources/Textures/Clothing/Head/Hats/mitre_clown.rsi/inhand-right.png new file mode 100644 index 0000000000000000000000000000000000000000..afb2afbac9ae5a4f3eab779737f2f48e31beb8ef GIT binary patch literal 574 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`0Z1^9%xUSnYR&%j{1Ni+6}_ohvoUOant^XAR}4931+eIMrV0ww?dXZTbv z0aPCM@iGUH;wuUA3kHfJ0E6p+J12lLoCO|{#XtoIL6~vJ#O${~L5ULAh?3y^w370~ zqEv=}#LT=BJwMkF1yemkJ)_@yn70AdY)g&sO!M^AVgQ-X${@wa%D@O@c>%FBlnwHo z1|u_AoC(M_WMpCx0Mb!FoY~F-7S95*LEvT?Bf|?|Ai!ueOBsMt6WAG8fGQ1)j13qU zKuiVM$hrVx(i9*Y1ek#4GJ#bFSy}*DP+f)w1|Zqx8vEA$7Ci_G=eeFPjv*25Z>JsP zV>0A$x!&};to8r@*w$5=aociiXI8Rbn(&hQcw~tXhk}7Z!+{^-2a7UOa^7z_?{p+w zO>3W3TZsM3=?&L!pSrzbf&X^vth?{4*Zz}R;J@3!WyVj-+ESnSdEy;Y*7E$nZg-bO z*ny#uk&%UsML=S|=$x0FinHHx9-FRkUAU(rcayq=k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`1^2Ka=ye%a1$o zqB(UsPy@r?tHpB`U~wiO+mMloK>$cc0dZzK3s^i0$OZwPaz=(1z<_|!XqGYnr6#a5umDvW z7#SNdE`XQ{vXONG#H1-eHV7~Q&1C|s46?KUvY@&Q4GciC?_TLj96JLPW?=Z@>Eaj? z;r@2oL7`>^9+y@&m!*IHKR0_Nc3P!v*B}plje~h z9fmb`9&&^WX)1VGK67cA31TbGI5b5g_>u5Q$*s%0TN*UZhh&_)&h?(-amSqm&f}L8 zcbYl06uNL+TkEFORT@3<3s6Gf%38ahFV*LJQ~_n~FF3(#amYf6we)GcN2kAvEs)KU ztJ`WH?p43veTLhT1yhB6*p+q!u$aX>Xp$A*k#L+ftnr#`L0$VLSs$jQ);CzZ$_?@q zb~pPjC}E%N@RH9y$DoXTu|pY<^P11B@sey0(@9^)Z2g8K*B+i?t#1Q`g{P~Z%Q~lo FCII*?(L?|M delta 679 zcmbQqx`lOuWIY=L1H-D!!h1l9v%n*=n1O-s2naJy)#j6CU|>q~ba4!+nDchl{%jFP zk>mThyB|DLl55mhB5->5AvL#koI79Z^ku(uuy)+AZI_ddE5{4_#N`E73te|Tc~zo0 zDJX~|&|tRGPLBwqbF)({FcSIy;#J@eqJ(uTa_E8IQuB)CrmnA~I1NZug1Sm*J1+mJVQ zRTZzU=SuUsEvWB#_x9cF(`g4^Mz9%tV=DJuFFxB(J$b(1J*GaRZJE)xpZs2SDpkSv zVqW@<4k7i&TALs2iLgJNYMh(AF!oH&2Qdf9x|GSSBH%fcCkGXBBR+(MOiv4rH*7t?7{L??UwSIQL z{NMVHV~6jjpWmIiM(Kn2ha=mb)_ne*cJKeD_Y9}zNKf~h^IUp0L&UU$zk6@ot$C{u zzpHOq?%T|shqBdwr0k0W*CAbeTzJbk&@#Kg}o4I4(9TUT)Xt zn&gW!q^C(fkUOUrzQxzPW|O^L{X5Cmj1?RwF1YvL%QrJ6?qdhm{*jQnepq;W z&;biXEMSWuzX?nhuLYY#4n+0~%wafJ#iC@vIOi*eO94o@;{i*-HD!T=oC#~)9XdcF x4F=O4&sDX+#1$-FD*!b+6kJti(QfG8ANxjAvcGCdGcYYOc)I$ztaD0e0syihEIt4L diff --git a/Resources/Textures/Clothing/OuterClothing/Coats/clownpriest.rsi/meta.json b/Resources/Textures/Clothing/OuterClothing/Coats/clownpriest.rsi/meta.json index bacde82361..4e665a18e8 100644 --- a/Resources/Textures/Clothing/OuterClothing/Coats/clownpriest.rsi/meta.json +++ b/Resources/Textures/Clothing/OuterClothing/Coats/clownpriest.rsi/meta.json @@ -1,26 +1,26 @@ { - "version": 1, - "license": "CC-BY-SA-3.0", - "copyright": "Taken from Yogstation at https://github.com/yogstation13/Yogstation/commit/18ab2203bc47b7590f2c72b5f7969eafa723f033", - "size": { - "x": 32, - "y": 32 - }, - "states": [ - { - "name": "icon" + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from /tg/station 13 at commit https://github.com/tgstation/tgstation/commit/3dbc8c3c121e3ade9158303c2819b47206b0f93d. inhand-left modified by K-Dynamic (github).", + "size": { + "x": 32, + "y": 32 }, - { - "name": "equipped-OUTERCLOTHING", - "directions": 4 - }, - { - "name": "inhand-left", - "directions": 4 - }, - { - "name": "inhand-right", - "directions": 4 - } - ] -} \ No newline at end of file + "states": [ + { + "name": "icon" + }, + { + "name": "equipped-OUTERCLOTHING", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} From 0eadb5086783ec79d3ae287bf13380a419f13664 Mon Sep 17 00:00:00 2001 From: PJBot Date: Sun, 5 Apr 2026 10:18:28 +0000 Subject: [PATCH 083/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index f493a253d5..05531749e5 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: aada - changes: - - message: Grenade penguins have new AI. They won't bite your hand while holding - them, and hone in on a single target when released. - type: Tweak - id: 9106 - time: '2025-10-15T23:59:05.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/34935 - author: DinnerCalzone changes: - message: ID card sprites have been tweaked to unsquish their job icons. @@ -4036,3 +4028,12 @@ id: 9617 time: '2026-04-05T05:36:30.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43458 +- author: K-Dynamic + changes: + - message: Clowns may change their loadout to start with the honkmother mitre. + type: Add + - message: Renames robes of the honkmother to honkmother coat. + type: Tweak + id: 9618 + time: '2026-04-05T10:17:20.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/35237 From 666032f88a295d543bf2d9eed1fb070b737575dc Mon Sep 17 00:00:00 2001 From: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Date: Sun, 5 Apr 2026 03:25:53 -0700 Subject: [PATCH 084/126] Atmospherics GetBulkTileAtmosphere/GasMixturePressures API (#42745) --- .../EntitySystems/AtmosphereSystem.API.cs | 90 +++++++++++++++++++ .../AtmosphereSystem.DeltaPressure.cs | 65 -------------- 2 files changed, 90 insertions(+), 65 deletions(-) diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs index e68bf7b2ec..8c3681d543 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.Diagnostics; using Content.Server.NodeContainer.NodeGroups; using Content.Shared.Atmos; @@ -289,6 +290,95 @@ public partial class AtmosphereSystem GetTileMixture(entity, excite)?.AdjustMoles(gas, mols); } + /// + /// Retrieves the pressures of all gas mixtures + /// in the given array of s, and stores the results in the + /// provided span. + /// + /// The tiles span to find the pressures of. + /// The span to store the pressures to - this should be the same length + /// as the tile array. + /// Thrown when the length of the provided spans do not match. + /// Note that for or s that are null, + /// this method will return a value close to zero but not exactly zero. + [PublicAPI] + public static void GetBulkTileAtmospherePressures(Span tiles, Span pressures) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(tiles.Length, pressures.Length); + + var len = tiles.Length; + var arr1 = ArrayPool.Shared.Rent(len); + + try + { + var mixtSpan = arr1.AsSpan(0, len); + for (var i = 0; i < tiles.Length; i++) + { + mixtSpan[i] = tiles[i]?.Air; + } + + GetBulkGasMixturePressures(mixtSpan, pressures); + } + finally + { + ArrayPool.Shared.Return(arr1); + } + } + + /// + /// Gets the pressures of a of s. + /// + /// The to get the pressures of. + /// The to store the pressures to - this should be the same length + /// as the mixtures array. + /// Thrown when the length of the provided spans do not match. + /// Note that for GasMixtures that are null, this method will return a value close to zero but not exactly zero. + [PublicAPI] + public static void GetBulkGasMixturePressures(Span mixtures, Span pressures) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(mixtures.Length, pressures.Length); + + var len = mixtures.Length; + + var arr1 = ArrayPool.Shared.Rent(len); + var arr2 = ArrayPool.Shared.Rent(len); + var arr3 = ArrayPool.Shared.Rent(len); + try + { + var mixtVol = arr1.AsSpan(0, len); + var mixtTemp = arr2.AsSpan(0, len); + var mixtMoles = arr3.AsSpan(0, len); + + for (var i = 0; i < len; i++) + { + if (mixtures[i] is not { } mixture) + { + // To prevent any NaN/Div/0 errors, we just bite the bullet + // and set everything to the lowest possible value. + mixtVol[i] = 1; + mixtTemp[i] = 1; + mixtMoles[i] = float.Epsilon; + continue; + } + + mixtVol[i] = mixture.Volume; + mixtTemp[i] = mixture.Temperature; + mixtMoles[i] = mixture.TotalMoles; + } + + // TODO NumericsHelpers need a method that substitutes NaNs with zeros. AVX-512 has one iirc but for 256/128 we need to do some masking bs + NumericsHelpers.Multiply(mixtMoles, Atmospherics.R); + NumericsHelpers.Multiply(mixtMoles, mixtTemp); + NumericsHelpers.Divide(mixtMoles, mixtVol, pressures); + } + finally + { + ArrayPool.Shared.Return(arr1); + ArrayPool.Shared.Return(arr2); + ArrayPool.Shared.Return(arr3); + } + } + /// /// Triggers a tile's to react. /// diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs index 9457c5689c..59ccbbb8c8 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs @@ -200,71 +200,6 @@ public sealed partial class AtmosphereSystem } } - /// - /// A DeltaPressure helper method that retrieves the pressures of all gas mixtures - /// in the given array of s, and stores the results in the - /// provided span. - /// - /// The tiles span to find the pressures of. - /// The span to store the pressures to - this should be the same length - /// as the tile array. - /// Thrown when the length of the provided spans do not match. - private static void GetBulkTileAtmospherePressures(Span tiles, Span pressures) - { - // this shit is internal because I don't even trust myself - if (tiles.Length != pressures.Length) - throw new ArgumentException("Length of Tiles and Pressures span must be the same!"); - - var len = pressures.Length; - - // Once again, ArrayPool might return arrays that are longer than the length. - // We really need them to be all the same length, so slice them here. - var arr1 = ArrayPool.Shared.Rent(len); - var arr2 = ArrayPool.Shared.Rent(len); - var arr3 = ArrayPool.Shared.Rent(len); - - var mixtVol = arr1.AsSpan(0, len); - var mixtTemp = arr2.AsSpan(0, len); - var mixtMoles = arr3.AsSpan(0, len); - - try - { - for (var i = 0; i < len; i++) - { - if (tiles[i] is not { Air: { } mixture }) - { - // To prevent any NaN/Div/0 errors, we just bite the bullet - // and set everything to the lowest possible value. - mixtVol[i] = 1; - mixtTemp[i] = 1; - mixtMoles[i] = float.Epsilon; - continue; - } - - mixtVol[i] = mixture.Volume; - mixtTemp[i] = mixture.Temperature; - mixtMoles[i] = mixture.TotalMoles; - } - - /* - Retrieval of single tile pressures requires calling a get method for each tile, - which does a bunch of scalar operations. - - So we go ahead and batch-retrieve the pressures of all tiles - and process them in bulk. - */ - NumericsHelpers.Multiply(mixtMoles, Atmospherics.R); - NumericsHelpers.Multiply(mixtMoles, mixtTemp); - NumericsHelpers.Divide(mixtMoles, mixtVol, pressures); - } - finally - { - ArrayPool.Shared.Return(arr1); - ArrayPool.Shared.Return(arr2); - ArrayPool.Shared.Return(arr3); - } - } - /// /// Packs data into a data struct and enqueues it /// into the queue for From 974eb80ff40e5ef05ca955aaababf0c746fad1fa Mon Sep 17 00:00:00 2001 From: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Date: Sun, 5 Apr 2026 05:17:08 -0700 Subject: [PATCH 085/126] Fix LINDA pressure delta nonsense and weighting (#43471) * Fix LINDA equalization * Apply suggestion from @slarticodefast --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --- .../EntitySystems/AtmosphereSystem.LINDA.cs | 65 +++++++++++++++---- Content.Shared/Atmos/Atmospherics.cs | 13 +++- 2 files changed, 63 insertions(+), 15 deletions(-) diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs index 7ece546e4c..d4a70dfbc0 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs @@ -117,8 +117,11 @@ namespace Content.Server.Atmos.EntitySystems private void Archive(TileAtmosphere tile, int fireCount) { if (tile.Air != null) + { + // TODO ATMOS: This is an extremely large hotspot in LINDA, accounting for 1/5th of its time. + // Please make GasMixture a struct or use a FauxGasMixture with an InlineArray to handle copying this sanely. tile.AirArchived = new GasMixture(tile.Air); - + } tile.ArchivedCycle = fireCount; } @@ -197,12 +200,25 @@ namespace Content.Server.Atmos.EntitySystems } /// - /// Shares gas between two tiles. Part of LINDA. + /// Performs a share operation between two tiles, sharing both physical gas and temperature. /// + /// The receiving the share. + /// The sharing its air. + /// The number of s next to the receiver that air can flow to. + /// The pressure difference between the two tiles after sharing. + /// LINDA is an FEA-like solver and this method is basically the core of it. + /// In FEA we divide the problem into infinitesimal parts and try to step towards the desired end state: + /// a steady state where all air is equalized between tiles. + /// To do this we share the tiles air between other tiles over time (as well as the temperature). + /// Note that the timestep is actually a cyclestep, so running the cycles faster leads to a faster equalization. + /// Hilarious, I know. public float Share(TileAtmosphere tileReceiver, TileAtmosphere tileSharer, int atmosAdjacentTurfs) { - if (tileReceiver.Air is not {} receiver || tileSharer.Air is not {} sharer || - tileReceiver.AirArchived == null || tileSharer.AirArchived == null) + // TODO ATMOS: Method needs to timestep over deltaTime instead of per cycle + // TODO ATMOS: Method needs to account for adjacent turfs in the situation where air is moving from receiver to sharer. + // See https://github.com/tgstation/tgstation/pull/63785 + if (tileReceiver.Air is not { } receiver || tileSharer.Air is not { } sharer || + tileReceiver.AirArchived == null || tileSharer.AirArchived == null) return 0f; var temperatureDelta = tileReceiver.AirArchived.Temperature - tileSharer.AirArchived.Temperature; @@ -221,12 +237,13 @@ namespace Content.Server.Atmos.EntitySystems var movedMoles = 0f; var absMovedMoles = 0f; - for(var i = 0; i < Atmospherics.TotalNumberOfGases; i++) + for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) { var thisValue = receiver.Moles[i]; var sharerValue = sharer.Moles[i]; var delta = (thisValue - sharerValue) / (atmosAdjacentTurfs + 1); - if (!(MathF.Abs(delta) >= Atmospherics.GasMinMoles)) continue; + if (!(MathF.Abs(delta) >= Atmospherics.GasMinMoles)) + continue; if (absTemperatureDelta > Atmospherics.MinimumTemperatureDeltaToConsider) { var gasHeatCapacity = delta * GasMolarHeatCapacities[i]; @@ -240,8 +257,10 @@ namespace Content.Server.Atmos.EntitySystems } } - if (!receiver.Immutable) receiver.Moles[i] -= delta; - if (!sharer.Immutable) sharer.Moles[i] += delta; + if (!receiver.Immutable) + receiver.Moles[i] -= delta; + if (!sharer.Immutable) + sharer.Moles[i] += delta; movedMoles += delta; absMovedMoles += MathF.Abs(delta); } @@ -256,16 +275,21 @@ namespace Content.Server.Atmos.EntitySystems // Transfer of thermal energy (via changed heat capacity) between self and sharer. if (!receiver.Immutable && newHeatCapacity > Atmospherics.MinimumHeatCapacity) { - receiver.Temperature = ((oldHeatCapacity * receiver.Temperature) - (heatCapacityToSharer * tileReceiver.AirArchived.Temperature) + (heatCapacitySharerToThis * tileSharer.AirArchived.Temperature)) / newHeatCapacity; + receiver.Temperature = + (oldHeatCapacity * receiver.Temperature - + heatCapacityToSharer * tileReceiver.AirArchived.Temperature + + heatCapacitySharerToThis * tileSharer.AirArchived.Temperature) / newHeatCapacity; } if (!sharer.Immutable && newSharerHeatCapacity > Atmospherics.MinimumHeatCapacity) { - sharer.Temperature = ((oldSharerHeatCapacity * sharer.Temperature) - (heatCapacitySharerToThis * tileSharer.AirArchived.Temperature) + (heatCapacityToSharer * tileReceiver.AirArchived.Temperature)) / newSharerHeatCapacity; + sharer.Temperature = + (oldSharerHeatCapacity * sharer.Temperature - + heatCapacitySharerToThis * tileSharer.AirArchived.Temperature + + heatCapacityToSharer * tileReceiver.AirArchived.Temperature) / newSharerHeatCapacity; } // Thermal energy of the system (self and sharer) is unchanged. - if (MathF.Abs(oldSharerHeatCapacity) > Atmospherics.MinimumHeatCapacity) { if (MathF.Abs(newSharerHeatCapacity / oldSharerHeatCapacity - 1) < 0.1) @@ -275,12 +299,25 @@ namespace Content.Server.Atmos.EntitySystems } } - if (!(temperatureDelta > Atmospherics.MinimumTemperatureToMove) && - !(MathF.Abs(movedMoles) > Atmospherics.MinimumMolesDeltaToMove)) return 0f; + // If we didn't move enough air or if the temperature difference is too small, + // we don't consider there to be a pressure difference. + // TODO ATMOS: This is a very weird early return, please figure out why this exists because this logic seems to be double checked + // in a lot of other places (ex. HighPressureDelta). + if (!(absTemperatureDelta > Atmospherics.MinimumTemperatureToMove) && + !(MathF.Abs(movedMoles) > Atmospherics.MinimumMolesDeltaToMove)) + return 0f; + var moles = receiver.TotalMoles; var theirMoles = sharer.TotalMoles; - return (tileReceiver.AirArchived.Temperature * (moles + movedMoles)) - (tileSharer.AirArchived.Temperature * (theirMoles - movedMoles)) * Atmospherics.R / receiver.Volume; + /* + To get the pressure delta: + PV = nRT + P = nRT / V + \Delta P = ((n_1 * T_1) - (n_2 * T_2)) * R / V + */ + return (tileReceiver.AirArchived.Temperature * (moles + movedMoles) - + tileSharer.AirArchived.Temperature * (theirMoles - movedMoles)) * Atmospherics.R / receiver.Volume; } /// diff --git a/Content.Shared/Atmos/Atmospherics.cs b/Content.Shared/Atmos/Atmospherics.cs index 02d4a7deb8..12af967526 100644 --- a/Content.Shared/Atmos/Atmospherics.cs +++ b/Content.Shared/Atmos/Atmospherics.cs @@ -138,8 +138,19 @@ namespace Content.Shared.Atmos /// public const float MinimumAirToSuspend = (MolesCellStandard * MinimumAirRatioToSuspend); - public const float MinimumTemperatureToMove = (T20C + 100f); + /// + /// The minimum difference in temperature between s + /// (s) required + /// for LINDA to report a pressure difference between them for space wind. + /// In Kelvin. + /// + public const float MinimumTemperatureToMove = 5f; + /// + /// The minimum difference in moles between s + /// (s) required for LINDA to + /// report a pressure difference between them for space wind. + /// public const float MinimumMolesDeltaToMove = (MolesCellStandard * MinimumAirRatioToMove); /// From 9ad745045be6b12d35251c27d92809bfa4e37e09 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Sun, 5 Apr 2026 10:01:01 -0400 Subject: [PATCH 086/126] Fix `IdCardConsoleSystem` missing prototype error (#42884) * Fix IdCardConsoleSystem missing prototype error * Resolve -> TryIndex * Poke --- Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs | 2 +- Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs | 2 +- Content.Server/Access/Systems/IdCardConsoleSystem.cs | 4 ++-- Content.Shared/Access/Components/IdCardConsoleComponent.cs | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs b/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs index 81b1c087d8..31f65dc78d 100644 --- a/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs +++ b/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs @@ -65,7 +65,7 @@ namespace Content.Client.Access.UI _window?.UpdateState(castState); } - public void SubmitData(string newFullName, string newJobTitle, List> newAccessList, ProtoId newJobPrototype) + public void SubmitData(string newFullName, string newJobTitle, List> newAccessList, ProtoId? newJobPrototype) { SendMessage(new WriteToTargetIdMessage( newFullName, diff --git a/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs b/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs index bb44ae2615..2169277f0d 100644 --- a/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs +++ b/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs @@ -216,7 +216,7 @@ namespace Content.Client.Access.UI JobTitleLineEdit.Text, // Iterate over the buttons dictionary, filter by `Pressed`, only get key from the key/value pair _accessButtons.ButtonsList.Where(x => x.Value.Pressed).Select(x => x.Key).ToList(), - jobProtoDirty ? _jobPrototypeIds[JobPresetOptionButton.SelectedId] : string.Empty); + jobProtoDirty ? _jobPrototypeIds[JobPresetOptionButton.SelectedId] : null); } } } diff --git a/Content.Server/Access/Systems/IdCardConsoleSystem.cs b/Content.Server/Access/Systems/IdCardConsoleSystem.cs index 0ea55eee91..1e4a044901 100644 --- a/Content.Server/Access/Systems/IdCardConsoleSystem.cs +++ b/Content.Server/Access/Systems/IdCardConsoleSystem.cs @@ -135,7 +135,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem string newFullName, string newJobTitle, List> newAccessList, - ProtoId newJobProto, + ProtoId? newJobProto, EntityUid player, IdCardConsoleComponent? component = null) { @@ -158,7 +158,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem _idCard.TryChangeFullName(targetId, newFullName, player: player); _idCard.TryChangeJobTitle(targetId, newJobTitle, player: player); - if (_prototype.Resolve(newJobProto, out var job) + if (_prototype.TryIndex(newJobProto, out var job) && _prototype.Resolve(job.Icon, out var jobIcon)) { _idCard.TryChangeJobIcon(targetId, jobIcon, player: player); diff --git a/Content.Shared/Access/Components/IdCardConsoleComponent.cs b/Content.Shared/Access/Components/IdCardConsoleComponent.cs index 35af5972c3..89c24940ba 100644 --- a/Content.Shared/Access/Components/IdCardConsoleComponent.cs +++ b/Content.Shared/Access/Components/IdCardConsoleComponent.cs @@ -26,9 +26,9 @@ public sealed partial class IdCardConsoleComponent : Component public readonly string FullName; public readonly string JobTitle; public readonly List> AccessList; - public readonly ProtoId JobPrototype; + public readonly ProtoId? JobPrototype; - public WriteToTargetIdMessage(string fullName, string jobTitle, List> accessList, ProtoId jobPrototype) + public WriteToTargetIdMessage(string fullName, string jobTitle, List> accessList, ProtoId? jobPrototype) { FullName = fullName; JobTitle = jobTitle; From 4a88b375df99a884df0ce5053efcfa8d1124a123 Mon Sep 17 00:00:00 2001 From: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> Date: Sun, 5 Apr 2026 17:03:29 +0200 Subject: [PATCH 087/126] Make cameras react to the AI eye nearby (#43466) * init TODO: Make this part of the surveillance camera system Make this be bearable when unpowered(Probably commented out until we make AI lose vision without power) Remove leftover shit Probably clean up the camera RSI * move to surv cameras, unpowered, remove leftovers * silly * review * review * I can see the light * reverse fixtures * review * dependency query * unused oops --- .../SurveillanceCameraMicrophoneSystem.cs | 4 +- .../SurveillanceCameraSystem.Collide.cs | 101 ++++++++++++++++++ .../Systems/SurveillanceCameraSystem.cs | 34 ++++-- .../LightOnCollideColliderComponent.cs | 13 --- .../Components/LightOnCollideComponent.cs | 11 -- .../Light/EntitySystems/LightCollideSystem.cs | 87 --------------- .../CameraActiveOnCollideColliderComponent.cs | 16 +++ .../CameraActiveOnCollideComponent.cs | 22 ++++ .../Components/SurveillanceCameraComponent.cs | 25 +++-- .../SharedSurveillanceCameraSystem.cs | 10 +- Resources/Prototypes/Actions/station_ai.yml | 28 ----- .../Entities/Mobs/Player/silicon.yml | 12 ++- .../WallmountMachines/surveillance_camera.yml | 5 +- 13 files changed, 207 insertions(+), 161 deletions(-) create mode 100644 Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.Collide.cs delete mode 100644 Content.Shared/Light/Components/LightOnCollideColliderComponent.cs delete mode 100644 Content.Shared/Light/Components/LightOnCollideComponent.cs delete mode 100644 Content.Shared/Light/EntitySystems/LightCollideSystem.cs create mode 100644 Content.Shared/SurveillanceCamera/Components/CameraActiveOnCollideColliderComponent.cs create mode 100644 Content.Shared/SurveillanceCamera/Components/CameraActiveOnCollideComponent.cs diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs index e8c53de9eb..723ddbf0ff 100644 --- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs @@ -30,7 +30,7 @@ public sealed class SurveillanceCameraMicrophoneSystem : EntitySystem // This function ensures that chat popups appear on camera views that have connected microphones. foreach (var (_, __, camera, xform) in EntityQuery()) { - if (camera.ActiveViewers.Count == 0) + if (camera.ActivePvsViewers.Count == 0) continue; // get range to camera. This way wispers will still appear as obfuscated if they are too far from the camera's microphone @@ -41,7 +41,7 @@ public sealed class SurveillanceCameraMicrophoneSystem : EntitySystem if (range < 0 || range > ev.VoiceRange) continue; - foreach (var viewer in camera.ActiveViewers) + foreach (var viewer in camera.ActivePvsViewers) { // if the player has not already received the chat message, send it to them but don't log it to the chat // window. This is simply so that it appears in camera. diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.Collide.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.Collide.cs new file mode 100644 index 0000000000..4015a65cc9 --- /dev/null +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.Collide.cs @@ -0,0 +1,101 @@ +using Content.Shared.Power.EntitySystems; +using Content.Shared.SurveillanceCamera; +using Content.Shared.SurveillanceCamera.Components; +using Robust.Shared.Physics.Events; +using Robust.Shared.Physics.Systems; + +namespace Content.Server.SurveillanceCamera; + +public partial class SurveillanceCameraSystem +{ + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedPowerReceiverSystem _power = default!; + [Dependency] private readonly EntityQuery _cameraQuery = default!; + + public void InitializeCollide() + { + SubscribeLocalEvent(OnPreventCollide); + SubscribeLocalEvent(OnStart); + SubscribeLocalEvent(OnEnd); + + SubscribeLocalEvent(OnCollideShutdown); + SubscribeLocalEvent(OnOverrideState); + } + + private void OnCollideShutdown(Entity ent, ref ComponentShutdown args) + { + // TODO: Check this on the event. + if (TerminatingOrDeleted(ent.Owner)) + return; + + // Regenerate contacts for everything we were colliding with. + var contacts = _physics.GetContacts(ent.Owner); + + while (contacts.MoveNext(out var contact)) + { + if (!contact.IsTouching) + continue; + + var other = contact.OtherEnt(ent.Owner); + + if (_cameraQuery.HasComp(other)) + { + _physics.RegenerateContacts(other); + } + } + } + + // You may be wondering what de fok this is doing here. + // At the moment there's no easy way to do collision whitelists based on components. + private void OnPreventCollide(Entity ent, ref PreventCollideEvent args) + { + if (!_cameraQuery.HasComp(args.OtherEntity)) + { + args.Cancelled = true; + } + } + + private void OnEnd(Entity ent, ref EndCollideEvent args) + { + if (args.OurFixtureId != ent.Comp.FixtureId) + return; + + if (!_cameraQuery.TryComp(args.OtherEntity, out var cameraCollider)) + return; + + // TODO: Engine bug IsTouching box2d yay. + var contacts = _physics.GetTouchingContacts(args.OtherEntity) - 1; + + if (contacts > 0) + return; + + cameraCollider.Enabled = false; + Dirty(args.OtherEntity, cameraCollider); + UpdateVisuals(args.OtherEntity); + } + + private void OnStart(Entity ent, ref StartCollideEvent args) + { + if (args.OurFixtureId != ent.Comp.FixtureId) + return; + + if (!_cameraQuery.TryComp(args.OtherEntity, out var cameraCollider)) + return; + + cameraCollider.Enabled = true; + Dirty(args.OtherEntity, cameraCollider); + UpdateVisuals(args.OtherEntity); + } + + private void OnOverrideState(Entity ent, ref SurveillanceCameraGetIsViewedExternallyEvent args) + { + if (ent.Comp.RequiresPower && !_power.IsPowered(ent.Owner)) + return; + + if (!ent.Comp.Enabled) + return; + + args.Viewed = true; + } +} + diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs index cdd032c79c..09c2b39447 100644 --- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs @@ -13,15 +13,15 @@ using Content.Shared.DeviceNetwork.Components; namespace Content.Server.SurveillanceCamera; -public sealed class SurveillanceCameraSystem : SharedSurveillanceCameraSystem +public sealed partial class SurveillanceCameraSystem : SharedSurveillanceCameraSystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly ViewSubscriberSystem _viewSubscriberSystem = default!; [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!; [Dependency] private readonly UserInterfaceSystem _userInterface = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly SurveillanceCameraMapSystem _cameraMapSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; // Pings a surveillance camera subnet. All cameras will always respond // with a data message if they are on the same subnet. @@ -61,6 +61,8 @@ public sealed class SurveillanceCameraSystem : SharedSurveillanceCameraSystem SubscribeLocalEvent(OnPacketReceived); SubscribeLocalEvent(OnSetName); SubscribeLocalEvent(OnSetNetwork); + + InitializeCollide(); } private void OnPacketReceived(EntityUid uid, SurveillanceCameraComponent component, DeviceNetworkPacketEvent args) @@ -232,7 +234,7 @@ public sealed class SurveillanceCameraSystem : SharedSurveillanceCameraSystem var ev = new SurveillanceCameraDeactivateEvent(camera); - RemoveActiveViewers(camera, new(component.ActiveViewers), null, component); + RemoveActiveViewers(camera, new(component.ActivePvsViewers), null, component); component.Active = false; // Send a targetted event to all monitors. @@ -249,6 +251,25 @@ public sealed class SurveillanceCameraSystem : SharedSurveillanceCameraSystem UpdateVisuals(camera, component); } + /// + /// Checks whether the camera is being viewed through by anyone at all. + /// + /// The camera to check + /// True if the camera is looked through, otherwise False. + public bool IsGettingViewed(Entity ent) + { + if (!Resolve(ent, ref ent.Comp)) + return false; + + if (ent.Comp.ActivePvsViewers.Count > 0 || ent.Comp.ActiveMonitors.Count > 0) + return true; + + var ev = new SurveillanceCameraGetIsViewedExternallyEvent(); + RaiseLocalEvent(ent, ref ev); + + return ev.Viewed; + } + public override void SetActive(EntityUid camera, bool setting, SurveillanceCameraComponent? component = null) { if (!Resolve(camera, ref component)) @@ -284,7 +305,8 @@ public sealed class SurveillanceCameraSystem : SharedSurveillanceCameraSystem } _viewSubscriberSystem.AddViewSubscriber(camera, actor.PlayerSession); - component.ActiveViewers.Add(player); + + component.ActivePvsViewers.Add(player); if (monitor != null) { @@ -346,7 +368,7 @@ public sealed class SurveillanceCameraSystem : SharedSurveillanceCameraSystem if (Resolve(player, ref actor)) _viewSubscriberSystem.RemoveViewSubscriber(camera, actor.PlayerSession); - component.ActiveViewers.Remove(player); + component.ActivePvsViewers.Remove(player); if (monitor != null) { @@ -391,7 +413,7 @@ public sealed class SurveillanceCameraSystem : SharedSurveillanceCameraSystem key = SurveillanceCameraVisuals.Active; } - if (component.ActiveViewers.Count > 0 || component.ActiveMonitors.Count > 0) + if (IsGettingViewed((uid, component))) { key = SurveillanceCameraVisuals.InUse; } diff --git a/Content.Shared/Light/Components/LightOnCollideColliderComponent.cs b/Content.Shared/Light/Components/LightOnCollideColliderComponent.cs deleted file mode 100644 index 39be05a148..0000000000 --- a/Content.Shared/Light/Components/LightOnCollideColliderComponent.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Robust.Shared.GameStates; - -namespace Content.Shared.Light.Components; - -/// -/// Can activate when collided with. -/// -[RegisterComponent, NetworkedComponent] -public sealed partial class LightOnCollideColliderComponent : Component -{ - [DataField] - public string FixtureId = "lightTrigger"; -} diff --git a/Content.Shared/Light/Components/LightOnCollideComponent.cs b/Content.Shared/Light/Components/LightOnCollideComponent.cs deleted file mode 100644 index c3b4bd7396..0000000000 --- a/Content.Shared/Light/Components/LightOnCollideComponent.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Robust.Shared.GameStates; - -namespace Content.Shared.Light.Components; - -/// -/// Enables / disables pointlight whenever entities are contacting with it -/// -[RegisterComponent, NetworkedComponent] -public sealed partial class LightOnCollideComponent : Component -{ -} diff --git a/Content.Shared/Light/EntitySystems/LightCollideSystem.cs b/Content.Shared/Light/EntitySystems/LightCollideSystem.cs deleted file mode 100644 index 2de7c5591f..0000000000 --- a/Content.Shared/Light/EntitySystems/LightCollideSystem.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Content.Shared.Light.Components; -using Robust.Shared.Physics.Events; -using Robust.Shared.Physics.Systems; - -namespace Content.Shared.Light.EntitySystems; - -public sealed class LightCollideSystem : EntitySystem -{ - [Dependency] private readonly SharedPhysicsSystem _physics = default!; - [Dependency] private readonly SlimPoweredLightSystem _lights = default!; - - private EntityQuery _lightQuery; - - public override void Initialize() - { - base.Initialize(); - - _lightQuery = GetEntityQuery(); - - SubscribeLocalEvent(OnPreventCollide); - SubscribeLocalEvent(OnStart); - SubscribeLocalEvent(OnEnd); - - SubscribeLocalEvent(OnCollideShutdown); - } - - private void OnCollideShutdown(Entity ent, ref ComponentShutdown args) - { - // TODO: Check this on the event. - if (TerminatingOrDeleted(ent.Owner)) - return; - - // Regenerate contacts for everything we were colliding with. - var contacts = _physics.GetContacts(ent.Owner); - - while (contacts.MoveNext(out var contact)) - { - if (!contact.IsTouching) - continue; - - var other = contact.OtherEnt(ent.Owner); - - if (_lightQuery.HasComp(other)) - { - _physics.RegenerateContacts(other); - } - } - } - - // You may be wondering what de fok this is doing here. - // At the moment there's no easy way to do collision whitelists based on components. - private void OnPreventCollide(Entity ent, ref PreventCollideEvent args) - { - if (!_lightQuery.HasComp(args.OtherEntity)) - { - args.Cancelled = true; - } - } - - private void OnEnd(Entity ent, ref EndCollideEvent args) - { - if (args.OurFixtureId != ent.Comp.FixtureId) - return; - - if (!_lightQuery.HasComp(args.OtherEntity)) - return; - - // TODO: Engine bug IsTouching box2d yay. - var contacts = _physics.GetTouchingContacts(args.OtherEntity) - 1; - - if (contacts > 0) - return; - - _lights.SetEnabled(args.OtherEntity, false); - } - - private void OnStart(Entity ent, ref StartCollideEvent args) - { - if (args.OurFixtureId != ent.Comp.FixtureId) - return; - - if (!_lightQuery.HasComp(args.OtherEntity)) - return; - - _lights.SetEnabled(args.OtherEntity, true); - } -} diff --git a/Content.Shared/SurveillanceCamera/Components/CameraActiveOnCollideColliderComponent.cs b/Content.Shared/SurveillanceCamera/Components/CameraActiveOnCollideColliderComponent.cs new file mode 100644 index 0000000000..f92f395eb9 --- /dev/null +++ b/Content.Shared/SurveillanceCamera/Components/CameraActiveOnCollideColliderComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.SurveillanceCamera.Components; + +/// +/// Can activate when collided with. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class CameraActiveOnCollideColliderComponent : Component +{ + /// + /// The fixture id used for detecting the collision. + /// + [DataField] + public string FixtureId = "lightTrigger"; +} diff --git a/Content.Shared/SurveillanceCamera/Components/CameraActiveOnCollideComponent.cs b/Content.Shared/SurveillanceCamera/Components/CameraActiveOnCollideComponent.cs new file mode 100644 index 0000000000..27c4bf7c09 --- /dev/null +++ b/Content.Shared/SurveillanceCamera/Components/CameraActiveOnCollideComponent.cs @@ -0,0 +1,22 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.SurveillanceCamera.Components; + +/// +/// Marks an entity with whenever entities are contacting with it. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class CameraActiveOnCollideComponent : Component +{ + /// + /// Whether this camera is currently being collided with. + /// + [DataField, AutoNetworkedField] + public bool Enabled; + + /// + /// Whether the entity must be powered for this component to work. + /// + [DataField, AutoNetworkedField] + public bool RequiresPower = true; +} diff --git a/Content.Shared/SurveillanceCamera/Components/SurveillanceCameraComponent.cs b/Content.Shared/SurveillanceCamera/Components/SurveillanceCameraComponent.cs index 21381e753f..914ba4a63f 100644 --- a/Content.Shared/SurveillanceCamera/Components/SurveillanceCameraComponent.cs +++ b/Content.Shared/SurveillanceCamera/Components/SurveillanceCameraComponent.cs @@ -8,18 +8,23 @@ namespace Content.Shared.SurveillanceCamera.Components; [Access(typeof(SharedSurveillanceCameraSystem))] public sealed partial class SurveillanceCameraComponent : Component { - // List of active viewers. This is for bookkeeping purposes, - // so that when a camera shuts down, any entity viewing it - // will immediately have their subscription revoked. + /// + /// List of active viewers who have a PVS view subscription on this camera. + /// This is for bookkeeping purposes, + /// so that when a camera shuts down, any entity viewing it + /// will immediately have their subscription revoked. + /// [ViewVariables] - public HashSet ActiveViewers { get; } = new(); + public HashSet ActivePvsViewers { get; } = new(); - // Monitors != Viewers, as viewers are entities that are tied - // to a player session that's viewing from this camera - // - // Monitors are grouped sets of viewers, and may be - // completely different monitor types (e.g., monitor console, - // AI, etc.) + /// + /// Monitors != Viewers, as viewers are entities that are tied + /// to a player session that's viewing from this camera + /// + /// Monitors are grouped sets of viewers, and may be + /// completely different monitor types (e.g., monitor console, + /// AI, etc.) + /// [ViewVariables] public HashSet ActiveMonitors { get; } = new(); diff --git a/Content.Shared/SurveillanceCamera/SharedSurveillanceCameraSystem.cs b/Content.Shared/SurveillanceCamera/SharedSurveillanceCameraSystem.cs index b6743669e9..1be49f8fc8 100644 --- a/Content.Shared/SurveillanceCamera/SharedSurveillanceCameraSystem.cs +++ b/Content.Shared/SurveillanceCamera/SharedSurveillanceCameraSystem.cs @@ -5,7 +5,7 @@ using Robust.Shared.Serialization; namespace Content.Shared.SurveillanceCamera; -public abstract class SharedSurveillanceCameraSystem : EntitySystem +public abstract partial class SharedSurveillanceCameraSystem : EntitySystem { public override void Initialize() { @@ -68,3 +68,11 @@ public enum SurveillanceCameraVisuals : byte Xray, Emp } + +/// +/// Raised on a camera entity to find whether it is externally viewed by some entity. +/// This does not use the actual viewers or monitors camera has and is simply used to see whether the camera is "technically" +/// being looked through by somebody, such as the Station AI. +/// +[ByRefEvent] +public record struct SurveillanceCameraGetIsViewedExternallyEvent(bool Viewed = false); diff --git a/Resources/Prototypes/Actions/station_ai.yml b/Resources/Prototypes/Actions/station_ai.yml index 4dbaf07aab..9e9bc9f650 100644 --- a/Resources/Prototypes/Actions/station_ai.yml +++ b/Resources/Prototypes/Actions/station_ai.yml @@ -14,34 +14,6 @@ - type: InstantAction event: !type:JumpToCoreEvent -- type: entity - parent: BaseAction - id: ActionSurvCameraLights - name: Toggle camera lights - description: Enable surveillance camera lights near wherever you're viewing. - components: - - type: Action - priority: -5 - itemIconStyle: BigAction - icon: - sprite: Interface/Actions/actions_ai.rsi - state: camera_light - - type: InstantAction - event: !type:RelayedActionComponentChangeEvent - components: - - type: LightOnCollideCollider - - type: FixturesChange - fixtures: - lightTrigger: - shape: - !type:PhysShapeCircle - radius: 0.35 - density: 80 - hard: false - layer: - - GhostImpassable - - - type: entity parent: BaseMentalAction id: ActionAIViewLaws diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml index 5e06100098..7a6e51cb48 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -34,7 +34,6 @@ - type: ActionGrant actions: - ActionJumpToCore - - ActionSurvCameraLights - ActionAIViewLaws - type: UserInterface interfaces: @@ -487,6 +486,16 @@ description: The AI's viewer. categories: [ HideSpawnMenu, DoNotMap ] components: + - type: Fixtures + fixtures: + lightTrigger: + shape: + !type:PhysShapeCircle + radius: 8 + density: 80 + hard: false + layer: + - GhostImpassable - type: NoFTL - type: Tag tags: @@ -506,6 +515,7 @@ - state: ai_camera shader: unshaded map: ["base"] + - type: CameraActiveOnCollideCollider # The holographic representation of the AI that is projected from a holopad. - type: entity diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/WallmountMachines/surveillance_camera.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/WallmountMachines/surveillance_camera.yml index 561991cd26..a12bca758d 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/WallmountMachines/surveillance_camera.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/WallmountMachines/surveillance_camera.yml @@ -11,7 +11,7 @@ light: shape: !type:PhysShapeCircle - radius: 5 + radius: 0.5 hard: false mask: - GhostImpassable @@ -24,7 +24,8 @@ - None layer: - TabletopMachineLayer - - type: LightOnCollide + - type: CameraActiveOnCollide + requiresPower: false # TODO: Change this when AI can no longer see via unpowered cameras - type: PointLight enabled: false radius: 5 From d904647eb37fc763837f47f232c172264e53a6fd Mon Sep 17 00:00:00 2001 From: PJBot Date: Sun, 5 Apr 2026 15:18:30 +0000 Subject: [PATCH 088/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 05531749e5..9f703312dd 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: DinnerCalzone - changes: - - message: ID card sprites have been tweaked to unsquish their job icons. - type: Tweak - id: 9107 - time: '2025-10-16T03:35:01.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40414 - author: redmushie changes: - message: Fixed power sensors not respecting their configured network setting @@ -4037,3 +4030,12 @@ id: 9618 time: '2026-04-05T10:17:20.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/35237 +- author: ScarKy0 + changes: + - message: Cameras show as in-use when AI is nearby. + type: Add + - message: AI can no longer enable lights on cameras. + type: Remove + id: 9619 + time: '2026-04-05T15:17:21.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43466 From bed228f37f8172e680104705f80a5bf0ce12f6bd Mon Sep 17 00:00:00 2001 From: salarua Date: Sun, 5 Apr 2026 09:55:36 -0700 Subject: [PATCH 089/126] Fix radio quotes (#43483) Change radio straight quotes to curly quotes --- Resources/Locale/en-US/headset/headset-component.ftl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Resources/Locale/en-US/headset/headset-component.ftl b/Resources/Locale/en-US/headset/headset-component.ftl index d61fb8edb2..f9f9c17411 100644 --- a/Resources/Locale/en-US/headset/headset-component.ftl +++ b/Resources/Locale/en-US/headset/headset-component.ftl @@ -1,6 +1,6 @@ # Chat window radio wrap (prefix and postfix) -chat-radio-message-wrap = [color={$color}]{$channel} [bold]{$name}[/bold] {$verb}, [font={$fontType} size={$fontSize}]"{$message}"[/font][/color] -chat-radio-message-wrap-bold = [color={$color}]{$channel} [bold]{$name}[/bold] {$verb}, [font={$fontType} size={$fontSize}][bold]"{$message}"[/bold][/font][/color] +chat-radio-message-wrap = [color={$color}]{$channel} [bold]{$name}[/bold] {$verb}, [font={$fontType} size={$fontSize}]“{$message}”[/font][/color] +chat-radio-message-wrap-bold = [color={$color}]{$channel} [bold]{$name}[/bold] {$verb}, [font={$fontType} size={$fontSize}][bold]“{$message}”[/bold][/font][/color] examine-headset-default-channel = Use {$prefix} for the default channel ([color={$color}]{$channel}[/color]). From fbe66e6557eba1378c35a8f977d544f053fa3599 Mon Sep 17 00:00:00 2001 From: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> Date: Sun, 5 Apr 2026 22:16:00 +0200 Subject: [PATCH 090/126] Prevent changelings from repeated devourings take 2 (#43463) * init * review * check on ling instead --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --- .../ChangelingTransformBoundUserInterface.cs | 3 +- .../Components/ChangelingDevouredComponent.cs | 18 ++ .../Components/ChangelingIdentityComponent.cs | 7 +- .../Systems/ChangelingDevourSystem.cs | 179 +++++++++++------- .../Systems/ChangelingTransformSystem.cs | 3 +- .../Systems/SharedChangelingIdentitySystem.cs | 56 +++++- .../Locale/en-US/changeling/changeling.ftl | 2 + 7 files changed, 184 insertions(+), 84 deletions(-) create mode 100644 Content.Shared/Changeling/Components/ChangelingDevouredComponent.cs diff --git a/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs b/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs index 64d809c0c5..dc40147220 100644 --- a/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs +++ b/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs @@ -23,7 +23,6 @@ public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owne _menu.OpenOverMouseScreenPosition(); } - public override void Update() { if (_menu == null) @@ -32,7 +31,7 @@ public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owne if (!EntMan.TryGetComponent(Owner, out var lingIdentity)) return; - var models = ConvertToButtons(lingIdentity.ConsumedIdentities, lingIdentity?.CurrentIdentity); + var models = ConvertToButtons(lingIdentity.ConsumedIdentities.Keys, lingIdentity?.CurrentIdentity); _menu.SetButtons(models); } diff --git a/Content.Shared/Changeling/Components/ChangelingDevouredComponent.cs b/Content.Shared/Changeling/Components/ChangelingDevouredComponent.cs new file mode 100644 index 0000000000..c235ea071a --- /dev/null +++ b/Content.Shared/Changeling/Components/ChangelingDevouredComponent.cs @@ -0,0 +1,18 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Changeling.Components; + +/// +/// Component used for marking entities devoured by a changeling. +/// Used to prevent granting the identity several times. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ChangelingDevouredComponent : Component +{ + /// + /// HashSet of all changelings that have devoured this entity. + /// + // TODO: This should be using some sort of relation system in the future. + [DataField, AutoNetworkedField] + public HashSet DevouredBy = new(); +} diff --git a/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs b/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs index 8e74f83537..fa667f4bf2 100644 --- a/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs +++ b/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs @@ -13,11 +13,12 @@ public sealed partial class ChangelingIdentityComponent : Component { /// /// The list of entities that exist on a paused map. They are paused clones of the victims that the ling has consumed, with all relevant components copied from the original. + /// The key is the EntityUid of the stored identity, the value is the original entity the identity came from. + /// The value will be set to null if that entity is deleted. /// - // TODO: Store a reference to the original entity as well so you cannot infinitely devour somebody. Currently very tricky due the inability to send over EntityUid if the original is ever deleted. Can be fixed by something like WeakEntityReference. + // TODO: This should be handled via a relation system in the future. [DataField, AutoNetworkedField] - public List ConsumedIdentities = new(); - + public Dictionary ConsumedIdentities = new(); /// /// The currently assumed identity. diff --git a/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs b/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs index d08e6315ce..b9bdfa29c7 100644 --- a/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs +++ b/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs @@ -59,29 +59,6 @@ public sealed class ChangelingDevourSystem : EntitySystem } } - /// - /// Checkes if the targets outerclothing is beyond a DamageCoefficientThreshold to protect them from being devoured. - /// - /// The Targeted entity - /// Changelings Devour Component - /// Is the target Protected from the attack - private bool IsTargetProtected(EntityUid target, Entity ent) - { - var ev = new CoefficientQueryEvent(SlotFlags.OUTERCLOTHING); - - RaiseLocalEvent(target, ev); - - foreach (var compProtectiveDamageType in ent.Comp.ProtectiveDamageTypes) - { - if (!ev.DamageModifiers.Coefficients.TryGetValue(compProtectiveDamageType, out var coefficient)) - continue; - if (coefficient < 1f - ent.Comp.DevourPreventionPercentageThreshold) - return true; - } - - return false; - } - // The action was used. // Start the first doafter for the windup. private void OnDevourAction(Entity ent, ref ChangelingDevourActionEvent args) @@ -94,33 +71,13 @@ public sealed class ChangelingDevourSystem : EntitySystem args.Handled = true; var target = args.Target; - if (target == ent.Owner) - return; // don't eat yourself - - if (!_mobState.IsDead(target)) - { - _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-not-dead"), args.Performer, args.Performer, PopupType.Medium); + if (!CanDevour(ent.AsNullable(), target)) return; - } - - if (HasComp(target)) - { - _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-rotting"), args.Performer, args.Performer, PopupType.Medium); - return; - } - - if (IsTargetProtected(target, ent)) - { - _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-protected"), ent, ent, PopupType.Medium); - return; - } if (_net.IsServer) { ent.Comp.CurrentDevourSound = _audio.Stop(ent.Comp.CurrentDevourSound); - var pvsSound = _audio.PlayPvs(ent.Comp.DevourWindupNoise, ent); - if (pvsSound != null) - ent.Comp.CurrentDevourSound = pvsSound.Value.Entity; + ent.Comp.CurrentDevourSound = _audio.PlayPvs(ent.Comp.DevourWindupNoise, ent)?.Entity; } _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ent:player} started changeling devour windup against {target:player}"); @@ -162,17 +119,12 @@ public sealed class ChangelingDevourSystem : EntitySystem _popupSystem.PopupPredicted( selfMessage, othersMessage, - args.User, - args.User, + ent.Owner, + ent.Owner, PopupType.LargeCaution); if (_net.IsServer) - { - var pvsSound = _audio.PlayPvs(ent.Comp.ConsumeNoise, ent); - - if (pvsSound != null) - ent.Comp.CurrentDevourSound = pvsSound.Value.Entity; - } + ent.Comp.CurrentDevourSound = _audio.PlayPvs(ent.Comp.ConsumeNoise, ent)?.Entity; _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} began to devour {ToPrettyString(target):player}'s identity"); @@ -203,37 +155,120 @@ public sealed class ChangelingDevourSystem : EntitySystem if (args.Target is not { } target) return; + // Damage first before the CanDevour check to make sure they don't gib in-between and to kill them again in case they somehow revived. _damageable.ChangeDamage(target, ent.Comp.DevourDamage, true, true, ent.Owner); - if (!_mobState.IsDead(target)) - { - _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-not-dead"), args.User, args.User, PopupType.Medium); - _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} unsuccessfully devoured {ToPrettyString(args.Target):player}'s identity"); + if (!CanDevour(ent.AsNullable(), target)) // Check again if the conditions are still met. return; - } - var selfMessage = Loc.GetString("changeling-devour-consume-complete-self", ("user", Identity.Entity(args.User, EntityManager))); - var othersMessage = Loc.GetString("changeling-devour-consume-complete-others", ("user", Identity.Entity(args.User, EntityManager))); + var selfMessage = Loc.GetString("changeling-devour-consume-complete-self", ("user", Identity.Entity(ent.Owner, EntityManager))); + var othersMessage = Loc.GetString("changeling-devour-consume-complete-others", ("user", Identity.Entity(ent.Owner, EntityManager))); _popupSystem.PopupPredicted( selfMessage, othersMessage, - args.User, - args.User, + ent.Owner, + ent.Owner, PopupType.LargeCaution); - if (_mobState.IsDead(target) - && HasComp(target) - && TryComp(args.User, out var identityStorage)) - { - _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} successfully devoured {ToPrettyString(target):player}'s identity"); - _changelingIdentitySystem.CloneToPausedMap((ent, identityStorage), target); + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} successfully devoured {ToPrettyString(target):player}'s identity"); - if (_inventorySystem.TryGetSlotEntity(target, "jumpsuit", out var item) - && TryComp(item, out var butcherable)) - RipClothing(target, (item.Value, butcherable)); + if (_inventorySystem.TryGetSlotEntity(target, "jumpsuit", out var item) + && TryComp(item, out var butcherable)) + RipClothing(target, (item.Value, butcherable)); + + if (!TryComp(ent.Owner, out var identityStorage)) + return; + + _changelingIdentitySystem.CloneToPausedMap((ent, identityStorage), target); + + // We add a reference to ourselves to prevent repeated identity gain. + var targetDevoured = EnsureComp(target); + targetDevoured.DevouredBy.Add(ent.Owner); + Dirty(target, targetDevoured); + Dirty(ent); + } + + /// + /// Has the given victim been devoured by the given changeling before? + /// + public bool HasDevoured(Entity changeling, EntityUid devoured) + { + if (!Resolve(changeling, ref changeling.Comp, false)) + return false; + + return changeling.Comp.ConsumedIdentities.ContainsValue(devoured); + } + + /// + /// Can the given changeling devour the given victim? + /// + public bool CanDevour(Entity changeling, EntityUid victim, bool showPopup = true) + { + if (!Resolve(changeling, ref changeling.Comp)) + return false; + + if (changeling.Owner == victim) + return false; // Can't devour yourself. + + if (!HasComp(victim)) + { + if (showPopup) + _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-cannot-devour"), changeling.Owner, changeling.Owner, PopupType.Medium); + return false; } - Dirty(ent); + if (HasDevoured(changeling.Owner, victim)) + { + if (showPopup) + _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-already-devoured"), changeling.Owner, changeling.Owner, PopupType.Medium); + return false; + } + + if (!_mobState.IsDead(victim)) + { + if (showPopup) + _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-not-dead"), changeling.Owner, changeling.Owner, PopupType.Medium); + return false; + } + + if (HasComp(victim)) + { + if (showPopup) + _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-rotting"), changeling.Owner, changeling.Owner, PopupType.Medium); + return false; + } + + if (IsTargetProtected(victim, changeling!)) + { + if (showPopup) + _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-protected"), changeling.Owner, changeling.Owner, PopupType.Medium); + return false; + } + + return true; + } + + /// + /// Checks if the target's outerclothing is beyond a DamageCoefficientThreshold to protect them from being devoured. + /// + /// The Targeted entity + /// Changelings Devour Component + /// Is the target Protected from the attack + private bool IsTargetProtected(EntityUid target, Entity ent) + { + var ev = new CoefficientQueryEvent(SlotFlags.OUTERCLOTHING); + + RaiseLocalEvent(target, ev); + + foreach (var compProtectiveDamageType in ent.Comp.ProtectiveDamageTypes) + { + if (!ev.DamageModifiers.Coefficients.TryGetValue(compProtectiveDamageType, out var coefficient)) + continue; + if (coefficient < 1f - ent.Comp.DevourPreventionPercentageThreshold) + return true; + } + + return false; } // TODO: This should just be an API method in the butcher system diff --git a/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs b/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs index 94d2cd77a8..8bea4dedc6 100644 --- a/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs +++ b/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs @@ -139,7 +139,7 @@ public sealed partial class ChangelingTransformSystem : EntitySystem if (identity.CurrentIdentity == targetIdentity) return; // don't transform into ourselves - if (!identity.ConsumedIdentities.Contains(targetIdentity.Value)) + if (!identity.ConsumedIdentities.ContainsKey(targetIdentity.Value)) return; // this identity does not belong to this player TransformInto(ent.AsNullable(), targetIdentity.Value); @@ -170,6 +170,7 @@ public sealed partial class ChangelingTransformSystem : EntitySystem _adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent.Owner):player} successfully transformed into \"{Name(targetIdentity)}\" ({storedIdentity.OriginalSession:player})"); else _adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent.Owner):player} successfully transformed into \"{Name(targetIdentity)}\""); + _metaData.SetEntityName(ent, Name(targetIdentity), raiseEvents: false); // Don't raise events because we don't want to rename the ID card. _identity.QueueIdentityUpdate(ent); // We have to manually refresh the identity because we did not raise events. diff --git a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs index a3b620d68a..12d8f162a9 100644 --- a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs +++ b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using System.Linq; +using System.Numerics; using Content.Shared.Body; using Content.Shared.Changeling.Components; using Content.Shared.Cloning; @@ -34,6 +35,8 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem SubscribeLocalEvent(OnPlayerAttached); SubscribeLocalEvent(OnPlayerDetached); SubscribeLocalEvent(OnStoredRemove); + + SubscribeLocalEvent(OnDevouredShutdown); } private void OnPlayerAttached(Entity ent, ref PlayerAttachedEvent args) @@ -57,7 +60,32 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem { if (TryComp(ent, out var actor)) CleanupPvsOverride(ent, actor.PlayerSession); + CleanupChangelingNullspaceIdentities(ent); + CleanupDevouredReferences(ent); + } + + // Set all references to this entity to null to prevent PVS errors when networking. + private void OnDevouredShutdown(Entity ent, ref ComponentShutdown args) + { + foreach (var ling in ent.Comp.DevouredBy) + { + if (!TryComp(ling, out var identityComp)) + continue; + + var keysToUpdate = identityComp.ConsumedIdentities + .Where(kvp => kvp.Value == ent.Owner) + .Select(kvp => kvp.Key) + .ToList(); + + if (keysToUpdate.Count == 0) + continue; // No need to dirty. + + foreach (var key in keysToUpdate) + identityComp.ConsumedIdentities[key] = null; + + Dirty(ling, identityComp); + } } private void OnStoredRemove(Entity ent, ref ComponentRemove args) @@ -78,7 +106,23 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem foreach (var consumedIdentity in ent.Comp.ConsumedIdentities) { - QueueDel(consumedIdentity); + QueueDel(consumedIdentity.Key); + } + } + + /// + /// Removes all references to the owning changeling from ChangelingDevouredComponents. + /// + /// The changeling entity + private void CleanupDevouredReferences(Entity ent) + { + foreach (var devouredUid in ent.Comp.ConsumedIdentities.Values) + { + if (!TryComp(devouredUid, out var devouredComp)) + continue; + + if (devouredComp.DevouredBy.Remove(ent.Owner)) + Dirty(devouredUid.Value, devouredComp); } } @@ -132,7 +176,7 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem if (clone == null) return null; - ent.Comp.ConsumedIdentities.Add(clone.Value); + ent.Comp.ConsumedIdentities.Add(clone.Value, target); Dirty(ent); HandlePvsOverride(ent, clone.Value); @@ -157,12 +201,12 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem /// Cleanup all PVS overrides for the owner of the ChangelingIdentity /// /// The changeling storing the identities. - /// + /// The session you wish to remove the overrides from. private void CleanupPvsOverride(Entity ent, ICommonSession session) { foreach (var identity in ent.Comp.ConsumedIdentities) { - _pvsOverrideSystem.RemoveSessionOverride(identity, session); + _pvsOverrideSystem.RemoveSessionOverride(identity.Key, session); } } @@ -175,7 +219,7 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem { foreach (var identity in ent.Comp.ConsumedIdentities) { - _pvsOverrideSystem.AddSessionOverride(identity, session); + _pvsOverrideSystem.AddSessionOverride(identity.Key, session); } } diff --git a/Resources/Locale/en-US/changeling/changeling.ftl b/Resources/Locale/en-US/changeling/changeling.ftl index e33e5bc715..873744fa06 100644 --- a/Resources/Locale/en-US/changeling/changeling.ftl +++ b/Resources/Locale/en-US/changeling/changeling.ftl @@ -1,6 +1,8 @@ roles-antag-changeling-name = Changeling roles-antag-changeling-objective = A intelligent predator that assumes the identities of its victims. +changeling-devour-attempt-failed-cannot-devour = We cannot devour this! +changeling-devour-attempt-failed-already-devoured = We already consumed this body! changeling-devour-attempt-failed-not-dead = This body yet lives! We cannot consume it alive! changeling-devour-attempt-failed-rotting = This corpse has only rotted biomass. changeling-devour-attempt-failed-protected = This victim's biomass is protected by armor! From 7c2c66571a8832e3c6f66008159f3bf9f5e5af10 Mon Sep 17 00:00:00 2001 From: ruddygreat <53099277+ruddygreat@users.noreply.github.com> Date: Sun, 5 Apr 2026 22:23:52 +0100 Subject: [PATCH 091/126] Add support for unshaded displacement maps (#34876) * first commit. helpful commit messages yay! * incredibly minor cleanup * hoping this appeases the trailing whitespace check * helps if you actually commit the files you change * bring shader in line w/ what pjb's doing in that other pr * comments about why this is bad * this seems to magically work now somehow? --------- Co-authored-by: Ruddygreat --- .../DisplacementMap/DisplacementMapSystem.cs | 19 +++++++++++++++--- .../DisplacementMap/DisplacementData.cs | 3 +++ Resources/Prototypes/Shaders/displacement.yml | 7 +++++++ .../Shaders/displacement_unshaded.swsl | 20 +++++++++++++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 Resources/Textures/Shaders/displacement_unshaded.swsl diff --git a/Content.Client/DisplacementMap/DisplacementMapSystem.cs b/Content.Client/DisplacementMap/DisplacementMapSystem.cs index 6986e1c868..14075caba3 100644 --- a/Content.Client/DisplacementMap/DisplacementMapSystem.cs +++ b/Content.Client/DisplacementMap/DisplacementMapSystem.cs @@ -2,14 +2,18 @@ using System.Diagnostics.CodeAnalysis; using Content.Shared.DisplacementMap; using Robust.Client.GameObjects; using Robust.Client.Graphics; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager; namespace Content.Client.DisplacementMap; public sealed class DisplacementMapSystem : EntitySystem { - [Dependency] private readonly ISerializationManager _serialization = default!; - [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly ISerializationManager _serialization = null!; + [Dependency] private readonly SpriteSystem _sprite = null!; + + //needs to be replaced later: see comment on line 48 + private static readonly ProtoId UnshadedID = "unshaded"; private static string? BuildDisplacementLayerKey(object key) { @@ -40,7 +44,16 @@ public sealed class DisplacementMapSystem : EntitySystem EnsureDisplacementIsNotOnSprite(sprite, key); if (data.ShaderOverride is not null) - sprite.Comp.LayerSetShader(index, data.ShaderOverride); + { + //TODO : this is a kinda janky workaround for the fact that the current rendering pipeline does not have + //proper support for multiple shaders on a given layer (or an ubershader to handle stacking all of the effects well) + //should be replaced by an engine-level solution, but this is an adequate temporary solution. + //what's that phrase about temporary solutions? + sprite.Comp.LayerSetShader(index, + (sprite.Comp[index] is SpriteComponent.Layer layer && layer.ShaderPrototype == UnshadedID) + ? data.ShaderOverrideUnshaded + : data.ShaderOverride); + } //allows you not to write it every time in the YML foreach (var pair in data.SizeMaps) diff --git a/Content.Shared/DisplacementMap/DisplacementData.cs b/Content.Shared/DisplacementMap/DisplacementData.cs index 79f89a1d25..8bc922708f 100644 --- a/Content.Shared/DisplacementMap/DisplacementData.cs +++ b/Content.Shared/DisplacementMap/DisplacementData.cs @@ -13,4 +13,7 @@ public sealed partial class DisplacementData [DataField] public string? ShaderOverride = "DisplacedDraw"; + + [DataField] + public string ShaderOverrideUnshaded = "DisplacedDrawUnshaded"; } diff --git a/Resources/Prototypes/Shaders/displacement.yml b/Resources/Prototypes/Shaders/displacement.yml index 70c9dce6f7..debb68fb1f 100644 --- a/Resources/Prototypes/Shaders/displacement.yml +++ b/Resources/Prototypes/Shaders/displacement.yml @@ -15,3 +15,10 @@ path: "/Textures/Shaders/displacement.swsl" params: displacementSize: 127 + +- type: shader + id: DisplacedDrawUnshaded + kind: source + path: "/Textures/Shaders/displacement_unshaded.swsl" + params: + displacementSize: 127 diff --git a/Resources/Textures/Shaders/displacement_unshaded.swsl b/Resources/Textures/Shaders/displacement_unshaded.swsl new file mode 100644 index 0000000000..cc389b9a9b --- /dev/null +++ b/Resources/Textures/Shaders/displacement_unshaded.swsl @@ -0,0 +1,20 @@ +light_mode unshaded; + +uniform sampler2D displacementMap; +uniform highp float displacementSize; +uniform highp vec4 displacementUV; + +varying highp vec2 displacementUVOut; + +void vertex() { + displacementUVOut = mix(displacementUV.xy, displacementUV.zw, tCoord2); +} + +void fragment() { + highp vec4 displacementSample = texture2D(displacementMap, displacementUVOut); + highp vec2 displacementValue = (displacementSample.xy - vec2(128.0 / 255.0)) / (1.0 - 128.0 / 255.0); + COLOR = zTexture(UV + displacementValue * TEXTURE_PIXEL_SIZE * displacementSize * vec2(1.0, -1.0)); + COLOR.a *= displacementSample.a; +} + + From 32b617e1953a7229a9f68e9b00d13747ca1eb704 Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:43:29 -0700 Subject: [PATCH 092/126] Web vest resprites (#43486) * resprite time! * fix them being off by a pixel * push!!! --------- Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> --- .../equipped-OUTERCLOTHING-vox.png | Bin 5654 -> 0 bytes .../elitevest.rsi/equipped-OUTERCLOTHING.png | Bin 5732 -> 7922 bytes .../Vests/elitevest.rsi/icon.png | Bin 5027 -> 5784 bytes .../Vests/elitevest.rsi/inhand-left.png | Bin 5063 -> 6484 bytes .../Vests/elitevest.rsi/inhand-right.png | Bin 5052 -> 7103 bytes .../Vests/elitevest.rsi/meta.json | 4 ---- .../equipped-OUTERCLOTHING.png | Bin 1009 -> 9668 bytes .../Vests/mercwebvest.rsi/icon.png | Bin 298 -> 5718 bytes .../Vests/mercwebvest.rsi/inhand-left.png | Bin 345 -> 6345 bytes .../Vests/mercwebvest.rsi/inhand-right.png | Bin 374 -> 6451 bytes .../equipped-OUTERCLOTHING-vox.png | Bin 615 -> 0 bytes .../webvest.rsi/equipped-OUTERCLOTHING.png | Bin 1233 -> 7701 bytes .../OuterClothing/Vests/webvest.rsi/icon.png | Bin 375 -> 5403 bytes .../Vests/webvest.rsi/inhand-left.png | Bin 358 -> 6232 bytes .../Vests/webvest.rsi/inhand-right.png | Bin 350 -> 6168 bytes .../OuterClothing/Vests/webvest.rsi/meta.json | 4 ---- 16 files changed, 8 deletions(-) delete mode 100644 Resources/Textures/Clothing/OuterClothing/Vests/elitevest.rsi/equipped-OUTERCLOTHING-vox.png delete mode 100644 Resources/Textures/Clothing/OuterClothing/Vests/webvest.rsi/equipped-OUTERCLOTHING-vox.png diff --git a/Resources/Textures/Clothing/OuterClothing/Vests/elitevest.rsi/equipped-OUTERCLOTHING-vox.png b/Resources/Textures/Clothing/OuterClothing/Vests/elitevest.rsi/equipped-OUTERCLOTHING-vox.png deleted file mode 100644 index c075eb9a7b7a41f54aa9a9310f64410fc2535ade..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5654 zcmeHKd0Z3M79N(e1l*qTM8O&(Dq50ELIO!lWD81!076s{st(D73}iD4VY4i?Doa7J z3RSDE?G=f@2JPfz`2WdZKR*s81GQI{wP(}W;8yi%nQV^S2aNl>DAG#(Q} zQ2Z5HaKz{uylvmj33E+rJGG9-<67khuC;hBD_czLWM>DgZ~vepH%aRkTu8|mgg#ia zWM6B+1dsC;mis@nyy*08{)xHfh)s3@%8N_sU01VmZN1Bm?8f#r-E#y+?sdAjo75=HnC0^YIx>2&A!n^+t|gji+PX z3XdbZ1yk*`@{1#O%d~LCe{8nWoMw}9(qm7Z%B?H@FzayX=+boj zFD6|i#_Z#M887{D#3(l7=Z4Cp+)7<)`C?&aXRcXh%d%x zZ$d8DIYu_LaoEXKS@&I@s8e!GDkO0A^sx<}?%7uBP zV`>G7*YZM<_7Yo(`oKh zn86%2T7s&KF;GbqnB-~M!zST>XaX9A<)cCo4ax-LqKt7!7z);d=I98 z9|pi7YlTXnP)IohJz02tjzelbSgS;c1{s2*vtSyFLK6TK8qA{8h_Ks8IKToXPWUrx z{mO)A8~fZ3Q)?8e1cS)9T!o^mjBmzQDP~xw@OZxm2lxW9 z{+-7_ll`48@c6e*-iY6~biJkPjTm?%{TjjKwa&4#UeI!Td)9O02= z`!23$oA!K6nKIj{$Qf6BY0hcb99n~{ zk150XF2b{|PuQ3S6o&TS?kfl^JT~cLcw6(mc2W9SQMxX@EoFBIFaK&{l?7DN7P|(q z3g5D5ub`Hc_V_&y-_!OV78e&?igC_w0{7s>cG(BLuXU@c8|Ir;w?$sCJg{tEYwxx` zLdGtuhfmp`!9v@IPaPLcu$vq-=g??6-zU!rv)|H$au*4lt?e$gr~<~V-di*-eA2Um zS*5#cCb5|U*OVokDWVty0KW9_uTWmid#cdZtn)u6yo^?a!b8ervDeU CV>YA! diff --git a/Resources/Textures/Clothing/OuterClothing/Vests/elitevest.rsi/equipped-OUTERCLOTHING.png b/Resources/Textures/Clothing/OuterClothing/Vests/elitevest.rsi/equipped-OUTERCLOTHING.png index 350f6a288ba1846b3a2015c3c57ed3a732bedb34..1ca03449f8a4e528fc42774aaab6f182dbe0c643 100644 GIT binary patch delta 4797 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 XUy1GzW$CXd00000NkvXXu0mjf45QhS delta 2557 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 E25CG{v;Y7A delta 1700 zcma)62~ZPf6yBhgkRaf}VU4nYg4Ar-O*RLSa0O6A5D+9m6qoESi2{iv7%nG>%7`c+ zq2Ngi^@tWz3n+u2qNuU9Dz>&BZB?*dc%rCPtaQQgn(1`>Xa0ZQ|GxLX_rCYu{2QQe zyvjWcKu2zzOc#skK~1KbRHYH1E=NOvM7D|qfb7fZ(Ltj-KziGgHU2@gk^UC+&MZUc zEq#1Naiso)ql^qy4zwlrDX*EfslHFjv@39el19l~Oj{M5XC~Ym{diZ@&h{xb8RK>l z4+DC7QI~|!N>11_9kz8<-2-(%*y{#EQ*mNjOUx3FF1HY&>H0i3=XvI2*>kw#L~Yoj zCl{OZk38GIQok~%dJ2tQrvOx5>*IzqO6!!zPOffAh${`>TW-h`2PR*%mfPgN+{yz0 z+DesF8XYc`zU4!)*s^TBWJX6I>v(cN)AqOt_rUJ*TJfdOP*#Map=4oglVf5Q+|tL~ zgiiA2lol>Na-{6|?iD4CW;=5s_&2SaBl12lqj(^K0j2nf9#~rQ%3AdW&GCvgU1h`8 zEb7^a2cLAFyQ+QaE0>UfLVEE}ow1jWvyv}6E8X?yKd;x$etlFEGyjLotv6jNWWfv-y}UEfk-uT}x!n~hptRq{dBAjk zwlTsz!Er5%X_78i_8%Fzrf&{Y<(>*>coiIS{_&-YwN-0itBR0cwN(A;Ps=~s{^jMT z7v{D9;dSS<*-2tmw10WYU9;zXTJ^N{lSa>;I_QNmH)8U&f7Tw_1=wD-s^#x&KI0$d zTENQMmp_cfvKm92TA{2oYlSTr+H(U#ou;yc>;jFx$(tkJ7~iPXUd4so#=yt z55j?s6?CDEMyb}RwMw;S5E}XJHiWm)b?oeP4F^}@Vuc9B6fh!y5Uv1+M82p95|Ow7 z5(pJM7*ima5EdG78_+|9;kW|hLNH14Ak-H@A&f*s5I=>FrU-dLp+JEcciMOvKE6lL9u1d#J2_H>H>JTu`HEX`4~$IMHxtxSh2Nr!lyJLH$n7;UPhBvX1+ek&9OZ zIuyBP?VHk2J_mGQ_Zj+rzF9S3-}2OHmFf1(^YyL=D?2M3F2>rgTE^ksyVvmW5RWZm zJCz&!RZbLjHKEIHqdycNUNM_}-dZf90GpPpQ&^w$hrz zkEbKH9XH5p*P~cPUF4s`Eqxx0!UB*!&Bd}_wF!8%%+<0adS=XpeT^G{f~UEX=JdSg hP2}9KgZag@;Ckn1dG<{tHr^hqnL# diff --git a/Resources/Textures/Clothing/OuterClothing/Vests/elitevest.rsi/inhand-left.png b/Resources/Textures/Clothing/OuterClothing/Vests/elitevest.rsi/inhand-left.png index 520d5c63408782867db122dac0a72230843b3e5a..00083227489aeb307f683e8311b084b10b532295 100644 GIT binary patch delta 3347 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 delta 1737 zcmca&bX8f};Gi%$!t(lFEWqh1817GzNx>Te$(Zc@Hb_{7LWX zW3c-y)o!x#3a?cif59S^Q;K~1OKz0ie*e30Js;;QlV_8%yyjh46)M&HNd4A>2c;hx zqPqU6zUyl-VA(e#&ogZQ@xzJ+^WC)PwVb*wblNoVExV?<;_rHny~+zW&I&M9JmJG4 zoKdVZKkMXk!D(mX1D}=7ozVVGvFN*LadmczfRZ$mjpT)!48MB$dfO&3Pif7Wx$T3A z%NylFMgQts5C1(fiWHep5)sD0z}S-M>>S|f>wSZC)#=(c91z5Z@*<}i0m7M zM-vuW-E?)8srMG`PFT1_G^*5K=NIl-CMueGeNEX15B48j)!e;#9pAbpg&&NM^ei-Y zd365w=F&~Fv3PfUcXj{n@`ml9$z2conY*%H%}jARAs*IxK>Lq`i+vME=a~yZH;>B1 z9KUXBQ?s}Gqt5r@`aZ^;83qTG1&+^LHv5S5kN zEdLzanX`F%y4#nSw0RazI#a46UL}TI6n!N5XvLZpQ{JDo)+xUCXw4O=HO2n_4iyB( zo-CUCOkz@m6IUk3gbNO$rZYM&u9}pj;QX6Y{Qru#{*%3x!bN*zxK}tgX8b<*|3Ue7 zx6Gcq)2$Djn5_0{k?zloDsR;Xe|z?=WZ=2aw9s_<_H%ZA@(nVjTWgqQWSE3gPsNp2 zmT%a;z5aa3uiuQCiA9~OA3juLVBlZ3`8MY=Mix-2n*5MUj042bH`FtjT*7V6S&)%m zl3$#WUjUNgh4aAj^SDcNDsl_-QZiGlk}XUUlMM{bbPbKnQgkgWO)PXR(o)QH&CHXH z4HAEX>R%+w)4wK(xUF z24bIYehSb!29}CI$k{5H8yNtBz9EQ2 zD2c?Y1Q-QY&PAz-C8;hz3^d!o$OIS-x`q}ZhUQk>1_o9}mf8k}Rt5%>3wbStO|1+} ztPD*;fFedlR;I?f24<7@@=9YBf6eQ`o(wiRWpV(YQUuX)0*sd864$a4tK$5$lJdl& zRLAtxJfOcFQ}UBi6#`(1Nƍ#SFvPTs?(D1qc?BsVMRPZr=yQZY5Lu&_upN!B&D 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 delta 1773 zcma)6X;c$e6rLa!j4YwbCa5tYN@2nz8L}awB%nFSB8DYZ3p2?CVj;;%Mp?uL5vhOz z5=0UA)3UhGmbHi|hzMF07dRALSoBmual;Ko?F5fq6&BiulW&<@OmW3ju%mE*p z)h86?3mj`KXP&EicWQRhI<@$k*sW{tq=TWZ9Ag{jo<2MLNy>?|v5*o$RK9V^#g2;J z-Um;i-Ae`efqpO7BX#7?$f$bzQGxE7Cu)4uA(Zp|ejj`@`?R&DTJ!!)#TM6C<&YDW zFx&an>Z!%G&eOLf>KBmQ4p))}J$Jyyi%zBLud6d*% zUKBVx#c^GA@+3z`qiNWMpW+ka|E#Umwc~Hzl3Wl?#?k(MX8^D)R}dnL2w2XSs^aK~ zOod|fgg7+-0F{wu*(76w+Vnbe8&iYf&Y(m3K6B0#b&LvEX=7As17!aBZyYCE;I0G% zU8a!nrCb=1LJT&@V6tT(mj`n}u3W|j*&HbgLZl3Y1L^;=pgM7-Fh?%sA|MOJFMNUs%l7eFGtfMy67!GAUoK(!?RS7flg|L}Tu1W%M{;=z}j% z#i}$b)rb`HqA@1i!HNVdHbSU~!<1TuO6f&o!(W1yv63%Py~I*z0mE#D2MgwTFqsg< z(I2(6*L@88d@djJ1*l|1I@UNNu8_<5K^g_7#1VWv)yFc%{UMlE6}!&h4pl0M2AC5e z5fhjfjYH&vAa{mgo!BA?!GxdUzp{rI4PT(a5F8T{A3+W=VPZTWgDYlm_z=Wr@?0SX zVd!sJ@}_z4A(+pA#e|2+jCn>auZ=Mw~E-mi69_MRS zay%Z4dviAiM;#6vMIQ*UIol;QzG1kvaAc$E^X7*m@Lqe2#O0lq{1mkNP!!UDAF z(%EARn33*?| zD4V>7UlU&B+BVrZXmpd4@m{l%l~lof%VA3YD~VHD`rLh5SY%{K001fFBe!qR3imjH zd1Z-@!J60ZjL3GCNCoomJ%9H6;CUJPM|+M=bJ=&-+0jMeE2gu1??pcHSAyvjw}6$H zr1<)v6YILXQiSr73DC|AUpTAt!HcR}%1$`o*&VX!ockNALr(X0wI&^>_=vkXtm-&d z(b^kf@|X4GC%22Vf%irvl7c}sH>UQ+JVpblE6!on*vHa{wXwl&yI)vWNcHws>4UqI z#i3Z9xT`ue)cTf5rY^Jq>)4t5zS{bBfxr2$%}VUuiM!F2g*&nC<2G$0H|clc599(s Z{KIK?hmdXUSI9mFT9I#{u-qpq^?!vxp@aYc diff --git a/Resources/Textures/Clothing/OuterClothing/Vests/elitevest.rsi/meta.json b/Resources/Textures/Clothing/OuterClothing/Vests/elitevest.rsi/meta.json index 84009efde3..1797234d0d 100644 --- a/Resources/Textures/Clothing/OuterClothing/Vests/elitevest.rsi/meta.json +++ b/Resources/Textures/Clothing/OuterClothing/Vests/elitevest.rsi/meta.json @@ -14,10 +14,6 @@ "name": "equipped-OUTERCLOTHING", "directions": 4 }, - { - "name": "equipped-OUTERCLOTHING-vox", - "directions": 4 - }, { "name": "inhand-left", "directions": 4 diff --git a/Resources/Textures/Clothing/OuterClothing/Vests/mercwebvest.rsi/equipped-OUTERCLOTHING.png b/Resources/Textures/Clothing/OuterClothing/Vests/mercwebvest.rsi/equipped-OUTERCLOTHING.png index b9273f213f241089aa01238b26b1e8413024a4c9..4f08ace7129dc4d842857cb68494ecab3b74d4e2 100644 GIT binary patch literal 9668 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 IRI`uxKl!_+jsO4v delta 988 zcmV<210(#zOYsMgBYy+9Nkl4e-7@Z4;7~v36xzpwn2)4FoBZ!4q1uSBl zN=p%cgr(T}57a7HS%{_ATG&V+iPas65jij-G10g3*15Ua-Ea1GmfhPK*v#yFGv9mj z=9}5e&bGck8>uFzr6vy^V-Es^2xzzhVF@%m0Kz61mVmD%aDV=B&zI{${W?X!`qr&e zTs8)2He35Vb$4FgIK^e_n`X1MXPuJ(=rkByQ%}bar%2!)^<|`f|C};Kz&+~oq_#^Z z401I3rh2^#Ga{hAED^xj6v1U2dRr#}*zw@ti<(S+@m6&@Xa1Hu@x8ACW*J!h{wzXL z{cOwWnp^_F#ecs+Pdh8?u1IY09)L}5IQ$%qaeMoxIzH|wJ%<7Iq)l>Oaik$HZAU)h z2DbqdNFv{LK(jVU4MMDe_GDXX4$LS2}Q3vH+!GF=uH35UO4D5D$61gBJh(Jyf-?`5P z0Em+!9G1EON=Rk3As(=CC__QWDXYJ37e@d(QDr&+I4%&uwYGMaCT}?~h{qEt9n9pY z+iYdA001=;l{f3lyti?%aZTbR769J{n2Cp_u;Ic76TTBLhzBfk&~a^Cb-P{`3t&@~ zA%D2LyB~irVLtwJuzAbyjgK?1@c|Qw9CY6{uDV?>iv`d-vCQxTDWuiaE=}HYVi1!Y z@&bwlKv*R`=13|pi9G$1Fu~Ew5_o?7Rg0?I2;-4;R9WyJU{MHi0EEs>3RUU!zW_F&&CQ$f&tDV7MR~KY^gfFN{J8LN z5`KNKB0!?jBDZOL1pEP?aDG`@8G{l40000< KMNUMnLSTYyp38aw diff --git a/Resources/Textures/Clothing/OuterClothing/Vests/mercwebvest.rsi/icon.png b/Resources/Textures/Clothing/OuterClothing/Vests/mercwebvest.rsi/icon.png index aac777b0f9583f08f18781a675ddfe16d34aec68..61811311f757e51405c7d763b8dc991e133d17b3 100644 GIT binary patch literal 5718 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 U5OtU2K>@>DS#C~+jDWcR0PY;qm;e9( delta 272 zcmV+r0q_3SEUE&KBYyz#Nkl1P5F`@~Y4$h6?85xFv$L?yWn<`5gI?V&fCaDs7C;IR#bFV} zVIkf78vuY(bJppqX0%RMg;H~ADFB92b7Rf~!T1Bhm@{o4M1KG;9ghdD*L#_J-U#s8 zlxhd?HvL_VXCR8hLhC&MNpd64y;=m&dXIDv8+X9>2e|HpEIToqO}|EoM6VzS#*JcU z0A0wDe94x2QQmKwKFW39f1hKkc8&x<)PYy=s(Ce+nuD+dU8wgNumBdo0$70F0iNL+ WU18C~^vnPN00{s|MNUMnLSTX+TXyIG diff --git a/Resources/Textures/Clothing/OuterClothing/Vests/mercwebvest.rsi/inhand-left.png b/Resources/Textures/Clothing/OuterClothing/Vests/mercwebvest.rsi/inhand-left.png index 8ecff94859b36d3e00423953fec042edad9573a3..0aca74f6b6d516c4c984b45a1531bda36501ca54 100644 GIT binary patch literal 6345 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 a$w94qGS`0ZQy-WGhy!K4O^KENj(-DTiuT_C delta 319 zcmV-F0l@yrG1&r;BYy!PNklP=qODQrs>j3 zQkpJp6fJC|fgPk%qo_x;JoHK-fUvi|Fo z91S4LHg#N<#X-J5shcl~L$CD#000000000002q$Xor5?IY#ay9_6KfOl7wfCCrQ|822{sG|Mdjkc*G%66VYIqK|}ItXE2|n@%4zK~y2s`(();yAOt5$=Lb?CLG4&LC!OlUJdmv z#l#D_IzM%`u^M|J?R7H;rQWkdd-W!tm-k9~i|wb7`Tj~(!T96l{YI97H$w^Y2!Z)q zs19s+3vb`-y!3~ht!E{}YlEc=n;PD4y+#ls-c|c#YFu9Go9i^w2cPvFdc{#2`ONW) z8mWAi72;8Wl{MT@y8$6@pU6^#&ZVSdR!=*hi_8>$n4GEh=hk&6gGR3QTy=kPL{goA z2t3*1^|C5tf^bo`jvN)lVh0XX1x-|hoKHx}oYD$uA^%1y9{y8aHeu9HYv^ zRh=_7rv)-bVjX(o)d$(o!O@ly@T-)Swrz#>JA!PKWyEd7)@S;U%aubDcvOC zY(O{#C4h)UcVtx}*-WA}dV zoCmGFn%u}av(e(3O0%17uE3$#y`Nq>oXWX!@wbCXRJc>|b|Q+lS3}J*8g3b<<+UI8 z>Wmk|eD6*to95iJPEO(nEmECq9~M7#I&A7;${rqX2=5Mj-9fhRv3*=Q+uH{X+O34| zXu1@O?_FoB`WN}i1&<59B_r}VMK3ze9XbMZb?N4m)Q{iwy_(_w%(qnDQ_{gGI5aFZ zG+R!>rUY4KJxqIaKx(ifY7-SA=R6C|6c-f4D^TsHQL9ZKAy(#@I( zBfkV4Rm)<1ZN`x0+lxCMcNL$unvp`~F6GXaS!+JDxJPwqQCVmx*qG(b^uBdeDYo;F z7?$|gN3zZh$HdT`&dKZc8Mk=Fh&C-c&6SDnxPIFzg?cgKNqBx!sh-;YjwUnD^OsNW zDuo`+)I9CjN;Oig5o*uxzShicAFL6t@EOa@;mR%SphkA6EjMKzH(!=NcH-{jyC;Lh z&*h4&@@-dQhU#Fdg=;j2DcuV5dFSuk`tV-yrpim1{31V%lEC`5w5Cb5jLH9Vxi>

L|--g~w{QDW?VTL9+8s5Z9VJuTu@Z_=59 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�}i@y~$y? i#g>$!QlMs+>y>Y}_OspIoGAsX03n+1H_O}Q7XB~c4`5#a delta 348 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 delta 1214 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 L7O)SwMXve}P!2Bv delta 349 zcmV-j0iyn!D)$1ABYy!tNklp_66jjAOW_uWTl{#%`G)iJ zxw=$PN@%Etw)UnItq z&2&0N2tm^{&c(s;2#hhedEfUx=J)$g2c$(lNGS=!ki+3X+qPUT7p%3EWr>s$DJ8BK z(WwJjmQhs|i^YPfsz{Oqtu@Q#lGEu#mSryK+yHSLqqPROjo+8m|F27`HkyZ~;6cI%cecyAvUICa)CImr1UDxb4yTAqD)PenG7YHFR#&ABLvDRX( v#TY}^b)ho|N0ntfCqSh2aFM4!zhBvE>zue00000NkvXXu0mjf95tmQ diff --git a/Resources/Textures/Clothing/OuterClothing/Vests/webvest.rsi/inhand-left.png b/Resources/Textures/Clothing/OuterClothing/Vests/webvest.rsi/inhand-left.png index bb043734cdca3653be7d9719eeb498103ab64f07..910c02b87f2d6678f4c44d7af774c583591858af 100644 GIT binary patch literal 6232 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 Date: Sun, 5 Apr 2026 21:59:11 +0000 Subject: [PATCH 093/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 9f703312dd..1f2eab8a5b 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: redmushie - changes: - - message: Fixed power sensors not respecting their configured network setting - type: Fix - id: 9108 - time: '2025-10-16T12:38:04.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40934 - author: Quasr changes: - message: Sky blue fancy tables and curtains are now construct-able @@ -4039,3 +4032,10 @@ id: 9619 time: '2026-04-05T15:17:21.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43466 +- author: Princess-Cheeseballs + changes: + - message: Web vests have new more visible sprites. + type: Add + id: 9620 + time: '2026-04-05T21:58:02.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43486 From 38f3eed150fe65e3c6b0f831c1f156998aadd1af Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com> Date: Sun, 5 Apr 2026 15:01:59 -0700 Subject: [PATCH 094/126] Uplink implants connect to the same store as the PDA, and also fix not giving stores to PDAs (#43485) * WHISKEY ECHO WHISKEY * add fallback behavior * add comment * buy from the proper store ent --------- Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> --- Content.IntegrationTests/Tests/StoreTests.cs | 14 ++-- Content.Server/PDA/PdaSystem.cs | 6 +- Content.Server/PDA/Ringer/RingerSystem.cs | 13 ++-- Content.Server/Store/Systems/StoreSystem.cs | 10 ++- Content.Server/Traitor/Uplink/UplinkSystem.cs | 73 +++++++++++-------- .../PDA/Ringer/RingerUplinkComponent.cs | 6 -- Content.Shared/PDA/SharedRingerSystem.cs | 12 ++- Content.Shared/Store/SharedStoreSystem.cs | 11 ++- .../Objects/Misc/subdermal_implants.yml | 4 +- 9 files changed, 86 insertions(+), 63 deletions(-) diff --git a/Content.IntegrationTests/Tests/StoreTests.cs b/Content.IntegrationTests/Tests/StoreTests.cs index 411f4762dd..5a63dfa563 100644 --- a/Content.IntegrationTests/Tests/StoreTests.cs +++ b/Content.IntegrationTests/Tests/StoreTests.cs @@ -1,6 +1,7 @@ using System.Linq; using Content.IntegrationTests.Fixtures; using Content.IntegrationTests.Fixtures.Attributes; +using Content.Server.PDA.Ringer; using Content.Server.Traitor.Uplink; using Content.Shared.FixedPoint; using Content.Shared.Inventory; @@ -22,7 +23,7 @@ public sealed class StoreTests : GameTest - type: entity name: InventoryPdaDummy id: InventoryPdaDummy - parent: [BasePDA, StorePresetUplink] + parent: BasePDA components: - type: Clothing QuickEquip: false @@ -68,6 +69,7 @@ public sealed class StoreTests : GameTest EntityUid pda = default; var uplinkSystem = entManager.System(); + var ringerSystem = entManager.System(); var listingPrototypes = prototypeManager.EnumeratePrototypes() .ToArray(); @@ -89,10 +91,10 @@ public sealed class StoreTests : GameTest mindSystem.TransferTo(mind, human, mind: mind); FixedPoint2 originalBalance = 20; - uplinkSystem.AddUplink(human, originalBalance, out var notes, pda, null, true); + uplinkSystem.AddUplink(human, originalBalance, out var notes, pda, true); - var remote = entManager.GetComponent(pda); - var storeEnt = remote.Store; + Assert.That(notes != null); + ringerSystem.TryMatchRingtoneToStore(notes, out var storeEnt); Assert.That(storeEnt.HasValue); var storeComponent = entManager.GetComponent(storeEnt.Value); var discountComponent = entManager.GetComponent(storeEnt.Value); @@ -142,7 +144,7 @@ public sealed class StoreTests : GameTest var buyMsg = new StoreBuyListingMessage(discountedListingItem.ID, null){Actor = human}; - server.EntMan.EventBus.RaiseLocalEvent(pda, buyMsg); + server.EntMan.EventBus.RaiseLocalEvent(storeEnt.Value, buyMsg); var newBalance = storeComponent.Balance[UplinkSystem.TelecrystalCurrencyPrototype]; Assert.That(newBalance.Value, Is.EqualTo((originalBalance - plainDiscountedCost).Value), "Expected to have balance reduced by discounted cost"); @@ -155,7 +157,7 @@ public sealed class StoreTests : GameTest Assert.That(costAfterBuy.Value, Is.EqualTo(prototypeCost.Value), "Expected cost after discount refund to be equal to prototype cost."); var refundMsg = new StoreRequestRefundMessage { Actor = human }; - server.EntMan.EventBus.RaiseLocalEvent(pda, refundMsg); + server.EntMan.EventBus.RaiseLocalEvent(storeEnt.Value, refundMsg); // get refreshed item after refund re-generated items discountedListingItem = storeComponent.FullListingsCatalog.First(x => x.ID == itemId); diff --git a/Content.Server/PDA/PdaSystem.cs b/Content.Server/PDA/PdaSystem.cs index afc5876760..724c9dc491 100644 --- a/Content.Server/PDA/PdaSystem.cs +++ b/Content.Server/PDA/PdaSystem.cs @@ -311,11 +311,11 @@ namespace Content.Server.PDA private bool TryGetUnlockedStore(EntityUid uid, [NotNullWhen(true)] out EntityUid? store) { store = null; - if (!TryComp(uid, out var uplink) || !uplink.Unlocked || uplink.TargetStore == null) + if (!TryComp(uid, out var uplink) || !uplink.Unlocked) return false; - store = uplink.TargetStore; - return true; + store = _store.GetStore(uid); + return store != null; } private void UpdateStationName(EntityUid uid, PdaComponent pda) diff --git a/Content.Server/PDA/Ringer/RingerSystem.cs b/Content.Server/PDA/Ringer/RingerSystem.cs index 06df92bb68..3efb6cbfdd 100644 --- a/Content.Server/PDA/Ringer/RingerSystem.cs +++ b/Content.Server/PDA/Ringer/RingerSystem.cs @@ -4,6 +4,7 @@ using Content.Server.Store.Systems; using Content.Shared.GameTicking; using Content.Shared.PDA; using Content.Shared.PDA.Ringer; +using Content.Shared.Store.Components; using Robust.Shared.Random; using Robust.Shared.Utility; @@ -95,18 +96,20 @@ public sealed class RingerSystem : SharedRingerSystem } /// - public override bool TryToggleUplink(EntityUid uid, Note[] ringtone, EntityUid? user = null) + public override bool TryToggleUplink(Entity entity, Note[] ringtone, EntityUid? user = null) { - if (!TryComp(uid, out var uplink)) + if (!Resolve(entity, ref entity.Comp)) return false; // On the server, we always check if the code matches - if (!TryMatchRingtoneToStore(ringtone, out var store, uid)) + if (!TryMatchRingtoneToStore(ringtone, out var store, entity)) return false; - uplink.TargetStore = store; + // If the store is not this entity, we make sure to properly set the remote store. + if (store != entity.Owner) + Store.SetRemoteStore(entity.Owner, store); - return ToggleUplinkInternal((uid, uplink)); + return ToggleUplinkInternal((entity, entity.Comp)); } ///

diff --git a/Content.Server/Store/Systems/StoreSystem.cs b/Content.Server/Store/Systems/StoreSystem.cs index 0fcabba30f..63c6d48b0b 100644 --- a/Content.Server/Store/Systems/StoreSystem.cs +++ b/Content.Server/Store/Systems/StoreSystem.cs @@ -33,9 +33,10 @@ public sealed partial class StoreSystem : SharedStoreSystem SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnShutdown); - SubscribeLocalEvent(OnImplantActivate); SubscribeLocalEvent(OnIntrinsicStoreAction); + SubscribeLocalEvent(OnImplantActivate); + InitializeUi(); InitializeCommand(); InitializeRefund(); @@ -110,9 +111,12 @@ public sealed partial class StoreSystem : SharedStoreSystem _popup.PopupEntity(msg, target, args.User); } - private void OnImplantActivate(EntityUid uid, StoreComponent component, OpenUplinkImplantEvent args) + private void OnImplantActivate(Entity entity, ref OpenUplinkImplantEvent args) { - ToggleUi(args.Performer, uid, component); + if (GetRemoteStore(entity.AsNullable()) is not { } store) + return; + + ToggleUi(args.Performer, store, store.Comp, entity, entity.Comp); } /// diff --git a/Content.Server/Traitor/Uplink/UplinkSystem.cs b/Content.Server/Traitor/Uplink/UplinkSystem.cs index a8c1ab3da5..c5c66e1a40 100644 --- a/Content.Server/Traitor/Uplink/UplinkSystem.cs +++ b/Content.Server/Traitor/Uplink/UplinkSystem.cs @@ -30,6 +30,33 @@ public sealed class UplinkSystem : EntitySystem private static readonly EntProtoId FallbackUplinkImplant = "UplinkImplant"; private static readonly ProtoId FallbackUplinkCatalog = "UplinkUplinkImplanter"; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnRemoteStoreImplanted); + } + + private void OnRemoteStoreImplanted(Entity entity, ref ImplantImplantedEvent args) + { + if (_mind.GetMind(args.Implanted) is not { } mind ) + return; + + var storeEnumerator = EntityQueryEnumerator(); + while (storeEnumerator.MoveNext(out var uid, out _, out var store)) + { + if (store.AccountOwner != mind) + continue; + + entity.Comp.Store = uid; + return; + } + + // If we didn't have an uplink, make an empty one. + SetUplink(args.Implanted, Spawn(TraitorUplinkStore, MapCoordinates.Nullspace), 0, false); + Log.Error($"{ToPrettyString(args.Implanted)} did not have an uplink when they were implanted."); + } + /// /// Adds an uplink to the target /// @@ -37,7 +64,6 @@ public sealed class UplinkSystem : EntitySystem /// The amount of currency on the uplink. If null, will just use the amount specified in the preset. /// The code which was generated, if any. /// The entity that will actually have the uplink functionality. Defaults to the PDA if null. - /// The entity that will have the store in it. /// Marker that enables discounts for uplink items. /// Binds the uplink to the specific uplink entity. /// Whether the uplink was added successfully to a PDA, implant or not at all. @@ -46,23 +72,24 @@ public sealed class UplinkSystem : EntitySystem FixedPoint2 balance, out Note[]? code, EntityUid? uplinkEntity = null, - EntityUid? storeEntity = null, bool giveDiscounts = false, bool bindToPda = false) { code = null; + var storeEntity = Spawn(TraitorUplinkStore, MapCoordinates.Nullspace); if (TryAddEntityUplink(user, balance, out var generatedCode, uplinkEntity, storeEntity, giveDiscounts, bindToPda)) { code = generatedCode; return AddUplinkResult.Pda; } - if (TryImplantUplink(user, balance, giveDiscounts)) + if (TryImplantUplink(user, storeEntity, balance, giveDiscounts)) { return AddUplinkResult.Implant; } + Del(storeEntity); return AddUplinkResult.Failure; } @@ -71,20 +98,18 @@ public sealed class UplinkSystem : EntitySystem FixedPoint2 balance, out Note[]? code, EntityUid? uplinkEntity, - EntityUid? storeEntity, + EntityUid storeEntity, bool giveDiscounts = false, bool bindToPda = false) { code = null; - - storeEntity ??= Spawn(TraitorUplinkStore, MapCoordinates.Nullspace); uplinkEntity ??= FindUplinkTarget(user); if (uplinkEntity == null) return false; var ev = new GenerateUplinkCodeEvent(); - RaiseLocalEvent(storeEntity.Value, ref ev); + RaiseLocalEvent(storeEntity, ref ev); if (ev.Code == null) { @@ -96,25 +121,15 @@ public sealed class UplinkSystem : EntitySystem if (bindToPda) { - var accessComp = EnsureComp(storeEntity.Value); - _ringer.SetBoundUplinkEntity((storeEntity.Value, accessComp), uplinkEntity.Value); + var accessComp = EnsureComp(storeEntity); + _ringer.SetBoundUplinkEntity((storeEntity, accessComp), uplinkEntity.Value); } - SetUplink(user, storeEntity.Value, uplinkEntity.Value, balance, giveDiscounts); + SetUplink(user, storeEntity, balance, giveDiscounts); return true; } - /// - /// Configure TC for the uplink - /// - private void SetUplink(EntityUid user, EntityUid store, EntityUid uplink, FixedPoint2 balance, bool giveDiscounts) - { - SetUplink(user, store, balance, giveDiscounts); - var remote = EnsureComp(uplink); - remote.Store = store; - } - /// /// Configure TC for the uplink /// @@ -144,9 +159,9 @@ public sealed class UplinkSystem : EntitySystem /// /// Implant an uplink as a fallback measure if the traitor had no PDA /// - public bool TryImplantUplink(EntityUid user, FixedPoint2 balance, bool giveDiscounts) + public bool TryImplantUplink(EntityUid user, EntityUid storeEntity, FixedPoint2 balance, bool giveDiscounts) { - if (!_proto.Resolve(FallbackUplinkCatalog, out var catalog)) + if (!_proto.Resolve(FallbackUplinkCatalog, out var catalog)) return false; if (!catalog.Cost.TryGetValue(TelecrystalCurrencyPrototype, out var cost)) @@ -157,15 +172,15 @@ public sealed class UplinkSystem : EntitySystem else balance = balance - cost; + SetUplink(user, storeEntity, balance, giveDiscounts); var implant = _subdermalImplant.AddImplant(user, FallbackUplinkImplant); - if (!HasComp(implant)) + if (!HasComp(implant)) { Log.Error($"Implant does not have the store component {implant}"); return false; } - SetUplink(user, implant.Value, balance, giveDiscounts); return true; } @@ -173,7 +188,7 @@ public sealed class UplinkSystem : EntitySystem /// Finds the entity that can hold an uplink for a user. /// Usually this is a pda in their pda slot, but can also be in their hands. (but not pockets or inside bag, etc.) /// - public Entity? FindUplinkTarget(EntityUid user) + public EntityUid? FindUplinkTarget(EntityUid user) { // Try to find PDA in inventory if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator)) @@ -182,16 +197,16 @@ public sealed class UplinkSystem : EntitySystem { var pdaUid = containerSlot.ContainedEntity; - if (HasComp(pdaUid) && TryComp(pdaUid, out var remote)) - return (pdaUid.Value, remote); + if (HasComp(pdaUid) && HasComp(pdaUid)) + return pdaUid.Value; } } // Also check hands foreach (var item in _handsSystem.EnumerateHeld(user)) { - if (HasComp(item) && TryComp(item, out var remote)) - return (item, remote); + if (HasComp(item) && HasComp(item)) + return item; } return null; diff --git a/Content.Shared/PDA/Ringer/RingerUplinkComponent.cs b/Content.Shared/PDA/Ringer/RingerUplinkComponent.cs index dd0de10493..327cd608b5 100644 --- a/Content.Shared/PDA/Ringer/RingerUplinkComponent.cs +++ b/Content.Shared/PDA/Ringer/RingerUplinkComponent.cs @@ -14,10 +14,4 @@ public sealed partial class RingerUplinkComponent : Component /// [DataField] public bool Unlocked; - - /// - /// The store which the ringer is targetting. - /// - [DataField] - public EntityUid? TargetStore; } diff --git a/Content.Shared/PDA/SharedRingerSystem.cs b/Content.Shared/PDA/SharedRingerSystem.cs index 3c1e392f13..ee0a7fb0bc 100644 --- a/Content.Shared/PDA/SharedRingerSystem.cs +++ b/Content.Shared/PDA/SharedRingerSystem.cs @@ -27,6 +27,7 @@ public abstract class SharedRingerSystem : EntitySystem [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedPdaSystem _pda = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] protected readonly SharedStoreSystem Store = default!; [Dependency] private readonly SharedTransformSystem _xform = default!; [Dependency] protected readonly SharedUserInterfaceSystem UI = default!; @@ -137,7 +138,6 @@ public abstract class SharedRingerSystem : EntitySystem return; ent.Comp.Unlocked = false; - ent.Comp.TargetStore = null; UI.CloseUi(ent.Owner, StoreUiKey.Key); } @@ -146,12 +146,12 @@ public abstract class SharedRingerSystem : EntitySystem /// On the client side, it does nothing since the client cannot know the code in advance. /// On the server side, the code is verified. ///
- /// The entity with the RingerUplinkComponent. + /// The entity with the RingerUplinkComponent. /// The ringtone to check against the uplink code. /// The entity attempting to toggle the uplink. /// True if the uplink state was toggled, false otherwise. [PublicAPI] - public virtual bool TryToggleUplink(EntityUid uid, Note[] ringtone, EntityUid? user = null) + public virtual bool TryToggleUplink(Entity entity, Note[] ringtone, EntityUid? user = null) { return false; } @@ -178,7 +178,7 @@ public abstract class SharedRingerSystem : EntitySystem return; // Try to toggle the uplink first - if (TryToggleUplink(ent, args.Ringtone)) + if (TryToggleUplink(ent.Owner, args.Ringtone)) return; // Don't save the uplink code as the ringtone UpdateRingerRingtone(ent, args.Ringtone); @@ -245,13 +245,11 @@ public abstract class SharedRingerSystem : EntitySystem ent.Comp.Unlocked = !ent.Comp.Unlocked; // Update PDA UI if needed - if (TryComp(ent, out var pda)) - _pda.UpdatePdaUi(ent, pda); + _pda.UpdatePdaUi(ent.Owner); // Close store UI if we're locking if (!ent.Comp.Unlocked) { - ent.Comp.TargetStore = null; UI.CloseUi(ent.Owner, StoreUiKey.Key); } diff --git a/Content.Shared/Store/SharedStoreSystem.cs b/Content.Shared/Store/SharedStoreSystem.cs index 0abd12742e..c281685077 100644 --- a/Content.Shared/Store/SharedStoreSystem.cs +++ b/Content.Shared/Store/SharedStoreSystem.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Content.Shared.Implants; using Content.Shared.Store.Components; namespace Content.Shared.Store; @@ -49,11 +50,19 @@ public abstract partial class SharedStoreSystem : EntitySystem /// The store entity and component if found. public Entity? GetRemoteStore(Entity entity) { - if (RemoteStoreQuery.Resolve(entity, ref entity.Comp, false) + if (RemoteStoreQuery.Resolve(entity, ref entity.Comp) && entity.Comp.Store != null && StoreQuery.TryComp(entity.Comp.Store, out var storeComp)) return (entity.Comp.Store.Value, storeComp); return null; } + + public void SetRemoteStore(Entity entity, EntityUid? store) + { + if (!RemoteStoreQuery.Resolve(entity, ref entity.Comp)) + return; + + entity.Comp.Store = store; + } } diff --git a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml index 8e29fdd88f..8232a82a2b 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml @@ -169,9 +169,7 @@ whitelist: components: - Hands # prevent mouse buying grenade penguin since its not telepathic - - type: Store - balance: - Telecrystal: 0 + - type: RemoteStore - type: UserInterface interfaces: enum.StoreUiKey.Key: From 80257bc9821cddb92e9cfd3b3e46bf3befe79c10 Mon Sep 17 00:00:00 2001 From: PJBot Date: Sun, 5 Apr 2026 22:17:53 +0000 Subject: [PATCH 095/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 1f2eab8a5b..064fdc8e90 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,13 +1,4 @@ Entries: -- author: Quasr - changes: - - message: Sky blue fancy tables and curtains are now construct-able - type: Add - - message: Sky blue carpet is now printable - type: Fix - id: 9109 - time: '2025-10-16T13:37:25.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40867 - author: Worldwaker changes: - message: Fixed parcels being indestructible for all damage types except Slash @@ -4039,3 +4030,10 @@ id: 9620 time: '2026-04-05T21:58:02.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43486 +- author: Princess-Cheeseballs + changes: + - message: Uplink implant now links to the same store as your PDA ringtone. + type: Add + id: 9621 + time: '2026-04-05T22:16:46.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43485 From 54e56cd1fc9c3a367b5eeb76e8defeae8a03c9de Mon Sep 17 00:00:00 2001 From: AndrewFenriz <78079974+AndrewFenriz@users.noreply.github.com> Date: Mon, 6 Apr 2026 01:15:52 +0300 Subject: [PATCH 096/126] Expanded Tile Recipes for Cutter Machine (#43431) * recipes for tiles * lil fix --- .../Locale/en-US/lathe/lathe-categories.ftl | 4 + .../Entities/Structures/Machines/lathe.yml | 5 + .../Prototypes/Recipes/Lathes/Packs/tiles.yml | 50 +++++ .../Prototypes/Recipes/Lathes/categories.yml | 16 ++ Resources/Prototypes/Recipes/Lathes/tiles.yml | 207 ++++++++++++++++++ .../Prototypes/Research/civilianservices.yml | 1 + 6 files changed, 283 insertions(+) diff --git a/Resources/Locale/en-US/lathe/lathe-categories.ftl b/Resources/Locale/en-US/lathe/lathe-categories.ftl index 209daf1ad3..54841b2101 100644 --- a/Resources/Locale/en-US/lathe/lathe-categories.ftl +++ b/Resources/Locale/en-US/lathe/lathe-categories.ftl @@ -31,8 +31,12 @@ lathe-category-faux-tile = Faux lathe-category-maints-tile = Maints lathe-category-marble = Marble lathe-category-steel-tile = Steel +lathe-category-shuttle-tile = Shuttle lathe-category-white-tile = White lathe-category-wood-tile = Wood +lathe-category-plastic-tile = Plastic +lathe-category-precious-tile = Precious +lathe-category-industrial-tile = Industrial # Science lathe-category-mechs = Mechs diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 31fef56da6..b740396326 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -690,10 +690,15 @@ - FloorSteelTilesStatic - FloorWhiteTilesStatic - FloorMaintsTilesStatic + - FloorIndustrialTilesStatic - FloorWoodTilesStatic - FloorConcreteTilesStatic - CircuitFloorsStatic - FloorMarbleTilesStatic + - FloorShuttleTilesStatic + - PlasticTilesStatic + - CarpetTilesStatic + - PreciousTilesStatic dynamicPacks: - FauxTiles - type: MaterialStorage diff --git a/Resources/Prototypes/Recipes/Lathes/Packs/tiles.yml b/Resources/Prototypes/Recipes/Lathes/Packs/tiles.yml index 2df2891b7c..f9147efe4f 100644 --- a/Resources/Prototypes/Recipes/Lathes/Packs/tiles.yml +++ b/Resources/Prototypes/Recipes/Lathes/Packs/tiles.yml @@ -15,6 +15,7 @@ - FloorTileItemDarkPavement - FloorTileItemDarkPavementVertical - FloorTileItemDarkOffset + - FloorTileItemDarkSquiggly - type: latheRecipePack id: FloorSteelTilesStatic @@ -58,6 +59,18 @@ - FloorTileItemSteelMaint - FloorTileItemTechmaintDark +- type: latheRecipePack + id: FloorIndustrialTilesStatic + recipes: + - FloorTileItemMining + - FloorTileItemMiningDark + - FloorTileItemMiningLight + - FloorTileItemElevatorShaft + - FloorTileItemRockVault + - FloorTileItemMetalDiamond + - FloorTileItemFreezer + - FloorTileItemShowroom + - type: latheRecipePack id: FloorWoodTilesStatic recipes: @@ -93,6 +106,42 @@ recipes: - FloorTileItemWhiteMarble - FloorTileItemDarkMarble + - FloorTileItemUraniumMarble + - FloorTileItemPlasmaMarble + +- type: latheRecipePack + id: FloorShuttleTilesStatic + recipes: + - FloorTileItemShuttleWhite + - FloorTileItemShuttleBlue + - FloorTileItemShuttleOrange + - FloorTileItemShuttlePurple + - FloorTileItemShuttleRed + - FloorTileItemShuttleGrey + - FloorTileItemShuttleBlack + +- type: latheRecipePack + id: PlasticTilesStatic + recipes: + - FloorTileItemLino + - FloorTileItemBoxing + - FloorTileItemGym + +- type: latheRecipePack + id: CarpetTilesStatic + recipes: + - FloorTileItemArcadeBlue + - FloorTileItemArcadeBlue2 + - FloorTileItemArcadeRed + - FloorTileItemEighties + - FloorTileItemCarpetClown + - FloorTileItemCarpetOffice + +- type: latheRecipePack + id: PreciousTilesStatic + recipes: + - FloorTileItemGold + - FloorTileItemSilver ## Dynamic @@ -107,6 +156,7 @@ - FauxTileAstroIce - FauxTileAstroSnow - FauxTileAstroAsteroidSand + - FauxTileAstroAsteroidSandBorderless - FauxTileDesertAstroSand - FauxTileAstroIronsand - FauxTileAstroIronsandBorderless diff --git a/Resources/Prototypes/Recipes/Lathes/categories.yml b/Resources/Prototypes/Recipes/Lathes/categories.yml index fd25394e67..99c1ac30e5 100644 --- a/Resources/Prototypes/Recipes/Lathes/categories.yml +++ b/Resources/Prototypes/Recipes/Lathes/categories.yml @@ -114,6 +114,22 @@ id: Marble name: lathe-category-marble +- type: latheCategory + id: Shuttle + name: lathe-category-shuttle-tile + +- type: latheCategory + id: Plastic + name: lathe-category-plastic-tile + +- type: latheCategory + id: Precious + name: lathe-category-precious-tile + +- type: latheCategory + id: Industrial + name: lathe-category-industrial-tile + # Science - type: latheCategory id: Mech diff --git a/Resources/Prototypes/Recipes/Lathes/tiles.yml b/Resources/Prototypes/Recipes/Lathes/tiles.yml index e30174a6ae..29aa64cac5 100644 --- a/Resources/Prototypes/Recipes/Lathes/tiles.yml +++ b/Resources/Prototypes/Recipes/Lathes/tiles.yml @@ -42,6 +42,16 @@ - Tiles - Maints +- type: latheRecipe + abstract: true + parent: BaseSteelTileRecipe + id: BaseIndustrialTileRecipe + categories: + - Tiles + - Industrial + materials: + Steel: 50 + - type: latheRecipe abstract: true parent: BaseSteelTileRecipe @@ -92,6 +102,46 @@ Steel: 25 Glass: 25 +- type: latheRecipe + abstract: true + parent: BaseTileRecipe + id: BaseShuttleTileRecipe + categories: + - Tiles + - Shuttle + completetime: 1 + materials: + Plasteel: 25 + +- type: latheRecipe + abstract: true + parent: BaseTileRecipe + id: BasePlasticTileRecipe + categories: + - Tiles + - Plastic + materials: + Plastic: 25 + +- type: latheRecipe + abstract: true + parent: BaseTileRecipe + id: BaseCarpetTileRecipe + categories: + - Tiles + - Carpets + materials: + Cloth: 25 + +- type: latheRecipe + abstract: true + parent: BaseTileRecipe + id: BasePreciousTileRecipe + applyMaterialDiscount: false + categories: + - Tiles + - Precious + ## Recipes # Steel tiles @@ -313,6 +363,47 @@ id: FloorTileItemTechmaintDark result: FloorTileItemTechmaintDark +# Industrial +- type: latheRecipe + parent: BaseIndustrialTileRecipe + id: FloorTileItemMining + result: FloorTileItemMining + +- type: latheRecipe + parent: BaseIndustrialTileRecipe + id: FloorTileItemMiningDark + result: FloorTileItemMiningDark + +- type: latheRecipe + parent: BaseIndustrialTileRecipe + id: FloorTileItemMiningLight + result: FloorTileItemMiningLight + +- type: latheRecipe + parent: BaseIndustrialTileRecipe + id: FloorTileItemElevatorShaft + result: FloorTileItemElevatorShaft + +- type: latheRecipe + parent: BaseIndustrialTileRecipe + id: FloorTileItemRockVault + result: FloorTileItemRockVault + +- type: latheRecipe + parent: BaseIndustrialTileRecipe + id: FloorTileItemMetalDiamond + result: FloorTileItemMetalDiamond + +- type: latheRecipe + parent: BaseIndustrialTileRecipe + id: FloorTileItemShowroom + result: FloorTileItemShowroom + +- type: latheRecipe + parent: BaseIndustrialTileRecipe + id: FloorTileItemFreezer + result: FloorTileItemFreezer + # Circuit - type: latheRecipe parent: BaseCircuitTileRecipe @@ -477,3 +568,119 @@ parent: BaseMarbleTileRecipe id: FloorTileItemDarkMarble result: FloorTileItemDarkMarble + +- type: latheRecipe + parent: BaseMarbleTileRecipe + id: FloorTileItemUraniumMarble + result: FloorTileItemUraniumMarble + materials: + Steel: 25 + Glass: 25 + Uranium: 25 + +- type: latheRecipe + parent: BaseMarbleTileRecipe + id: FloorTileItemPlasmaMarble + result: FloorTileItemPlasmaMarble + materials: + Steel: 25 + Glass: 25 + Plasma: 25 + +# Shuttle +- type: latheRecipe + parent: BaseShuttleTileRecipe + id: FloorTileItemShuttleWhite + result: FloorTileItemShuttleWhite + +- type: latheRecipe + parent: BaseShuttleTileRecipe + id: FloorTileItemShuttleBlue + result: FloorTileItemShuttleBlue + +- type: latheRecipe + parent: BaseShuttleTileRecipe + id: FloorTileItemShuttleOrange + result: FloorTileItemShuttleOrange + +- type: latheRecipe + parent: BaseShuttleTileRecipe + id: FloorTileItemShuttlePurple + result: FloorTileItemShuttlePurple + +- type: latheRecipe + parent: BaseShuttleTileRecipe + id: FloorTileItemShuttleRed + result: FloorTileItemShuttleRed + +- type: latheRecipe + parent: BaseShuttleTileRecipe + id: FloorTileItemShuttleGrey + result: FloorTileItemShuttleGrey + +- type: latheRecipe + parent: BaseShuttleTileRecipe + id: FloorTileItemShuttleBlack + result: FloorTileItemShuttleBlack + +# Precious +- type: latheRecipe + parent: BasePreciousTileRecipe + id: FloorTileItemGold + result: FloorTileItemGold + materials: + Gold: 50 + +- type: latheRecipe + parent: BasePreciousTileRecipe + id: FloorTileItemSilver + result: FloorTileItemSilver + materials: + Silver: 50 + +# Carpets +- type: latheRecipe + parent: BaseCarpetTileRecipe + id: FloorTileItemArcadeBlue + result: FloorTileItemArcadeBlue + +- type: latheRecipe + parent: BaseCarpetTileRecipe + id: FloorTileItemArcadeBlue2 + result: FloorTileItemArcadeBlue2 + +- type: latheRecipe + parent: BaseCarpetTileRecipe + id: FloorTileItemArcadeRed + result: FloorTileItemArcadeRed + +- type: latheRecipe + parent: BaseCarpetTileRecipe + id: FloorTileItemEighties + result: FloorTileItemEighties + +- type: latheRecipe + parent: BaseCarpetTileRecipe + id: FloorTileItemCarpetClown + result: FloorTileItemCarpetClown + +- type: latheRecipe + parent: BaseCarpetTileRecipe + id: FloorTileItemCarpetOffice + result: FloorTileItemCarpetOffice + +# Plastic +- type: latheRecipe + parent: BasePlasticTileRecipe + id: FloorTileItemLino + result: FloorTileItemLino + +- type: latheRecipe + parent: BasePlasticTileRecipe + id: FloorTileItemBoxing + result: FloorTileItemBoxing + +- type: latheRecipe + parent: BasePlasticTileRecipe + id: FloorTileItemGym + result: FloorTileItemGym diff --git a/Resources/Prototypes/Research/civilianservices.yml b/Resources/Prototypes/Research/civilianservices.yml index 622df1ba5e..b90649b438 100644 --- a/Resources/Prototypes/Research/civilianservices.yml +++ b/Resources/Prototypes/Research/civilianservices.yml @@ -103,6 +103,7 @@ - FauxTileAstroIce - FauxTileAstroSnow - FauxTileAstroAsteroidSand + - FauxTileAstroAsteroidSandBorderless - FauxTileDesertAstroSand - FauxTileAstroIronsand - FauxTileAstroIronsandBorderless From 0aa13a7dc2bec4f793e9368798f530c0323b13fa Mon Sep 17 00:00:00 2001 From: PJBot Date: Sun, 5 Apr 2026 22:34:09 +0000 Subject: [PATCH 097/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 064fdc8e90..f70eb16b2f 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: Worldwaker - changes: - - message: Fixed parcels being indestructible for all damage types except Slash - damage. - type: Fix - id: 9110 - time: '2025-10-16T16:28:48.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40940 - author: FairlySadPanda changes: - message: Two new instrument options for the microphone, Wa and Wah. @@ -4037,3 +4029,10 @@ id: 9621 time: '2026-04-05T22:16:46.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43485 +- author: AndrewFenriz + changes: + - message: Added a wide variety of new tile recipes to the Cutter Machine. + type: Add + id: 9622 + time: '2026-04-05T22:33:00.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43431 From a5e9f24f5bf700c371262d8750abd716b3d64d52 Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Mon, 6 Apr 2026 19:30:41 +0200 Subject: [PATCH 098/126] Use dependencies for EntityQueries in Systems (Part 1: Client) (#43478) * cleanup * vsc lied to me * private readonly * use proxies --- Content.Client/Audio/AmbientSoundSystem.cs | 8 +- .../Audio/AmbientSoundTreeSystem.cs | 3 +- .../CardboardBox/CardboardBoxSystem.cs | 13 +-- Content.Client/Clickable/ClickableSystem.cs | 14 +-- Content.Client/DoAfter/DoAfterSystem.cs | 4 +- .../IconSmoothing/IconSmoothSystem.cs | 86 +++++++++---------- Content.Client/Interaction/DragDropSystem.cs | 5 +- .../Systems/ClientSpriteMovementSystem.cs | 5 +- .../Movement/Systems/FloorOcclusionSystem.cs | 4 +- Content.Client/Outline/TargetOutlineSystem.cs | 4 +- .../Systems/ChameleonProjectorSystem.cs | 8 +- .../ReplaySpectatorSystem.Movement.cs | 3 +- .../Silicons/Borgs/BorgSystem.Battery.cs | 5 -- Content.Client/Silicons/Borgs/BorgSystem.cs | 3 + Content.Client/Sprite/SpriteFadeSystem.cs | 14 +-- .../Visualizers/StickyVisualizerSystem.cs | 4 +- Content.Client/SubFloor/TrayScannerSystem.cs | 15 ++-- .../UserInterface/BuiPreTickUpdateSystem.cs | 10 +-- Content.Client/Verbs/VerbSystem.cs | 4 +- .../Melee/MeleeWeaponSystem.Effects.cs | 4 +- .../Weapons/Melee/MeleeWeaponSystem.cs | 6 +- Content.Client/Weather/WeatherSystem.cs | 10 +-- .../DoAfter/SharedDoAfterSystem.Update.cs | 26 +++--- Content.Shared/DoAfter/SharedDoAfterSystem.cs | 2 +- 24 files changed, 96 insertions(+), 164 deletions(-) diff --git a/Content.Client/Audio/AmbientSoundSystem.cs b/Content.Client/Audio/AmbientSoundSystem.cs index 9929751b22..92bfc677eb 100644 --- a/Content.Client/Audio/AmbientSoundSystem.cs +++ b/Content.Client/Audio/AmbientSoundSystem.cs @@ -235,8 +235,6 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem ///
private void ProcessNearbyAmbience(TransformComponent playerXform) { - var query = GetEntityQuery(); - var metaQuery = GetEntityQuery(); var mapPos = _xformSystem.GetMapCoordinates(playerXform); // Remove out-of-range ambiences @@ -249,9 +247,9 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem if (comp.Enabled && // Don't keep playing sounds that have changed since. sound.Sound == comp.Sound && - query.TryGetComponent(owner, out var xform) && + TryComp(owner, out TransformComponent? xform) && xform.MapID == playerXform.MapID && - !metaQuery.GetComponent(owner).EntityPaused) + !Paused(owner)) { // TODO: This is just trydistance for coordinates. var distance = (xform.ParentUid == playerXform.ParentUid) @@ -294,7 +292,7 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem var comp = sourceEntity.Comp; if (_playingSounds.ContainsKey(sourceEntity) || - metaQuery.GetComponent(uid).EntityPaused) + Paused(uid)) continue; var audioParams = _params diff --git a/Content.Client/Audio/AmbientSoundTreeSystem.cs b/Content.Client/Audio/AmbientSoundTreeSystem.cs index 7a9439c9df..2844374452 100644 --- a/Content.Client/Audio/AmbientSoundTreeSystem.cs +++ b/Content.Client/Audio/AmbientSoundTreeSystem.cs @@ -23,8 +23,7 @@ public sealed class AmbientSoundTreeSystem : ComponentTreeSystem()); + entry.Component.TreeUid.Value); return ExtractAabb(in entry, pos, default); } diff --git a/Content.Client/CardboardBox/CardboardBoxSystem.cs b/Content.Client/CardboardBox/CardboardBoxSystem.cs index ecebe16727..a179f14a3b 100644 --- a/Content.Client/CardboardBox/CardboardBoxSystem.cs +++ b/Content.Client/CardboardBox/CardboardBoxSystem.cs @@ -14,15 +14,12 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem [Dependency] private readonly TransformSystem _transform = default!; [Dependency] private readonly ExamineSystemShared _examine = default!; [Dependency] private readonly SpriteSystem _sprite = default!; - - private EntityQuery _mobStateQuery; + [Dependency] private readonly EntityQuery _mobStateQuery = default!; public override void Initialize() { base.Initialize(); - _mobStateQuery = GetEntityQuery(); - SubscribeNetworkEvent(OnBoxEffect); } @@ -33,11 +30,7 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem if (!TryComp(source, out var box)) return; - var xformQuery = GetEntityQuery(); - - if (!xformQuery.TryGetComponent(source, out var xform)) - return; - + var xform = Transform(source); var sourcePos = _transform.GetMapCoordinates(source, xform); //Any mob that can move should be surprised? @@ -72,7 +65,7 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem var ent = Spawn(box.Effect, mapPos); - if (!xformQuery.TryGetComponent(ent, out var entTransform) || !TryComp(ent, out var sprite)) + if (!TryComp(ent, out TransformComponent? entTransform) || !TryComp(ent, out var sprite)) continue; _sprite.SetOffset((ent, sprite), new Vector2(0, 1)); diff --git a/Content.Client/Clickable/ClickableSystem.cs b/Content.Client/Clickable/ClickableSystem.cs index 8834f023f4..ae3487dc87 100644 --- a/Content.Client/Clickable/ClickableSystem.cs +++ b/Content.Client/Clickable/ClickableSystem.cs @@ -16,17 +16,9 @@ public sealed class ClickableSystem : EntitySystem [Dependency] private readonly SharedTransformSystem _transforms = default!; [Dependency] private readonly SpriteSystem _sprites = default!; - private EntityQuery _clickableQuery; - private EntityQuery _xformQuery; - private EntityQuery _fadingSpriteQuery; - - public override void Initialize() - { - base.Initialize(); - _clickableQuery = GetEntityQuery(); - _xformQuery = GetEntityQuery(); - _fadingSpriteQuery = GetEntityQuery(); - } + [Dependency] private readonly EntityQuery _clickableQuery = default!; + [Dependency] private readonly EntityQuery _xformQuery = default!; + [Dependency] private readonly EntityQuery _fadingSpriteQuery = default!; /// /// Used to check whether a click worked. Will first check if the click falls inside of some explicit bounding diff --git a/Content.Client/DoAfter/DoAfterSystem.cs b/Content.Client/DoAfter/DoAfterSystem.cs index 5802059537..407cf3e7c1 100644 --- a/Content.Client/DoAfter/DoAfterSystem.cs +++ b/Content.Client/DoAfter/DoAfterSystem.cs @@ -50,9 +50,7 @@ public sealed class DoAfterSystem : SharedDoAfterSystem var time = GameTiming.CurTime; var comp = Comp(playerEntity.Value); - var xformQuery = GetEntityQuery(); - var handsQuery = GetEntityQuery(); - Update(playerEntity.Value, active, comp, time, xformQuery, handsQuery); + Update(playerEntity.Value, active, comp, time); } /// diff --git a/Content.Client/IconSmoothing/IconSmoothSystem.cs b/Content.Client/IconSmoothing/IconSmoothSystem.cs index a1c47ca6be..4b8ec7be79 100644 --- a/Content.Client/IconSmoothing/IconSmoothSystem.cs +++ b/Content.Client/IconSmoothing/IconSmoothSystem.cs @@ -18,6 +18,8 @@ namespace Content.Client.IconSmoothing { [Dependency] private readonly SharedMapSystem _mapSystem = default!; [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly EntityQuery _iconSmoothQuery = default!; + [Dependency] private readonly EntityQuery _spriteQuery = default!; private readonly Queue _dirtyEntities = new(); private readonly Queue _anchorChangedEntities = new(); @@ -106,13 +108,10 @@ namespace Content.Client.IconSmoothing { base.FrameUpdate(frameTime); - var xformQuery = GetEntityQuery(); - var smoothQuery = GetEntityQuery(); - // first process anchor state changes. while (_anchorChangedEntities.TryDequeue(out var uid)) { - if (!xformQuery.TryGetComponent(uid, out var xform)) + if (!TryComp(uid, out TransformComponent? xform)) continue; if (xform.MapID == MapId.Nullspace) @@ -123,7 +122,7 @@ namespace Content.Client.IconSmoothing continue; } - DirtyNeighbours(uid, comp: null, xform, smoothQuery); + DirtyNeighbours(uid, comp: null, xform); } // Next, update actual sprites. @@ -132,19 +131,16 @@ namespace Content.Client.IconSmoothing _generation += 1; - var spriteQuery = GetEntityQuery(); - // Performance: This could be spread over multiple updates, or made parallel. while (_dirtyEntities.TryDequeue(out var uid)) { - CalculateNewSprite(uid, spriteQuery, smoothQuery, xformQuery); + CalculateNewSprite(uid); } } - public void DirtyNeighbours(EntityUid uid, IconSmoothComponent? comp = null, TransformComponent? transform = null, EntityQuery? smoothQuery = null) + public void DirtyNeighbours(EntityUid uid, IconSmoothComponent? comp = null, TransformComponent? transform = null) { - smoothQuery ??= GetEntityQuery(); - if (!smoothQuery.Value.Resolve(uid, ref comp) || !comp.Running) + if (!_iconSmoothQuery.Resolve(uid, ref comp) || !comp.Running) return; _dirtyEntities.Enqueue(uid); @@ -206,11 +202,7 @@ namespace Content.Client.IconSmoothing _anchorChangedEntities.Enqueue(uid); } - private void CalculateNewSprite(EntityUid uid, - EntityQuery spriteQuery, - EntityQuery smoothQuery, - EntityQuery xformQuery, - IconSmoothComponent? smooth = null) + private void CalculateNewSprite(EntityUid uid, IconSmoothComponent? smooth = null) { TransformComponent? xform; Entity? gridEntity = null; @@ -218,7 +210,7 @@ namespace Content.Client.IconSmoothing // The generation check prevents updating an entity multiple times per tick. // As it stands now, it's totally possible for something to get queued twice. // Generation on the component is set after an update so we can cull updates that happened this generation. - if (!smoothQuery.Resolve(uid, ref smooth, false) + if (!_iconSmoothQuery.Resolve(uid, ref smooth, false) || smooth.Mode == IconSmoothingMode.NoSprite || smooth.UpdateGeneration == _generation || !smooth.Enabled @@ -226,7 +218,7 @@ namespace Content.Client.IconSmoothing { if (smooth is { Enabled: true } && TryComp(uid, out var edge) && - xformQuery.TryGetComponent(uid, out xform)) + TryComp(uid, out xform)) { var directions = DirectionFlag.None; @@ -237,13 +229,13 @@ namespace Content.Client.IconSmoothing gridEntity = (gridUid, grid); - if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.North)), smoothQuery)) + if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.North)))) directions |= DirectionFlag.North; - if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.South)), smoothQuery)) + if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.South)))) directions |= DirectionFlag.South; - if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.East)), smoothQuery)) + if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.East)))) directions |= DirectionFlag.East; - if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.West)), smoothQuery)) + if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.West)))) directions |= DirectionFlag.West; } @@ -253,10 +245,10 @@ namespace Content.Client.IconSmoothing return; } - xform = xformQuery.GetComponent(uid); + xform = Transform(uid); smooth.UpdateGeneration = _generation; - if (!spriteQuery.TryGetComponent(uid, out var sprite)) + if (!_spriteQuery.TryGetComponent(uid, out var sprite)) { Log.Error($"Encountered a icon-smoothing entity without a sprite: {ToPrettyString(uid)}"); RemCompDeferred(uid, smooth); @@ -281,13 +273,13 @@ namespace Content.Client.IconSmoothing switch (smooth.Mode) { case IconSmoothingMode.Corners: - CalculateNewSpriteCorners(gridEntity, smooth, spriteEnt, xform, smoothQuery); + CalculateNewSpriteCorners(gridEntity, smooth, spriteEnt, xform); break; case IconSmoothingMode.CardinalFlags: - CalculateNewSpriteCardinal(gridEntity, smooth, spriteEnt, xform, smoothQuery); + CalculateNewSpriteCardinal(gridEntity, smooth, spriteEnt, xform); break; case IconSmoothingMode.Diagonal: - CalculateNewSpriteDiagonal(gridEntity, smooth, spriteEnt, xform, smoothQuery); + CalculateNewSpriteDiagonal(gridEntity, smooth, spriteEnt, xform); break; default: throw new ArgumentOutOfRangeException(); @@ -295,7 +287,7 @@ namespace Content.Client.IconSmoothing } private void CalculateNewSpriteDiagonal(Entity? gridEntity, IconSmoothComponent smooth, - Entity sprite, TransformComponent xform, EntityQuery smoothQuery) + Entity sprite, TransformComponent xform) { if (gridEntity == null) { @@ -320,7 +312,7 @@ namespace Content.Client.IconSmoothing for (var i = 0; i < neighbors.Length; i++) { var neighbor = (Vector2i)rotation.RotateVec(neighbors[i]); - matching = matching && MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos + neighbor), smoothQuery); + matching = matching && MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos + neighbor)); } if (matching) @@ -333,7 +325,7 @@ namespace Content.Client.IconSmoothing } } - private void CalculateNewSpriteCardinal(Entity? gridEntity, IconSmoothComponent smooth, Entity sprite, TransformComponent xform, EntityQuery smoothQuery) + private void CalculateNewSpriteCardinal(Entity? gridEntity, IconSmoothComponent smooth, Entity sprite, TransformComponent xform) { var dirs = CardinalConnectDirs.None; @@ -347,13 +339,13 @@ namespace Content.Client.IconSmoothing var grid = gridEntity.Value.Comp; var pos = _mapSystem.TileIndicesFor(gridUid, grid, xform.Coordinates); - if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.North)), smoothQuery)) + if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.North)))) dirs |= CardinalConnectDirs.North; - if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.South)), smoothQuery)) + if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.South)))) dirs |= CardinalConnectDirs.South; - if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.East)), smoothQuery)) + if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.East)))) dirs |= CardinalConnectDirs.East; - if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.West)), smoothQuery)) + if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.West)))) dirs |= CardinalConnectDirs.West; _sprite.LayerSetRsiState(sprite.AsNullable(), 0, $"{smooth.StateBase}{(int)dirs}"); @@ -372,11 +364,11 @@ namespace Content.Client.IconSmoothing CalculateEdge(sprite, directions, sprite); } - private bool MatchingEntity(IconSmoothComponent smooth, AnchoredEntitiesEnumerator candidates, EntityQuery smoothQuery) + private bool MatchingEntity(IconSmoothComponent smooth, AnchoredEntitiesEnumerator candidates) { while (candidates.MoveNext(out var entity)) { - if (smoothQuery.TryGetComponent(entity, out var other) && + if (_iconSmoothQuery.TryGetComponent(entity, out var other) && other.SmoothKey != null && (other.SmoothKey == smooth.SmoothKey || smooth.AdditionalKeys.Contains(other.SmoothKey)) && other.Enabled) @@ -388,11 +380,11 @@ namespace Content.Client.IconSmoothing return false; } - private void CalculateNewSpriteCorners(Entity? gridEntity, IconSmoothComponent smooth, Entity spriteEnt, TransformComponent xform, EntityQuery smoothQuery) + private void CalculateNewSpriteCorners(Entity? gridEntity, IconSmoothComponent smooth, Entity spriteEnt, TransformComponent xform) { var (cornerNE, cornerNW, cornerSW, cornerSE) = gridEntity == null ? (CornerFill.None, CornerFill.None, CornerFill.None, CornerFill.None) - : CalculateCornerFill(gridEntity.Value, smooth, xform, smoothQuery); + : CalculateCornerFill(gridEntity.Value, smooth, xform); // TODO figure out a better way to set multiple sprite layers. // This will currently re-calculate the sprite bounding box 4 times. @@ -422,20 +414,20 @@ namespace Content.Client.IconSmoothing CalculateEdge(spriteEnt, directions, sprite); } - private (CornerFill ne, CornerFill nw, CornerFill sw, CornerFill se) CalculateCornerFill(Entity gridEntity, IconSmoothComponent smooth, TransformComponent xform, EntityQuery smoothQuery) + private (CornerFill ne, CornerFill nw, CornerFill sw, CornerFill se) CalculateCornerFill(Entity gridEntity, IconSmoothComponent smooth, TransformComponent xform) { var gridUid = gridEntity.Owner; var grid = gridEntity.Comp; var pos = _mapSystem.TileIndicesFor(gridUid, grid, xform.Coordinates); - var n = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.North)), smoothQuery); - var ne = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.NorthEast)), smoothQuery); - var e = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.East)), smoothQuery); - var se = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.SouthEast)), smoothQuery); - var s = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.South)), smoothQuery); - var sw = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.SouthWest)), smoothQuery); - var w = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.West)), smoothQuery); - var nw = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.NorthWest)), smoothQuery); + var n = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.North))); + var ne = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.NorthEast))); + var e = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.East))); + var se = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.SouthEast))); + var s = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.South))); + var sw = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.SouthWest))); + var w = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.West))); + var nw = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.NorthWest))); // ReSharper disable InconsistentNaming var cornerNE = CornerFill.None; diff --git a/Content.Client/Interaction/DragDropSystem.cs b/Content.Client/Interaction/DragDropSystem.cs index a1af9f9e20..7ff8dd150f 100644 --- a/Content.Client/Interaction/DragDropSystem.cs +++ b/Content.Client/Interaction/DragDropSystem.cs @@ -48,6 +48,7 @@ public sealed class DragDropSystem : SharedDragDropSystem [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly EntityQuery _spriteQuery = default!; // how often to recheck possible targets (prevents calling expensive // check logic each update) @@ -433,11 +434,9 @@ public sealed class DragDropSystem : SharedDragDropSystem var bounds = new Box2(mousePos.Position - expansion, mousePos.Position + expansion); var pvsEntities = _lookup.GetEntitiesIntersecting(mousePos.MapId, bounds); - var spriteQuery = GetEntityQuery(); - foreach (var entity in pvsEntities) { - if (!spriteQuery.TryGetComponent(entity, out var inRangeSprite) || + if (!_spriteQuery.TryGetComponent(entity, out var inRangeSprite) || !inRangeSprite.Visible || entity == _draggedEntity) { diff --git a/Content.Client/Movement/Systems/ClientSpriteMovementSystem.cs b/Content.Client/Movement/Systems/ClientSpriteMovementSystem.cs index eb60e4fbb6..c806ae5aed 100644 --- a/Content.Client/Movement/Systems/ClientSpriteMovementSystem.cs +++ b/Content.Client/Movement/Systems/ClientSpriteMovementSystem.cs @@ -10,15 +10,12 @@ namespace Content.Client.Movement.Systems; public sealed class ClientSpriteMovementSystem : SharedSpriteMovementSystem { [Dependency] private readonly SpriteSystem _sprite = default!; - - private EntityQuery _spriteQuery; + [Dependency] private readonly EntityQuery _spriteQuery = default!; public override void Initialize() { base.Initialize(); - _spriteQuery = GetEntityQuery(); - SubscribeLocalEvent(OnAfterAutoHandleState); } diff --git a/Content.Client/Movement/Systems/FloorOcclusionSystem.cs b/Content.Client/Movement/Systems/FloorOcclusionSystem.cs index 6520d98ac9..3e1084bebb 100644 --- a/Content.Client/Movement/Systems/FloorOcclusionSystem.cs +++ b/Content.Client/Movement/Systems/FloorOcclusionSystem.cs @@ -12,14 +12,12 @@ public sealed class FloorOcclusionSystem : SharedFloorOcclusionSystem [Dependency] private readonly IPrototypeManager _proto = default!; - private EntityQuery _spriteQuery; + [Dependency] private readonly EntityQuery _spriteQuery = default!; public override void Initialize() { base.Initialize(); - _spriteQuery = GetEntityQuery(); - SubscribeLocalEvent(OnOcclusionStartup); SubscribeLocalEvent(OnOcclusionShutdown); SubscribeLocalEvent(OnOcclusionAuto); diff --git a/Content.Client/Outline/TargetOutlineSystem.cs b/Content.Client/Outline/TargetOutlineSystem.cs index 73d45a17a9..23752fbb9b 100644 --- a/Content.Client/Outline/TargetOutlineSystem.cs +++ b/Content.Client/Outline/TargetOutlineSystem.cs @@ -27,6 +27,7 @@ public sealed class TargetOutlineSystem : EntitySystem [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!; + [Dependency] private readonly EntityQuery _spriteQuery = default!; private bool _enabled = false; @@ -130,11 +131,10 @@ public sealed class TargetOutlineSystem : EntitySystem var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition).Position; var bounds = new Box2(mousePos - LookupVector, mousePos + LookupVector); var pvsEntities = _lookup.GetEntitiesIntersecting(_eyeManager.CurrentEye.Position.MapId, bounds, LookupFlags.Approximate | LookupFlags.Static); - var spriteQuery = GetEntityQuery(); foreach (var entity in pvsEntities) { - if (!spriteQuery.TryGetComponent(entity, out var sprite) || !sprite.Visible) + if (!_spriteQuery.TryGetComponent(entity, out var sprite) || !sprite.Visible) continue; // Check the predicate diff --git a/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs b/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs index 1319a3d74e..f0cc9b2f59 100644 --- a/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs +++ b/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs @@ -4,7 +4,6 @@ using Content.Shared.Chemistry.Components; using Content.Shared.Polymorph.Components; using Content.Shared.Polymorph.Systems; using Robust.Client.GameObjects; -using Robust.Shared.Player; namespace Content.Client.Polymorph.Systems; @@ -13,16 +12,13 @@ public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SpriteSystem _sprite = default!; - private EntityQuery _appearanceQuery; - private EntityQuery _spriteQuery; + [Dependency] private readonly EntityQuery _appearanceQuery = default!; + [Dependency] private readonly EntityQuery _spriteQuery = default!; public override void Initialize() { base.Initialize(); - _appearanceQuery = GetEntityQuery(); - _spriteQuery = GetEntityQuery(); - SubscribeLocalEvent(OnHandleState); SubscribeLocalEvent(OnStartup); diff --git a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Movement.cs b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Movement.cs index e7d01713e5..3408a2af26 100644 --- a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Movement.cs +++ b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Movement.cs @@ -74,8 +74,7 @@ public sealed partial class ReplaySpectatorSystem if ((Direction & DirectionFlag.East) != 0) effectiveDir &= ~DirectionFlag.West; - var query = GetEntityQuery(); - var xform = query.GetComponent(player); + var xform = Transform(player); var pos = _transform.GetWorldPosition(xform); if (!xform.ParentUid.IsValid()) diff --git a/Content.Client/Silicons/Borgs/BorgSystem.Battery.cs b/Content.Client/Silicons/Borgs/BorgSystem.Battery.cs index 52398921d4..6b946f9cf8 100644 --- a/Content.Client/Silicons/Borgs/BorgSystem.Battery.cs +++ b/Content.Client/Silicons/Borgs/BorgSystem.Battery.cs @@ -13,16 +13,11 @@ public sealed partial class BorgSystem // Don't put this on the component because we only need to track the time for a single entity // and we don't want to TryComp it every single tick. private TimeSpan _nextAlertUpdate = TimeSpan.Zero; - private EntityQuery _chassisQuery; - private EntityQuery _slotQuery; public void InitializeBattery() { SubscribeLocalEvent(OnPlayerAttached); SubscribeLocalEvent(OnPlayerDetached); - - _chassisQuery = GetEntityQuery(); - _slotQuery = GetEntityQuery(); } private void OnPlayerAttached(Entity ent, ref LocalPlayerAttachedEvent args) diff --git a/Content.Client/Silicons/Borgs/BorgSystem.cs b/Content.Client/Silicons/Borgs/BorgSystem.cs index 517027d082..09e6e387f7 100644 --- a/Content.Client/Silicons/Borgs/BorgSystem.cs +++ b/Content.Client/Silicons/Borgs/BorgSystem.cs @@ -2,6 +2,7 @@ using Content.Shared.Mobs; using Content.Shared.Power.EntitySystems; using Content.Shared.PowerCell; +using Content.Shared.PowerCell.Components; using Content.Shared.Silicons.Borgs; using Content.Shared.Silicons.Borgs.Components; using Robust.Client.GameObjects; @@ -22,6 +23,8 @@ public sealed partial class BorgSystem : SharedBorgSystem [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly EntityQuery _chassisQuery = default!; + [Dependency] private readonly EntityQuery _slotQuery = default!; public override void Initialize() { diff --git a/Content.Client/Sprite/SpriteFadeSystem.cs b/Content.Client/Sprite/SpriteFadeSystem.cs index b347411040..bdc49bd25f 100644 --- a/Content.Client/Sprite/SpriteFadeSystem.cs +++ b/Content.Client/Sprite/SpriteFadeSystem.cs @@ -27,16 +27,15 @@ public sealed class SpriteFadeSystem : EntitySystem [Dependency] private readonly IInputManager _inputManager = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly EntityQuery _spriteQuery = default!; + [Dependency] private readonly EntityQuery _fadeQuery = default!; + [Dependency] private readonly EntityQuery _fadingQuery = default!; + [Dependency] private readonly EntityQuery _fixturesQuery = default!; private List<(MapCoordinates Point, bool ExcludeBoundingBox)> _points = new(); private readonly HashSet _comps = new(); - private EntityQuery _spriteQuery; - private EntityQuery _fadeQuery; - private EntityQuery _fadingQuery; - private EntityQuery _fixturesQuery; - private const float TargetAlpha = 0.4f; private const float ChangeRate = 1f; @@ -44,11 +43,6 @@ public sealed class SpriteFadeSystem : EntitySystem { base.Initialize(); - _spriteQuery = GetEntityQuery(); - _fadeQuery = GetEntityQuery(); - _fadingQuery = GetEntityQuery(); - _fixturesQuery = GetEntityQuery(); - SubscribeLocalEvent(OnFadingShutdown); } diff --git a/Content.Client/Sticky/Visualizers/StickyVisualizerSystem.cs b/Content.Client/Sticky/Visualizers/StickyVisualizerSystem.cs index 85c6b8e066..4f58919d78 100644 --- a/Content.Client/Sticky/Visualizers/StickyVisualizerSystem.cs +++ b/Content.Client/Sticky/Visualizers/StickyVisualizerSystem.cs @@ -5,14 +5,12 @@ namespace Content.Client.Sticky.Visualizers; public sealed class StickyVisualizerSystem : VisualizerSystem { - private EntityQuery _spriteQuery; + [Dependency] private readonly EntityQuery _spriteQuery = default!; public override void Initialize() { base.Initialize(); - _spriteQuery = GetEntityQuery(); - SubscribeLocalEvent(OnInit); } diff --git a/Content.Client/SubFloor/TrayScannerSystem.cs b/Content.Client/SubFloor/TrayScannerSystem.cs index 4c67890f6a..a23d67a98b 100644 --- a/Content.Client/SubFloor/TrayScannerSystem.cs +++ b/Content.Client/SubFloor/TrayScannerSystem.cs @@ -20,6 +20,8 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SpriteSystem _sprite = default!; [Dependency] private readonly TrayScanRevealSystem _trayScanReveal = default!; + [Dependency] private readonly EntityQuery _trayScannerQuery = default!; + [Dependency] private readonly EntityQuery _subFloorHideQuery = default!; private const string TRayAnimationKey = "trays"; private const double AnimationLength = 0.3; @@ -35,16 +37,14 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem // TODO: Multiple viewports or w/e var player = _player.LocalEntity; - var xformQuery = GetEntityQuery(); - if (!xformQuery.TryGetComponent(player, out var playerXform)) + if (!TryComp(player, out TransformComponent? playerXform)) return; - var playerPos = _transform.GetWorldPosition(playerXform, xformQuery); + var playerPos = _transform.GetWorldPosition(playerXform); var playerMap = playerXform.MapID; var range = 0f; HashSet> inRange; - var scannerQuery = GetEntityQuery(); // TODO: Should probably sub to player attached changes / inventory changes but inventory's // API is extremely skrungly. If this ever shows up on dottrace ping me and laugh. @@ -57,7 +57,7 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem { foreach (var ent in slot.ContainedEntities) { - if (!scannerQuery.TryGetComponent(ent, out var sneakScanner) || !sneakScanner.Enabled) + if (!_trayScannerQuery.TryGetComponent(ent, out var sneakScanner) || !sneakScanner.Enabled) continue; canSee = true; @@ -71,7 +71,7 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem if (!_hands.TryGetHeldItem(player.Value, hand, out var heldEntity)) continue; - if (!scannerQuery.TryGetComponent(heldEntity, out var heldScanner) || !heldScanner.Enabled) + if (!_trayScannerQuery.TryGetComponent(heldEntity, out var heldScanner) || !heldScanner.Enabled) continue; range = MathF.Max(heldScanner.Range, range); @@ -93,13 +93,12 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem } var revealedQuery = AllEntityQuery(); - var subfloorQuery = GetEntityQuery(); while (revealedQuery.MoveNext(out var uid, out _, out var sprite)) { // Revealing // Add buffer range to avoid flickers. - if (subfloorQuery.TryGetComponent(uid, out var subfloor) && + if (_subFloorHideQuery.TryGetComponent(uid, out var subfloor) && inRange.Contains((uid, subfloor))) { // Due to the fact client is predicting this server states will reset it constantly diff --git a/Content.Client/UserInterface/BuiPreTickUpdateSystem.cs b/Content.Client/UserInterface/BuiPreTickUpdateSystem.cs index 330cb51dcc..a25d89543d 100644 --- a/Content.Client/UserInterface/BuiPreTickUpdateSystem.cs +++ b/Content.Client/UserInterface/BuiPreTickUpdateSystem.cs @@ -33,15 +33,7 @@ public sealed class BuiPreTickUpdateSystem : EntitySystem [Dependency] private readonly IPlayerManager _playerManager = null!; [Dependency] private readonly UserInterfaceSystem _uiSystem = null!; [Dependency] private readonly IGameTiming _gameTiming = null!; - - private EntityQuery _userQuery; - - public override void Initialize() - { - base.Initialize(); - - _userQuery = GetEntityQuery(); - } + [Dependency] private readonly EntityQuery _userQuery = default!; public void RunUpdates() { diff --git a/Content.Client/Verbs/VerbSystem.cs b/Content.Client/Verbs/VerbSystem.cs index 77b8531888..e0bf99d1cb 100644 --- a/Content.Client/Verbs/VerbSystem.cs +++ b/Content.Client/Verbs/VerbSystem.cs @@ -34,6 +34,7 @@ namespace Content.Client.Verbs [Dependency] private readonly SharedContainerSystem _containers = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly EntityQuery _spriteQuery = default!; private float _lookupSize; @@ -159,10 +160,9 @@ namespace Content.Client.Verbs if (container == null && (visibility & MenuVisibility.InContainer) == 0) return entities.Count != 0; - var spriteQuery = GetEntityQuery(); for (var i = entities.Count - 1; i >= 0; i--) { - if (!spriteQuery.TryGetComponent(entities[i], out var spriteComponent) || !spriteComponent.Visible) + if (!_spriteQuery.TryGetComponent(entities[i], out var spriteComponent) || !spriteComponent.Visible) entities.RemoveSwap(i); } diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs index 8971508969..c792910fcf 100644 --- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs +++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs @@ -32,7 +32,7 @@ public sealed partial class MeleeWeaponSystem if (localPos == Vector2.Zero || animation == null) return; - if (!_xformQuery.TryGetComponent(user, out var userXform) || userXform.MapID == MapId.Nullspace) + if (!TryComp(user, out TransformComponent? userXform) || userXform.MapID == MapId.Nullspace) return; var animationUid = Spawn(animation, userXform.Coordinates); @@ -64,7 +64,7 @@ public sealed partial class MeleeWeaponSystem } _sprite.SetRotation((animationUid, sprite), localPos.ToWorldAngle()); - var xform = _xformQuery.GetComponent(animationUid); + var xform = Transform(animationUid); TrackUserComponent track; switch (arcComponent.Animation) diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs index 420e18748f..5ecbaf63b8 100644 --- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs +++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs @@ -35,14 +35,12 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem [Dependency] private readonly SpriteSystem _sprite = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; - private EntityQuery _xformQuery; - private const string MeleeLungeKey = "melee-lunge"; public override void Initialize() { base.Initialize(); - _xformQuery = GetEntityQuery(); + SubscribeNetworkEvent(OnMeleeLunge); UpdatesOutsidePrediction = true; } @@ -177,7 +175,7 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem private void ClientHeavyAttack(EntityUid user, EntityCoordinates coordinates, EntityUid meleeUid, MeleeWeaponComponent component) { // Only run on first prediction to avoid the potential raycast entities changing. - if (!_xformQuery.TryGetComponent(user, out var userXform) || + if (!TryComp(user, out TransformComponent? userXform) || !Timing.IsFirstTimePredicted) { return; diff --git a/Content.Client/Weather/WeatherSystem.cs b/Content.Client/Weather/WeatherSystem.cs index 4b63c05991..a7d8343fb2 100644 --- a/Content.Client/Weather/WeatherSystem.cs +++ b/Content.Client/Weather/WeatherSystem.cs @@ -20,19 +20,15 @@ public sealed class WeatherSystem : SharedWeatherSystem [Dependency] private readonly MapSystem _mapSystem = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; - private EntityQuery _audioQuery; - private EntityQuery _gridQuery; - private EntityQuery _roofQuery; + [Dependency] private readonly EntityQuery _audioQuery = default!; + [Dependency] private readonly EntityQuery _gridQuery = default!; + [Dependency] private readonly EntityQuery _roofQuery = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnComponentShutdown); - - _audioQuery = GetEntityQuery(); - _gridQuery = GetEntityQuery(); - _roofQuery = GetEntityQuery(); } private void OnComponentShutdown(Entity ent, ref ComponentShutdown args) diff --git a/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs b/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs index 14c2ae67d3..0b5902b5e3 100644 --- a/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs +++ b/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs @@ -17,6 +17,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem [Dependency] private readonly SharedGravitySystem _gravity = default!; [Dependency] private readonly SharedInteractionSystem _interaction = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] EntityQuery _handsQuery = default!; private DoAfter[] _doAfters = Array.Empty(); @@ -34,7 +35,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem try { - Update(uid, active, comp, time, xformQuery, handsQuery); + Update(uid, active, comp, time); } // ReSharper disable once RedundantCatchClause #if EXCEPTION_TOLERANCE @@ -87,9 +88,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem EntityUid uid, ActiveDoAfterComponent active, DoAfterComponent comp, - TimeSpan time, - EntityQuery xformQuery, - EntityQuery handsQuery) + TimeSpan time) { var dirty = false; @@ -122,7 +121,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem continue; } - if (ShouldCancel(doAfter, xformQuery, handsQuery)) + if (ShouldCancel(doAfter)) { InternalCancel(doAfter, comp); dirty = true; @@ -196,27 +195,24 @@ public abstract partial class SharedDoAfterSystem : EntitySystem } } - private bool ShouldCancel(DoAfter doAfter, - EntityQuery xformQuery, - EntityQuery handsQuery) + private bool ShouldCancel(DoAfter doAfter) { var args = doAfter.Args; - //re-using xformQuery for Exists() checks. - if (args.Used is { } used && !xformQuery.HasComponent(used)) + if (args.Used is { } used && !Exists(used)) return true; - if (args.EventTarget is { Valid: true } eventTarget && !xformQuery.HasComponent(eventTarget)) + if (args.EventTarget is { Valid: true } eventTarget && !Exists(eventTarget)) return true; - if (!xformQuery.TryGetComponent(args.User, out var userXform)) + if (!TryComp(args.User, out TransformComponent? userXform)) return true; TransformComponent? targetXform = null; - if (args.Target is { } target && !xformQuery.TryGetComponent(target, out targetXform)) + if (args.Target is { } target && !TryComp(target, out targetXform)) return true; - if (args.Used is { } @using && !xformQuery.HasComp(@using)) + if (args.Used is { } @using && !Exists(@using)) return true; // TODO: Re-use existing xform query for these calculations. @@ -265,7 +261,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem // This does not mean their hand needs to be empty. if (args.NeedHand) { - if (!handsQuery.TryGetComponent(args.User, out var hands) || hands.Count == 0) + if (!_handsQuery.TryGetComponent(args.User, out var hands) || hands.Count == 0) return true; // If an item was in the user's hand to begin with, diff --git a/Content.Shared/DoAfter/SharedDoAfterSystem.cs b/Content.Shared/DoAfter/SharedDoAfterSystem.cs index 0b72692ea0..8841d8dd26 100644 --- a/Content.Shared/DoAfter/SharedDoAfterSystem.cs +++ b/Content.Shared/DoAfter/SharedDoAfterSystem.cs @@ -251,7 +251,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem doAfter.NetInitialItem = GetNetEntity(doAfter.InitialItem); // Initial checks - if (ShouldCancel(doAfter, GetEntityQuery(), GetEntityQuery())) + if (ShouldCancel(doAfter)) return false; if (args.AttemptFrequency == AttemptFrequency.StartAndEnd && !TryAttemptEvent(doAfter)) From 8187ade22cf4f6b1df103d52fc6fa8aeb8280690 Mon Sep 17 00:00:00 2001 From: ProPeperos <61984624+ProPeperos@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:20:27 +0200 Subject: [PATCH 099/126] Elk Evac Shuttle - 2 Fixes (#43481) Fix to elk evac shuttle --- Resources/Maps/Shuttles/emergency_delta.yml | 219 +++++--------------- 1 file changed, 51 insertions(+), 168 deletions(-) diff --git a/Resources/Maps/Shuttles/emergency_delta.yml b/Resources/Maps/Shuttles/emergency_delta.yml index c2d2628fb9..1797ce0ecb 100644 --- a/Resources/Maps/Shuttles/emergency_delta.yml +++ b/Resources/Maps/Shuttles/emergency_delta.yml @@ -1,11 +1,11 @@ meta: format: 7 category: Grid - engineVersion: 266.0.0 + engineVersion: 275.2.0 forkId: "" forkVersion: "" - time: 09/17/2025 04:14:52 - entityCount: 957 + time: 04/05/2026 13:24:37 + entityCount: 959 maps: [] grids: - 1 @@ -516,6 +516,9 @@ entities: - type: GasTileOverlay - type: RadiationGridResistance - type: ImplicitRoof + - type: TileHistory + chunkHistory: {} + - type: ExplosionAirtightGrid - proto: AirCanister entities: - uid: 326 @@ -523,10 +526,10 @@ entities: - type: Transform pos: -13.5,-3.5 parent: 1 - - uid: 445 + - uid: 643 components: - type: Transform - pos: -1.5,-18.5 + pos: 0.5,-20.5 parent: 1 - proto: AirlockBrigGlassLocked entities: @@ -735,6 +738,14 @@ entities: parent: 1 - type: Fixtures fixtures: {} + - uid: 795 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -9.5,0.5 + parent: 1 + - type: Fixtures + fixtures: {} - proto: AtmosDeviceFanDirectional entities: - uid: 71 @@ -1714,6 +1725,11 @@ entities: - type: Transform pos: -2.5,-22.5 parent: 1 + - uid: 802 + components: + - type: Transform + pos: -9.5,0.5 + parent: 1 - proto: CableHV entities: - uid: 33 @@ -2223,6 +2239,16 @@ entities: - type: Transform pos: -4.5,-17.5 parent: 1 + - uid: 958 + components: + - type: Transform + pos: -9.5,0.5 + parent: 1 + - uid: 959 + components: + - type: Transform + pos: -8.5,0.5 + parent: 1 - proto: CableTerminal entities: - uid: 277 @@ -3134,11 +3160,11 @@ entities: parent: 1 - proto: GasOutletInjector entities: - - uid: 801 + - uid: 687 components: - type: Transform rot: -1.5707963267948966 rad - pos: 1.5,-18.5 + pos: 1.5,-20.5 parent: 1 - proto: GasPassiveVent entities: @@ -3146,10 +3172,16 @@ entities: components: - type: Transform rot: -1.5707963267948966 rad - pos: 1.5,-20.5 + pos: 1.5,-19.5 parent: 1 - proto: GasPipeBend entities: + - uid: 680 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -2.5,-20.5 + parent: 1 - uid: 856 components: - type: Transform @@ -3181,7 +3213,7 @@ entities: components: - type: Transform rot: -1.5707963267948966 rad - pos: 0.5,-20.5 + pos: -0.5,-19.5 parent: 1 - uid: 514 components: @@ -3189,45 +3221,28 @@ entities: rot: 3.141592653589793 rad pos: -2.5,-16.5 parent: 1 - - uid: 643 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -1.5,-20.5 - parent: 1 - uid: 644 components: - type: Transform rot: -1.5707963267948966 rad - pos: -0.5,-20.5 + pos: -1.5,-19.5 parent: 1 - uid: 675 components: - type: Transform pos: -2.5,-17.5 parent: 1 - - uid: 680 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -0.5,-18.5 - parent: 1 - uid: 699 components: - type: Transform - pos: -2.5,-19.5 + rot: -1.5707963267948966 rad + pos: 0.5,-19.5 parent: 1 - uid: 707 components: - type: Transform pos: -2.5,-18.5 parent: 1 - - uid: 802 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 0.5,-18.5 - parent: 1 - uid: 840 components: - type: Transform @@ -3627,11 +3642,11 @@ entities: parent: 1 - proto: GasPipeTJunction entities: - - uid: 795 + - uid: 445 components: - type: Transform - rot: 3.141592653589793 rad - pos: -2.5,-20.5 + rot: 1.5707963267948966 rad + pos: -2.5,-19.5 parent: 1 - uid: 841 components: @@ -3741,11 +3756,11 @@ entities: parent: 1 - proto: GasPort entities: - - uid: 687 + - uid: 460 components: - type: Transform rot: 1.5707963267948966 rad - pos: -1.5,-18.5 + pos: 0.5,-20.5 parent: 1 - proto: GasVentPump entities: @@ -4270,32 +4285,24 @@ entities: rot: -1.5707963267948966 rad pos: 1.5,-18.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 119 components: - type: Transform rot: -1.5707963267948966 rad pos: 1.5,-20.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 130 components: - type: Transform rot: -1.5707963267948966 rad pos: 1.5,-19.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 293 components: - type: Transform rot: -1.5707963267948966 rad pos: 1.5,-21.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - proto: PosterLegitEnlist entities: - uid: 788 @@ -4657,393 +4664,281 @@ entities: - type: Transform pos: 2.5,1.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 7 components: - type: Transform pos: 2.5,2.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 15 components: - type: Transform pos: -3.5,1.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 18 components: - type: Transform pos: 0.5,3.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 19 components: - type: Transform pos: -0.5,3.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 81 components: - type: Transform pos: -0.5,-1.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 82 components: - type: Transform pos: -1.5,-2.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 101 components: - type: Transform pos: -12.5,3.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 142 components: - type: Transform pos: -4.5,-1.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 170 components: - type: Transform pos: -1.5,6.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 171 components: - type: Transform pos: -10.5,6.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 173 components: - type: Transform pos: -10.5,7.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 174 components: - type: Transform pos: -1.5,7.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 182 components: - type: Transform pos: -8.5,9.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 183 components: - type: Transform pos: -3.5,9.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 184 components: - type: Transform pos: -4.5,9.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 185 components: - type: Transform pos: -5.5,9.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 186 components: - type: Transform pos: -6.5,9.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 187 components: - type: Transform pos: -7.5,9.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 261 components: - type: Transform pos: -14.5,-3.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 262 components: - type: Transform pos: -14.5,-4.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 263 components: - type: Transform pos: -14.5,-5.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 264 components: - type: Transform pos: -14.5,-6.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 265 components: - type: Transform pos: -14.5,-7.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 267 components: - type: Transform pos: -1.5,-22.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 274 components: - type: Transform pos: -6.5,-1.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 327 components: - type: Transform pos: -2.5,-22.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 330 components: - type: Transform pos: -9.5,-11.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 331 components: - type: Transform pos: -9.5,-14.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 338 components: - type: Transform pos: -11.5,-17.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 339 components: - type: Transform pos: -12.5,-17.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 342 components: - type: Transform pos: -14.5,-12.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 343 components: - type: Transform pos: -14.5,-13.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 344 components: - type: Transform pos: -14.5,-14.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 345 components: - type: Transform pos: -14.5,-15.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 430 components: - type: Transform pos: -8.5,-17.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 432 components: - type: Transform pos: -6.5,-17.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 469 components: - type: Transform pos: 2.5,-4.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 470 components: - type: Transform pos: 2.5,-5.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 471 components: - type: Transform pos: 2.5,-6.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 478 components: - type: Transform pos: 2.5,-13.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 479 components: - type: Transform pos: 2.5,-14.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 513 components: - type: Transform pos: -1.5,-8.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 515 components: - type: Transform pos: -1.5,-4.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 530 components: - type: Transform pos: -5.5,-10.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 533 components: - type: Transform pos: -1.5,-10.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 535 components: - type: Transform pos: -1.5,-14.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 557 components: - type: Transform pos: -5.5,-4.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 712 components: - type: Transform pos: -8.5,-1.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 760 components: - type: Transform pos: -9.5,-7.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 764 components: - type: Transform pos: -9.5,-6.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 817 components: - type: Transform pos: -5.5,-8.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 819 components: - type: Transform pos: -5.5,-14.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 820 components: - type: Transform pos: -5.5,-16.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 821 components: - type: Transform pos: -1.5,-16.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 837 components: - type: Transform pos: -5.5,-2.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - proto: SignBridge entities: - uid: 11 @@ -5064,10 +4959,10 @@ entities: fixtures: {} - proto: SignEVA entities: - - uid: 460 + - uid: 801 components: - type: Transform - pos: -9.5,0.5 + pos: -9.5,2.5 parent: 1 - type: Fixtures fixtures: {} @@ -5952,8 +5847,6 @@ entities: rot: 1.5707963267948966 rad pos: -1.5,-20.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - proto: WindoorSecureSecurityLocked entities: - uid: 37 @@ -5962,8 +5855,6 @@ entities: rot: 3.141592653589793 rad pos: -2.5,1.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - proto: WindowReinforcedDirectional entities: - uid: 660 @@ -5972,30 +5863,22 @@ entities: rot: 1.5707963267948966 rad pos: -1.5,-19.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 682 components: - type: Transform rot: 1.5707963267948966 rad pos: -1.5,-18.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 746 components: - type: Transform rot: 1.5707963267948966 rad pos: -1.5,-21.5 parent: 1 - - type: DeltaPressure - gridUid: 1 - uid: 755 components: - type: Transform rot: -1.5707963267948966 rad pos: -1.5,2.5 parent: 1 - - type: DeltaPressure - gridUid: 1 ... From 59663fc9b1519c334da40ddfef7afdfb2a6ec30a Mon Sep 17 00:00:00 2001 From: PJBot Date: Mon, 6 Apr 2026 18:35:57 +0000 Subject: [PATCH 100/126] Automatic changelog update --- Resources/Changelog/Maps.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Resources/Changelog/Maps.yml b/Resources/Changelog/Maps.yml index b971abf643..df01e9ce16 100644 --- a/Resources/Changelog/Maps.yml +++ b/Resources/Changelog/Maps.yml @@ -1078,4 +1078,13 @@ id: 132 time: '2026-03-24T21:43:08.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43046 +- author: ProPeperos + changes: + - message: On Fland Evac shuttle (Delta) Added missing APC in the upper left room + type: Tweak + - message: On Fland Evac shuttle (Delta) Moved air canister connector + type: Tweak + id: 133 + time: '2026-04-06T18:34:45.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43481 Order: 2 From a4594108d5e5f5abd3e5fbb06d89a1054d770217 Mon Sep 17 00:00:00 2001 From: psykana <36602558+psykana@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:26:08 +0100 Subject: [PATCH 101/126] Add nuke disk's location to nukeops greentext (#39767) * Add nuke disk's location to nukeops greentext * robust --- .../GameTicking/Rules/NukeopsRuleSystem.cs | 154 +++++++++++++++++- .../game-presets/preset-nukeops.ftl | 13 +- 2 files changed, 163 insertions(+), 4 deletions(-) diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs index 0dc906738f..086aeceaef 100644 --- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs @@ -3,31 +3,45 @@ using Content.Server.Communications; using Content.Server.GameTicking.Rules.Components; using Content.Server.Nuke; using Content.Server.NukeOps; +using Content.Server.Pinpointer; using Content.Server.Popups; using Content.Server.Roles; using Content.Server.RoundEnd; using Content.Server.Shuttles.Events; using Content.Server.Shuttles.Systems; using Content.Server.Station.Components; +using Content.Server.StationRecords.Systems; using Content.Server.Store.Systems; +using Content.Shared.Access.Systems; using Content.Shared.GameTicking.Components; +using Content.Shared.Mind; +using Content.Shared.Mind.Components; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.NPC.Components; using Content.Shared.NPC.Systems; using Content.Shared.Nuke; using Content.Shared.NukeOps; +using Content.Shared.Roles; using Content.Shared.Roles.Components; +using Content.Shared.Roles.Jobs; +using Content.Shared.Station; +using Content.Shared.Station.Components; +using Content.Shared.StationRecords; using Content.Shared.Store; +using Content.Shared.Store.Components; using Content.Shared.Tag; using Content.Shared.Zombies; +using Robust.Server.Player; +using Robust.Shared.Containers; using Robust.Shared.Map; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Utility; +using System.Data; using System.Linq; -using Content.Shared.Station.Components; -using Content.Shared.Store.Components; -using Robust.Shared.Prototypes; +using System.Text; namespace Content.Server.GameTicking.Rules; @@ -35,9 +49,18 @@ public sealed class NukeopsRuleSystem : GameRuleSystem { [Dependency] private readonly AntagSelectionSystem _antag = default!; [Dependency] private readonly EmergencyShuttleSystem _emergency = default!; + [Dependency] private readonly SharedIdCardSystem _idCard = default!; + [Dependency] private readonly SharedJobSystem _jobs = default!; + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly NavMapSystem _navMap = default!; [Dependency] private readonly NpcFactionSystem _npcFaction = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly RoundEndSystem _roundEndSystem = default!; + [Dependency] private readonly SharedContainerSystem _containers = default!; + [Dependency] private readonly SharedRoleSystem _roles = default!; + [Dependency] private readonly SharedStationSystem _station = default!; + [Dependency] private readonly StationRecordsSystem _records = default!; [Dependency] private readonly StoreSystem _store = default!; [Dependency] private readonly TagSystem _tag = default!; @@ -105,6 +128,63 @@ public sealed class NukeopsRuleSystem : GameRuleSystem args.AddLine(text); } + // Print disk location if nuke didn't explode and is not armed + List diskWinConditions = [WinCondition.NukeDiskOnCentCom, WinCondition.NukeDiskNotOnCentCom]; + if (component.WinConditions.Any(diskWinConditions.Contains)) + { + var diskQuery = AllEntityQuery(); + while (diskQuery.MoveNext(out var diskUid, out _, out var transform)) + { + StringBuilder text = new StringBuilder(Loc.GetString("nukeops-disk-location-title")); + + List containers = new List(); + bool carriedByMob = false; + + var tempParent = diskUid; + while (_containers.TryGetContainingContainer((tempParent, null), out var container) && !carriedByMob) + { + if (HasComp(container.Owner)) + { + carriedByMob = true; + } + var containermeta = MetaData(container.Owner); + containers.Add(containermeta.EntityName); + tempParent = container.Owner; + } + + string location = FormattedMessage.RemoveMarkupOrThrow(_navMap.GetNearestBeaconString((diskUid, transform))); + + if (carriedByMob) + { + GetDiskCarrierData(tempParent, out var name, out var job, out var username); + text.Append(Loc.GetString("nukeops-disk-carried-by", + ("name", name), + ("job", job), + ("user", username), + ("location", location))); + } + else + { + if (containers.Count > 0) + { + string hierarchy = string.Empty; + for (var i = 0; i < containers.Count; i++) + { + hierarchy = (Loc.GetString( + "storage-hierarchy-list", + ("item", containers[i]), + ("existing-text", hierarchy), + ("items-left", containers.Count - i - 1))); + } + text.Append(hierarchy); + } + text.Append(" "); + text.Append(location); + } + args.AddLine(text.ToString()); + } + } + args.AddLine(Loc.GetString("nukeops-list-start")); var antags = _antag.GetAntagIdentifiers(uid); @@ -550,6 +630,74 @@ public sealed class NukeopsRuleSystem : GameRuleSystem return null; } + + private void GetDiskCarrierData(EntityUid carrier, + out string name, + out string job, + out string username) + { + name = Name(carrier); + job = Loc.GetString("job-name-unknown"); + username = "unknown"; // magic word in Fluent selector + + Entity? mind = null; + + if (_mind.TryGetMind(carrier, out _, out var mindComp)) + { + mind = (carrier, mindComp); + } + else + { + var allMinds = EntityQueryEnumerator(); + while (allMinds.MoveNext(out _, out mindComp)) + { + if (mindComp.CharacterName != name) + continue; + + mind = (carrier, mindComp); + break; + } + } + + if (mind is not null) + { + NetUserId? userId = mind.Value.Comp.UserId; + if (userId is not null && _player.TryGetPlayerData(userId.Value, out var sessionData)) + username = sessionData.UserName; + + // Role/job is the trickiest since it can be unknown in some cases + // For example, after "make ghost role" verb + var roles = _roles.MindGetAllRoleInfo(mind.Value.Owner); + if (roles.Count > 0) + { + job = Loc.GetString(roles.First().Name); + return; + } + + if (_jobs.MindTryGetJobName(mind, out var jobName)) + { + job = jobName; + return; + } + } + + // Try station records + var xform = Transform(carrier); + var station = _station.GetStationInMap(xform.MapID); + if (station != null && _records.GetRecordByName(station.Value, name) is { } id) + { + var key = new StationRecordKey(id, station.Value); + if (_records.TryGetRecord(key, out var record)) + { + job = record.JobTitle; + return; + } + } + + // Fallback to ID + if (_idCard.TryFindIdCard(carrier, out var idCard)) + job = idCard.Comp.LocalizedJobTitle ?? job; + } } /// diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-nukeops.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-nukeops.ftl index 1343aaec7e..da1794badb 100644 --- a/Resources/Locale/en-US/game-ticking/game-presets/preset-nukeops.ftl +++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-nukeops.ftl @@ -1,4 +1,4 @@ -nukeops-title = Nuclear Operatives +nukeops-title = Nuclear Operatives nukeops-description = Nuclear operatives have targeted the station. Try to keep them from arming and detonating the nuke by protecting the nuke disk! nukeops-welcome = @@ -24,6 +24,17 @@ nukeops-cond-allnukiesdead = All nuclear operatives have died. nukeops-cond-somenukiesalive = Some nuclear operatives died. nukeops-cond-allnukiesalive = No nuclear operatives died. +nukeops-disk-location-title = Final location of Disk: +nukeops-disk-carried-by = {" "}carried by [color=White]{$name}[/color], [color=orange]{$job}[/color], {$location} { $user -> + [unknown] { "" } + *[other] ([color=gray]{$user}[/color]) +} + +storage-hierarchy-list = { $items-left -> + [0] { $existing-text } { $item }, + *[other] { $existing-text } { $item }, in +} + nukeops-list-start = The nuclear operatives were: nukeops-list-name = - [color=White]{$name}[/color] nukeops-list-name-user = - [color=White]{$name}[/color] ([color=gray]{$user}[/color]) From ee3bf6fe08af33f94607a35026d92b75309ce8c9 Mon Sep 17 00:00:00 2001 From: PJBot Date: Mon, 6 Apr 2026 21:42:25 +0000 Subject: [PATCH 102/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index f70eb16b2f..4a704decc0 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,17 +1,4 @@ Entries: -- author: FairlySadPanda - changes: - - message: Two new instrument options for the microphone, Wa and Wah. - type: Add - - message: Fixed the gilded bike horn's instrument not playing properly, causing - listening to it to be even worse than it is supposed to be. - type: Fix - - message: Fixed the microphone's Kweh instrument not playing properly, causing - listening it to be even worse than it is supposed to be. - type: Fix - id: 9111 - time: '2025-10-16T17:03:08.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/39210 - author: MilenVolf changes: - message: Go go hat's activation phrase can be reset to the default without having @@ -4036,3 +4023,11 @@ id: 9622 time: '2026-04-05T22:33:00.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43431 +- author: psykana + changes: + - message: Nuclear operatives round end summary now shows the Disk's location. If + it wasn't atomized, that is. + type: Add + id: 9623 + time: '2026-04-06T21:41:16.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/39767 From 6f83236c1d11102f2dd819df016453045079a434 Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:08:35 -0700 Subject: [PATCH 103/126] KILL THE AI [New Traitor Objective] (#43462) * KILL THE AI * cleanup and review * fix :( * Make it about as common as "KillRandomHead" * me when I forget to fucking push * remove AI dependency * move to AI system * whtiespace * asfsaf --------- Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> --- .../Cloning/RandomCloneSpawnerSystem.cs | 5 +- .../GameTicking/GameTicker.Spawning.cs | 10 +-- .../Rules/ParadoxCloneRuleSystem.cs | 11 +-- .../GameTicking/Rules/SurvivorRuleSystem.cs | 12 ++-- .../GameTicking/Rules/XenoborgsRuleSystem.cs | 10 +-- .../Mind/Filters/TargetObjectiveMindFilter.cs | 2 +- .../Components/PickRandomPersonComponent.cs | 2 +- .../Systems/PickObjectiveTargetSystem.cs | 11 +-- Content.Shared/Magic/SharedMagicSystem.cs | 4 +- Content.Shared/Mind/Filters/AliveAiPool.cs | 16 +++++ .../Mind/Filters/AliveHumansPool.cs | 10 +-- .../Mind/Filters/AntagonistMindFilter.cs | 2 +- Content.Shared/Mind/Filters/BodyMindFilter.cs | 2 +- .../Mind/Filters/HasRoleMindFilter.cs | 2 +- Content.Shared/Mind/Filters/JobMindFilter.cs | 2 +- Content.Shared/Mind/Filters/MindFilter.cs | 10 +-- .../Filters/{IMindPool.cs => MindPool.cs} | 10 +-- .../Mind/Filters/ObjectiveMindFilter.cs | 2 +- Content.Shared/Mind/SharedMindSystem.cs | 57 ++------------- .../Objectives/Systems/TargetSystem.cs | 66 ++++++++++++++++++ .../StationAi/SharedStationAiSystem.cs | 23 ++++++ .../objectives/conditions/kill-person.ftl | 1 + .../Prototypes/Objectives/objectiveGroups.yml | 1 + Resources/Prototypes/Objectives/traitor.yml | 25 +++++++ .../Mobs/Silicon/station_ai.rsi/broken.png | Bin 0 -> 8017 bytes .../Mobs/Silicon/station_ai.rsi/meta.json | 3 + 26 files changed, 200 insertions(+), 99 deletions(-) create mode 100644 Content.Shared/Mind/Filters/AliveAiPool.cs rename Content.Shared/Mind/Filters/{IMindPool.cs => MindPool.cs} (51%) create mode 100644 Content.Shared/Objectives/Systems/TargetSystem.cs create mode 100644 Resources/Textures/Mobs/Silicon/station_ai.rsi/broken.png diff --git a/Content.Server/Cloning/RandomCloneSpawnerSystem.cs b/Content.Server/Cloning/RandomCloneSpawnerSystem.cs index a645a10890..a18ccd2e71 100644 --- a/Content.Server/Cloning/RandomCloneSpawnerSystem.cs +++ b/Content.Server/Cloning/RandomCloneSpawnerSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Cloning.Components; using Content.Shared.Mind; using Content.Shared.Mobs.Systems; +using Content.Shared.Objectives.Systems; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -15,7 +16,7 @@ public sealed class RandomCloneSpawnerSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!; - [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly TargetSystem _target = default!; public override void Initialize() { @@ -34,7 +35,7 @@ public sealed class RandomCloneSpawnerSystem : EntitySystem return; } - var allHumans = _mind.GetAliveHumans(); + var allHumans = _target.GetAliveHumans(); if (allHumans.Count == 0) return; diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs index 5f99123db3..4b00dc3143 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -347,17 +347,17 @@ namespace Content.Server.GameTicking DebugTools.AssertNotNull(data); - var newMind = _mind.CreateMind(data!.UserId, character.Name); - _mind.SetUserId(newMind, data.UserId); - jobPrototype = _prototypeManager.Index(jobId); - _playTimeTrackings.PlayerRolesChanged(player); - var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, jobId, character); DebugTools.AssertNotNull(mobMaybe); mob = mobMaybe!.Value; + var newMind = _mind.CreateMind(data.UserId, Name(mob)); + _mind.SetUserId(newMind, data.UserId); + + _playTimeTrackings.PlayerRolesChanged(player); + _mind.TransferTo(newMind, mob); _roles.MindAddJobRole(newMind, silent: silent, jobPrototype: jobId); diff --git a/Content.Server/GameTicking/Rules/ParadoxCloneRuleSystem.cs b/Content.Server/GameTicking/Rules/ParadoxCloneRuleSystem.cs index ab8864caaa..7eb076d0d5 100644 --- a/Content.Server/GameTicking/Rules/ParadoxCloneRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/ParadoxCloneRuleSystem.cs @@ -7,17 +7,20 @@ using Content.Shared.GameTicking.Components; using Content.Shared.Gibbing.Components; using Content.Shared.Medical.SuitSensor; using Content.Shared.Mind; +using Content.Shared.Objectives.Systems; +using Content.Shared.Random.Helpers; using Robust.Shared.Random; namespace Content.Server.GameTicking.Rules; public sealed class ParadoxCloneRuleSystem : GameRuleSystem { - [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly SharedMindSystem _mind = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly CloningSystem _cloning = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; [Dependency] private readonly SuitSensorSystem _sensor = default!; + [Dependency] private readonly TargetSystem _target = default!; public override void Initialize() { @@ -32,7 +35,7 @@ public sealed class ParadoxCloneRuleSystem : GameRuleSystem { - [Dependency] private readonly RoleSystem _role = default!; - [Dependency] private readonly MindSystem _mind = default!; [Dependency] private readonly AntagSelectionSystem _antag = default!; - [Dependency] private readonly TransformSystem _xform = default!; [Dependency] private readonly EmergencyShuttleSystem _eShuttle = default!; - [Dependency] private readonly TagSystem _tag = default!; [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly RoleSystem _role = default!; + [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly TargetSystem _target = default!; + [Dependency] private readonly TransformSystem _xform = default!; private static readonly ProtoId InvalidForSurvivorAntagTag = "InvalidForSurvivorAntag"; @@ -38,7 +38,7 @@ public sealed class SurvivorRuleSystem : GameRuleSystem { base.Started(uid, component, gameRule, args); - var allAliveHumanMinds = _mind.GetAliveHumans(); + var allAliveHumanMinds = _target.GetAliveHumans(); foreach (var humanMind in allAliveHumanMinds) { diff --git a/Content.Server/GameTicking/Rules/XenoborgsRuleSystem.cs b/Content.Server/GameTicking/Rules/XenoborgsRuleSystem.cs index 0b6a700d11..932774e9a1 100644 --- a/Content.Server/GameTicking/Rules/XenoborgsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/XenoborgsRuleSystem.cs @@ -7,6 +7,7 @@ using Content.Shared.Destructible; using Content.Shared.GameTicking.Components; using Content.Shared.Mind; using Content.Shared.Mobs.Systems; +using Content.Shared.Objectives.Systems; using Content.Shared.Xenoborgs.Components; using Robust.Shared.Timing; @@ -14,13 +15,14 @@ namespace Content.Server.GameTicking.Rules; public sealed class XenoborgsRuleSystem : GameRuleSystem { + [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly AntagSelectionSystem _antag = default!; [Dependency] private readonly ChatSystem _chatSystem = default!; [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly SharedMindSystem _mindSystem = default!; [Dependency] private readonly RoundEndSystem _roundEnd = default!; + [Dependency] private readonly SharedMindSystem _mindSystem = default!; [Dependency] private readonly StationSystem _station = default!; - [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly TargetSystem _target = default!; private static readonly Color AnnouncmentColor = Color.Gold; @@ -53,7 +55,7 @@ public sealed class XenoborgsRuleSystem : GameRuleSystem base.AppendRoundEndText(uid, component, gameRule, ref args); var numXenoborgs = GetNumberXenoborgs(); - var numHumans = _mindSystem.GetAliveHumans().Count; + var numHumans = _target.GetAliveHumans().Count; if (numXenoborgs < 5) args.AddLine(Loc.GetString("xenoborgs-crewmajor")); @@ -96,7 +98,7 @@ public sealed class XenoborgsRuleSystem : GameRuleSystem private void CheckRoundEnd(XenoborgsRuleComponent xenoborgsRuleComponent) { var numXenoborgs = GetNumberXenoborgs(); - var numHumans = _mindSystem.GetAliveHumans().Count; + var numHumans = _target.GetAliveHumans().Count; xenoborgsRuleComponent.MaxNumberXenoborgs = Math.Max(xenoborgsRuleComponent.MaxNumberXenoborgs, numXenoborgs); diff --git a/Content.Server/Mind/Filters/TargetObjectiveMindFilter.cs b/Content.Server/Mind/Filters/TargetObjectiveMindFilter.cs index 6fc031d7c1..975f4c19bd 100644 --- a/Content.Server/Mind/Filters/TargetObjectiveMindFilter.cs +++ b/Content.Server/Mind/Filters/TargetObjectiveMindFilter.cs @@ -20,7 +20,7 @@ public sealed partial class TargetObjectiveMindFilter : MindFilter [DataField] public EntityWhitelist? Blacklist; - protected override bool ShouldRemove(Entity mind, EntityUid? excluded, IEntityManager entMan, SharedMindSystem mindSys) + protected override bool ShouldRemove(Entity mind, EntityUid? excluded, IEntityManager entMan) { // ignore this filter if there is no user to check if (!entMan.TryGetComponent(excluded, out var excludedMind)) diff --git a/Content.Server/Objectives/Components/PickRandomPersonComponent.cs b/Content.Server/Objectives/Components/PickRandomPersonComponent.cs index 2c864a80d4..16e539f320 100644 --- a/Content.Server/Objectives/Components/PickRandomPersonComponent.cs +++ b/Content.Server/Objectives/Components/PickRandomPersonComponent.cs @@ -16,7 +16,7 @@ public sealed partial class PickRandomPersonComponent : Component /// A pool to pick potential targets from. /// [DataField] - public IMindPool Pool = new AliveHumansPool(); + public MindPool Pool = new AliveHumansPool(); /// /// Filters to apply to . diff --git a/Content.Server/Objectives/Systems/PickObjectiveTargetSystem.cs b/Content.Server/Objectives/Systems/PickObjectiveTargetSystem.cs index b3075b2274..5b48b25ad4 100644 --- a/Content.Server/Objectives/Systems/PickObjectiveTargetSystem.cs +++ b/Content.Server/Objectives/Systems/PickObjectiveTargetSystem.cs @@ -5,6 +5,7 @@ using Content.Server.GameTicking.Rules; using Content.Server.Revolutionary.Components; using Robust.Shared.Random; using System.Linq; +using Content.Shared.Objectives.Systems; namespace Content.Server.Objectives.Systems; @@ -14,8 +15,8 @@ namespace Content.Server.Objectives.Systems; /// public sealed class PickObjectiveTargetSystem : EntitySystem { - [Dependency] private readonly TargetObjectiveSystem _target = default!; - [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly TargetObjectiveSystem _objective = default!; + [Dependency] private readonly TargetSystem _target = default!; public override void Initialize() { @@ -51,7 +52,7 @@ public sealed class PickObjectiveTargetSystem : EntitySystem return; } - _target.SetTarget(ent.Owner, targetComp.Target.Value); + _objective.SetTarget(ent.Owner, targetComp.Target.Value); } private void OnRandomPersonAssigned(Entity ent, ref ObjectiveAssignedEvent args) @@ -68,12 +69,12 @@ public sealed class PickObjectiveTargetSystem : EntitySystem return; // couldn't find a target :( - if (_mind.PickFromPool(ent.Comp.Pool, ent.Comp.Filters, args.MindId) is not {} picked) + if (_target.PickFromPool(ent.Comp.Pool, ent.Comp.Filters, args.MindId) is not {} picked) { args.Cancelled = true; return; } - _target.SetTarget(ent, picked, target); + _objective.SetTarget(ent, picked, target); } } diff --git a/Content.Shared/Magic/SharedMagicSystem.cs b/Content.Shared/Magic/SharedMagicSystem.cs index 82cae19ec1..62d3dbdbd4 100644 --- a/Content.Shared/Magic/SharedMagicSystem.cs +++ b/Content.Shared/Magic/SharedMagicSystem.cs @@ -15,6 +15,7 @@ using Content.Shared.Magic.Components; using Content.Shared.Magic.Events; using Content.Shared.Maps; using Content.Shared.Mind; +using Content.Shared.Objectives.Systems; using Content.Shared.Physics; using Content.Shared.Popups; using Content.Shared.Speech.Muting; @@ -67,6 +68,7 @@ public abstract class SharedMagicSystem : EntitySystem [Dependency] private readonly TurfSystem _turf = default!; [Dependency] private readonly SharedChargesSystem _charges = default!; [Dependency] private readonly ExamineSystemShared _examine= default!; + [Dependency] private readonly TargetSystem _target = default!; private static readonly ProtoId InvalidForGlobalSpawnSpellTag = "InvalidForGlobalSpawnSpell"; @@ -472,7 +474,7 @@ public abstract class SharedMagicSystem : EntitySystem ev.Handled = true; - var allHumans = _mind.GetAliveHumans(); + var allHumans = _target.GetAliveHumans(); foreach (var human in allHumans) { diff --git a/Content.Shared/Mind/Filters/AliveAiPool.cs b/Content.Shared/Mind/Filters/AliveAiPool.cs new file mode 100644 index 0000000000..88fd557bbf --- /dev/null +++ b/Content.Shared/Mind/Filters/AliveAiPool.cs @@ -0,0 +1,16 @@ +using Content.Shared.Objectives.Systems; +using Content.Shared.Silicons.StationAi; + +namespace Content.Shared.Mind.Filters; + +/// +/// A mind pool that uses . +/// +public sealed partial class AliveAiPool : MindPool +{ + public override void FindMinds(HashSet> minds, EntityUid? exclude, IEntityManager entMan, TargetSystem targetSys) + { + var aiSys = entMan.System(); + aiSys.AddAliveAis(minds, exclude); + } +} diff --git a/Content.Shared/Mind/Filters/AliveHumansPool.cs b/Content.Shared/Mind/Filters/AliveHumansPool.cs index c8e5c55bae..1dfa2d2379 100644 --- a/Content.Shared/Mind/Filters/AliveHumansPool.cs +++ b/Content.Shared/Mind/Filters/AliveHumansPool.cs @@ -1,12 +1,14 @@ +using Content.Shared.Objectives.Systems; + namespace Content.Shared.Mind.Filters; /// -/// A mind pool that uses . +/// A mind pool that uses . /// -public sealed partial class AliveHumansPool : IMindPool +public sealed partial class AliveHumansPool : MindPool { - void IMindPool.FindMinds(HashSet> minds, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys) + public override void FindMinds(HashSet> minds, EntityUid? exclude, IEntityManager entMan, TargetSystem targetSystem) { - mindSys.AddAliveHumans(minds, exclude); + targetSystem.AddAliveHumans(minds, exclude); } } diff --git a/Content.Shared/Mind/Filters/AntagonistMindFilter.cs b/Content.Shared/Mind/Filters/AntagonistMindFilter.cs index d805138ac3..ff192698f5 100644 --- a/Content.Shared/Mind/Filters/AntagonistMindFilter.cs +++ b/Content.Shared/Mind/Filters/AntagonistMindFilter.cs @@ -7,7 +7,7 @@ namespace Content.Shared.Mind.Filters; /// public sealed partial class AntagonistMindFilter : MindFilter { - protected override bool ShouldRemove(Entity mind, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys) + protected override bool ShouldRemove(Entity mind, EntityUid? exclude, IEntityManager entMan) { var roleSys = entMan.System(); return !roleSys.MindIsAntagonist(mind); diff --git a/Content.Shared/Mind/Filters/BodyMindFilter.cs b/Content.Shared/Mind/Filters/BodyMindFilter.cs index 334539634f..0e471b0187 100644 --- a/Content.Shared/Mind/Filters/BodyMindFilter.cs +++ b/Content.Shared/Mind/Filters/BodyMindFilter.cs @@ -10,7 +10,7 @@ public sealed partial class BodyMindFilter : MindFilter [DataField(required: true)] public EntityWhitelist Whitelist = new(); - protected override bool ShouldRemove(Entity ent, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys) + protected override bool ShouldRemove(Entity ent, EntityUid? exclude, IEntityManager entMan) { if (ent.Comp.OwnedEntity is not {} mob) return true; diff --git a/Content.Shared/Mind/Filters/HasRoleMindFilter.cs b/Content.Shared/Mind/Filters/HasRoleMindFilter.cs index e8098d72a2..8e76e5a47e 100644 --- a/Content.Shared/Mind/Filters/HasRoleMindFilter.cs +++ b/Content.Shared/Mind/Filters/HasRoleMindFilter.cs @@ -14,7 +14,7 @@ public sealed partial class HasRoleMindFilter : MindFilter [DataField(required: true)] public EntityWhitelist Whitelist; - protected override bool ShouldRemove(Entity mind, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys) + protected override bool ShouldRemove(Entity mind, EntityUid? exclude, IEntityManager entMan) { var roleSys = entMan.System(); return !roleSys.MindHasRole(mind, Whitelist); diff --git a/Content.Shared/Mind/Filters/JobMindFilter.cs b/Content.Shared/Mind/Filters/JobMindFilter.cs index a6565e4d33..d3bec4d8d6 100644 --- a/Content.Shared/Mind/Filters/JobMindFilter.cs +++ b/Content.Shared/Mind/Filters/JobMindFilter.cs @@ -13,7 +13,7 @@ public sealed partial class JobMindFilter : MindFilter [DataField(required: true)] public ProtoId Job; - protected override bool ShouldRemove(Entity mind, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys) + protected override bool ShouldRemove(Entity mind, EntityUid? exclude, IEntityManager entMan) { var jobSys = entMan.System(); return jobSys.MindHasJobWithId(mind, Job); diff --git a/Content.Shared/Mind/Filters/MindFilter.cs b/Content.Shared/Mind/Filters/MindFilter.cs index c2daf3e361..47c8c4178b 100644 --- a/Content.Shared/Mind/Filters/MindFilter.cs +++ b/Content.Shared/Mind/Filters/MindFilter.cs @@ -3,25 +3,25 @@ using Robust.Shared.Serialization.Manager.Attributes; namespace Content.Shared.Mind.Filters; /// -/// A mind filter that can be used to filter out minds from a . +/// A mind filter that can be used to filter out minds from a . /// [ImplicitDataDefinitionForInheritors] public abstract partial class MindFilter { /// /// The actual filter function, this has to return false for minds that get removed from the pool. - /// An excluded mind will be the same one passed to . + /// An excluded mind will be the same one passed to . /// /// The mind to check /// The same mind passed to FindMinds - protected abstract bool ShouldRemove(Entity mind, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys); + protected abstract bool ShouldRemove(Entity mind, EntityUid? exclude, IEntityManager entMan); /// /// The high-level filter function to be used by the mind system. /// - public bool Filter(Entity mind, EntityUid? exclude, EntityManager entMan, SharedMindSystem mindSys) + public bool Filter(Entity mind, EntityUid? exclude, EntityManager entMan) { - return ShouldRemove(mind, exclude, entMan, mindSys) ^ Inverted; + return ShouldRemove(mind, exclude, entMan) ^ Inverted; } /// diff --git a/Content.Shared/Mind/Filters/IMindPool.cs b/Content.Shared/Mind/Filters/MindPool.cs similarity index 51% rename from Content.Shared/Mind/Filters/IMindPool.cs rename to Content.Shared/Mind/Filters/MindPool.cs index 263d15d812..0886e0716d 100644 --- a/Content.Shared/Mind/Filters/IMindPool.cs +++ b/Content.Shared/Mind/Filters/MindPool.cs @@ -1,13 +1,13 @@ -using Robust.Shared.Serialization.Manager.Attributes; +using Content.Shared.Objectives.Systems; namespace Content.Shared.Mind.Filters; /// /// A mind pool that can find minds to use for objectives etc. -/// Further filtered by . +/// Further filtered by . /// [ImplicitDataDefinitionForInheritors] -public partial interface IMindPool +public abstract partial class MindPool { /// /// Add minds for this pool to a hashset. @@ -15,5 +15,7 @@ public partial interface IMindPool /// /// The hashset to add to /// A mind entity that must not be returned - void FindMinds(HashSet> minds, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys); + /// entity Manager for further control + /// targeting system which explicitly searches for targets. + public abstract void FindMinds(HashSet> minds, EntityUid? exclude, IEntityManager entMan, TargetSystem targetSys); } diff --git a/Content.Shared/Mind/Filters/ObjectiveMindFilter.cs b/Content.Shared/Mind/Filters/ObjectiveMindFilter.cs index 5b64fc36c6..275f49872d 100644 --- a/Content.Shared/Mind/Filters/ObjectiveMindFilter.cs +++ b/Content.Shared/Mind/Filters/ObjectiveMindFilter.cs @@ -10,7 +10,7 @@ public sealed partial class ObjectiveMindFilter : MindFilter [DataField(required: true)] public EntityWhitelist Blacklist = new(); - protected override bool ShouldRemove(Entity mind, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys) + protected override bool ShouldRemove(Entity mind, EntityUid? exclude, IEntityManager entMan) { var whitelistSys = entMan.System(); foreach (var obj in mind.Comp.Objectives) diff --git a/Content.Shared/Mind/SharedMindSystem.cs b/Content.Shared/Mind/SharedMindSystem.cs index a21642c4ec..8571c35aa2 100644 --- a/Content.Shared/Mind/SharedMindSystem.cs +++ b/Content.Shared/Mind/SharedMindSystem.cs @@ -14,6 +14,7 @@ using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Objectives.Systems; using Content.Shared.Players; +using Content.Shared.Silicons.StationAi; using Content.Shared.Speech; using Content.Shared.Whitelist; using Robust.Shared.Containers; @@ -32,20 +33,17 @@ namespace Content.Shared.Mind; public abstract partial class SharedMindSystem : EntitySystem { [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; - [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; + [Dependency] private readonly MetaDataSystem _metadata = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedObjectivesSystem _objectives = default!; - [Dependency] private readonly SharedPlayerSystem _player = default!; - [Dependency] private readonly MetaDataSystem _metadata = default!; - [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedPlayerSystem _player = default!; [ViewVariables] protected readonly Dictionary UserMinds = new(); - private HashSet> _pickingMinds = new(); - private readonly EntProtoId _mindProto = "MindBase"; public override void Initialize() @@ -595,57 +593,12 @@ public abstract partial class SharedMindSystem : EntitySystem return TryGetMind(userId, out _, out var mind) ? mind.CharacterName : null; } - /// - /// Returns a list of every living humanoid player's minds, except for a single one which is exluded. - /// A new hashset is allocated for every call, consider using instead. - /// - public HashSet> GetAliveHumans(EntityUid? exclude = null) - { - var allHumans = new HashSet>(); - AddAliveHumans(allHumans, exclude); - return allHumans; - } - - /// - /// Adds to a hashset every living humanoid player's minds, except for a single one which is exluded. - /// - public void AddAliveHumans(HashSet> allHumans, EntityUid? exclude = null) - { - // HumanoidProfileComponent is used to prevent mice, pAIs, etc from being chosen - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out _, out var mobState)) - { - // the player needs to have a mind and not be the excluded one + - // the player has to be alive - if (!TryGetMind(uid, out var mind, out var mindComp) || mind == exclude || !_mobState.IsAlive(uid, mobState)) - continue; - - allHumans.Add((mind, mindComp)); - } - } - - /// - /// Picks a random mind from a pool after applying a list of filters. - /// Returns null if no valid mind could be found. - /// - public Entity? PickFromPool(IMindPool pool, List filters, EntityUid? exclude = null) - { - _pickingMinds.Clear(); - pool.FindMinds(_pickingMinds, exclude, EntityManager, this); - FilterMinds(_pickingMinds, filters, exclude); - - if (_pickingMinds.Count == 0) - return null; - - return _random.Pick(_pickingMinds); - } - /// /// Filters minds from a hashset using a single . /// public void FilterMinds(HashSet> minds, MindFilter filter, EntityUid? exclude = null) { - minds.RemoveWhere(mind => filter.Filter(mind, exclude, EntityManager, this)); + minds.RemoveWhere(mind => filter.Filter(mind, exclude, EntityManager)); } /// diff --git a/Content.Shared/Objectives/Systems/TargetSystem.cs b/Content.Shared/Objectives/Systems/TargetSystem.cs new file mode 100644 index 0000000000..215d87d62f --- /dev/null +++ b/Content.Shared/Objectives/Systems/TargetSystem.cs @@ -0,0 +1,66 @@ +using Content.Shared.Humanoid; +using Content.Shared.Mind; +using Content.Shared.Mind.Filters; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Robust.Shared.Random; + +namespace Content.Shared.Objectives.Systems; + +/// +/// This system stores enumerators to find valid Targets, typically searching for minds. +/// Typically used in conjunction with a or an Objective. +/// +public sealed class TargetSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + + private HashSet> _pickingMinds = new(); + + /// + /// Returns a list of every living humanoid player's minds, except for a single one which is exluded. + /// A new hashset is allocated for every call, consider using instead. + /// + public HashSet> GetAliveHumans(EntityUid? exclude = null) + { + var allHumans = new HashSet>(); + AddAliveHumans(allHumans, exclude); + return allHumans; + } + + /// + /// Adds to a hashset every living humanoid player's minds, except for a single one which is exluded. + /// + public void AddAliveHumans(HashSet> allHumans, EntityUid? exclude = null) + { + // HumanoidProfileComponent is used to prevent mice, pAIs, etc from being chosen + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _, out var mobState)) + { + // the player needs to have a mind and not be the excluded one + + // the player has to be alive + if (!_mind.TryGetMind(uid, out var mind, out var mindComp) || mind == exclude || !_mobState.IsAlive(uid, mobState)) + continue; + + allHumans.Add((mind, mindComp)); + } + } + + /// + /// Picks a random mind from a pool after applying a list of filters. + /// Returns null if no valid mind could be found. + /// + public Entity? PickFromPool(MindPool pool, List filters, EntityUid? exclude = null) + { + _pickingMinds.Clear(); + pool.FindMinds(_pickingMinds, exclude, EntityManager, this); + _mind.FilterMinds(_pickingMinds, filters, exclude); + + if (_pickingMinds.Count == 0) + return null; + + return _random.Pick(_pickingMinds); + } +} diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs index fde4952f86..cd801511ca 100644 --- a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs +++ b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs @@ -623,6 +623,29 @@ public abstract partial class SharedStationAiSystem : EntitySystem return _blocker.CanComplexInteract(entity.Owner); } + + /// + /// Gets all alive AI minds and adds them to the inputted hashset, excluding one optional mind + /// + /// Hashset of alive AI minds + /// Optional mind to exclude + public void AddAliveAis(HashSet> aliveAis, EntityUid? exclude = null) + { + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out _, out var aiHolder)) + { + // the player needs to have a mind and not be the excluded one + + // the player has to be alive + if (!TryGetHeld((uid, aiHolder), out var held) || _mobState.IsDead(held.Value)) + continue; + + if (!_mind.TryGetMind(held.Value, out var mind, out var mindComp) || mind == exclude) + continue; + + aliveAis.Add((mind, mindComp)); + } + } } public sealed partial class JumpToCoreEvent : InstantActionEvent diff --git a/Resources/Locale/en-US/objectives/conditions/kill-person.ftl b/Resources/Locale/en-US/objectives/conditions/kill-person.ftl index aad31d26f9..c5ea6f47fc 100644 --- a/Resources/Locale/en-US/objectives/conditions/kill-person.ftl +++ b/Resources/Locale/en-US/objectives/conditions/kill-person.ftl @@ -1,3 +1,4 @@ objective-condition-kill-person-title = Kill or maroon {$targetName}, {CAPITALIZE($job)} objective-condition-kill-maroon-title = Kill and maroon {$targetName}, {CAPITALIZE($job)} +objective-condition-kill-station-ai = Destroy {$targetName}, {CAPITALIZE($job)} and ensure they remain out of commission. objective-condition-maroon-person-title = Prevent {$targetName}, {CAPITALIZE($job)} from reaching CentComm. diff --git a/Resources/Prototypes/Objectives/objectiveGroups.yml b/Resources/Prototypes/Objectives/objectiveGroups.yml index bbdd6fa236..0480a5c9ad 100644 --- a/Resources/Prototypes/Objectives/objectiveGroups.yml +++ b/Resources/Prototypes/Objectives/objectiveGroups.yml @@ -29,6 +29,7 @@ id: TraitorObjectiveGroupKill weights: KillRandomPersonObjective: 1 + KillStationAiObjective: 0.25 KillRandomHeadObjective: 0.25 - type: weightedRandom diff --git a/Resources/Prototypes/Objectives/traitor.yml b/Resources/Prototypes/Objectives/traitor.yml index 9ffc0146a2..f8cc42a222 100644 --- a/Resources/Prototypes/Objectives/traitor.yml +++ b/Resources/Prototypes/Objectives/traitor.yml @@ -131,6 +131,31 @@ requireDead: true requireMaroon: true +- type: entity + parent: [BaseTraitorObjective, BaseKillObjective] + id: KillStationAiObjective + description: Nanotrasen proudly boasts about their state of the art artificial intelligence. Remind them it's just another toy that can be broken. + components: + - type: Objective + difficulty: 3.0 + # there's probably only one AI + unique: true + icon: + sprite: Mobs/Silicon/station_ai.rsi + state: ai_dead # TODO: Change this to broken when we have a better objectives UI, broken sprite is currently too big! + - type: TargetObjective + title: objective-condition-kill-station-ai + - type: PickRandomPerson + pool: !type:AliveAiPool + filters: + # Can't have multiple objectives to kill the same person. + - !type:TargetObjectiveMindFilter + blacklist: + components: + - KillPersonCondition + - type: KillPersonCondition + requireDead: true + # social - type: entity diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/broken.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/broken.png new file mode 100644 index 0000000000000000000000000000000000000000..02fc551ed7cab687da49589bbc75373441b8effd GIT binary patch literal 8017 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 Qn-hT1H91zG?Qr3L02Up4Z2$lO literal 0 HcmV?d00001 diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/meta.json b/Resources/Textures/Mobs/Silicon/station_ai.rsi/meta.json index 55bd389b63..5c389a8da4 100644 --- a/Resources/Textures/Mobs/Silicon/station_ai.rsi/meta.json +++ b/Resources/Textures/Mobs/Silicon/station_ai.rsi/meta.json @@ -253,6 +253,9 @@ }, { "name": "frame_4" + }, + { + "name": "broken" } ] } From a07bd251b478520f8add32c0c47a9b5fc83fca1c Mon Sep 17 00:00:00 2001 From: PJBot Date: Mon, 6 Apr 2026 22:25:02 +0000 Subject: [PATCH 104/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 4a704decc0..2552eaea39 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: MilenVolf - changes: - - message: Go go hat's activation phrase can be reset to the default without having - to record it manually to reset the phrase. - type: Tweak - id: 9112 - time: '2025-10-16T18:47:31.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/35636 - author: Wolfkey-SomeoneElseTookMyUsername changes: - message: The recipes for grilled cheese, cotton buns, cotton cakes, and cotton @@ -4031,3 +4023,13 @@ id: 9623 time: '2026-04-06T21:41:16.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/39767 +- author: Princess-Cheeseballs + changes: + - message: Added a new Traitor objective to kill the Station AI! + type: Add + - message: Traitor kill objectives will now show stage names instead of character + names if applicable + type: Fix + id: 9624 + time: '2026-04-06T22:23:53.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43462 From 6789f1ee88e1045926700fba5d337304502e2fdf Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> Date: Mon, 6 Apr 2026 19:21:29 -0700 Subject: [PATCH 105/126] shared... --- .../Implants/SubdermalImplantSystem.cs | 41 +--- Content.Server/PDA/Ringer/RingerSystem.cs | 3 +- .../Store/Systems/StoreSystem.Listings.cs | 172 ---------------- .../Store/Systems/StoreSystem.Refund.cs | 3 + .../Store/Systems/StoreSystem.Ui.cs | 113 +---------- Content.Server/Store/Systems/StoreSystem.cs | 144 +------------ Content.Server/Traitor/Uplink/UplinkSystem.cs | 3 +- .../SharedSubdermalImplantSystem.Relays.cs | 29 ++- .../Store/Components/CurrencyComponent.cs | 3 +- .../Store/SharedStoreSystem.Listings.cs | 184 +++++++++++++++++ Content.Shared/Store/SharedStoreSystem.UI.cs | 105 ++++++++++ Content.Shared/Store/SharedStoreSystem.cs | 191 +++++++++++++++++- 12 files changed, 522 insertions(+), 469 deletions(-) rename {Content.Server => Content.Shared}/Store/Components/CurrencyComponent.cs (95%) create mode 100644 Content.Shared/Store/SharedStoreSystem.Listings.cs create mode 100644 Content.Shared/Store/SharedStoreSystem.UI.cs diff --git a/Content.Server/Implants/SubdermalImplantSystem.cs b/Content.Server/Implants/SubdermalImplantSystem.cs index 582b9cb2ac..08fe5b54cf 100644 --- a/Content.Server/Implants/SubdermalImplantSystem.cs +++ b/Content.Server/Implants/SubdermalImplantSystem.cs @@ -1,44 +1,5 @@ -using Content.Server.Store.Components; -using Content.Server.Store.Systems; using Content.Shared.Implants; -using Content.Shared.Interaction; -using Content.Shared.Popups; -using Content.Shared.Store.Components; namespace Content.Server.Implants; -public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem -{ - [Dependency] private readonly StoreSystem _store = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent>(OnStoreRelay); - } - - // TODO: This shouldn't be in the SubdermalImplantSystem - private void OnStoreRelay(EntityUid uid, StoreComponent store, ImplantRelayEvent implantRelay) - { - var args = implantRelay.Event; - - if (args.Handled) - return; - - // can only insert into yourself to prevent uplink checking with renault - if (args.Target != args.User) - return; - - if (!TryComp(args.Used, out var currency)) - return; - - // same as store code, but message is only shown to yourself - if (!_store.TryAddCurrency((args.Used, currency), (uid, store))) - return; - - args.Handled = true; - var msg = Loc.GetString("store-currency-inserted-implant", ("used", args.Used)); - _popup.PopupEntity(msg, args.User, args.User); - } -} +public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem; diff --git a/Content.Server/PDA/Ringer/RingerSystem.cs b/Content.Server/PDA/Ringer/RingerSystem.cs index 3efb6cbfdd..a86b44d389 100644 --- a/Content.Server/PDA/Ringer/RingerSystem.cs +++ b/Content.Server/PDA/Ringer/RingerSystem.cs @@ -1,10 +1,9 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; -using Content.Server.Store.Systems; using Content.Shared.GameTicking; using Content.Shared.PDA; using Content.Shared.PDA.Ringer; -using Content.Shared.Store.Components; +using Content.Shared.Store; using Robust.Shared.Random; using Robust.Shared.Utility; diff --git a/Content.Server/Store/Systems/StoreSystem.Listings.cs b/Content.Server/Store/Systems/StoreSystem.Listings.cs index 412114ce03..79e0683738 100644 --- a/Content.Server/Store/Systems/StoreSystem.Listings.cs +++ b/Content.Server/Store/Systems/StoreSystem.Listings.cs @@ -8,177 +8,5 @@ namespace Content.Server.Store.Systems; public sealed partial class StoreSystem { - /// - /// Refreshes all listings on a store. - /// Do not use if you don't know what you're doing. - /// - /// The store to refresh - public void RefreshAllListings(StoreComponent component) - { - var previousState = component.FullListingsCatalog; - var newState = GetAllListings(); - // if we refresh list with existing cost modifiers - they will be removed, - // need to restore them - if (previousState.Count != 0) - { - foreach (var previousStateListingItem in previousState) - { - if (!previousStateListingItem.IsCostModified - || !TryGetListing(newState, previousStateListingItem.ID, out var found)) - { - continue; - } - foreach (var (modifierSourceId, costModifier) in previousStateListingItem.CostModifiersBySourceId) - { - found.AddCostModifier(modifierSourceId, costModifier); - } - } - } - - component.FullListingsCatalog = newState; - } - - /// - /// Gets all listings from a prototype. - /// - /// All the listings - public HashSet GetAllListings() - { - var clones = new HashSet(); - foreach (var prototype in _proto.EnumeratePrototypes()) - { - clones.Add(new ListingDataWithCostModifiers(prototype)); - } - - return clones; - } - - /// - /// Adds a listing from an Id to a store - /// - /// The store to add the listing to - /// The id of the listing - /// Whether or not the listing was added successfully - public bool TryAddListing(StoreComponent component, string listingId) - { - if (!_proto.TryIndex(listingId, out var proto)) - { - Log.Error("Attempted to add invalid listing."); - return false; - } - - return TryAddListing(component, proto); - } - - /// - /// Adds a listing to a store - /// - /// The store to add the listing to - /// The listing - /// Whether or not the listing was add successfully - public bool TryAddListing(StoreComponent component, ListingPrototype listing) - { - return component.FullListingsCatalog.Add(new ListingDataWithCostModifiers(listing)); - } - - /// - /// Gets the available listings for a store - /// - /// Either the account owner, user, or an inanimate object (e.g., surplus bundle) - /// - /// The store the listings are coming from. - /// The available listings. - public IEnumerable GetAvailableListings(EntityUid buyer, EntityUid store, StoreComponent component) - { - return GetAvailableListings(buyer, component.FullListingsCatalog, component.Categories, store); - } - - /// - /// Gets the available listings for a user given an overall set of listings and categories to filter by. - /// - /// Either the account owner, user, or an inanimate object (e.g., surplus bundle) - /// All of the listings that are available. If null, will just get all listings from the prototypes. - /// What categories to filter by. - /// The physial entity of the store. Can be null. - /// The available listings. - public IEnumerable GetAvailableListings( - EntityUid buyer, - IReadOnlyCollection? listings, - HashSet> categories, - EntityUid? storeEntity = null - ) - { - listings ??= GetAllListings(); - - foreach (var listing in listings) - { - if (!ListingHasCategory(listing, categories)) - continue; - - if (listing.Conditions != null) - { - var args = new ListingConditionArgs(GetBuyerMind(buyer), storeEntity, listing, EntityManager); - var conditionsMet = true; - - foreach (var condition in listing.Conditions) - { - if (!condition.Condition(args)) - { - conditionsMet = false; - break; - } - } - - if (!conditionsMet) - continue; - } - - yield return listing; - } - } - - /// - /// Returns the entity's mind entity, if it has one, to be used for listing conditions. - /// If it doesn't have one, or is a mind entity already, it returns itself. - /// - /// The buying entity. - public EntityUid GetBuyerMind(EntityUid buyer) - { - if (!HasComp(buyer) && _mind.TryGetMind(buyer, out var buyerMind, out var _)) - return buyerMind; - - return buyer; - } - - /// - /// Checks if a listing appears in a list of given categories - /// - /// The listing itself. - /// The categories to check through. - /// If the listing was present in one of the categories. - public bool ListingHasCategory(ListingData listing, HashSet> categories) - { - foreach (var cat in categories) - { - if (listing.Categories.Contains(cat)) - return true; - } - return false; - } - - private bool TryGetListing(IReadOnlyCollection collection, string listingId, [MaybeNullWhen(false)] out ListingDataWithCostModifiers found) - { - foreach(var current in collection) - { - if (current.ID == listingId) - { - found = current; - return true; - } - } - - found = null!; - return false; - } } diff --git a/Content.Server/Store/Systems/StoreSystem.Refund.cs b/Content.Server/Store/Systems/StoreSystem.Refund.cs index cb92a42c10..f4ecd87040 100644 --- a/Content.Server/Store/Systems/StoreSystem.Refund.cs +++ b/Content.Server/Store/Systems/StoreSystem.Refund.cs @@ -4,11 +4,14 @@ using Content.Shared.Interaction.Events; using Content.Shared.Store.Components; using Content.Shared.Weapons.Ranged.Systems; using Robust.Shared.Containers; +using Robust.Shared.Timing; namespace Content.Server.Store.Systems; public sealed partial class StoreSystem { + [Dependency] private readonly IGameTiming _timing = default!; + private void InitializeRefund() { SubscribeLocalEvent(OnStoreTerminating); diff --git a/Content.Server/Store/Systems/StoreSystem.Ui.cs b/Content.Server/Store/Systems/StoreSystem.Ui.cs index ccf7c3f36c..7a87f8b162 100644 --- a/Content.Server/Store/Systems/StoreSystem.Ui.cs +++ b/Content.Server/Store/Systems/StoreSystem.Ui.cs @@ -7,32 +7,25 @@ using Content.Shared.Actions; using Content.Shared.Database; using Content.Shared.FixedPoint; using Content.Shared.Hands.EntitySystems; -using Content.Shared.Mind; using Content.Shared.Mindshield.Components; using Content.Shared.NPC.Systems; -using Content.Shared.PDA.Ringer; using Content.Shared.Store; using Content.Shared.Store.Components; using Content.Shared.UserInterface; -using Robust.Server.GameObjects; using Robust.Shared.Audio.Systems; -using Robust.Shared.Player; -using Robust.Shared.Prototypes; namespace Content.Server.Store.Systems; public sealed partial class StoreSystem { [Dependency] private readonly IAdminLogManager _admin = default!; - [Dependency] private readonly SharedHandsSystem _hands = default!; - [Dependency] private readonly ActionsSystem _actions = default!; [Dependency] private readonly ActionContainerSystem _actionContainer = default!; + [Dependency] private readonly ActionsSystem _actions = default!; [Dependency] private readonly ActionUpgradeSystem _actionUpgrade = default!; - [Dependency] private readonly SharedMindSystem _mind = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly StackSystem _stack = default!; - [Dependency] private readonly UserInterfaceSystem _ui = default!; [Dependency] private readonly NpcFactionSystem _npcFaction = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly StackSystem _stack = default!; private void InitializeUi() { @@ -66,98 +59,6 @@ public sealed partial class StoreSystem RaiseLocalEvent(entity.Comp.Store.Value, ev); } - /// - /// Toggles the store Ui open and closed - /// - /// the person doing the toggling - /// the store being toggled - /// - /// The entity remotely accessing the store, if any. - /// The remote access component, if any. - public void ToggleUi(EntityUid user, EntityUid storeEnt, StoreComponent? component = null, EntityUid? remoteAccess = null, RemoteStoreComponent? remoteComponent = null) - { - if (!Resolve(storeEnt, ref component)) - return; - - if (remoteAccess != null && !Resolve(remoteAccess.Value, ref remoteComponent) && remoteComponent!.Store != storeEnt) - return; - - if (!TryComp(user, out var actor)) - return; - - if (!_ui.TryToggleUi(remoteAccess != null ? remoteAccess.Value : storeEnt, StoreUiKey.Key, actor.PlayerSession)) - return; - - UpdateUserInterface(user, storeEnt, component); - } - - /// - /// Closes the store UI for everyone, if it's open - /// - public void CloseUi(EntityUid uid, StoreComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - _ui.CloseUi(uid, StoreUiKey.Key); - } - - /// - /// Updates the user interface for a store and refreshes the listings - /// - /// The person who if opening the store ui. Listings are filtered based on this. - /// The store entity itself - /// The store component being refreshed. - public void UpdateUserInterface(EntityUid? user, EntityUid store, StoreComponent? component = null) - { - if (!Resolve(store, ref component)) - return; - - //this is the person who will be passed into logic for all listing filtering. - if (user != null) //if we have no "buyer" for this update, then don't update the listings - { - component.LastAvailableListings = GetAvailableListings(component.AccountOwner ?? user.Value, store, component) - .ToHashSet(); - } - - //dictionary for all currencies, including 0 values for currencies on the whitelist - Dictionary, FixedPoint2> allCurrency = new(); - foreach (var supported in component.CurrencyWhitelist) - { - allCurrency.Add(supported, FixedPoint2.Zero); - - if (component.Balance.TryGetValue(supported, out var value)) - allCurrency[supported] = value; - } - - // TODO: if multiple users are supposed to be able to interact with a single BUI & see different - // stores/listings, this needs to use session specific BUI states. - - // only tell operatives to lock their uplink if it can be locked - var showFooter = HasComp(store); - - var state = new StoreUpdateState(component.LastAvailableListings, allCurrency, showFooter, component.RefundAllowed); - UpdateRemoteStores(store, state); - _ui.SetUiState(store, StoreUiKey.Key, state); - } - - /// - /// Updates any remote store connections to a specific store. - /// - /// The store being updated. - /// The state being applied. - public void UpdateRemoteStores(EntityUid store, StoreUpdateState state) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var remote, out var ui)) - { - if (remote.Store != store) - continue; - - _ui.SetUiState((uid, ui), StoreUiKey.Key, state); - } - } - private void OnRequestUpdate(EntityUid uid, StoreComponent component, StoreRequestUpdateInterfaceMessage args) { UpdateUserInterface(args.Actor, GetEntity(args.Entity), component); @@ -246,7 +147,7 @@ public sealed partial class StoreSystem EntityUid? actionId; // I guess we just allow duplicate actions? // Allow duplicate actions and just have a single list buy for the buy-once ones. - if (listing.ApplyToMob || !_mind.TryGetMind(buyer, out var mind, out _)) + if (listing.ApplyToMob || !Mind.TryGetMind(buyer, out var mind, out _)) actionId = _actions.AddAction(buyer, listing.ProductAction); else actionId = _actionContainer.AddAction(mind, listing.ProductAction); @@ -322,7 +223,7 @@ public sealed partial class StoreSystem _admin.Add(LogType.StorePurchase, logImpact, - $"{ToPrettyString(buyer):player} purchased listing \"{ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, _proto)}\" from {ToPrettyString(uid)}{logExtraInfo}."); + $"{ToPrettyString(buyer):player} purchased listing \"{ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, Proto)}\" from {ToPrettyString(uid)}{logExtraInfo}."); listing.PurchaseAmount++; //track how many times something has been purchased if (msg.SoundSource != null && GetEntity(msg.SoundSource) != null) @@ -355,7 +256,7 @@ public sealed partial class StoreSystem return; //make sure a malicious client didn't send us random shit - if (!_proto.TryIndex(msg.Currency, out var proto)) + if (!Proto.TryIndex(msg.Currency, out var proto)) return; //we need an actually valid entity to spawn. This check has been done earlier, but just in case. diff --git a/Content.Server/Store/Systems/StoreSystem.cs b/Content.Server/Store/Systems/StoreSystem.cs index 63c6d48b0b..b8e92f10bf 100644 --- a/Content.Server/Store/Systems/StoreSystem.cs +++ b/Content.Server/Store/Systems/StoreSystem.cs @@ -1,39 +1,23 @@ -using System.Linq; -using Content.Server.Store.Components; -using Content.Shared.FixedPoint; using Content.Shared.Implants.Components; -using Content.Shared.Interaction; -using Content.Shared.Popups; -using Content.Shared.Stacks; using Content.Shared.Store; using Content.Shared.Store.Components; -using Content.Shared.Store.Events; using Content.Shared.UserInterface; -using Robust.Shared.Prototypes; -using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Server.Store.Systems; public sealed partial class StoreSystem : SharedStoreSystem { - [Dependency] private readonly IPrototypeManager _proto = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; - public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnStoreOpenAttempt); - SubscribeLocalEvent(OnAfterInteract); SubscribeLocalEvent(BeforeActivatableUiOpen); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnShutdown); - SubscribeLocalEvent(OnIntrinsicStoreAction); SubscribeLocalEvent(OnImplantActivate); @@ -48,8 +32,8 @@ public sealed partial class StoreSystem : SharedStoreSystem component.StartingMap = Transform(uid).MapUid; // Add the bui key if it does not exist already (the check is needed to make sure that we don't overwrite existing InterfaceData). - if (!_uiSystem.HasUi(uid, StoreUiKey.Key)) - _uiSystem.SetUi(uid, StoreUiKey.Key, new InterfaceData("StoreBoundUserInterface")); + if (!UI.HasUi(uid, StoreUiKey.Key)) + UI.SetUi(uid, StoreUiKey.Key, new InterfaceData("StoreBoundUserInterface")); } private void OnStartup(EntityUid uid, StoreComponent component, ComponentStartup args) @@ -75,7 +59,7 @@ public sealed partial class StoreSystem : SharedStoreSystem if (!component.OwnerOnly) return; - if (!_mind.TryGetMind(args.User, out var mind, out _)) + if (!Mind.TryGetMind(args.User, out var mind, out _)) return; component.AccountOwner ??= mind; @@ -85,32 +69,11 @@ public sealed partial class StoreSystem : SharedStoreSystem return; if (!args.Silent) - _popup.PopupEntity(Loc.GetString("store-not-account-owner", ("store", uid)), uid, args.User); + Popup.PopupEntity(Loc.GetString("store-not-account-owner", ("store", uid)), uid, args.User); args.Cancel(); } - private void OnAfterInteract(EntityUid uid, CurrencyComponent component, AfterInteractEvent args) - { - if (args.Handled || !args.CanReach || args.Target is not { } target) - return; - - if (!TryGetStore(target, out var store)) - return; - - var ev = new CurrencyInsertAttemptEvent(args.User, target, args.Used, store.Value.Comp); - RaiseLocalEvent(target, ev); - if (ev.Cancelled) - return; - - if (!TryAddCurrency((uid, component), (store.Value, store.Value.Comp))) - return; - - args.Handled = true; - var msg = Loc.GetString("store-currency-inserted", ("used", args.Used), ("target", target)); - _popup.PopupEntity(msg, target, args.User); - } - private void OnImplantActivate(Entity entity, ref OpenUplinkImplantEvent args) { if (GetRemoteStore(entity.AsNullable()) is not { } store) @@ -118,103 +81,4 @@ public sealed partial class StoreSystem : SharedStoreSystem ToggleUi(args.Performer, store, store.Comp, entity, entity.Comp); } - - /// - /// Gets the value from an entity's currency component. - /// Scales with stacks. - /// - /// - /// If this result is intended to be used with , - /// consider using instead to ensure that the currency is consumed in the process. - /// - /// - /// - /// The value of the currency - public Dictionary GetCurrencyValue(EntityUid uid, CurrencyComponent component) - { - var amount = EntityManager.GetComponentOrNull(uid)?.Count ?? 1; - return component.Price.ToDictionary(v => v.Key, p => p.Value * amount); - } - - /// - /// Tries to add a currency to a store's balance. Note that if successful, this will consume the currency in the process. - /// - public bool TryAddCurrency(Entity currency, Entity store) - { - if (!Resolve(currency.Owner, ref currency.Comp)) - return false; - - if (!Resolve(store.Owner, ref store.Comp)) - return false; - - var value = currency.Comp.Price; - if (TryComp(currency.Owner, out StackComponent? stack) && stack.Count != 1) - { - value = currency.Comp.Price - .ToDictionary(v => v.Key, p => p.Value * stack.Count); - } - - if (!TryAddCurrency(value, store, store.Comp)) - return false; - - // Avoid having the currency accidentally be re-used. E.g., if multiple clients try to use the currency in the - // same tick - currency.Comp.Price.Clear(); - if (stack != null) - _stack.SetCount((currency.Owner, stack), 0); - - QueueDel(currency); - return true; - } - - /// - /// Tries to add a currency to a store's balance - /// - /// The value to add to the store - /// - /// The store to add it to - /// Whether or not the currency was succesfully added - public bool TryAddCurrency(Dictionary currency, EntityUid uid, StoreComponent? store = null) - { - if (!Resolve(uid, ref store)) - return false; - - //verify these before values are modified - foreach (var type in currency) - { - if (!store.CurrencyWhitelist.Contains(type.Key)) - return false; - } - - foreach (var type in currency) - { - if (!store.Balance.TryAdd(type.Key, type.Value)) - store.Balance[type.Key] += type.Value; - } - - UpdateUserInterface(null, uid, store); - return true; - } - - private void OnIntrinsicStoreAction(Entity ent, ref IntrinsicStoreActionEvent args) - { - ToggleUi(args.Performer, ent.Owner, ent.Comp); - } - -} - -public sealed class CurrencyInsertAttemptEvent : CancellableEntityEventArgs -{ - public readonly EntityUid User; - public readonly EntityUid Target; - public readonly EntityUid Used; - public readonly StoreComponent Store; - - public CurrencyInsertAttemptEvent(EntityUid user, EntityUid target, EntityUid used, StoreComponent store) - { - User = user; - Target = target; - Used = used; - Store = store; - } } diff --git a/Content.Server/Traitor/Uplink/UplinkSystem.cs b/Content.Server/Traitor/Uplink/UplinkSystem.cs index c5c66e1a40..1daee849dd 100644 --- a/Content.Server/Traitor/Uplink/UplinkSystem.cs +++ b/Content.Server/Traitor/Uplink/UplinkSystem.cs @@ -53,7 +53,8 @@ public sealed class UplinkSystem : EntitySystem } // If we didn't have an uplink, make an empty one. - SetUplink(args.Implanted, Spawn(TraitorUplinkStore, MapCoordinates.Nullspace), 0, false); + entity.Comp.Store = Spawn(TraitorUplinkStore, MapCoordinates.Nullspace); + SetUplink(args.Implanted, entity.Comp.Store.Value, 0, false); Log.Error($"{ToPrettyString(args.Implanted)} did not have an uplink when they were implanted."); } diff --git a/Content.Shared/Implants/SharedSubdermalImplantSystem.Relays.cs b/Content.Shared/Implants/SharedSubdermalImplantSystem.Relays.cs index 24b76e15c5..59e2d1baa1 100644 --- a/Content.Shared/Implants/SharedSubdermalImplantSystem.Relays.cs +++ b/Content.Shared/Implants/SharedSubdermalImplantSystem.Relays.cs @@ -1,9 +1,9 @@ using Content.Shared.Chat; using Content.Shared.IdentityManagement.Components; using Content.Shared.Implants.Components; -using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Mobs; +using Content.Shared.Store; namespace Content.Shared.Implants; @@ -12,11 +12,14 @@ public abstract partial class SharedSubdermalImplantSystem public void InitializeRelay() { SubscribeLocalEvent(RelayToImplantEvent); - SubscribeLocalEvent(RelayToImplantEvent); SubscribeLocalEvent(RelayToImplantEvent); SubscribeLocalEvent(RelayToImplantEvent); SubscribeLocalEvent(RelayToImplantEvent); SubscribeLocalEvent(RelayToImplantEvent); + + // Ref relays, for when you need to write to the event! + SubscribeLocalEvent(RefRelayToImplantEvent); + SubscribeLocalEvent(RefRelayToImplantEvent); } /// @@ -36,6 +39,26 @@ public abstract partial class SharedSubdermalImplantSystem RaiseLocalEvent(implant, relayEv); } } + + /// + /// Relays events from the implanted to the implant. + /// + private void RefRelayToImplantEvent(Entity entity, ref T args) where T : notnull + { + if (!_container.TryGetContainer(entity, ImplanterComponent.ImplantSlotId, out var implantContainer)) + return; + + var relayEv = new ImplantRelayEvent(args, entity); + foreach (var implant in implantContainer.ContainedEntities) + { + if (args is HandledEntityEventArgs { Handled: true }) + return; + + RaiseLocalEvent(implant, relayEv); + } + + args = relayEv.Event; + } } /// @@ -43,7 +66,7 @@ public abstract partial class SharedSubdermalImplantSystem /// public sealed class ImplantRelayEvent where T : notnull { - public readonly T Event; + public T Event; public readonly EntityUid ImplantedEntity; diff --git a/Content.Server/Store/Components/CurrencyComponent.cs b/Content.Shared/Store/Components/CurrencyComponent.cs similarity index 95% rename from Content.Server/Store/Components/CurrencyComponent.cs rename to Content.Shared/Store/Components/CurrencyComponent.cs index eb4ca1c0e3..a4c5b3c027 100644 --- a/Content.Server/Store/Components/CurrencyComponent.cs +++ b/Content.Shared/Store/Components/CurrencyComponent.cs @@ -1,8 +1,7 @@ using Content.Shared.FixedPoint; -using Content.Shared.Store; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; -namespace Content.Server.Store.Components; +namespace Content.Shared.Store.Components; /// /// Identifies a component that can be inserted into a store diff --git a/Content.Shared/Store/SharedStoreSystem.Listings.cs b/Content.Shared/Store/SharedStoreSystem.Listings.cs new file mode 100644 index 0000000000..fa0f29bb4c --- /dev/null +++ b/Content.Shared/Store/SharedStoreSystem.Listings.cs @@ -0,0 +1,184 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Mind; +using Content.Shared.Store.Components; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Store; + + +public abstract partial class SharedStoreSystem +{ + /// + /// Refreshes all listings on a store. + /// Do not use if you don't know what you're doing. + /// + /// The store to refresh + public void RefreshAllListings(StoreComponent component) + { + var previousState = component.FullListingsCatalog; + var newState = GetAllListings(); + // if we refresh list with existing cost modifiers - they will be removed, + // need to restore them + if (previousState.Count != 0) + { + foreach (var previousStateListingItem in previousState) + { + if (!previousStateListingItem.IsCostModified + || !TryGetListing(newState, previousStateListingItem.ID, out var found)) + { + continue; + } + + foreach (var (modifierSourceId, costModifier) in previousStateListingItem.CostModifiersBySourceId) + { + found.AddCostModifier(modifierSourceId, costModifier); + } + } + } + + component.FullListingsCatalog = newState; + } + + /// + /// Gets all listings from a prototype. + /// + /// All the listings + public HashSet GetAllListings() + { + var clones = new HashSet(); + foreach (var prototype in Proto.EnumeratePrototypes()) + { + clones.Add(new ListingDataWithCostModifiers(prototype)); + } + + return clones; + } + + /// + /// Adds a listing from an Id to a store + /// + /// The store to add the listing to + /// The id of the listing + /// Whether or not the listing was added successfully + public bool TryAddListing(StoreComponent component, string listingId) + { + if (!Proto.TryIndex(listingId, out var proto)) + { + Log.Error("Attempted to add invalid listing."); + return false; + } + + return TryAddListing(component, proto); + } + + /// + /// Adds a listing to a store + /// + /// The store to add the listing to + /// The listing + /// Whether or not the listing was add successfully + public bool TryAddListing(StoreComponent component, ListingPrototype listing) + { + return component.FullListingsCatalog.Add(new ListingDataWithCostModifiers(listing)); + } + + /// + /// Gets the available listings for a store + /// + /// Either the account owner, user, or an inanimate object (e.g., surplus bundle) + /// + /// The store the listings are coming from. + /// The available listings. + public IEnumerable GetAvailableListings(EntityUid buyer, EntityUid store, StoreComponent component) + { + return GetAvailableListings(buyer, component.FullListingsCatalog, component.Categories, store); + } + + /// + /// Gets the available listings for a user given an overall set of listings and categories to filter by. + /// + /// Either the account owner, user, or an inanimate object (e.g., surplus bundle) + /// All of the listings that are available. If null, will just get all listings from the prototypes. + /// What categories to filter by. + /// The physial entity of the store. Can be null. + /// The available listings. + public IEnumerable GetAvailableListings( + EntityUid buyer, + IReadOnlyCollection? listings, + HashSet> categories, + EntityUid? storeEntity = null + ) + { + listings ??= GetAllListings(); + + foreach (var listing in listings) + { + if (!ListingHasCategory(listing, categories)) + continue; + + if (listing.Conditions != null) + { + var args = new ListingConditionArgs(GetBuyerMind(buyer), storeEntity, listing, EntityManager); + var conditionsMet = true; + + foreach (var condition in listing.Conditions) + { + if (!condition.Condition(args)) + { + conditionsMet = false; + break; + } + } + + if (!conditionsMet) + continue; + } + + yield return listing; + } + } + + /// + /// Returns the entity's mind entity, if it has one, to be used for listing conditions. + /// If it doesn't have one, or is a mind entity already, it returns itself. + /// + /// The buying entity. + public EntityUid GetBuyerMind(EntityUid buyer) + { + if (!HasComp(buyer) && Mind.TryGetMind(buyer, out var buyerMind, out _)) + return buyerMind; + + return buyer; + } + + /// + /// Checks if a listing appears in a list of given categories + /// + /// The listing itself. + /// The categories to check through. + /// If the listing was present in one of the categories. + public bool ListingHasCategory(ListingData listing, HashSet> categories) + { + foreach (var cat in categories) + { + if (listing.Categories.Contains(cat)) + return true; + } + return false; + } + + private bool TryGetListing(IReadOnlyCollection collection, string listingId, [MaybeNullWhen(false)] out ListingDataWithCostModifiers found) + { + foreach(var current in collection) + { + if (current.ID == listingId) + { + found = current; + return true; + } + } + + found = null!; + return false; + } +} diff --git a/Content.Shared/Store/SharedStoreSystem.UI.cs b/Content.Shared/Store/SharedStoreSystem.UI.cs new file mode 100644 index 0000000000..2d4b166c27 --- /dev/null +++ b/Content.Shared/Store/SharedStoreSystem.UI.cs @@ -0,0 +1,105 @@ +using System.Linq; +using Content.Shared.FixedPoint; +using Content.Shared.PDA.Ringer; +using Content.Shared.Store.Components; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Store; + +/// +/// This handles... +/// +public abstract partial class SharedStoreSystem +{ + /// + /// Toggles the store Ui open and closed + /// + /// the person doing the toggling + /// the store being toggled + /// + /// The entity remotely accessing the store, if any. + /// The remote access component, if any. + public void ToggleUi(EntityUid user, EntityUid storeEnt, StoreComponent? component = null, EntityUid? remoteAccess = null, RemoteStoreComponent? remoteComponent = null) + { + if (!Resolve(storeEnt, ref component)) + return; + + if (remoteAccess != null && !Resolve(remoteAccess.Value, ref remoteComponent) && remoteComponent!.Store != storeEnt) + return; + + if (!TryComp(user, out var actor)) + return; + + if (!UI.TryToggleUi(remoteAccess != null ? remoteAccess.Value : storeEnt, StoreUiKey.Key, actor.PlayerSession)) + return; + + UpdateUserInterface(user, storeEnt, component); + } + + /// + /// Closes the store UI for everyone, if it's open + /// + public void CloseUi(EntityUid uid, StoreComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + UI.CloseUi(uid, StoreUiKey.Key); + } + + /// + /// Updates the user interface for a store and refreshes the listings + /// + /// The person who if opening the store ui. Listings are filtered based on this. + /// The store entity itself + /// The store component being refreshed. + public void UpdateUserInterface(EntityUid? user, EntityUid store, StoreComponent? component = null) + { + if (!Resolve(store, ref component)) + return; + + //this is the person who will be passed into logic for all listing filtering. + if (user != null) //if we have no "buyer" for this update, then don't update the listings + { + component.LastAvailableListings = GetAvailableListings(component.AccountOwner ?? user.Value, store, component).ToHashSet(); + } + + //dictionary for all currencies, including 0 values for currencies on the whitelist + Dictionary, FixedPoint2> allCurrency = new(); + foreach (var supported in component.CurrencyWhitelist) + { + allCurrency.Add(supported, FixedPoint2.Zero); + + if (component.Balance.TryGetValue(supported, out var value)) + allCurrency[supported] = value; + } + + // TODO: if multiple users are supposed to be able to interact with a single BUI & see different + // stores/listings, this needs to use session specific BUI states. + + // only tell operatives to lock their uplink if it can be locked + var showFooter = HasComp(store); + + var state = new StoreUpdateState(component.LastAvailableListings, allCurrency, showFooter, component.RefundAllowed); + UpdateRemoteStores(store, state); + UI.SetUiState(store, StoreUiKey.Key, state); + } + + /// + /// Updates any remote store connections to a specific store. + /// + /// The store being updated. + /// The state being applied. + public void UpdateRemoteStores(EntityUid store, StoreUpdateState state) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var remote, out var ui)) + { + if (remote.Store != store) + continue; + + UI.SetUiState((uid, ui), StoreUiKey.Key, state); + } + } +} diff --git a/Content.Shared/Store/SharedStoreSystem.cs b/Content.Shared/Store/SharedStoreSystem.cs index c281685077..6e3e5f3e42 100644 --- a/Content.Shared/Store/SharedStoreSystem.cs +++ b/Content.Shared/Store/SharedStoreSystem.cs @@ -1,6 +1,14 @@ using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Shared.FixedPoint; using Content.Shared.Implants; +using Content.Shared.Interaction; +using Content.Shared.Mind; +using Content.Shared.Popups; +using Content.Shared.Stacks; using Content.Shared.Store.Components; +using Content.Shared.Store.Events; +using Robust.Shared.Prototypes; namespace Content.Shared.Store; @@ -10,9 +18,75 @@ namespace Content.Shared.Store; /// public abstract partial class SharedStoreSystem : EntitySystem { + [Dependency] protected readonly IPrototypeManager Proto = default!; + [Dependency] protected readonly SharedMindSystem Mind = default!; + [Dependency] protected readonly SharedPopupSystem Popup = default!; + [Dependency] protected readonly SharedStackSystem Stack = default!; + [Dependency] protected readonly SharedUserInterfaceSystem UI = default!; + [Dependency] protected readonly EntityQuery StoreQuery = default!; [Dependency] protected readonly EntityQuery RemoteStoreQuery = default!; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnGetStore); + SubscribeLocalEvent>((x, ref y) => + { + var ev = y.Event; + OnGetStore(x, ref ev); + y.Event = ev; + }); + SubscribeLocalEvent>(OnImplantInsertAttempt); + SubscribeLocalEvent(OnIntrinsicStoreAction); + } + + private void OnGetStore(Entity entity, ref GetStoreEvent args) + { + if (args.Handled) + return; + + if (!StoreQuery.TryComp(entity.Comp.Store, out var store)) + return; + + args.Store = (entity.Comp.Store.Value, store); + } + + private void OnImplantInsertAttempt(Entity implant, ref ImplantRelayEvent args) + { + var ev = args.Event; + + if (ev.User == ev.Target) + ev.TargetOverride = implant; + else + ev.Cancel(); + + args.Event = ev; + } + + private void OnAfterInteract(EntityUid uid, CurrencyComponent component, AfterInteractEvent args) + { + if (args.Handled || !args.CanReach || args.Target is not { } target) + return; + + if (!TryGetStore(target, out var store)) + return; + + var ev = new CurrencyInsertAttemptEvent(args.User, target, args.Used, store.Value.Comp); + RaiseLocalEvent(target, ev); + if (ev.Cancelled) + return; + + if (!TryAddCurrency((uid, component), (store.Value, store.Value.Comp))) + return; + + args.Handled = true; + var msg = Loc.GetString("store-currency-inserted", ("used", args.Used), ("target", ev.TargetOverride ?? target)); + Popup.PopupEntity(msg, target, args.User); + } + /// /// Attempts to find a store connected to this entity. /// First checking for a on this entity, @@ -21,7 +95,7 @@ public abstract partial class SharedStoreSystem : EntitySystem /// Entity we're checking for an attached store on /// Store entity we're returning. /// True if a store was found. - public bool TryGetStore(Entity entity, [NotNullWhen(true)] out Entity? store) + public bool TryGetStore(EntityUid entity, [NotNullWhen(true)] out Entity? store) { store = GetStore(entity); return store != null; @@ -34,12 +108,14 @@ public abstract partial class SharedStoreSystem : EntitySystem /// /// Entity we're checking for an attached store on /// The store entity and component if found. - public Entity? GetStore(Entity entity) + public Entity? GetStore(EntityUid entity) { if (StoreQuery.TryComp(entity, out var storeComp)) return (entity, storeComp); - return GetRemoteStore(entity); + var ev = new GetStoreEvent(); + RaiseLocalEvent(entity, ref ev); + return ev.Store; } /// @@ -65,4 +141,113 @@ public abstract partial class SharedStoreSystem : EntitySystem entity.Comp.Store = store; } + + /// + /// Gets the value from an entity's currency component. + /// Scales with stacks. + /// + /// + /// If this result is intended to be used with , + /// consider using instead to ensure that the currency is consumed in the process. + /// + /// + /// + /// The value of the currency + public Dictionary GetCurrencyValue(EntityUid uid, CurrencyComponent component) + { + var amount = EntityManager.GetComponentOrNull(uid)?.Count ?? 1; + return component.Price.ToDictionary(v => v.Key, p => p.Value * amount); + } + + /// + /// Tries to add a currency to a store's balance. Note that if successful, this will consume the currency in the process. + /// + public bool TryAddCurrency(Entity currency, Entity store) + { + if (!Resolve(currency.Owner, ref currency.Comp)) + return false; + + if (!Resolve(store.Owner, ref store.Comp)) + return false; + + var value = currency.Comp.Price; + if (TryComp(currency.Owner, out StackComponent? stack) && stack.Count != 1) + { + value = currency.Comp.Price + .ToDictionary(v => v.Key, p => p.Value * stack.Count); + } + + if (!TryAddCurrency(value, store, store.Comp)) + return false; + + // Avoid having the currency accidentally be re-used. E.g., if multiple clients try to use the currency in the + // same tick + currency.Comp.Price.Clear(); + if (stack != null) + Stack.SetCount((currency.Owner, stack), 0); + + QueueDel(currency); + return true; + } + + /// + /// Tries to add a currency to a store's balance + /// + /// The value to add to the store + /// + /// The store to add it to + /// Whether or not the currency was succesfully added + public bool TryAddCurrency(Dictionary currency, EntityUid uid, StoreComponent? store = null) + { + if (!Resolve(uid, ref store)) + return false; + + //verify these before values are modified + foreach (var type in currency) + { + if (!store.CurrencyWhitelist.Contains(type.Key)) + return false; + } + + foreach (var type in currency) + { + if (!store.Balance.TryAdd(type.Key, type.Value)) + store.Balance[type.Key] += type.Value; + } + + UpdateUserInterface(null, uid, store); + return true; + } + + private void OnIntrinsicStoreAction(Entity ent, ref IntrinsicStoreActionEvent args) + { + ToggleUi(args.Performer, ent.Owner, ent.Comp); + } } + +[ByRefEvent] +public record struct GetStoreEvent +{ + public readonly bool Handled => Store != null; + public Entity? Store; +} + +public sealed class CurrencyInsertAttemptEvent : CancellableEntityEventArgs +{ + public readonly EntityUid User; + public readonly EntityUid Target; + public readonly EntityUid Used; + public readonly StoreComponent Store; + + // An optional override for the "Target" of this interaction, used to change the name that pops up! + public EntityUid? TargetOverride; + + public CurrencyInsertAttemptEvent(EntityUid user, EntityUid target, EntityUid used, StoreComponent store) + { + User = user; + Target = target; + Used = used; + Store = store; + } +} + From e0af5216b6165773d0f017c7caaef0584ea3f35f Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> Date: Mon, 6 Apr 2026 19:33:27 -0700 Subject: [PATCH 106/126] comment and delete empty system --- Content.Server/Store/Systems/StoreSystem.Listings.cs | 12 ------------ Content.Shared/Store/SharedStoreSystem.cs | 1 + 2 files changed, 1 insertion(+), 12 deletions(-) delete mode 100644 Content.Server/Store/Systems/StoreSystem.Listings.cs diff --git a/Content.Server/Store/Systems/StoreSystem.Listings.cs b/Content.Server/Store/Systems/StoreSystem.Listings.cs deleted file mode 100644 index 79e0683738..0000000000 --- a/Content.Server/Store/Systems/StoreSystem.Listings.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Content.Shared.Mind; -using Content.Shared.Store; -using Content.Shared.Store.Components; -using Robust.Shared.Prototypes; - -namespace Content.Server.Store.Systems; - -public sealed partial class StoreSystem -{ - -} diff --git a/Content.Shared/Store/SharedStoreSystem.cs b/Content.Shared/Store/SharedStoreSystem.cs index 6e3e5f3e42..315d70d52a 100644 --- a/Content.Shared/Store/SharedStoreSystem.cs +++ b/Content.Shared/Store/SharedStoreSystem.cs @@ -58,6 +58,7 @@ public abstract partial class SharedStoreSystem : EntitySystem { var ev = args.Event; + // Only allow insertion if the person implanted is doing the action. if (ev.User == ev.Target) ev.TargetOverride = implant; else From 8522930dca3e51b4a927a11b3058144402fb7083 Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:13:16 -0700 Subject: [PATCH 107/126] fix double purchase bug --- .../Prototypes/Entities/Objects/Misc/subdermal_implants.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml index 8232a82a2b..f114ece546 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml @@ -158,7 +158,7 @@ - Syndicate - type: entity - parent: [ BaseSubdermalImplant, StorePresetUplink ] + parent: BaseSubdermalImplant id: UplinkImplant name: uplink implant description: This implant lets the user access a hidden Syndicate uplink at will. From 750d11262ab343e441a25fc18af8c86831496755 Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:14:37 -0700 Subject: [PATCH 108/126] fix a couple radiation system bugs as a treat --- .../Radiation/Systems/RadiationSystem.cs | 2 +- .../Anomaly/Effects/GravityAnomalySystem.cs | 7 +++--- .../Radiation/Systems/RadiationSystem.cs | 22 ++++++++----------- .../Components/RadiationSourceComponent.cs | 2 ++ .../Systems/SharedRadiationSystem.cs | 22 +++++++++++++++++++ .../EntitySystems/SharedSingularitySystem.cs | 15 ++++++------- 6 files changed, 45 insertions(+), 25 deletions(-) create mode 100644 Content.Shared/Radiation/Systems/SharedRadiationSystem.cs diff --git a/Content.Client/Radiation/Systems/RadiationSystem.cs b/Content.Client/Radiation/Systems/RadiationSystem.cs index f4f109adc7..f719b7b5b8 100644 --- a/Content.Client/Radiation/Systems/RadiationSystem.cs +++ b/Content.Client/Radiation/Systems/RadiationSystem.cs @@ -5,7 +5,7 @@ using Robust.Client.Graphics; namespace Content.Client.Radiation.Systems; -public sealed class RadiationSystem : EntitySystem +public sealed class RadiationSystem : SharedRadiationSystem { [Dependency] private readonly IOverlayManager _overlayMan = default!; diff --git a/Content.Server/Anomaly/Effects/GravityAnomalySystem.cs b/Content.Server/Anomaly/Effects/GravityAnomalySystem.cs index 02cd0aafc6..e053377d57 100644 --- a/Content.Server/Anomaly/Effects/GravityAnomalySystem.cs +++ b/Content.Server/Anomaly/Effects/GravityAnomalySystem.cs @@ -1,9 +1,9 @@ using Content.Server.Physics.Components; +using Content.Server.Radiation.Systems; using Content.Server.Singularity.Components; using Content.Shared.Anomaly.Components; using Content.Shared.Anomaly.Effects; using Content.Shared.Anomaly.Effects.Components; -using Content.Shared.Radiation.Components; namespace Content.Server.Anomaly.Effects; @@ -12,6 +12,8 @@ namespace Content.Server.Anomaly.Effects; /// public sealed class GravityAnomalySystem : SharedGravityAnomalySystem { + [Dependency] private readonly RadiationSystem _radiation = default!; + /// public override void Initialize() { @@ -22,8 +24,7 @@ public sealed class GravityAnomalySystem : SharedGravityAnomalySystem private void OnSeverityChanged(Entity anomaly, ref AnomalySeverityChangedEvent args) { - if (TryComp(anomaly, out var radSource)) - radSource.Intensity = anomaly.Comp.MaxRadiationIntensity * args.Severity; + _radiation.SetIntensity(anomaly.Owner, anomaly.Comp.MaxRadiationIntensity * args.Severity); if (TryComp(anomaly, out var gravityWell)) { diff --git a/Content.Server/Radiation/Systems/RadiationSystem.cs b/Content.Server/Radiation/Systems/RadiationSystem.cs index 39d8d6cd33..fd1a60710d 100644 --- a/Content.Server/Radiation/Systems/RadiationSystem.cs +++ b/Content.Server/Radiation/Systems/RadiationSystem.cs @@ -7,10 +7,11 @@ using Robust.Shared.Map; using Robust.Shared.Physics; using Robust.Shared.Threading; using System.Numerics; +using Content.Shared.Radiation.Systems; namespace Content.Server.Radiation.Systems; -public sealed partial class RadiationSystem : EntitySystem +public sealed partial class RadiationSystem : SharedRadiationSystem { [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; @@ -20,9 +21,8 @@ public sealed partial class RadiationSystem : EntitySystem [Dependency] private readonly IParallelManager _parallel = default!; [Dependency] private readonly EntityQuery _receiverQuery = default!; - private EntityQuery _blockerQuery; - private EntityQuery _resistanceQuery; - private EntityQuery _stackQuery; + [Dependency] private EntityQuery _blockerQuery = default; + [Dependency] private EntityQuery _resistanceQuery = default; private readonly B2DynamicTree _sourceTree = new(); private readonly Dictionary _sourceDataMap = new(); @@ -36,20 +36,16 @@ public sealed partial class RadiationSystem : EntitySystem SubscribeCvars(); InitRadBlocking(); - _blockerQuery = GetEntityQuery(); - _resistanceQuery = GetEntityQuery(); - _stackQuery = GetEntityQuery(); - - SubscribeLocalEvent(OnSourceInit); + SubscribeLocalEvent(OnSourceStartup); SubscribeLocalEvent(OnSourceShutdown); SubscribeLocalEvent(OnSourceMove); SubscribeLocalEvent(OnSourceStackChanged); - SubscribeLocalEvent(OnReceiverInit); + SubscribeLocalEvent(OnReceiverStartup); SubscribeLocalEvent(OnReceiverShutdown); } - private void OnSourceInit(Entity entity, ref ComponentInit args) + private void OnSourceStartup(Entity entity, ref ComponentStartup args) { UpdateSource(entity); } @@ -78,7 +74,7 @@ public sealed partial class RadiationSystem : EntitySystem UpdateSource(entity); } - private void OnReceiverInit(EntityUid uid, RadiationReceiverComponent component, ComponentInit args) + private void OnReceiverStartup(EntityUid uid, RadiationReceiverComponent component, ComponentStartup args) { _activeReceivers.Add(uid); } @@ -88,7 +84,7 @@ public sealed partial class RadiationSystem : EntitySystem _activeReceivers.Remove(uid); } - private void UpdateSource(Entity entity) + protected override void UpdateSource(Entity entity) { var (uid, component) = entity; var xform = Transform(uid); diff --git a/Content.Shared/Radiation/Components/RadiationSourceComponent.cs b/Content.Shared/Radiation/Components/RadiationSourceComponent.cs index 337e9c648e..367d38309f 100644 --- a/Content.Shared/Radiation/Components/RadiationSourceComponent.cs +++ b/Content.Shared/Radiation/Components/RadiationSourceComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Radiation.Systems; using Robust.Shared.Physics; namespace Content.Shared.Radiation.Components; @@ -6,6 +7,7 @@ namespace Content.Shared.Radiation.Components; /// Irradiate all objects in range. /// [RegisterComponent] +[Access(typeof(SharedRadiationSystem))] public sealed partial class RadiationSourceComponent : Component { /// diff --git a/Content.Shared/Radiation/Systems/SharedRadiationSystem.cs b/Content.Shared/Radiation/Systems/SharedRadiationSystem.cs new file mode 100644 index 0000000000..caded9c3e6 --- /dev/null +++ b/Content.Shared/Radiation/Systems/SharedRadiationSystem.cs @@ -0,0 +1,22 @@ +using Content.Shared.Radiation.Components; + +namespace Content.Shared.Radiation.Systems; + +public abstract partial class SharedRadiationSystem : EntitySystem +{ + [Dependency] protected readonly EntityQuery SourceQuery = default!; + + public void SetIntensity(Entity entity, float intensity) + { + if (!SourceQuery.Resolve(entity, ref entity.Comp)) + return; + + entity.Comp.Intensity = intensity; + UpdateSource((entity, entity.Comp)); + } + + protected virtual void UpdateSource(Entity entity) + { + + } +} diff --git a/Content.Shared/Singularity/EntitySystems/SharedSingularitySystem.cs b/Content.Shared/Singularity/EntitySystems/SharedSingularitySystem.cs index b7bf071457..eccfbe58d8 100644 --- a/Content.Shared/Singularity/EntitySystems/SharedSingularitySystem.cs +++ b/Content.Shared/Singularity/EntitySystems/SharedSingularitySystem.cs @@ -1,5 +1,6 @@ using System.Numerics; using Content.Shared.Radiation.Components; +using Content.Shared.Radiation.Systems; using Content.Shared.Singularity.Components; using Content.Shared.Singularity.Events; using Robust.Shared.Containers; @@ -19,6 +20,7 @@ public abstract class SharedSingularitySystem : EntitySystem [Dependency] private readonly SharedContainerSystem _containers = default!; [Dependency] private readonly SharedEventHorizonSystem _horizons = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedRadiationSystem _radiation = default!; [Dependency] protected readonly IViewVariablesManager Vvm = default!; #endregion Dependencies @@ -138,10 +140,7 @@ public abstract class SharedSingularitySystem : EntitySystem _visualizer.SetData(uid, SingularityAppearanceKeys.Singularity, singularity.Level, appearance); } - if (TryComp(uid, out var radiationSource)) - { - UpdateRadiation(uid, singularity, radiationSource); - } + UpdateRadiation(uid, singularity); RaiseLocalEvent(uid, new SingularityLevelChangedEvent(singularity.Level, oldValue, singularity)); if (singularity.Level <= 0) @@ -165,12 +164,12 @@ public abstract class SharedSingularitySystem : EntitySystem /// /// The uid of the singularity to update the radiation of. /// The state of the singularity to update the radiation of. - /// The state of the radioactivity of the singularity to update. - private void UpdateRadiation(EntityUid uid, SingularityComponent? singularity = null, RadiationSourceComponent? rads = null) + private void UpdateRadiation(EntityUid uid, SingularityComponent? singularity = null) { - if(!Resolve(uid, ref singularity, ref rads, logMissing: false)) + if(!Resolve(uid, ref singularity, logMissing: false)) return; - rads.Intensity = singularity.Level * singularity.RadsPerLevel; + + _radiation.SetIntensity(uid, singularity.Level * singularity.RadsPerLevel); } #endregion Getters/Setters From 700bcbf28d39c7dee6e676491392e6307d012e8e Mon Sep 17 00:00:00 2001 From: Super <84590915+SuperGDPWYL@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:44:17 +0100 Subject: [PATCH 109/126] Cause Anomalies to Go Supercritical [Traitor Objective] (#43505) * science explosions are projected to go up by 1% * eof * huh * review * newline * make it exclusive --- ...upercriticalAnomaliesConditionComponent.cs | 14 +++++++ .../SupercriticalAnomaliesConditionSystem.cs | 41 +++++++++++++++++++ .../conditions/anomaly-supercrit.ftl | 1 + .../Prototypes/Objectives/objectiveGroups.yml | 6 +++ Resources/Prototypes/Objectives/traitor.yml | 19 +++++++++ 5 files changed, 81 insertions(+) create mode 100644 Content.Server/Objectives/Components/SupercriticalAnomaliesConditionComponent.cs create mode 100644 Content.Server/Objectives/Systems/SupercriticalAnomaliesConditionSystem.cs create mode 100644 Resources/Locale/en-US/objectives/conditions/anomaly-supercrit.ftl diff --git a/Content.Server/Objectives/Components/SupercriticalAnomaliesConditionComponent.cs b/Content.Server/Objectives/Components/SupercriticalAnomaliesConditionComponent.cs new file mode 100644 index 0000000000..038150d90c --- /dev/null +++ b/Content.Server/Objectives/Components/SupercriticalAnomaliesConditionComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Server.Objectives.Components; + +/// +/// Objective condition that requires a certain number of anomalies (defined by ) to go supercritical while the objective is in play. +/// +[RegisterComponent] +public sealed partial class SupercriticalAnomaliesConditionComponent : Component +{ + /// + /// The number of anomalies that have gone supercritical since this objective was added. + /// + [DataField] + public int SupercriticalAnomalies = 0; +} diff --git a/Content.Server/Objectives/Systems/SupercriticalAnomaliesConditionSystem.cs b/Content.Server/Objectives/Systems/SupercriticalAnomaliesConditionSystem.cs new file mode 100644 index 0000000000..93e649def2 --- /dev/null +++ b/Content.Server/Objectives/Systems/SupercriticalAnomaliesConditionSystem.cs @@ -0,0 +1,41 @@ +using Content.Server.Objectives.Components; +using Content.Shared.Anomaly.Components; +using Content.Shared.Objectives.Components; + +namespace Content.Server.Objectives.Systems; + +public sealed partial class SupercriticalAnomaliesConditionSystem : EntitySystem +{ + [Dependency] private readonly NumberObjectiveSystem _numberObjective = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAnomalySupercrit); + SubscribeLocalEvent(OnGetProgress); + } + + private void OnAnomalySupercrit(ref AnomalyShutdownEvent args) + { + if (!args.Supercritical) + return; + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var comp)) + { + comp.SupercriticalAnomalies += 1; + } + } + + private void OnGetProgress(Entity ent, ref ObjectiveGetProgressEvent args) + { + var target = _numberObjective.GetTarget(ent); + if (target == 0) + { + args.Progress = 0f; + return; + } + args.Progress = MathF.Min((float)ent.Comp.SupercriticalAnomalies / target, 1f); + } +} diff --git a/Resources/Locale/en-US/objectives/conditions/anomaly-supercrit.ftl b/Resources/Locale/en-US/objectives/conditions/anomaly-supercrit.ftl new file mode 100644 index 0000000000..358b9e8673 --- /dev/null +++ b/Resources/Locale/en-US/objectives/conditions/anomaly-supercrit.ftl @@ -0,0 +1 @@ +objective-condition-supercrit-anomalies-title = Cause {$count} anomalies to go supercritical diff --git a/Resources/Prototypes/Objectives/objectiveGroups.yml b/Resources/Prototypes/Objectives/objectiveGroups.yml index 0480a5c9ad..9c524e57af 100644 --- a/Resources/Prototypes/Objectives/objectiveGroups.yml +++ b/Resources/Prototypes/Objectives/objectiveGroups.yml @@ -6,6 +6,7 @@ TraitorObjectiveGroupKill: 1 TraitorObjectiveGroupState: 1 #As in, something about your character. Alive, dead, arrested, gained an ability... TraitorObjectiveGroupSocial: 1 #Involves helping/harming others without killing them or stealing their stuff + TraitorObjectiveGroupSabotage: 1 TraitorObjectiveGroupOther: 1 - type: weightedRandom @@ -45,6 +46,11 @@ # RandomTraitorAliveObjective: 1 - Removed because the objective was boring and didn't progress the round. RandomTraitorProgressObjective: 1 +- type: weightedRandom + id: TraitorObjectiveGroupSabotage + weights: + SupercritAnomaliesObjective: 1 + #misc objectives that don't fall other another category - type: weightedRandom id: TraitorObjectiveGroupOther diff --git a/Resources/Prototypes/Objectives/traitor.yml b/Resources/Prototypes/Objectives/traitor.yml index f8cc42a222..6829084207 100644 --- a/Resources/Prototypes/Objectives/traitor.yml +++ b/Resources/Prototypes/Objectives/traitor.yml @@ -378,6 +378,25 @@ stealGroup: NukeDisk owner: objective-condition-steal-station +# sabotage +- type: entity + parent: BaseTraitorObjective + id: SupercritAnomaliesObjective + description: Nanotrasen is very interested in anomalies with potentially catastrophic consequences. Introduce them to the fire they're playing with. + components: + - type: Objective + difficulty: 2 + icon: + sprite: Structures/Specific/anomaly.rsi + state: anom1 + - type: SupercriticalAnomaliesCondition + - type: NumberObjective + min: 3 + max: 4 + title: objective-condition-supercrit-anomalies-title + - type: ObjectiveLimit + limit: 1 + # other - type: entity parent: BaseTraitorObjective From 50b2a327bc5f97d7ed9d3100626185417b269dd3 Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:47:19 -0700 Subject: [PATCH 110/126] logmissing false --- Content.Shared/Radiation/Systems/SharedRadiationSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Shared/Radiation/Systems/SharedRadiationSystem.cs b/Content.Shared/Radiation/Systems/SharedRadiationSystem.cs index caded9c3e6..ea298d313d 100644 --- a/Content.Shared/Radiation/Systems/SharedRadiationSystem.cs +++ b/Content.Shared/Radiation/Systems/SharedRadiationSystem.cs @@ -8,7 +8,7 @@ public abstract partial class SharedRadiationSystem : EntitySystem public void SetIntensity(Entity entity, float intensity) { - if (!SourceQuery.Resolve(entity, ref entity.Comp)) + if (!SourceQuery.Resolve(entity, ref entity.Comp, false)) return; entity.Comp.Intensity = intensity; From ae916eb4d201e2cf4b160e95c55631b08b69d529 Mon Sep 17 00:00:00 2001 From: PJBot Date: Tue, 7 Apr 2026 19:00:25 +0000 Subject: [PATCH 111/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 2552eaea39..a086eaaaf8 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: Wolfkey-SomeoneElseTookMyUsername - changes: - - message: The recipes for grilled cheese, cotton buns, cotton cakes, and cotton - grilled cheese are now in the correct category in the guidebook - type: Fix - id: 9113 - time: '2025-10-17T18:56:01.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40949 - author: TrixxedHeart changes: - message: Added Enter/Leave Genpop access to Security by default, allowing them @@ -4033,3 +4025,10 @@ id: 9624 time: '2026-04-06T22:23:53.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43462 +- author: SuperGDPWYL + changes: + - message: Traitors can now be tasked with causing anomalies to go supercritical. + type: Add + id: 9625 + time: '2026-04-07T18:59:14.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43505 From 63ce4c9be160e27c6f5dbe5dd1a81267984d57a4 Mon Sep 17 00:00:00 2001 From: KeTuFaisPiKiNut <85769816+ketufaispikinut@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:18:04 -0400 Subject: [PATCH 112/126] Travel Camera (#43322) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Travel camera * Adds TravelCamera as rare maintenance loot * Prevent Travel Camera from converting people to Revolution * Removed AoE for travel_camera * Adds Travel Camera to Uplink * Camera sprites * camera working on modern code * camera melee only * every error fixed + camera sfx * PoC photography * textures * charges, description order, fixed typo * support for colors * typos plus different photograph colors * more typos * final typos? * oh god i dont know how to write so many typos * fix lint warnings * Update Resources/Audio/Misc/attributions.yml Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Resources/Textures/Objects/Misc/photograph.rsi/meta.json Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Content.Server/Photography/PhotographComponent.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Content.Server/Photography/PhotographComponent.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Content.Server/Photography/PhotographComponent.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * follow convention: bases shouldn't have any sprites * uses identity.entity system * Picture taker component comments. * Update Resources/Prototypes/Entities/Objects/Devices/travel_camera.yml Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Resources/Prototypes/Entities/Objects/Devices/travel_camera.yml Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * spaces inserted * 4-spaced indentation * 4-space indent * uses SharedChargesSystem * canconvert component instead of convertitem tag * removed mention of tags from head rev component * component no longer has a constructor * use tables + do the things on shared * fix abstract-ness warning * formatting + comments * cleanup and improvements * oops * fix warnings * 1 tc / disruption + recursion (also i'm pretty sure it can spawn in maints now) * proper recursion checking (i think!) * Added a dot for coherence. * minor cleanup * brought back the melee * fixed description * More accurate, albeit less funny camera description. * deduplicate code with flash system --------- Co-authored-by: SlamBamActionman Co-authored-by: nomdéraisonnablementlong Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --- .../Rules/RevolutionaryRuleSystem.cs | 3 +- .../RevolutionaryConverterComponent.cs | 8 ++ .../Flash/Components/FlashComponent.cs | 6 + Content.Shared/Flash/FlashEvents.cs | 8 +- Content.Shared/Flash/SharedFlashSystem.cs | 52 ++++++-- .../Photography/PhotographComponent.cs | 24 ++++ .../Photography/PhotographySystem.cs | 96 +++++++++++++++ .../Photography/PictureTakerComponent.cs | 19 +++ Resources/Audio/Misc/attributions.yml | 5 + Resources/Audio/Misc/camera_snap.ogg | Bin 0 -> 9075 bytes .../Locale/en-US/photography/photography.ftl | 7 ++ .../Locale/en-US/store/uplink-catalog.ftl | 3 + .../Prototypes/Catalog/uplink_catalog.yml | 10 ++ .../Markers/Spawners/Random/maintenance.yml | 1 + .../Objects/Devices/travel_camera.yml | 116 ++++++++++++++++++ .../Entities/Objects/Weapons/security.yml | 1 + .../travel_camera.rsi/equipped-NECK.png | Bin 0 -> 344 bytes .../Devices/travel_camera.rsi/icon.png | Bin 0 -> 754 bytes .../Devices/travel_camera.rsi/inhand-left.png | Bin 0 -> 291 bytes .../travel_camera.rsi/inhand-right.png | Bin 0 -> 297 bytes .../Devices/travel_camera.rsi/meta.json | 27 ++++ .../Objects/Misc/photograph.rsi/black.png | Bin 0 -> 212 bytes .../Objects/Misc/photograph.rsi/blue.png | Bin 0 -> 235 bytes .../Objects/Misc/photograph.rsi/green.png | Bin 0 -> 238 bytes .../Objects/Misc/photograph.rsi/meta.json | 32 +++++ .../Objects/Misc/photograph.rsi/purple.png | Bin 0 -> 243 bytes .../Objects/Misc/photograph.rsi/rainbow.png | Bin 0 -> 277 bytes .../Objects/Misc/photograph.rsi/red.png | Bin 0 -> 219 bytes .../Objects/Misc/photograph.rsi/yellow.png | Bin 0 -> 237 bytes 29 files changed, 403 insertions(+), 15 deletions(-) create mode 100644 Content.Server/Revolutionary/Components/RevolutionaryConverterComponent.cs create mode 100644 Content.Shared/Photography/PhotographComponent.cs create mode 100644 Content.Shared/Photography/PhotographySystem.cs create mode 100644 Content.Shared/Photography/PictureTakerComponent.cs create mode 100644 Resources/Audio/Misc/camera_snap.ogg create mode 100644 Resources/Locale/en-US/photography/photography.ftl create mode 100644 Resources/Prototypes/Entities/Objects/Devices/travel_camera.yml create mode 100644 Resources/Textures/Objects/Devices/travel_camera.rsi/equipped-NECK.png create mode 100644 Resources/Textures/Objects/Devices/travel_camera.rsi/icon.png create mode 100644 Resources/Textures/Objects/Devices/travel_camera.rsi/inhand-left.png create mode 100644 Resources/Textures/Objects/Devices/travel_camera.rsi/inhand-right.png create mode 100644 Resources/Textures/Objects/Devices/travel_camera.rsi/meta.json create mode 100644 Resources/Textures/Objects/Misc/photograph.rsi/black.png create mode 100644 Resources/Textures/Objects/Misc/photograph.rsi/blue.png create mode 100644 Resources/Textures/Objects/Misc/photograph.rsi/green.png create mode 100644 Resources/Textures/Objects/Misc/photograph.rsi/meta.json create mode 100644 Resources/Textures/Objects/Misc/photograph.rsi/purple.png create mode 100644 Resources/Textures/Objects/Misc/photograph.rsi/rainbow.png create mode 100644 Resources/Textures/Objects/Misc/photograph.rsi/red.png create mode 100644 Resources/Textures/Objects/Misc/photograph.rsi/yellow.png diff --git a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs index cf74513c87..f0cf3087ff 100644 --- a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs @@ -146,7 +146,8 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem(ev.Target) && !alwaysConvertible || !_mobState.IsAlive(ev.Target) || - HasComp(ev.Target)) + HasComp(ev.Target) || + !HasComp(ev.Used)) { return; } diff --git a/Content.Server/Revolutionary/Components/RevolutionaryConverterComponent.cs b/Content.Server/Revolutionary/Components/RevolutionaryConverterComponent.cs new file mode 100644 index 0000000000..61acf26d5b --- /dev/null +++ b/Content.Server/Revolutionary/Components/RevolutionaryConverterComponent.cs @@ -0,0 +1,8 @@ +namespace Content.Server.Revolutionary.Components; + +/// +/// This is a marker component that indicates that a flash can be used to convert someone into a revolutionary. +/// Viva la revolution! +/// +[RegisterComponent] +public sealed partial class RevolutionaryConverterComponent : Component; diff --git a/Content.Shared/Flash/Components/FlashComponent.cs b/Content.Shared/Flash/Components/FlashComponent.cs index d1a8b882d9..9484add5ef 100644 --- a/Content.Shared/Flash/Components/FlashComponent.cs +++ b/Content.Shared/Flash/Components/FlashComponent.cs @@ -16,6 +16,12 @@ public sealed partial class FlashComponent : Component [DataField, AutoNetworkedField] public bool FlashOnUse = true; + /// + /// Flash the area around the entity when the flash is used with ranged interaction? + /// + [DataField, AutoNetworkedField] + public bool FlashOnRangedInteract = false; + /// /// Flash the target when melee attacking them? /// diff --git a/Content.Shared/Flash/FlashEvents.cs b/Content.Shared/Flash/FlashEvents.cs index 1c18ca1676..70a61765ff 100644 --- a/Content.Shared/Flash/FlashEvents.cs +++ b/Content.Shared/Flash/FlashEvents.cs @@ -13,9 +13,15 @@ public record struct FlashAttemptEvent(EntityUid Target, EntityUid? User, Entity } /// -/// Called when a player is successfully flashed. +/// Called when a player is successfully flashed, once for each flashed player. /// Raised on the target hit by the flash, the user of the flash and the flash used. /// The Melee parameter is used to check for rev conversion. /// [ByRefEvent] public record struct AfterFlashedEvent(EntityUid Target, EntityUid? User, EntityUid? Used, bool Melee); + +/// +/// Raised once on the flash entity when it was used, regardless of the flashed status being applied or not. +/// +[ByRefEvent] +public record struct AfterFlashActivatedEvent(EntityUid? Target, EntityUid? User); diff --git a/Content.Shared/Flash/SharedFlashSystem.cs b/Content.Shared/Flash/SharedFlashSystem.cs index 4b8ff3ab7b..da43284bee 100644 --- a/Content.Shared/Flash/SharedFlashSystem.cs +++ b/Content.Shared/Flash/SharedFlashSystem.cs @@ -1,13 +1,18 @@ +using System.Linq; using Content.Shared.Charges.Components; using Content.Shared.Charges.Systems; +using Content.Shared.Clothing.Components; using Content.Shared.Examine; using Content.Shared.Eye.Blinding.Components; using Content.Shared.Flash.Components; using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Inventory; using Content.Shared.Light; +using Content.Shared.Movement.Systems; using Content.Shared.Popups; +using Content.Shared.Random.Helpers; using Content.Shared.StatusEffect; using Content.Shared.Stunnable; using Content.Shared.Tag; @@ -17,12 +22,7 @@ using Content.Shared.Weapons.Melee.Events; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Prototypes; -using Robust.Shared.Random; using Robust.Shared.Timing; -using System.Linq; -using Content.Shared.Movement.Systems; -using Content.Shared.Random.Helpers; -using Content.Shared.Clothing.Components; namespace Content.Shared.Flash; @@ -57,6 +57,7 @@ public abstract class SharedFlashSystem : EntitySystem SubscribeLocalEvent(OnFlashMeleeHit); SubscribeLocalEvent(OnFlashUseInHand); + SubscribeLocalEvent(OnRangedInteract); SubscribeLocalEvent(OnLightToggle); SubscribeLocalEvent(OnPermanentBlindnessFlashAttempt); SubscribeLocalEvent(OnTemporaryBlindnessFlashAttempt); @@ -72,7 +73,7 @@ public abstract class SharedFlashSystem : EntitySystem if (!ent.Comp.FlashOnMelee || !args.IsHit || !args.HitEntities.Any() || - !UseFlash(ent, args.User)) + !TryUseFlashItem(ent.AsNullable(), args.User)) { return; } @@ -82,41 +83,66 @@ public abstract class SharedFlashSystem : EntitySystem { Flash(target, args.User, ent.Owner, ent.Comp.MeleeDuration, ent.Comp.SlowTo, melee: true, stunDuration: ent.Comp.MeleeStunDuration); } + + EntityUid? firstTarget = args.HitEntities.Count > 0 ? args.HitEntities[0] : null; // Just pick the first hit entity. + var ev = new AfterFlashActivatedEvent(firstTarget, args.User); + RaiseLocalEvent(ent, ref ev); } private void OnFlashUseInHand(Entity ent, ref UseInHandEvent args) { - if (!ent.Comp.FlashOnUse || args.Handled || !UseFlash(ent, args.User)) + if (!ent.Comp.FlashOnUse || args.Handled || !TryUseFlashItem(ent.AsNullable(), args.User)) return; args.Handled = true; FlashArea(ent.Owner, args.User, ent.Comp.Range, ent.Comp.AoeFlashDuration, ent.Comp.SlowTo, true, ent.Comp.Probability); + var ev = new AfterFlashActivatedEvent(null, args.User); // No direct target. + RaiseLocalEvent(ent, ref ev); + } + + // TODO: This or most of the other systems subscribing to BeforeRangedInteractEvent shouldn't be using this event as handling it stops contact interaction with the used tool, + // but this will need some cleanup of how SharedInteractionSystem handles the code flow. Also the event is raised for both in-range and out of range interactions, which + // is what the subscribers are using it for, but does not seem originally intended from the naming convention. + private void OnRangedInteract(Entity ent, ref BeforeRangedInteractEvent args) + { + if (!ent.Comp.FlashOnRangedInteract || args.Handled || !TryUseFlashItem(ent.AsNullable(), args.User)) + return; + + args.Handled = true; + FlashArea(ent.Owner, args.User, ent.Comp.Range, ent.Comp.AoeFlashDuration, ent.Comp.SlowTo, true, ent.Comp.Probability); + var ev = new AfterFlashActivatedEvent(args.Target, args.User); + RaiseLocalEvent(ent, ref ev); } // needed for the flash lantern and interrogator lamp // TODO: This is awful and all the different components for toggleable lights need to be unified and changed to use Itemtoggle private void OnLightToggle(Entity ent, ref LightToggleEvent args) { - if (!args.IsOn || !UseFlash(ent, null)) + if (!args.IsOn || !TryUseFlashItem(ent.AsNullable(), null)) return; FlashArea(ent.Owner, null, ent.Comp.Range, ent.Comp.AoeFlashDuration, ent.Comp.SlowTo, true, ent.Comp.Probability); + var ev = new AfterFlashActivatedEvent(null, null); // TODO: Add user once someone made toggleable lights not a total mess. + RaiseLocalEvent(ent, ref ev); } /// - /// Use charges and set the visuals. + /// Try to use charges, play the sound and set the visuals of a flash item. + /// This does not actually cause the flash status effect by itself, you will need to either call or as well. /// /// False if no charges are left or the flash is currently in use. - private bool UseFlash(Entity ent, EntityUid? user) + public bool TryUseFlashItem(Entity ent, EntityUid? user) { + if (!Resolve(ent, ref ent.Comp)) + return false; + if (_useDelay.IsDelayed(ent.Owner)) return false; if (TryComp(ent.Owner, out var charges) - && _sharedCharges.IsEmpty((ent.Owner, charges))) + && !_sharedCharges.TryUseCharge((ent.Owner, charges))) return false; - _sharedCharges.TryUseCharge((ent.Owner, charges)); _audio.PlayPredicted(ent.Comp.Sound, ent.Owner, user); var active = EnsureComp(ent.Owner); @@ -126,7 +152,7 @@ public abstract class SharedFlashSystem : EntitySystem if (_sharedCharges.IsEmpty((ent.Owner, charges))) { - _appearance.SetData(ent.Owner, FlashVisuals.Burnt, true); + _appearance.SetData(ent.Owner, FlashVisuals.Burnt, true); // TODO: Reset if charges are refilled. _tag.AddTag(ent.Owner, TrashTag); _popup.PopupClient(Loc.GetString("flash-component-becomes-empty"), user); } diff --git a/Content.Shared/Photography/PhotographComponent.cs b/Content.Shared/Photography/PhotographComponent.cs new file mode 100644 index 0000000000..3fb065a6e7 --- /dev/null +++ b/Content.Shared/Photography/PhotographComponent.cs @@ -0,0 +1,24 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Utility; + +namespace Content.Shared.Photography; + +/// +/// Represents the photograph data on a picture. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class PhotographComponent : Component +{ + /// + /// The description of the photographed object. + /// + [DataField, AutoNetworkedField] + public FormattedMessage? Description; + + /// + /// The full text mentioning the name of the photographed object. + /// For example "This is a picture of Urist McHands" + /// + [DataField, AutoNetworkedField] + public string? NameText; +} diff --git a/Content.Shared/Photography/PhotographySystem.cs b/Content.Shared/Photography/PhotographySystem.cs new file mode 100644 index 0000000000..e8b2334b25 --- /dev/null +++ b/Content.Shared/Photography/PhotographySystem.cs @@ -0,0 +1,96 @@ +using Content.Shared.EntityTable; +using Content.Shared.Examine; +using Content.Shared.Flash; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.IdentityManagement; +using Robust.Shared.Network; +using Robust.Shared.Utility; + +namespace Content.Shared.Photography; + +/// +/// Handles everything related to photography. +/// +public sealed class PhotographySystem : EntitySystem +{ + [Dependency] private readonly ExamineSystemShared _examine = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly EntityTableSystem _tables = default!; + [Dependency] private readonly INetManager _net = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnFlashActivated); + } + + private void OnExamined(Entity ent, ref ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + using (args.PushGroup(nameof(PhotographComponent))) + { + if (string.IsNullOrEmpty(ent.Comp.NameText)) + args.PushText(Loc.GetString("photograph-name-text-empty")); + else + args.PushText(ent.Comp.NameText); + if (ent.Comp.Description != null) + // TODO: For some weird reason ExamineSystem is adding a new line at the end of message we are pushing with each examine. + // I'm not soaping this PR even more, so for now I'll just bandaid that by sending a clone to prevent it from getting modified. + args.PushMessage(new FormattedMessage(ent.Comp.Description)); + } + } + + // The flash system is handling charges and all interactions, we just print the picture afterwards. + private void OnFlashActivated(Entity ent, ref AfterFlashActivatedEvent args) + { + TakePicture(ent, args.Target, args.User); + } + + /// + /// Processes entity aimed at with a camera and prints a picture of it. + /// TODO: This is basically a placeholder mechanic for a more elaborate photography system. + /// However, this will need major refactoring to be possible. See + /// https://github.com/space-wizards/docs/pull/307 and + /// https://github.com/space-wizards/space-station-14/pull/43327 + /// for details. + /// + public void TakePicture(Entity camera, EntityUid? target, EntityUid? user) + { + if (_net.IsClient) + return; // Can't interact with predictively spawned entities yet. + + var tableResult = _tables.GetSpawns(camera.Comp.Photographs); + var coords = Transform(camera).Coordinates; + + FormattedMessage? description = null; + string? nameText = null; + if (target != null) + { + description = _examine.GetExamineText(target.Value, user); + // Get the full string now instead of indexing it later because we need the entity to know if it uses a proper noun or not. + nameText = Loc.GetString("photograph-name-text", ("entity", Identity.Entity(target.Value, EntityManager))); + // We don't want photographs to contain the descriptions of other photographs, because that makes entities with, in theory, infinite descriptions. + if (HasComp(target.Value)) + { + description = null; + nameText = Loc.GetString("photograph-name-text-photograph"); + } + } + + foreach (var prototype in tableResult) + { + // we generate an individual photograph (there should be only one tough) + var spawned = Spawn(prototype, coords); + var photoComp = EnsureComp(spawned); + photoComp.NameText = nameText; + photoComp.Description = description; + Dirty(spawned, photoComp); + + _hands.PickupOrDrop(user, spawned, dropNear: true); + } + } +} diff --git a/Content.Shared/Photography/PictureTakerComponent.cs b/Content.Shared/Photography/PictureTakerComponent.cs new file mode 100644 index 0000000000..a636b1ede2 --- /dev/null +++ b/Content.Shared/Photography/PictureTakerComponent.cs @@ -0,0 +1,19 @@ +using Content.Shared.EntityTable.EntitySelectors; +using Robust.Shared.GameStates; + +namespace Content.Shared.Photography; + +// since camera is taken... +/// +/// Marks an entity as able to take pictures (when you smash other entities with it). +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class PictureTakerComponent : Component +{ + /// + /// The entities that will be spawned & given a PhotographComponent when the owning entity is used. + /// The table should only select one item at a time. + /// + [DataField] + public EntityTableSelector? Photographs; +} diff --git a/Resources/Audio/Misc/attributions.yml b/Resources/Audio/Misc/attributions.yml index 8fda833b1d..ada6b70983 100644 --- a/Resources/Audio/Misc/attributions.yml +++ b/Resources/Audio/Misc/attributions.yml @@ -87,3 +87,8 @@ license: "CC-BY-SA-3.0" copyright: "Taken from Citadel station" source: "https://github.com/Citadel-Station-13/Citadel-Station-13/blob/e31455667ebf3331255c1143e1e7215bc737a287/sound/misc/deltakalaxon.ogg" + +- files: ["camera_snap.ogg"] + license: "CC-BY-4.0" + copyright: "Taken from freesound, edited by ketufaispikinut(GitHub)" + source: "https://freesound.org/people/thecheeseman/sounds/51360/" diff --git a/Resources/Audio/Misc/camera_snap.ogg b/Resources/Audio/Misc/camera_snap.ogg new file mode 100644 index 0000000000000000000000000000000000000000..5730547ad4551c07b3f6db876f150c9caa59f408 GIT binary patch literal 9075 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 literal 0 HcmV?d00001 diff --git a/Resources/Locale/en-US/photography/photography.ftl b/Resources/Locale/en-US/photography/photography.ftl new file mode 100644 index 0000000000..1876fb194d --- /dev/null +++ b/Resources/Locale/en-US/photography/photography.ftl @@ -0,0 +1,7 @@ +# TODO: Make this a fluent function in RT +photograph-name-text = This is a photograph of { PROPER($entity) -> + *[false] { INDEFINITE($entity) } { $entity } + [true] { $entity } + }. +photograph-name-text-empty = This is a photograph. +photograph-name-text-photograph = This is a photograph of another photograph. diff --git a/Resources/Locale/en-US/store/uplink-catalog.ftl b/Resources/Locale/en-US/store/uplink-catalog.ftl index 685a4d83dd..60f13dbc42 100644 --- a/Resources/Locale/en-US/store/uplink-catalog.ftl +++ b/Resources/Locale/en-US/store/uplink-catalog.ftl @@ -409,6 +409,9 @@ uplink-soap-desc = An untrustworthy bar of soap. Smells of fear. uplink-ultrabright-lantern-name = Extra-Bright Lantern uplink-ultrabright-lantern-desc = This ultra-bright lantern can be used to blind people, similar to a flash. +uplink-travel-camera-name = Travel Camera +uplink-travel-camera-desc = Stun people with your photography skills and the conveniently legal camera flash. Makes you look like a tourist. + uplink-combat-medkit-name = Combat Medical Kit uplink-combat-medkit-desc = A medkit made for fixing combat injuries. diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml index 98c2206f6a..6429a1d4a0 100644 --- a/Resources/Prototypes/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/Catalog/uplink_catalog.yml @@ -1092,6 +1092,16 @@ categories: - UplinkDisruption +- type: listing + id: UplinkTravelCamera + name: uplink-travel-camera-name + description: uplink-travel-camera-desc + productEntity: TravelCamera + cost: + Telecrystal: 1 + categories: + - UplinkDeception + # Note: Removed for the time being until surgery/newmed is added. Considered bloat until then. # - type: listing # id: UplinkDuffelSurgery diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/maintenance.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/maintenance.yml index 99d9a4be99..111bc041d2 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/maintenance.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/maintenance.yml @@ -95,6 +95,7 @@ - !type:GroupSelector weight: 23 children: + - id: TravelCamera - id: ClothingNeckCloakHerald - id: ClothingHeadHelmetTemplar # Cloaks diff --git a/Resources/Prototypes/Entities/Objects/Devices/travel_camera.yml b/Resources/Prototypes/Entities/Objects/Devices/travel_camera.yml new file mode 100644 index 0000000000..5ec8b87ae0 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Devices/travel_camera.yml @@ -0,0 +1,116 @@ +- type: entity + parent: BaseItem + id: TravelCamera + name: travel camera + description: A picture says more than a thousand words. Comes with an ultrabright flash and internal recharging photo roll. + components: + # aptly named component for picture taking + - type: PictureTaker + photographs: !type:GroupSelector + children: + - !type:NestedSelector + tableId: TravelCameraPhotographs + - type: Sprite + sprite: Objects/Devices/travel_camera.rsi + state: icon + - type: Flash + flashOnRangedInteract: true + sound: !type:SoundPathSpecifier + path: /Audio/Misc/camera_snap.ogg + - type: MeleeWeapon # allows using the camera in melee + damage: + types: + Blunt: 0 + - type: LimitedCharges + maxCharges: 4 + - type: AutoRecharge + rechargeDuration: 60 + - type: UseDelay + - type: StaticPrice + price: 100 + - type: Clothing + slots: + - NECK + sprite: Objects/Devices/travel_camera.rsi + +# we have a base prototype so that if we need to add components later on its easier +- type: entity + abstract: true + parent: BasePaper + id: BasePhotograph + name: photograph + +- type: entity + parent: BasePhotograph + id: PhotographBlack + components: + - type: Sprite + sprite: Objects/Misc/photograph.rsi + layers: + - state: black + +- type: entity + parent: BasePhotograph + id: PhotographRed + components: + - type: Sprite + sprite: Objects/Misc/photograph.rsi + layers: + - state: red + +- type: entity + parent: BasePhotograph + id: PhotographBlue + components: + - type: Sprite + sprite: Objects/Misc/photograph.rsi + layers: + - state: blue + +- type: entity + parent: BasePhotograph + id: PhotographGreen + components: + - type: Sprite + sprite: Objects/Misc/photograph.rsi + layers: + - state: green + +- type: entity + parent: BasePhotograph + id: PhotographYellow + components: + - type: Sprite + sprite: Objects/Misc/photograph.rsi + layers: + - state: yellow + +- type: entity + parent: BasePhotograph + id: PhotographPurple + components: + - type: Sprite + sprite: Objects/Misc/photograph.rsi + layers: + - state: purple + +- type: entity + parent: BasePhotograph + id: PhotographRainbow + components: + - type: Sprite + sprite: Objects/Misc/photograph.rsi + layers: + - state: rainbow + +- type: entityTable + id: TravelCameraPhotographs + table: !type:GroupSelector + children: + - id: PhotographBlack + - id: PhotographRed + - id: PhotographBlue + - id: PhotographGreen + - id: PhotographYellow + - id: PhotographPurple + - id: PhotographRainbow diff --git a/Resources/Prototypes/Entities/Objects/Weapons/security.yml b/Resources/Prototypes/Entities/Objects/Weapons/security.yml index 2f6ac834ba..6d5830e161 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/security.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/security.yml @@ -145,6 +145,7 @@ visible: false shader: unshaded - type: Flash + - type: RevolutionaryConverter - type: LimitedCharges maxCharges: 5 - type: MeleeWeapon diff --git a/Resources/Textures/Objects/Devices/travel_camera.rsi/equipped-NECK.png b/Resources/Textures/Objects/Devices/travel_camera.rsi/equipped-NECK.png new file mode 100644 index 0000000000000000000000000000000000000000..03ae539d6f846992c6bcde26ce226372b47982eb GIT binary patch literal 344 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D%zg*;sxLn`LH zy=9ws$Ux-4NA^`mCL}xbGeooLF$AyN<^Fc2mSIWW_A@y*ox}y6NHK0-mFVdQ&MBb@04^JiOaK4? literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Devices/travel_camera.rsi/icon.png b/Resources/Textures/Objects/Devices/travel_camera.rsi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2e13f71cfa0f859ccf7df3346792d63398af3128 GIT binary patch literal 754 zcmVPx%tVu*cR9J=WmAz{daTv!x*W~hY>D>jDTBJ=U2LnPD-AZa+x@b#j)j^7=^p6l1 zaS;bcaVn-15d;;fTWx}&x(PZ+g-+(E&`O2mu8pZ_at`SoX>(0_E)M29{qFfaFW={R ze$R8zMHgNC?+~1X(dZcf6h%f+6w{4EUDpL&Hvm{!`Pxyx$4NLiI0#_sy5TB#gMeWI z=AMs^aIC3cP&vrbV7kji4wp0J?72 zWj|=E1QnrJ(#?S1$ISRJvkS{dCr~UI;P;bGjd1(UJ(7ty*<79n_wRCT`WBJMkR9mR z(4rwv~2-EG7;a`ib!OLWFl^pvt*i9u!#ZG z1rMU=6^fce&yRgjVcnm=B`9z8(!wjpk$=(WZ zBoI`Oaa;;HKCswsFLLEOnLaLQ!VTS4~ilUl7#v1A~M-u5jhm$#9mkUehAkT0C3e(oDr zPsK1!4xlL8PA>3-`-NI@-SMtk-L=n$5Gb>`x#_qEJ}rDe{#YT?w?=d=h<_s7qN8aA zBuV0Rhcyg`{0l}$QvjwlUT)B2BP=d1b=2=ThBt3qcSYdfi2gMJNs~wSy!y0hs7ls> zK$ZXIrN7SS7H^9d1eyZ`d(?~?dwmlA?oEH3-kW!JWqkES&NsKBSC%AOuDy`9dvef9 zg}Z+7&$>6ZJd9jwJ+m*wsy}6m6Hv`UbAiitCpQ#Li7eLqne_8*-6gfmlH{y~ORnbZ zSiAlAzk9o1TZM+dy#uiZM1C-OYyBtxeC4cb=LIv*oXnDW5*DSu-qimu^AWqf^Xzy3 e1FAVtzk`8!d*XuE6U489^mw}ZxvXFVdQ&MBb@08{*a&Hw-a literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Devices/travel_camera.rsi/meta.json b/Resources/Textures/Objects/Devices/travel_camera.rsi/meta.json new file mode 100644 index 0000000000..acd1d82967 --- /dev/null +++ b/Resources/Textures/Objects/Devices/travel_camera.rsi/meta.json @@ -0,0 +1,27 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation, edited by SlamBamActionman", + "size": { + "x": 32, + "y": 32 + }, + + "states": [ + { + "name": "equipped-NECK", + "directions": 4 + }, + { + "name": "icon" + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/Objects/Misc/photograph.rsi/black.png b/Resources/Textures/Objects/Misc/photograph.rsi/black.png new file mode 100644 index 0000000000000000000000000000000000000000..2e1284ebeb77d5076b10d6f1d0ad2feb1a976368 GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}eV#6kArY-_ zC+`+KtRUcWUG8c@v1ovTAg7=uYtTVn-y0K|*(cSg|1^GFt9kkzH-kdUg!<=?IIlpVO9of4A}rCqqNW8n24u&P@ErBrdM@kMYm^$=W#O(s!0c zj~zbnP6;~H+|K%Jj=;pLo40ejwEGDLT)n%0-KzE$4#k$5Io9Ft<=5F-|8W4ijKR~@ K&t;ucLK6VDx>2qG literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Misc/photograph.rsi/blue.png b/Resources/Textures/Objects/Misc/photograph.rsi/blue.png new file mode 100644 index 0000000000000000000000000000000000000000..d32bb7534c55ab9649988d4ca0a13f9723c70884 GIT binary patch literal 235 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}OFdm2Ln2z= zPEq7za^!LSFZ6`Vz*pblLp8^`1GdNdrR<+8R_M%o!#XRF!$#xIJEz^u9KW7?dH%gK zjp2CVociS#TatUq1!tZyFb?v{|E_1sq|_vEC`Il0nw3{L85jf?-i~2Qe_pqG?PYG8 zo%i~f*H<;RhAt4h$`bbWT{+XWrwt$3_?DYC?5%vM(-_3nkx}xkkn!~*Ev7SnJ}pjs i7U|UY*y7yvN`~jF6r417&+-O3n8DN4&t;ucLK6Vl!d(^s literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Misc/photograph.rsi/green.png b/Resources/Textures/Objects/Misc/photograph.rsi/green.png new file mode 100644 index 0000000000000000000000000000000000000000..6635d990ece133881308ab5823b38edded2a2f38 GIT binary patch literal 238 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}D?D8sLn2z= zPT9!IWXR)c-!bXdCnk~W8xHv%RC*b_)c?4MkK;k6EjkH1m$i3wz2N(;r13#}^7{AU zC%FVz96!8Yqw{diXJ;2R<1;-$r;hKH?r!K%SZXqJ<<=;zTqXtwmm72W&+fimvFX}P z*0?+Cf7Myss=gN27*`^EKtK*h_SguZ5KI8iZdQ4|* meqS{Bth7*pqv^qG8^$%R3jBK#rsM+M%;4$j=d#Wzp$Pzu6JK2b literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Misc/photograph.rsi/meta.json b/Resources/Textures/Objects/Misc/photograph.rsi/meta.json new file mode 100644 index 0000000000..a76a07d169 --- /dev/null +++ b/Resources/Textures/Objects/Misc/photograph.rsi/meta.json @@ -0,0 +1,32 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Made by ketufaispikinut (Github)", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "black" + }, + { + "name": "red" + }, + { + "name": "blue" + }, + { + "name": "green" + }, + { + "name": "yellow" + }, + { + "name": "purple" + }, + { + "name": "rainbow" + } + ] +} diff --git a/Resources/Textures/Objects/Misc/photograph.rsi/purple.png b/Resources/Textures/Objects/Misc/photograph.rsi/purple.png new file mode 100644 index 0000000000000000000000000000000000000000..6bf8b7b25276d0b744b865edeacbc41c8cc7aaa0 GIT binary patch literal 243 zcmVPx#tw}^dR9J=WlrahdF%U(+V&MsfDQql+y@1$xsPw7Q=TNqS7qGP$hCP6l+PEON ztWL~o;k+i0eEz&*2;ex5^Y2V7;TYppLHt)6`mL7w9<7_s;^@n0xHXm8mAMIPx#&q+i&7^FnpLv z)6}Jss#6;9JP*2EhQXE~8`C=VSoNDENpd}N#AyPz?DkU*OUtfNWnG_>=Xs$X=LwWe zX#&3Q|4yKN92CA=YzMN882}?TKxeUG!sI&w*4n$iK+%Uv5fG85AB~8-C8Q{d bqI{hTOh_^>G`#X{00000NkvXXu0mjf4To@_ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Misc/photograph.rsi/red.png b/Resources/Textures/Objects/Misc/photograph.rsi/red.png new file mode 100644 index 0000000000000000000000000000000000000000..9df1683989142f49b813fced609a7ec08a8aa9af GIT binary patch literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}Q$1ZALn2z= zPT9!IWXR)c|H91TzZ Q4d_4yPgg&ebxsLQ0Ch)EXaE2J literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Misc/photograph.rsi/yellow.png b/Resources/Textures/Objects/Misc/photograph.rsi/yellow.png new file mode 100644 index 0000000000000000000000000000000000000000..6cfc8c71ed78430d756aea89eed86494b8367484 GIT binary patch literal 237 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}%ROBjLn2z= zPBG+SG8Ay#-<_Zyc34BF;@Fp!m!9Xy9q_x5xIut_fmzp4z170kBP`Z)dba(K7i6;N zd;G^*_;f+fM=hmh<1;-|F5TW4+}+Tj&|+XQxp!Z^AzyzpEp=VYVet8M kf7?uZp(2^%g{z90XE7?ikEmgK19UQjr>mdKI;Vst0H&B Date: Tue, 7 Apr 2026 20:34:33 +0000 Subject: [PATCH 113/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index a086eaaaf8..4522cc66e9 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,13 +1,4 @@ Entries: -- author: TrixxedHeart - changes: - - message: Added Enter/Leave Genpop access to Security by default, allowing them - to be able to fix Genpop turnstiles with the access configurator if they are - destroyed. - type: Tweak - id: 9114 - time: '2025-10-18T00:28:44.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/39515 - author: TrixxedHeart changes: - message: Added Vox Chitter and Clicking emotes @@ -4032,3 +4023,10 @@ id: 9625 time: '2026-04-07T18:59:14.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43505 +- author: ketufaispikinut + changes: + - message: Added the tourist's travel camera along with very basic textual photographs. + type: Add + id: 9626 + time: '2026-04-07T20:33:23.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43322 From e83ea368db0d0aba36886d225813eab27b105bae Mon Sep 17 00:00:00 2001 From: beck-thompson <107373427+beck-thompson@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:27:48 -0700 Subject: [PATCH 114/126] Remove unused isfirsttimepredicted checks (#43509) --- .../Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs index eb80b26b58..ffd1d71cc0 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs @@ -67,7 +67,6 @@ public abstract partial class SharedGunSystem { if (args.Handled || !component.MayTransfer || - !Timing.IsFirstTimePredicted || args.Target == null || args.Used == args.Target || Deleted(args.Target) || @@ -356,11 +355,8 @@ public abstract partial class SharedGunSystem public void UpdateBallisticAppearance(Entity ent) { - if (!Timing.IsFirstTimePredicted || !TryComp(ent, out var appearance)) - return; - - Appearance.SetData(ent, AmmoVisuals.AmmoCount, GetBallisticShots(ent.Comp), appearance); - Appearance.SetData(ent, AmmoVisuals.AmmoMax, ent.Comp.Capacity, appearance); + Appearance.SetData(ent, AmmoVisuals.AmmoCount, GetBallisticShots(ent.Comp)); + Appearance.SetData(ent, AmmoVisuals.AmmoMax, ent.Comp.Capacity); } public void SetBallisticUnspawned(Entity entity, int count) From 9261dc2e9b7e03125337a4662abe39c5d240584c Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Wed, 8 Apr 2026 00:24:54 +0200 Subject: [PATCH 115/126] remove redundant IsServer and IsClient checks (#43511) remove redundant checks --- .../FeedbackPopup/ClientFeedbackManager.cs | 5 +---- .../FeedbackSystem/ServerFeedbackManager.cs | 12 ------------ Content.Server/Sound/EmitSoundSystem.cs | 5 ----- 3 files changed, 1 insertion(+), 21 deletions(-) diff --git a/Content.Client/FeedbackPopup/ClientFeedbackManager.cs b/Content.Client/FeedbackPopup/ClientFeedbackManager.cs index dd390cc2d4..5b2b52b1b7 100644 --- a/Content.Client/FeedbackPopup/ClientFeedbackManager.cs +++ b/Content.Client/FeedbackPopup/ClientFeedbackManager.cs @@ -31,7 +31,7 @@ public sealed class ClientFeedbackManager : SharedFeedbackManager /// public override void Display(List>? prototypes) { - if (prototypes == null || !NetManager.IsClient) + if (prototypes == null) return; var count = _displayedPopups.Count; @@ -42,9 +42,6 @@ public sealed class ClientFeedbackManager : SharedFeedbackManager /// public override void Remove(List>? prototypes) { - if (!NetManager.IsClient) - return; - if (prototypes == null) { _displayedPopups.Clear(); diff --git a/Content.Server/FeedbackSystem/ServerFeedbackManager.cs b/Content.Server/FeedbackSystem/ServerFeedbackManager.cs index 09edd8eefe..4ecb22bcc2 100644 --- a/Content.Server/FeedbackSystem/ServerFeedbackManager.cs +++ b/Content.Server/FeedbackSystem/ServerFeedbackManager.cs @@ -29,9 +29,6 @@ public sealed class ServerFeedbackManager : SharedFeedbackManager /// public override void SendToSession(ICommonSession session, List> popupPrototypes, bool remove = false) { - if (!NetManager.IsServer) - return; - var msg = new FeedbackPopupMessage { FeedbackPrototypes = popupPrototypes, @@ -44,9 +41,6 @@ public sealed class ServerFeedbackManager : SharedFeedbackManager /// public override void SendToAllSessions(List> popupPrototypes, bool remove = false) { - if (!NetManager.IsServer) - return; - var msg = new FeedbackPopupMessage { FeedbackPrototypes = popupPrototypes, @@ -59,9 +53,6 @@ public sealed class ServerFeedbackManager : SharedFeedbackManager /// public override void OpenForSession(ICommonSession session) { - if (!NetManager.IsServer) - return; - var msg = new OpenFeedbackPopupMessage(); NetManager.ServerSendMessage(msg, session.Channel); } @@ -69,9 +60,6 @@ public sealed class ServerFeedbackManager : SharedFeedbackManager /// public override void OpenForAllSessions() { - if (!NetManager.IsServer) - return; - var msg = new OpenFeedbackPopupMessage(); NetManager.ServerSendToAll(msg); } diff --git a/Content.Server/Sound/EmitSoundSystem.cs b/Content.Server/Sound/EmitSoundSystem.cs index 1720d67d02..38878b147f 100644 --- a/Content.Server/Sound/EmitSoundSystem.cs +++ b/Content.Server/Sound/EmitSoundSystem.cs @@ -1,14 +1,12 @@ using Content.Shared.Sound; using Content.Shared.Sound.Components; using Robust.Shared.Timing; -using Robust.Shared.Network; namespace Content.Server.Sound; public sealed class EmitSoundSystem : SharedEmitSoundSystem { [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly INetManager _net = default!; public override void Update(float frameTime) { @@ -49,9 +47,6 @@ public sealed class EmitSoundSystem : SharedEmitSoundSystem private void SpamEmitSoundReset(Entity entity) { - if (_net.IsClient) - return; - entity.Comp.NextSound = _timing.CurTime + ((entity.Comp.MinInterval < entity.Comp.MaxInterval) ? Random.Next(entity.Comp.MinInterval, entity.Comp.MaxInterval) : entity.Comp.MaxInterval); From e1f43b1a6a8169323f50613cdd9f432c8582cdd5 Mon Sep 17 00:00:00 2001 From: alexalexmax <149889301+alexalexmax@users.noreply.github.com> Date: Tue, 7 Apr 2026 23:58:35 -0400 Subject: [PATCH 116/126] Fix unsticking items with StickySystem (#43514) * fix it * change to the surface the sticky thing is on --------- Co-authored-by: seanpimble <149889301+seanpimble@users.noreply.github.com> --- Content.Shared/Sticky/Systems/StickySystem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Content.Shared/Sticky/Systems/StickySystem.cs b/Content.Shared/Sticky/Systems/StickySystem.cs index bc04c81f55..f060cfe700 100644 --- a/Content.Shared/Sticky/Systems/StickySystem.cs +++ b/Content.Shared/Sticky/Systems/StickySystem.cs @@ -102,7 +102,7 @@ public sealed class StickySystem : EntitySystem private void OnStickyDoAfter(Entity ent, ref StickyDoAfterEvent args) { - // target is the sticky item when unsticking and the surface when sticking, it will never be null + // target is the surface when sticking/unsticking, it will never be null if (args.Handled || args.Cancelled || args.Args.Target is not {} target) return; @@ -141,7 +141,7 @@ public sealed class StickySystem : EntitySystem } // start unsticking object - _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, comp.UnstickDelay, new StickyDoAfterEvent(), uid, target: uid) + _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, comp.UnstickDelay, new StickyDoAfterEvent(), uid, target: stuckTo) { BreakOnMove = true, NeedHand = true, From 1b402f704b5f89b0ebc2e87d9309454091616fcd Mon Sep 17 00:00:00 2001 From: PJBot Date: Wed, 8 Apr 2026 04:14:26 +0000 Subject: [PATCH 117/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 4522cc66e9..cd2045dfa6 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: TrixxedHeart - changes: - - message: Added Vox Chitter and Clicking emotes - type: Add - id: 9115 - time: '2025-10-18T00:28:44.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/40878 - author: Kittygyat changes: - message: Added a new generic Artistry borg module! @@ -4030,3 +4023,10 @@ id: 9626 time: '2026-04-07T20:33:23.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43322 +- author: alexalexmax + changes: + - message: Items that can be stuck to walls are now able to be unstuck properly. + type: Fix + id: 9627 + time: '2026-04-08T04:13:18.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43514 From 6d08929e19458eea234db0399e57b407627229a4 Mon Sep 17 00:00:00 2001 From: TriviaSolari <154280615+TriviaSolari@users.noreply.github.com> Date: Wed, 8 Apr 2026 01:25:01 -0400 Subject: [PATCH 118/126] Add notification for AI core being damaged (#43513) * Add notification for AI core being damaged * No need to check damagePercent --- Content.Server/Silicons/StationAi/StationAiSystem.cs | 11 +++++++++-- Resources/Locale/en-US/silicons/station-ai.ftl | 1 + Resources/Prototypes/Chat/notifications.yml | 9 ++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Content.Server/Silicons/StationAi/StationAiSystem.cs b/Content.Server/Silicons/StationAi/StationAiSystem.cs index cc2f7a4e2f..b16c98f56b 100644 --- a/Content.Server/Silicons/StationAi/StationAiSystem.cs +++ b/Content.Server/Silicons/StationAi/StationAiSystem.cs @@ -67,6 +67,7 @@ public sealed class StationAiSystem : SharedStationAiSystem private readonly ProtoId _aiWireSnippedChatNotificationPrototype = "AiWireSnipped"; private readonly ProtoId _aiLosingPowerChatNotificationPrototype = "AiLosingPower"; private readonly ProtoId _aiCriticalPowerChatNotificationPrototype = "AiCriticalPower"; + private readonly ProtoId _aiTakingDamageChatNotificationPrototype = "AiTakingDamage"; private readonly ProtoId _stationAiJob = "StationAi"; private readonly EntProtoId _stationAiBrain = "StationAiBrain"; @@ -235,7 +236,7 @@ public sealed class StationAiSystem : SharedStationAiSystem private void OnDamageChanged(Entity entity, ref DamageChangedEvent args) { - UpdateCoreIntegrityAlert(entity); + UpdateCoreIntegrityAlert(entity, args.DamageIncreased); UpdateDamagedAccent(entity); } @@ -285,7 +286,7 @@ public sealed class StationAiSystem : SharedStationAiSystem } } - private void UpdateCoreIntegrityAlert(Entity ent) + private void UpdateCoreIntegrityAlert(Entity ent, bool damageIncreased = false) { if (!TryComp(ent, out var damageable)) return; @@ -303,6 +304,12 @@ public sealed class StationAiSystem : SharedStationAiSystem var damageLevel = Math.Round(damagePercent.Float() * proto.MaxSeverity); _alerts.ShowAlert(held.Value, _damageAlert, (short)Math.Clamp(damageLevel, 0, proto.MaxSeverity)); + + if (damageIncreased) + { + var ev = new ChatNotificationEvent(_aiTakingDamageChatNotificationPrototype, ent); + RaiseLocalEvent(held.Value, ref ev); + } } private void OnDoAfterAttempt(Entity ent, ref DoAfterAttemptEvent args) diff --git a/Resources/Locale/en-US/silicons/station-ai.ftl b/Resources/Locale/en-US/silicons/station-ai.ftl index cbe3ef6ec0..1f35af41a8 100644 --- a/Resources/Locale/en-US/silicons/station-ai.ftl +++ b/Resources/Locale/en-US/silicons/station-ai.ftl @@ -8,6 +8,7 @@ station-ai-has-no-power-for-upload = Upload failed - the AI core is unpowered. station-ai-is-too-damaged-for-upload = Upload failed - the AI core must be repaired. station-ai-core-losing-power = Your AI core is now running on reserve battery power. station-ai-core-critical-power = Your AI core is critically low on power. External power must be re-established or severe data corruption may occur! +station-ai-core-taking-damage = Your AI core is sustaining physical damage. # Ghost role station-ai-ghost-role-name = Station AI diff --git a/Resources/Prototypes/Chat/notifications.yml b/Resources/Prototypes/Chat/notifications.yml index cea67fa0ee..f155b99433 100644 --- a/Resources/Prototypes/Chat/notifications.yml +++ b/Resources/Prototypes/Chat/notifications.yml @@ -32,4 +32,11 @@ message: station-ai-core-critical-power sound: /Audio/Effects/alert.ogg color: Red - nextDelay: 120 \ No newline at end of file + nextDelay: 120 + +- type: chatNotification + id: AiTakingDamage + message: station-ai-core-taking-damage + sound: /Audio/Misc/notice2.ogg + color: Orange + nextDelay: 30 From 4f35ac5686f9a49d136da6fc237944cb4095b894 Mon Sep 17 00:00:00 2001 From: PJBot Date: Wed, 8 Apr 2026 05:41:10 +0000 Subject: [PATCH 119/126] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index cd2045dfa6..ef8f1b6bd6 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Kittygyat - changes: - - message: Added a new generic Artistry borg module! - type: Add - id: 9116 - time: '2025-10-18T07:13:42.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/39679 - author: Hitlinemoss changes: - message: Folders and clipboards now recycle into sensible material components, @@ -4030,3 +4023,10 @@ id: 9627 time: '2026-04-08T04:13:18.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/43514 +- author: TriviaSolari + changes: + - message: The Station AI is now notified when their core is taking damage. + type: Add + id: 9628 + time: '2026-04-08T05:40:02.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43513 From a6ec5e7d3eb87e3ede058f38def7aa664ff28818 Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:21:53 +0200 Subject: [PATCH 120/126] allow changelings to drop stored disguises (#43487) * drop option * popup and better icon --- .../ChangelingTransformBoundUserInterface.cs | 38 ++++++++++-- .../Systems/ChangelingTransformSystem.UI.cs | 16 ++++- .../Systems/ChangelingTransformSystem.cs | 61 ++++++++++++------- .../Systems/SharedChangelingIdentitySystem.cs | 16 +++++ .../Locale/en-US/changeling/changeling.ftl | 13 +++- 5 files changed, 115 insertions(+), 29 deletions(-) diff --git a/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs b/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs index dc40147220..25cfb1ae82 100644 --- a/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs +++ b/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs @@ -4,6 +4,7 @@ using Content.Shared.Changeling.Components; using Content.Shared.Changeling.Systems; using JetBrains.Annotations; using Robust.Client.UserInterface; +using Robust.Shared.Utility; namespace Content.Client.Changeling.UI; @@ -12,7 +13,9 @@ public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owne { private SimpleRadialMenu? _menu; private static readonly Color SelectedOptionBackground = Palettes.Green.Element.WithAlpha(128); + private static readonly Color DisabledOptionBackground = Palettes.Slate.Element.WithAlpha(128); private static readonly Color SelectedOptionHoverBackground = Palettes.Green.HoveredElement.WithAlpha(128); + private static readonly Color DisabledOptionHoverBackground = Palettes.Slate.HoveredElement.WithAlpha(128); protected override void Open() { @@ -42,21 +45,41 @@ public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owne ) { var buttons = new List(); + var dropButtons = new List(); + foreach (var identity in identities) { - if (!EntMan.TryGetComponent(identity, out var metadata)) - continue; - + // Options for selecting identities. var option = new RadialMenuActionOption(SendIdentitySelect, EntMan.GetNetEntity(identity)) { IconSpecifier = RadialMenuIconSpecifier.With(identity), - ToolTip = metadata.EntityName, - BackgroundColor = (currentIdentity == identity) ? SelectedOptionBackground : null, + ToolTip = Loc.GetString("changeling-transform-bui-select-entity", ("entity", identity)), + BackgroundColor = (currentIdentity == identity) ? SelectedOptionBackground : null, // mark as selected HoverBackgroundColor = (currentIdentity == identity) ? SelectedOptionHoverBackground : null }; buttons.Add(option); + + // Options for dropping identities. + var dropOption = new RadialMenuActionOption(SendIdentityDrop, EntMan.GetNetEntity(identity)) + { + IconSpecifier = RadialMenuIconSpecifier.With(identity), + ToolTip = (currentIdentity == identity) + ? Loc.GetString("changeling-transform-bui-drop-identity-cannot-drop") + : Loc.GetString("changeling-transform-bui-drop-identity-entity", ("entity", identity)), + BackgroundColor = (currentIdentity == identity) ? DisabledOptionBackground : null, // cannot drop your current identity + HoverBackgroundColor = (currentIdentity == identity) ? DisabledOptionHoverBackground : null + }; + dropButtons.Add(dropOption); } + // Menu category for dropping identities. + var dropMenuButton = new RadialMenuNestedLayerOption(dropButtons) + { + IconSpecifier = RadialMenuIconSpecifier.With(new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/delete.svg.192dpi.png"))), + ToolTip = Loc.GetString("changeling-transform-bui-drop-identity-menu") + }; + buttons.Add(dropMenuButton); + return buttons; } @@ -64,4 +87,9 @@ public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owne { SendPredictedMessage(new ChangelingTransformIdentitySelectMessage(identityId)); } + + private void SendIdentityDrop(NetEntity identityId) + { + SendPredictedMessage(new ChangelingTransformIdentityDropMessage(identityId)); + } } diff --git a/Content.Shared/Changeling/Systems/ChangelingTransformSystem.UI.cs b/Content.Shared/Changeling/Systems/ChangelingTransformSystem.UI.cs index e555147352..50f5ceb547 100644 --- a/Content.Shared/Changeling/Systems/ChangelingTransformSystem.UI.cs +++ b/Content.Shared/Changeling/Systems/ChangelingTransformSystem.UI.cs @@ -3,13 +3,25 @@ namespace Content.Shared.Changeling.Systems; /// -/// Send when a player selects an intentity to transform into in the radial menu. +/// Send when a player selects an identity to transform into in the radial menu. /// [Serializable, NetSerializable] public sealed class ChangelingTransformIdentitySelectMessage(NetEntity targetIdentity) : BoundUserInterfaceMessage { /// - /// The uid of the cloned identity. + /// The uid of the stored identity. + /// + public readonly NetEntity TargetIdentity = targetIdentity; +} + +/// +/// Send when a player selects an identity to drop from their storage. +/// +[Serializable, NetSerializable] +public sealed class ChangelingTransformIdentityDropMessage(NetEntity targetIdentity) : BoundUserInterfaceMessage +{ + /// + /// The uid of the stored identity. /// public readonly NetEntity TargetIdentity = targetIdentity; } diff --git a/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs b/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs index 8bea4dedc6..8ec2d36cd8 100644 --- a/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs +++ b/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs @@ -30,6 +30,7 @@ public sealed partial class ChangelingTransformSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly IdentitySystem _identity = default!; + [Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentity = default!; private const string ChangelingBuiXmlGeneratedName = "ChangelingTransformBoundUserInterface"; public override void Initialize() @@ -38,8 +39,9 @@ public sealed partial class ChangelingTransformSystem : EntitySystem SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnTransformAction); - SubscribeLocalEvent(OnSuccessfulTransform); SubscribeLocalEvent(OnTransformSelected); + SubscribeLocalEvent(OnTransformDrop); + SubscribeLocalEvent(OnSuccessfulTransform); SubscribeLocalEvent(OnShutdown); // Components that need special handling outside of cloning. @@ -79,6 +81,43 @@ public sealed partial class ChangelingTransformSystem : EntitySystem // but pressing the number does. } + private void OnTransformSelected(Entity ent, + ref ChangelingTransformIdentitySelectMessage args) + { + if (!TryGetEntity(args.TargetIdentity, out var targetIdentity)) + return; + + if (!TryComp(ent, out var identity)) + return; + + if (identity.CurrentIdentity == targetIdentity) + return; // don't transform into ourselves + + if (!identity.ConsumedIdentities.ContainsKey(targetIdentity.Value)) + return; // this identity does not belong to this player + + TransformInto(ent.AsNullable(), targetIdentity.Value); + } + + private void OnTransformDrop(Entity ent, + ref ChangelingTransformIdentityDropMessage args) + { + if (!TryGetEntity(args.TargetIdentity, out var targetIdentity)) + return; + + if (!TryComp(ent, out var identity)) + return; + + if (identity.CurrentIdentity == targetIdentity) + return; // don't drop our current identity + + if (!identity.ConsumedIdentities.ContainsKey(targetIdentity.Value)) + return; // this identity does not belong to this player + + _popup.PopupClient(Loc.GetString("changeling-transform-bui-drop-identity-entity-popup", ("entity", targetIdentity.Value)), ent.Owner, PopupType.Large); + _changelingIdentity.DropStoredIdentity(ent.Owner, targetIdentity.Value); + } + /// /// Transform the changeling into another identity. /// This can be any cloneable humanoid and doesn't have to be stored in the ChangelingIdentityComponent, @@ -125,26 +164,6 @@ public sealed partial class ChangelingTransformSystem : EntitySystem }); } - private void OnTransformSelected(Entity ent, - ref ChangelingTransformIdentitySelectMessage args) - { - _ui.CloseUi(ent.Owner, ChangelingTransformUiKey.Key, ent); - - if (!TryGetEntity(args.TargetIdentity, out var targetIdentity)) - return; - - if (!TryComp(ent, out var identity)) - return; - - if (identity.CurrentIdentity == targetIdentity) - return; // don't transform into ourselves - - if (!identity.ConsumedIdentities.ContainsKey(targetIdentity.Value)) - return; // this identity does not belong to this player - - TransformInto(ent.AsNullable(), targetIdentity.Value); - } - private void OnSuccessfulTransform(Entity ent, ref ChangelingTransformDoAfterEvent args) { diff --git a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs index 12d8f162a9..b9e2fbef3e 100644 --- a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs +++ b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs @@ -184,6 +184,22 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem return clone; } + /// + /// Drop a stored identity from the changeling's storage. + /// + public void DropStoredIdentity(Entity ent, EntityUid identity) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + if (!HasComp(identity)) + return; // Not a stored identity. + + PredictedQueueDel(identity); + if (ent.Comp.ConsumedIdentities.Remove(identity)) + Dirty(ent); + } + /// /// Simple helper to add a PVS override to a nullspace identity. /// diff --git a/Resources/Locale/en-US/changeling/changeling.ftl b/Resources/Locale/en-US/changeling/changeling.ftl index 873744fa06..d5c88d06f2 100644 --- a/Resources/Locale/en-US/changeling/changeling.ftl +++ b/Resources/Locale/en-US/changeling/changeling.ftl @@ -1,6 +1,8 @@ -roles-antag-changeling-name = Changeling +# antag selection +roles-antag-changeling-name = Changeling roles-antag-changeling-objective = A intelligent predator that assumes the identities of its victims. +# devour changeling-devour-attempt-failed-cannot-devour = We cannot devour this! changeling-devour-attempt-failed-already-devoured = We already consumed this body! changeling-devour-attempt-failed-not-dead = This body yet lives! We cannot consume it alive! @@ -14,7 +16,16 @@ changeling-devour-begin-consume-others = { CAPITALIZE(POSS-ADJ($user)) } uncanny changeling-devour-consume-complete-self = Our uncanny mouth retreats, biomass consumed. changeling-devour-consume-complete-others = { CAPITALIZE(POSS-ADJ($user)) } uncanny mouth retreats. +# transformation changeling-transform-attempt-self = Our bones snap, muscles tear, one flesh becomes another. changeling-transform-attempt-others = { CAPITALIZE(POSS-ADJ($user)) } bones snap, muscles tear, body shifts into another. +# transformation BUI +changeling-transform-bui-select-entity = {$entity} +changeling-transform-bui-drop-identity-menu = Drop a devoured identity from your memory. +changeling-transform-bui-drop-identity-entity = Drop {$entity} +changeling-transform-bui-drop-identity-entity-popup = You dropped {$entity} from your memory. +changeling-transform-bui-drop-identity-cannot-drop = You cannot drop your current identity. + +# other changeling-paused-map-name = Changeling identity storage map From 9ef3b237ebe03a808d12d8e7167256c6cee05d40 Mon Sep 17 00:00:00 2001 From: mikey <23003816+mikeysaurus@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:50:33 -0700 Subject: [PATCH 121/126] Add logic to Debug/Set Outfit command to also fill storage containers (#43346) * add logic to fill backpacks * updates per feedback * update naming * resolve * remove unnecessary reassign * consistency --- .../Clothing/Systems/OutfitSystem.cs | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/Content.Server/Clothing/Systems/OutfitSystem.cs b/Content.Server/Clothing/Systems/OutfitSystem.cs index b1efbffd60..c1187657fd 100644 --- a/Content.Server/Clothing/Systems/OutfitSystem.cs +++ b/Content.Server/Clothing/Systems/OutfitSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Hands.Systems; using Content.Server.Preferences.Managers; +using Content.Server.Storage.EntitySystems; using Content.Shared.Access.Components; using Content.Shared.Clothing; using Content.Shared.Hands.Components; @@ -11,6 +12,8 @@ using Content.Shared.Preferences; using Content.Shared.Preferences.Loadouts; using Content.Shared.Roles; using Content.Shared.Station; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Storage; using Robust.Shared.Player; using Robust.Shared.Prototypes; @@ -23,6 +26,8 @@ public sealed class OutfitSystem : EntitySystem [Dependency] private readonly HandsSystem _handSystem = default!; [Dependency] private readonly InventorySystem _invSystem = default!; [Dependency] private readonly SharedStationSpawningSystem _spawningSystem = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; + [Dependency] private readonly StorageSystem _storageSystem = default!; public bool SetOutfit(EntityUid target, string gear, Action? onEquipped = null, bool unremovable = false) { @@ -68,9 +73,35 @@ public sealed class OutfitSystem : EntitySystem } } + var coords = Transform(target).Coordinates; + foreach (var (slotName, storageContainers) in startingGear.Storage) + { + if (storageContainers.Count == 0) + continue; + + if (!_invSystem.TryGetSlotEntity(target, slotName, out var slotEnt)) + continue; + + if (TryComp(slotEnt, out var storage)) + { + foreach (var entProto in storageContainers) + { + var spawnedEntity = SpawnAtPosition(entProto, coords); + _storageSystem.Insert(slotEnt.Value, spawnedEntity, out _, user: null, storageComp: storage, playSound: false); + } + } + else if (TryComp(slotEnt, out var itemSlots)) + { + foreach (var entProto in storageContainers) + { + var spawnedEntity = SpawnAtPosition(entProto, coords); + _itemSlotsSystem.TryInsertEmpty((slotEnt.Value, itemSlots), spawnedEntity, null, excludeUserAudio: true); + } + } + } + if (TryComp(target, out HandsComponent? handsComponent)) { - var coords = Comp(target).Coordinates; foreach (var prototype in startingGear.Inhand) { var inhandEntity = Spawn(prototype, coords); From c921f18f33a9698d87c0e9afdff1ecbd66ed41fc Mon Sep 17 00:00:00 2001 From: PJBot Date: Thu, 9 Apr 2026 05:06:12 +0000 Subject: [PATCH 122/126] Automatic changelog update --- Resources/Changelog/Admin.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Resources/Changelog/Admin.yml b/Resources/Changelog/Admin.yml index cf6842025e..43420c1019 100644 --- a/Resources/Changelog/Admin.yml +++ b/Resources/Changelog/Admin.yml @@ -1699,5 +1699,13 @@ Entries: id: 207 time: '2026-04-04T20:14:15.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/38712 +- author: mikeysaurus + changes: + - message: The debug SetOutfit command now also fills target's storage containers + based on the gear preset. + type: Tweak + id: 208 + time: '2026-04-09T05:05:01.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/43346 Name: Admin Order: 3 From 50b15b7c459944af1d78d8b052263916db7771bf Mon Sep 17 00:00:00 2001 From: SnappingOpossum Date: Fri, 10 Apr 2026 06:38:43 +1000 Subject: [PATCH 123/126] Move food, drink, and vending random spawners to tables + minor cleanup (#42278) * Move food/drink related spawners to entity tables * Give RandomFoodBreakfast a different suffix * More unrelated convention stuff because I felt like it --- .../Random/Food_Drinks/donkpocketbox.yml | 41 +-- .../Random/Food_Drinks/drinks_glass.yml | 263 +++++++++--------- .../Random/Food_Drinks/food_baked_single.yml | 176 ++++++------ .../Random/Food_Drinks/food_baked_whole.yml | 113 ++++---- .../Random/Food_Drinks/food_breakfast.yml | 19 +- .../Spawners/Random/Food_Drinks/food_meal.yml | 178 ++++++------ .../Random/Food_Drinks/food_single.yml | 126 +++++---- .../Markers/Spawners/Random/vending.yml | 58 ++-- .../Markers/Spawners/Random/vendingdrinks.yml | 38 +-- .../Markers/Spawners/Random/vendingsnacks.yml | 30 +- 10 files changed, 530 insertions(+), 512 deletions(-) diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/donkpocketbox.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/donkpocketbox.yml index 05ad0ba725..b921582ff7 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/donkpocketbox.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/donkpocketbox.yml @@ -1,23 +1,24 @@ - type: entity - name: Donkpocket Box Spawner - id: DonkpocketBoxSpawner parent: MarkerBase + id: DonkpocketBoxSpawner + name: Donkpocket Box Spawner components: - - type: Sprite - layers: - - state: red - - sprite: Objects/Consumable/Food/Baked/donkpocket.rsi - state: box - - type: RandomSpawner - prototypes: - - FoodBoxDonkpocket - - FoodBoxDonkpocketSpicy - - FoodBoxDonkpocketTeriyaki - - FoodBoxDonkpocketPizza - - FoodBoxDonkpocketStonk - - FoodBoxDonkpocketBerry - - FoodBoxDonkpocketHonk - - FoodBoxDonkpocketDink - - FoodBoxDonkpocketMoth - chance: 0.5 - offset: 0.0 + - type: Sprite + layers: + - state: red + - sprite: Objects/Consumable/Food/Baked/donkpocket.rsi + state: box + - type: EntityTableSpawner + table: !type:GroupSelector + prob: 0.5 + children: + - id: FoodBoxDonkpocket + - id: FoodBoxDonkpocketSpicy + - id: FoodBoxDonkpocketTeriyaki + - id: FoodBoxDonkpocketPizza + - id: FoodBoxDonkpocketStonk + - id: FoodBoxDonkpocketBerry + - id: FoodBoxDonkpocketHonk + - id: FoodBoxDonkpocketDink + - id: FoodBoxDonkpocketMoth + offset: 0.0 diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/drinks_glass.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/drinks_glass.yml index d049250b55..ac845e55d4 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/drinks_glass.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/drinks_glass.yml @@ -1,140 +1,143 @@ - type: entity + parent: MarkerBase id: RandomDrinkGlass name: random drink spawner suffix: Glass - parent: MarkerBase placement: mode: AlignTileAny components: - type: Sprite layers: - - state: red - - sprite: Objects/Consumable/Drinks/beerglass.rsi - state: icon - - type: RandomSpawner - #small item - prototypes: - - DrinkAbsintheGlass - - DrinkAleGlass - - DrinkAlienBrainHemorrhage - - DrinkAloe - - DrinkAndalusia - - DrinkAntifreeze - - DrinkArnoldPalmer - - DrinkB52Glass - - DrinkBahamaMama - - DrinkBananaHonkGlass - - DrinkBarefootGlass - - DrinkBeerglass - - DrinkBerryJuice - - DrinkBlackRussianGlass - - DrinkBlueCuracaoGlass - - DrinkBlueHawaiianGlass - - DrinkBloodyMaryGlass - - DrinkBooger - - DrinkBraveBullGlass - - DrinkBronxGlass - - BudgetInsulsDrinkGlass - - DrinkCarrotJuice - - DrinkCoconutRum - - DrinkChocolateGlass - - DrinkCognacGlass - - DrinkCosmopolitan - - DrinkCrushDepthGlass - - DrinkCubaLibreGlass - - DrinkDarkandStormyGlass - - DrinkDeadRumGlass - - DrinkDevilsKiss - - DrinkDriestMartiniGlass - - DrinkDrGibbGlass - - DrinkElectricSharkGlass - - DrinkErikaSurprise - - DrinkFourteenLokoGlass - - DrinkGargleBlasterGlass - - DrinkGinFizzGlass - - DrinkGinGlass - - DrinkGinTonicglass - - DrinkGildlagerGlass - - DrinkGrapeJuice - - DrinkGreenTeaGlass - - DrinkGrogGlass - - DrinkHippiesDelightGlass - - DrinkIcedCoffeeGlass - - DrinkIcedGreenTeaGlass - - DrinkIcedBeerGlass - - DrinkIceCreamGlass - - IrishBoolGlass - - DrinkIrishSlammer - - DrinkIrishCoffeeGlass - - DrinkLemonadeGlass - - DrinkJackRoseGlass - - DrinkJungleBirdGlass - - DrinkKalimotxoGlass - - DrinkOrangeLimeSodaGlass - - DrinkLongIslandIcedTeaGlass - - DrinkManhattanGlass - - DrinkManlyDorfGlass - - DrinkMargaritaGlass - - DrinkMartiniGlass - - DrinkMeadGlass - - DrinkMilkshake - - DrinkMojito - - DrinkMonkeyBusinessGlass - - DrinkNTCahors - - DrinkPainkillerGlass - - DrinkPatronGlass - - DrinkPinaColadaGlass - - DrinkPoscaGlass - - DrinkRadlerGlass - - DrinkRedMeadGlass - - DrinkRewriter - - DrinkRoyRogersGlass - - DrinkRootBeerFloatGlass - - RubberneckGlass - - DrinkRumGlass - - DrinkSakeGlass - - DrinkSbitenGlass - - DrinkScrewdriverCocktailGlass - - DrinkShirleyTempleGlass - - DrinkSuiDreamGlass - - DrinkSingulo - - DrinkSoyLatte - - DrinkSyndicatebomb - - DrinkTequilaSunriseGlass - - DrinkThreeMileIslandGlass - - DrinkTortugaGlass - - DrinkToxinsSpecialGlass - - DrinkVampiroGlass - - DrinkVodkaMartiniGlass - - DrinkVodkaRedBool - - DrinkVodkaTonicGlass - - DrinkWatermelonJuice - - DrinkWatermelonWakeup - - DrinkWhiskeyColaGlass - - DrinkWhiskeySodaGlass - - DrinkWhiteRussianGlass - - DrinkWineGlass - - XenoBasherGlass - - DrinkShakeBlue - - DrinkShakeWhite - - DrinkTheMartinez - - DrinkMoonshineGlass - chance: 0.8 + - state: red + - sprite: Objects/Consumable/Drinks/beerglass.rsi + state: icon + - type: EntityTableSpawner + table: !type:GroupSelector + children: + - !type:GroupSelector #small item + weight: 0.95 + prob: 0.8 + children: + - id: DrinkAbsintheGlass + - id: DrinkAleGlass + - id: DrinkAlienBrainHemorrhage + - id: DrinkAloe + - id: DrinkAndalusia + - id: DrinkAntifreeze + - id: DrinkArnoldPalmer + - id: DrinkB52Glass + - id: DrinkBahamaMama + - id: DrinkBananaHonkGlass + - id: DrinkBarefootGlass + - id: DrinkBeerglass + - id: DrinkBerryJuice + - id: DrinkBlackRussianGlass + - id: DrinkBlueCuracaoGlass + - id: DrinkBlueHawaiianGlass + - id: DrinkBloodyMaryGlass + - id: DrinkBooger + - id: DrinkBraveBullGlass + - id: DrinkBronxGlass + - id: BudgetInsulsDrinkGlass + - id: DrinkCarrotJuice + - id: DrinkCoconutRum + - id: DrinkChocolateGlass + - id: DrinkCognacGlass + - id: DrinkCosmopolitan + - id: DrinkCrushDepthGlass + - id: DrinkCubaLibreGlass + - id: DrinkDarkandStormyGlass + - id: DrinkDeadRumGlass + - id: DrinkDevilsKiss + - id: DrinkDriestMartiniGlass + - id: DrinkDrGibbGlass + - id: DrinkElectricSharkGlass + - id: DrinkErikaSurprise + - id: DrinkFourteenLokoGlass + - id: DrinkGargleBlasterGlass + - id: DrinkGinFizzGlass + - id: DrinkGinGlass + - id: DrinkGinTonicglass + - id: DrinkGildlagerGlass + - id: DrinkGrapeJuice + - id: DrinkGreenTeaGlass + - id: DrinkGrogGlass + - id: DrinkHippiesDelightGlass + - id: DrinkIcedCoffeeGlass + - id: DrinkIcedGreenTeaGlass + - id: DrinkIcedBeerGlass + - id: DrinkIceCreamGlass + - id: IrishBoolGlass + - id: DrinkIrishSlammer + - id: DrinkIrishCoffeeGlass + - id: DrinkLemonadeGlass + - id: DrinkJackRoseGlass + - id: DrinkJungleBirdGlass + - id: DrinkKalimotxoGlass + - id: DrinkOrangeLimeSodaGlass + - id: DrinkLongIslandIcedTeaGlass + - id: DrinkManhattanGlass + - id: DrinkManlyDorfGlass + - id: DrinkMargaritaGlass + - id: DrinkMartiniGlass + - id: DrinkMeadGlass + - id: DrinkMilkshake + - id: DrinkMojito + - id: DrinkMonkeyBusinessGlass + - id: DrinkNTCahors + - id: DrinkPainkillerGlass + - id: DrinkPatronGlass + - id: DrinkPinaColadaGlass + - id: DrinkPoscaGlass + - id: DrinkRadlerGlass + - id: DrinkRedMeadGlass + - id: DrinkRewriter + - id: DrinkRoyRogersGlass + - id: DrinkRootBeerFloatGlass + - id: RubberneckGlass + - id: DrinkRumGlass + - id: DrinkSakeGlass + - id: DrinkSbitenGlass + - id: DrinkScrewdriverCocktailGlass + - id: DrinkShirleyTempleGlass + - id: DrinkSuiDreamGlass + - id: DrinkSingulo + - id: DrinkSoyLatte + - id: DrinkSyndicatebomb + - id: DrinkTequilaSunriseGlass + - id: DrinkThreeMileIslandGlass + - id: DrinkTortugaGlass + - id: DrinkToxinsSpecialGlass + - id: DrinkVampiroGlass + - id: DrinkVodkaMartiniGlass + - id: DrinkVodkaRedBool + - id: DrinkVodkaTonicGlass + - id: DrinkWatermelonJuice + - id: DrinkWatermelonWakeup + - id: DrinkWhiskeyColaGlass + - id: DrinkWhiskeySodaGlass + - id: DrinkWhiteRussianGlass + - id: DrinkWineGlass + - id: XenoBasherGlass + - id: DrinkShakeBlue + - id: DrinkShakeWhite + - id: DrinkTheMartinez + - id: DrinkMoonshineGlass + - !type:GroupSelector #rare + weight: 0.05 + children: + - id: DrinkAcidSpitGlass + - id: DrinkAlliesCocktail + - id: DrinkAmasecGlass + - id: DrinkAtomicBombGlass + - id: DrinkDemonsBlood + - id: DrinkDoctorsDelightGlass + - id: DrinkNeurotoxinGlass + - id: DrinkNuclearColaGlass + - id: DrinkSilencerGlass + - id: DrinkShakeMeat + - id: DrinkShakeRobo + - id: DrinkHoochGlass + - id: DrinkBeepskySmashGlass + - id: DrinkBacchusBlessing offset: 0.0 - #rare - rarePrototypes: - - DrinkAcidSpitGlass - - DrinkAlliesCocktail - - DrinkAmasecGlass - - DrinkAtomicBombGlass - - DrinkDemonsBlood - - DrinkDoctorsDelightGlass - - DrinkNeurotoxinGlass - - DrinkNuclearColaGlass - - DrinkSilencerGlass - - DrinkShakeMeat - - DrinkShakeRobo - - DrinkHoochGlass - - DrinkBeepskySmashGlass - - DrinkBacchusBlessing - rareChance: 0.05 diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_baked_single.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_baked_single.yml index 7d0f734acc..1a4d8defb8 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_baked_single.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_baked_single.yml @@ -1,96 +1,100 @@ - type: entity + parent: MarkerBase id: RandomFoodBakedSingle name: random baked food spawner suffix: Single Serving - parent: MarkerBase placement: mode: AlignTileAny components: - type: Sprite layers: - - state: red - - sprite: Objects/Consumable/Food/Baked/pie.rsi - state: plain-slice - - type: RandomSpawner - prototypes: - - FoodBreadPlainSlice - - FoodBreadMeatSlice - - FoodBreadBananaSlice - - FoodBreadCreamcheeseSlice - - FoodBreadMoldySlice - - FoodBreadGarlicSlice - - FoodBreadCornSlice - - FoodBreadSausageSlice - - FoodBreadButteredToast - - FoodBreadJellySlice - - FoodBreadFrenchToast - - FoodBreadTwoSlice - - FoodBreadVolcanicSlice - - FoodBreadTofuSlice - - FoodBreadBaguetteSlice - - FoodCakeBlueberrySlice - - FoodCakePlainSlice - - FoodCakeCarrotSlice - - FoodCakeCheeseSlice - - FoodCakeOrangeSlice - - FoodCakeLimeSlice - - FoodCakeLemonSlice - - FoodCakeChocolateSlice - - FoodCakeAppleSlice - - FoodCakeSlimeSlice - - FoodCakePumpkinSlice - - FoodCakeChristmasSlice - - FoodCakeVanillaSlice - - FoodCakeBirthdaySlice - - FoodCakeBerryDelightSlice - - FoodCakeCottonSlice - - FoodBakedMuffin - - FoodBakedMuffinBerry - - FoodBakedMuffinCherry - - FoodBakedMuffinBluecherry - - FoodBakedMuffinChocolate - - FoodBakedMuffinBanana - - FoodBakedBunHoney - - FoodBakedBunHotX - - FoodBakedBunMeat - - FoodBakedCookie - - FoodBakedCookieOatmeal - - FoodBakedCookieRaisin - - FoodBakedCookieSugar - - FoodBakedGrilledCheeseSandwich - - FoodBakedGrilledCheeseSandwichCotton - - FoodBakedNugget - - FoodBakedPancake - - FoodBakedPancakeBb - - FoodBakedPancakeCc - - FoodBakedWaffle - - FoodBakedWaffleSoy - - FoodBakedWaffleSoylent - - FoodBakedWaffleRoffle - - FoodBakedPretzel - - FoodBakedCannoli - - FoodPieAppleSlice - - FoodPieBaklavaSlice - - FoodPieClafoutisSlice - - FoodPieMeatSlice - - FoodPieCherrySlice - - FoodPieFrostySlice - - FoodTartGrape - - FoodTartCoco - - FoodBakedBrownie - - FoodPieBananaCreamSlice - - FoodTartMimeSlice - chance: 0.8 + - state: red + - sprite: Objects/Consumable/Food/Baked/pie.rsi + state: plain-slice + - type: EntityTableSpawner + table: !type:GroupSelector + children: + - !type:GroupSelector + weight: 0.95 + prob: 0.8 + children: + - id: FoodBreadPlainSlice + - id: FoodBreadMeatSlice + - id: FoodBreadBananaSlice + - id: FoodBreadCreamcheeseSlice + - id: FoodBreadMoldySlice + - id: FoodBreadGarlicSlice + - id: FoodBreadCornSlice + - id: FoodBreadSausageSlice + - id: FoodBreadButteredToast + - id: FoodBreadJellySlice + - id: FoodBreadFrenchToast + - id: FoodBreadTwoSlice + - id: FoodBreadVolcanicSlice + - id: FoodBreadTofuSlice + - id: FoodBreadBaguetteSlice + - id: FoodCakeBlueberrySlice + - id: FoodCakePlainSlice + - id: FoodCakeCarrotSlice + - id: FoodCakeCheeseSlice + - id: FoodCakeOrangeSlice + - id: FoodCakeLimeSlice + - id: FoodCakeLemonSlice + - id: FoodCakeChocolateSlice + - id: FoodCakeAppleSlice + - id: FoodCakeSlimeSlice + - id: FoodCakePumpkinSlice + - id: FoodCakeChristmasSlice + - id: FoodCakeVanillaSlice + - id: FoodCakeBirthdaySlice + - id: FoodCakeBerryDelightSlice + - id: FoodCakeCottonSlice + - id: FoodBakedMuffin + - id: FoodBakedMuffinBerry + - id: FoodBakedMuffinCherry + - id: FoodBakedMuffinBluecherry + - id: FoodBakedMuffinChocolate + - id: FoodBakedMuffinBanana + - id: FoodBakedBunHoney + - id: FoodBakedBunHotX + - id: FoodBakedBunMeat + - id: FoodBakedCookie + - id: FoodBakedCookieOatmeal + - id: FoodBakedCookieRaisin + - id: FoodBakedCookieSugar + - id: FoodBakedGrilledCheeseSandwich + - id: FoodBakedGrilledCheeseSandwichCotton + - id: FoodBakedNugget + - id: FoodBakedPancake + - id: FoodBakedPancakeBb + - id: FoodBakedPancakeCc + - id: FoodBakedWaffle + - id: FoodBakedWaffleSoy + - id: FoodBakedWaffleSoylent + - id: FoodBakedWaffleRoffle + - id: FoodBakedPretzel + - id: FoodBakedCannoli + - id: FoodPieAppleSlice + - id: FoodPieBaklavaSlice + - id: FoodPieClafoutisSlice + - id: FoodPieMeatSlice + - id: FoodPieCherrySlice + - id: FoodPieFrostySlice + - id: FoodTartGrape + - id: FoodTartCoco + - id: FoodBakedBrownie + - id: FoodPieBananaCreamSlice + - id: FoodTartMimeSlice + - !type:GroupSelector #rare + weight: 0.05 + children: + - id: FoodBreadMeatSpiderSlice + - id: FoodBreadMimanaSlice + - id: FoodCakeBrainSlice + - id: FoodCakeClownSlice + - id: FoodCakeSpacemanSlice + - id: FoodTartGapple + - id: FoodBreadMeatXenoSlice + - id: FoodPieXenoSlice + - id: FoodBakedCannabisBrownie offset: 0.0 - #rare - rarePrototypes: - - FoodBreadMeatSpiderSlice - - FoodBreadMimanaSlice - - FoodCakeBrainSlice - - FoodCakeClownSlice - - FoodCakeSpacemanSlice - - FoodTartGapple - - FoodBreadMeatXenoSlice - - FoodPieXenoSlice - - FoodBakedCannabisBrownie - rareChance: 0.05 diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_baked_whole.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_baked_whole.yml index 7683f19884..f7c553b022 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_baked_whole.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_baked_whole.yml @@ -1,65 +1,68 @@ - type: entity + parent: MarkerBase id: RandomFoodBakedWhole name: random baked food spawner suffix: Whole - parent: MarkerBase placement: mode: AlignTileAny components: - type: Sprite layers: - - state: red - - sprite: Objects/Consumable/Food/Baked/pie.rsi - state: plain - - type: RandomSpawner - #small item - prototypes: - - FoodBreadVolcanic - - FoodBreadPlain - - FoodBreadMeat - - FoodBreadCorn - - FoodBreadSausage - - FoodBreadBanana - - FoodBreadTofu - - FoodBreadCreamcheese - - FoodBreadBaguette - - FoodCakeBlueberry - - FoodCakePlain - - FoodCakeCheese - - FoodCakeCarrot - - FoodCakeOrange - - FoodCakeLime - - FoodCakeLemon - - FoodCakeChocolate - - FoodCakeApple - - FoodCakeSlime - - FoodCakePumpkin - - FoodCakeChristmas - - FoodCakeBirthday - - FoodCakeVanilla - - FoodCakeBerryDelight - - FoodCakeCotton - - FoodPieApple - - FoodPieBaklava - - FoodPieBananaCream - - FoodPieClafoutis - - FoodPieCherry - - FoodPieMeat - - FoodPieFrosty - - FoodBakedBrownieBatch - chance: 0.8 + - state: red + - sprite: Objects/Consumable/Food/Baked/pie.rsi + state: plain + - type: EntityTableSpawner + table: !type:GroupSelector + children: + - !type:GroupSelector #small item + weight: 0.95 + prob: 0.8 + children: + - id: FoodBreadVolcanic + - id: FoodBreadPlain + - id: FoodBreadMeat + - id: FoodBreadCorn + - id: FoodBreadSausage + - id: FoodBreadBanana + - id: FoodBreadTofu + - id: FoodBreadCreamcheese + - id: FoodBreadBaguette + - id: FoodCakeBlueberry + - id: FoodCakePlain + - id: FoodCakeCheese + - id: FoodCakeCarrot + - id: FoodCakeOrange + - id: FoodCakeLime + - id: FoodCakeLemon + - id: FoodCakeChocolate + - id: FoodCakeApple + - id: FoodCakeSlime + - id: FoodCakePumpkin + - id: FoodCakeChristmas + - id: FoodCakeBirthday + - id: FoodCakeVanilla + - id: FoodCakeBerryDelight + - id: FoodCakeCotton + - id: FoodPieApple + - id: FoodPieBaklava + - id: FoodPieBananaCream + - id: FoodPieClafoutis + - id: FoodPieCherry + - id: FoodPieMeat + - id: FoodPieFrosty + - id: FoodBakedBrownieBatch + - !type:GroupSelector #rare + weight: 0.05 + children: + - id: FoodBreadMeatSpider + - id: FoodBreadMimana + - id: FoodCakeBrain + - id: FoodCakeClown + - id: FoodCakeSpaceman + - id: FoodPieXeno + - id: FoodPieAmanita + - id: FoodPiePlump + - id: FoodBreadMeatXeno + - id: FoodPieXeno + - id: FoodBakedCannabisBrownieBatch offset: 0.0 - #rare - rarePrototypes: - - FoodBreadMeatSpider - - FoodBreadMimana - - FoodCakeBrain - - FoodCakeClown - - FoodCakeSpaceman - - FoodPieXeno - - FoodPieAmanita - - FoodPiePlump - - FoodBreadMeatXeno - - FoodPieXeno - - FoodBakedCannabisBrownieBatch - rareChance: 0.05 diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_breakfast.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_breakfast.yml index e4213ad31f..27256c673e 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_breakfast.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_breakfast.yml @@ -1,18 +1,19 @@ - type: entity + parent: MarkerBase id: RandomFoodBreakfast name: random food spawner - suffix: Meal - parent: MarkerBase + suffix: Breakfast placement: mode: AlignTileAny components: - type: Sprite layers: - - sprite: Objects/Consumable/Food/breakfast.rsi - state: fullamerican - - type: RandomSpawner - prototypes: - - FoodBreakfastAmerican - - FoodBreakfastEnglish - chance: 0.8 + - sprite: Objects/Consumable/Food/breakfast.rsi + state: fullamerican + - type: EntityTableSpawner + table: !type:GroupSelector + prob: 0.8 + children: + - id: FoodBreakfastAmerican + - id: FoodBreakfastEnglish offset: 0.0 diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_meal.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_meal.yml index 4c64bdd786..14aea5b79c 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_meal.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_meal.yml @@ -1,97 +1,101 @@ - type: entity + parent: MarkerBase id: RandomFoodMeal name: random food spawner suffix: Meal - parent: MarkerBase placement: mode: AlignTileAny components: - type: Sprite layers: - - state: red - - sprite: Objects/Consumable/Food/meals.rsi - state: cornedbeef - - type: RandomSpawner - prototypes: - - FoodMealPotatoLoaded - - FoodMealFries - - FoodMealFriesCheesy - - FoodMealFriesCarrot - - FoodMealNachos - - FoodMealNachosCheesy - - FoodMealNachosCuban - - FoodMealEggplantParm - - FoodMealPotatoYaki - - FoodMealCubancarp - - FoodMealCornedbeef - - FoodMealPigblanket - - FoodMealRibs - - FoodMealEggsbenedict - - FoodMealOmelette - - FoodMealFriedegg - - FoodMealQueso - - FoodMealSashimi - - FoodMealEnchiladas - - FoodNoodlesChowmein - - FoodNoodlesSpesslaw - - FoodNoodlesMeatball - - FoodNoodlesBoiled - - FoodNoodles - - FoodNoodlesButter - - FoodMealHappyHonkClown - - FoodSoupPea - - FoodSaladHerb - - FoodSaladValid - - FoodSaladFruit - - FoodSaladJungle - - FoodSaladCitrus - - FoodSaladCaesar - - FoodSaladColeslaw - - FoodSaladKimchi - - FoodSaladWatermelonFruitBowl - - FoodRiceBoiled - - FoodRiceEgg - - FoodRicePork - - FoodRicePudding - - FoodRiceGumbo - - FoodOatmeal - - FoodSoupMeatball - - FoodSoupVegetable - - FoodSoupNettle - - FoodSoupChiliHot - - FoodSoupChiliCold - - FoodSoupTomato - - FoodSoupMiso - - FoodSoupMushroom - - FoodSoupBeet - - FoodSoupBeetRed - - FoodSoupPotato - - FoodSoupOnion - - FoodSoupBisque - - FoodSoupBungo - - FoodMealCornInButter - - FoodSoupStew - chance: 0.8 + - state: red + - sprite: Objects/Consumable/Food/meals.rsi + state: cornedbeef + - type: EntityTableSpawner + table: !type:GroupSelector + children: + - !type:GroupSelector + weight: 0.95 + prob: 0.8 + children: + - id: FoodMealPotatoLoaded + - id: FoodMealFries + - id: FoodMealFriesCheesy + - id: FoodMealFriesCarrot + - id: FoodMealNachos + - id: FoodMealNachosCheesy + - id: FoodMealNachosCuban + - id: FoodMealEggplantParm + - id: FoodMealPotatoYaki + - id: FoodMealCubancarp + - id: FoodMealCornedbeef + - id: FoodMealPigblanket + - id: FoodMealRibs + - id: FoodMealEggsbenedict + - id: FoodMealOmelette + - id: FoodMealFriedegg + - id: FoodMealQueso + - id: FoodMealSashimi + - id: FoodMealEnchiladas + - id: FoodNoodlesChowmein + - id: FoodNoodlesSpesslaw + - id: FoodNoodlesMeatball + - id: FoodNoodlesBoiled + - id: FoodNoodles + - id: FoodNoodlesButter + - id: FoodMealHappyHonkClown + - id: FoodSoupPea + - id: FoodSaladHerb + - id: FoodSaladValid + - id: FoodSaladFruit + - id: FoodSaladJungle + - id: FoodSaladCitrus + - id: FoodSaladCaesar + - id: FoodSaladColeslaw + - id: FoodSaladKimchi + - id: FoodSaladWatermelonFruitBowl + - id: FoodRiceBoiled + - id: FoodRiceEgg + - id: FoodRicePork + - id: FoodRicePudding + - id: FoodRiceGumbo + - id: FoodOatmeal + - id: FoodSoupMeatball + - id: FoodSoupVegetable + - id: FoodSoupNettle + - id: FoodSoupChiliHot + - id: FoodSoupChiliCold + - id: FoodSoupTomato + - id: FoodSoupMiso + - id: FoodSoupMushroom + - id: FoodSoupBeet + - id: FoodSoupBeetRed + - id: FoodSoupPotato + - id: FoodSoupOnion + - id: FoodSoupBisque + - id: FoodSoupBungo + - id: FoodMealCornInButter + - id: FoodSoupStew + - !type:GroupSelector #rare + weight: 0.05 + children: + - id: FoodMealMint + - id: FoodMealBearsteak + - id: FoodMealMilkape + - id: DisgustingSweptSoup + - id: FoodMealMemoryleek + - id: FoodSaladAesir + - id: FoodSaladEden + - id: FoodJellyDuff + - id: FoodJellyAmanita + - id: FoodSoupSlime + - id: FoodSoupTomatoBlood + - id: FoodSoupWingFangChu + - id: FoodSoupClown + - id: FoodSoupMystery + - id: FoodSoupChiliClown + - id: FoodSoupMonkey + - id: FoodSoupEyeball + - id: FoodSoupElectron + - id: FoodNoodlesCopy offset: 0.0 - #rare - rarePrototypes: - - FoodMealMint - - FoodMealBearsteak - - FoodMealMilkape - - DisgustingSweptSoup - - FoodMealMemoryleek - - FoodSaladAesir - - FoodSaladEden - - FoodJellyDuff - - FoodJellyAmanita - - FoodSoupSlime - - FoodSoupTomatoBlood - - FoodSoupWingFangChu - - FoodSoupClown - - FoodSoupMystery - - FoodSoupChiliClown - - FoodSoupMonkey - - FoodSoupEyeball - - FoodSoupElectron - - FoodNoodlesCopy - rareChance: 0.05 diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_single.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_single.yml index fda7b85b75..ea44f18599 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_single.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_single.yml @@ -1,71 +1,75 @@ - type: entity + parent: MarkerBase id: RandomFoodSingle name: random food spawner suffix: Single Serving - parent: MarkerBase placement: mode: AlignTileAny components: - type: Sprite layers: - - state: red - - sprite: Objects/Consumable/Food/burger.rsi - state: plain - - type: RandomSpawner - prototypes: - - FoodBagel - - FoodBagelPoppy - - FoodBurgerJelly - - FoodBurgerCarp - - FoodBurgerTofu - - FoodBurgerXeno - - FoodBurgerBig - - FoodBurgerFive - - FoodBurgerRat - - FoodBurgerBacon - - FoodBurgerEmpowered - - FoodBurgerCrab - - FoodBurgerHuman - - FoodBurgerSoy - - FoodBurgerMcrib - - FoodBurgerMcguffin - - FoodBurgerChicken - - FoodBurgerDuck - - FoodBurgerCheese - - FoodNoodlesBoiled - - FoodNoodles - - FoodNoodlesCopy - - FoodNoodlesButter - - FoodPizzaMargheritaSlice - - FoodPizzaMeatSlice - - FoodPizzaMushroomSlice - - FoodPizzaVegetableSlice - - FoodPizzaDonkpocketSlice - - FoodPizzaDankSlice - - FoodPizzaSassysageSlice - - FoodPizzaPineappleSlice - - FoodPizzaMoldySlice - - FoodBakedDumplings - - FoodBakedChevreChaud - - FoodBakedNugget - - FoodTacoShell - chance: 0.8 + - state: red + - sprite: Objects/Consumable/Food/burger.rsi + state: plain + - type: EntityTableSpawner + table: !type:GroupSelector + children: + - !type:GroupSelector + weight: 0.95 + prob: 0.8 + children: + - id: FoodBagel + - id: FoodBagelPoppy + - id: FoodBurgerJelly + - id: FoodBurgerCarp + - id: FoodBurgerTofu + - id: FoodBurgerXeno + - id: FoodBurgerBig + - id: FoodBurgerFive + - id: FoodBurgerRat + - id: FoodBurgerBacon + - id: FoodBurgerEmpowered + - id: FoodBurgerCrab + - id: FoodBurgerHuman + - id: FoodBurgerSoy + - id: FoodBurgerMcrib + - id: FoodBurgerMcguffin + - id: FoodBurgerChicken + - id: FoodBurgerDuck + - id: FoodBurgerCheese + - id: FoodNoodlesBoiled + - id: FoodNoodles + - id: FoodNoodlesCopy + - id: FoodNoodlesButter + - id: FoodPizzaMargheritaSlice + - id: FoodPizzaMeatSlice + - id: FoodPizzaMushroomSlice + - id: FoodPizzaVegetableSlice + - id: FoodPizzaDonkpocketSlice + - id: FoodPizzaDankSlice + - id: FoodPizzaSassysageSlice + - id: FoodPizzaPineappleSlice + - id: FoodPizzaMoldySlice + - id: FoodBakedDumplings + - id: FoodBakedChevreChaud + - id: FoodBakedNugget + - id: FoodTacoShell + - !type:GroupSelector #rare + weight: 0.05 + children: + - id: FoodBurgerAppendix + - id: FoodBurgerRobot + - id: FoodBurgerBaseball + - id: FoodBurgerBear + - id: FoodBurgerCat + - id: FoodBurgerClown + - id: FoodBurgerMime + - id: FoodBurgerBrain + - id: FoodBurgerGhost + - id: FoodBurgerSpell + - id: FoodBurgerSuper + - id: FoodBurgerCrazy + - id: FoodPizzaArnoldSlice + - id: FoodPizzaUraniumSlice + - id: FoodPizzaWorldpeasSlice offset: 0.0 - #rare - rarePrototypes: - - FoodBurgerAppendix - - FoodBurgerRobot - - FoodBurgerBaseball - - FoodBurgerBear - - FoodBurgerCat - - FoodBurgerClown - - FoodBurgerMime - - FoodBurgerBrain - - FoodBurgerGhost - - FoodBurgerSpell - - FoodBurgerSuper - - FoodBurgerCrazy - - FoodPizzaArnoldSlice - - FoodPizzaUraniumSlice - - FoodPizzaWorldpeasSlice - rareChance: 0.05 diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/vending.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/vending.yml index 0b530c68b1..98dfc20c13 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/vending.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/vending.yml @@ -1,38 +1,38 @@ - type: entity + parent: MarkerBase id: RandomVending name: random vending machine spawner suffix: Any - parent: MarkerBase components: - type: Sprite layers: - state: red - sprite: Structures/Machines/VendingMachines/random.rsi state: any - - type: RandomSpawner - prototypes: - - VendingMachineChang - - VendingMachineCigs - - VendingMachineCoffee - - VendingMachineCola #Robust Sofdrinks - - VendingMachineColaBlack #Robust Sofdrinks [Black] - - VendingMachineColaRed #Space Cola - - VendingMachineDiscount - - VendingMachineDonut - - VendingMachineDrGibb - - VendingMachinePwrGame - - VendingMachineShamblersJuice - - VendingMachineSmite - - VendingMachineSnack - - VendingMachineSnackBlue - - VendingMachineSnackGreen - - VendingMachineSnackOrange - - VendingMachineSnackTeal - - VendingMachineSoda #Robust Sofdrinks [Soda] - - VendingMachineSovietSoda #Boda - - VendingMachineSpaceUp - - VendingMachineStarkist - chance: 1 + - type: EntityTableSpawner + table: !type:GroupSelector + children: + - id: VendingMachineChang + - id: VendingMachineCigs + - id: VendingMachineCoffee + - id: VendingMachineCola #Robust Sofdrinks + - id: VendingMachineColaBlack #Robust Sofdrinks [Black] + - id: VendingMachineColaRed #Space Cola + - id: VendingMachineDiscount + - id: VendingMachineDonut + - id: VendingMachineDrGibb + - id: VendingMachinePwrGame + - id: VendingMachineShamblersJuice + - id: VendingMachineSmite + - id: VendingMachineSnack + - id: VendingMachineSnackBlue + - id: VendingMachineSnackGreen + - id: VendingMachineSnackOrange + - id: VendingMachineSnackTeal + - id: VendingMachineSoda #Robust Sofdrinks [Soda] + - id: VendingMachineSovietSoda #Boda + - id: VendingMachineSpaceUp + - id: VendingMachineStarkist - type: entityTable @@ -40,19 +40,17 @@ table: !type:GroupSelector children: - id: VendingMachineClothing - weight: 40 + weight: 4 - id: VendingMachineWinter - weight: 40 + weight: 4 - id: VendingMachinePride - weight: 10 - id: VendingMachineTheater - weight: 10 - type: entity + parent: MarkerBase id: RandomVendingClothing name: random vending machine spawner suffix: Clothing - parent: MarkerBase components: - type: Sprite layers: diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/vendingdrinks.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/vendingdrinks.yml index 1c90664b3d..09c4ce8761 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/vendingdrinks.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/vendingdrinks.yml @@ -1,26 +1,26 @@ - type: entity + parent: MarkerBase id: RandomVendingDrinks name: random vending machine spawner suffix: Drinks - parent: MarkerBase components: - type: Sprite layers: - - state: red - - sprite: Structures/Machines/VendingMachines/random.rsi - state: drink - - type: RandomSpawner - prototypes: - - VendingMachineCoffee - - VendingMachineCola #Robust Sofdrinks - - VendingMachineColaBlack #Robust Sofdrinks [Black] - - VendingMachineColaRed #Space Cola - - VendingMachineDrGibb - - VendingMachinePwrGame - - VendingMachineShamblersJuice - - VendingMachineSmite - - VendingMachineSoda #Robust Sofdrinks [Soda] - - VendingMachineSovietSoda #Boda - - VendingMachineSpaceUp - - VendingMachineStarkist - chance: 1 + - state: red + - sprite: Structures/Machines/VendingMachines/random.rsi + state: drink + - type: EntityTableSpawner + table: !type:GroupSelector + children: + - id: VendingMachineCoffee + - id: VendingMachineCola #Robust Sofdrinks + - id: VendingMachineColaBlack #Robust Sofdrinks [Black] + - id: VendingMachineColaRed #Space Cola + - id: VendingMachineDrGibb + - id: VendingMachinePwrGame + - id: VendingMachineShamblersJuice + - id: VendingMachineSmite + - id: VendingMachineSoda #Robust Sofdrinks [Soda] + - id: VendingMachineSovietSoda #Boda + - id: VendingMachineSpaceUp + - id: VendingMachineStarkist diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/vendingsnacks.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/vendingsnacks.yml index b634d50cc6..e919cf192d 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/vendingsnacks.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/vendingsnacks.yml @@ -1,22 +1,22 @@ - type: entity + parent: MarkerBase id: RandomVendingSnacks name: random vending machine spawner suffix: Snacks - parent: MarkerBase components: - type: Sprite layers: - - state: red - - sprite: Structures/Machines/VendingMachines/random.rsi - state: snack - - type: RandomSpawner - prototypes: - - VendingMachineDiscount - - VendingMachineSnack - - VendingMachineSnackBlue - - VendingMachineSnackGreen - - VendingMachineSnackOrange - - VendingMachineSnackTeal - - VendingMachineChang - - VendingMachineDonut - chance: 1 + - state: red + - sprite: Structures/Machines/VendingMachines/random.rsi + state: snack + - type: EntityTableSpawner + table: !type:GroupSelector + children: + - id: VendingMachineDiscount + - id: VendingMachineSnack + - id: VendingMachineSnackBlue + - id: VendingMachineSnackGreen + - id: VendingMachineSnackOrange + - id: VendingMachineSnackTeal + - id: VendingMachineChang + - id: VendingMachineDonut From 2c8e76c4e0fbfe8067e121f29af2b7a3d1a45135 Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com> Date: Sun, 12 Apr 2026 11:16:20 -0700 Subject: [PATCH 124/126] [STAGING] Fix blood regeneration (#43576) fsafsaasffas Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> --- Content.Shared/Body/Systems/SharedBloodstreamSystem.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs b/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs index a3caad69be..bde65b99fe 100644 --- a/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs +++ b/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs @@ -404,8 +404,10 @@ public abstract class SharedBloodstreamSystem : EntitySystem || amount == 0) return false; + // TODO: Either make this percentage based regeneration and pre-pass the percentage. + // TODO: Solution regulation API that doesn't result in very minor FixedPoint2 errors (Currently gingerbreadman only regenerates 0.99u instead of 1.00u) referenceFactor = Math.Clamp(referenceFactor, 0f, ent.Comp.MaxVolumeModifier); - var ratio = amount / ent.Comp.BloodReferenceSolution.Volume; + var ratio = (float)amount / (float)ent.Comp.BloodReferenceSolution.Volume; foreach (var (referenceReagent, referenceQuantity) in ent.Comp.BloodReferenceSolution) { From 17d0f27b128901549e886cd1592f864c3dbe945b Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com> Date: Sun, 12 Apr 2026 11:16:38 -0700 Subject: [PATCH 125/126] [STAGING] Revert "Force Vent Critters to Attack (#42399)" (#43577) Revert "Force Vent Critters to Attack (#42399)" This reverts commit 81be6f25713f5770636368e0d1c6782922d5e7dd. Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> --- .../ForceAttack/ForceAttackComponent.cs | 35 -------- .../ForceAttack/ForceAttackSystem.cs | 82 ------------------- .../Weapons/Melee/SharedMeleeWeaponSystem.cs | 12 +-- .../components/force-attack-component.ftl | 1 - .../Prototypes/Entities/Mobs/NPCs/animals.yml | 1 - .../Entities/Mobs/NPCs/elemental.yml | 1 - .../Prototypes/Entities/Mobs/NPCs/slimes.yml | 3 - .../FeedbackPopup/feedbackpopups.yml | 13 --- 8 files changed, 6 insertions(+), 142 deletions(-) delete mode 100644 Content.Server/ForceAttack/ForceAttackComponent.cs delete mode 100644 Content.Server/ForceAttack/ForceAttackSystem.cs delete mode 100644 Resources/Locale/en-US/components/force-attack-component.ftl diff --git a/Content.Server/ForceAttack/ForceAttackComponent.cs b/Content.Server/ForceAttack/ForceAttackComponent.cs deleted file mode 100644 index ce24554714..0000000000 --- a/Content.Server/ForceAttack/ForceAttackComponent.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Robust.Shared.GameStates; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; - -namespace Content.Server.ForceAttack; - -/// -/// This is used to force a player-controlled mob to attack nearby enemies, preventing "friendly antag"ing. -/// -[RegisterComponent] -[AutoGenerateComponentPause] -public sealed partial class ForceAttackComponent : Component -{ - /// - /// The next time this component will attempt to force an attack. - /// - [AutoPausedField] - public TimeSpan NextAttack = TimeSpan.MaxValue; - - /// - /// Whether an enemy is in range. - /// - public bool InRange = false; - - /// - /// The time this component will wait before forcing an attack when an enemy is in range. - /// - [DataField] - public TimeSpan PassiveTime = TimeSpan.FromSeconds(5); - - /// - /// The message displayed on forced attack - /// - [DataField] - public LocId Message = "force-attack-component-message"; -} diff --git a/Content.Server/ForceAttack/ForceAttackSystem.cs b/Content.Server/ForceAttack/ForceAttackSystem.cs deleted file mode 100644 index 33512b0e68..0000000000 --- a/Content.Server/ForceAttack/ForceAttackSystem.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Linq; -using Content.Shared.CombatMode; -using Content.Shared.Mobs.Systems; -using Content.Shared.NPC.Components; -using Content.Shared.NPC.Systems; -using Content.Shared.Popups; -using Content.Shared.Weapons.Melee; -using Content.Shared.Weapons.Melee.Events; -using Robust.Shared.Player; -using Robust.Shared.Timing; -using Robust.Shared.Utility; - -namespace Content.Server.ForceAttack; - -/// -/// This handles forcing a player-controlled mob to attack nearby enemies. -/// -public sealed class ForceAttackSystem : EntitySystem -{ - [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly SharedMeleeWeaponSystem _melee = default!; - [Dependency] private readonly NpcFactionSystem _faction = default!; - [Dependency] private readonly SharedCombatModeSystem _mode = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly MobStateSystem _mob = default!; - - /// - public override void Initialize() - { - SubscribeLocalEvent(OnMeleeAttack); - } - - private void OnMeleeAttack(Entity ent, ref MeleeAttackEvent args) - { - ent.Comp.NextAttack = _timing.CurTime + ent.Comp.PassiveTime; - } - - /// - public override void Update(float frameTime) - { - base.Update(frameTime); - - var curTime = _timing.CurTime; - - // Query includes ActorComponent to only get mobs currently controlled by players - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var forceComp, out var factionComp, out var modeComp, out _)) - { - // Check if we have a weapon - if (!_melee.TryGetWeapon(uid, out var weaponUid, out var weapon) || weapon.NextAttack > curTime) - continue; - - // Find a target in range that isn't critical or dead - if (!_faction.GetNearbyHostiles((uid, factionComp), weapon.Range) - .Where((potTarget) => !_mob.IsIncapacitated(potTarget)) - .TryFirstOrNull(out var target)) - { - forceComp.InRange = false; - continue; - } - - if (!forceComp.InRange) // Just entered range - { - forceComp.InRange = true; - forceComp.NextAttack = curTime + forceComp.PassiveTime; - continue; - } - - if (forceComp.NextAttack > curTime) - continue; - - // Force mob to enter combat mode (necessary for AttemptAttack to succeed). - _mode.SetInCombatMode(uid, true, modeComp); - - var popupMessage = Loc.GetString(forceComp.Message); - if (popupMessage.Length != 0) - _popup.PopupEntity(popupMessage, uid, uid); - - _melee.AttemptLightAttack(uid, weaponUid, weapon, target.Value, false); - } - } -} diff --git a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs index 02d561d942..4937952dc4 100644 --- a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs +++ b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs @@ -344,12 +344,12 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem AttemptAttack(user, weaponUid, weapon, new LightAttackEvent(null, GetNetEntity(weaponUid), GetNetCoordinates(coordinates)), null); } - public bool AttemptLightAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, EntityUid target, bool predicted = true) + public bool AttemptLightAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, EntityUid target) { if (!TryComp(target, out TransformComponent? targetXform)) return false; - return AttemptAttack(user, weaponUid, weapon, new LightAttackEvent(GetNetEntity(target), GetNetEntity(weaponUid), GetNetCoordinates(targetXform.Coordinates)), null, predicted); + return AttemptAttack(user, weaponUid, weapon, new LightAttackEvent(GetNetEntity(target), GetNetEntity(weaponUid), GetNetCoordinates(targetXform.Coordinates)), null); } public bool AttemptDisarmAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, EntityUid target) @@ -364,7 +364,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem /// Called when a windup is finished and an attack is tried. /// /// True if attack successful - private bool AttemptAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, AttackEvent attack, ICommonSession? session, bool predicted = true) + private bool AttemptAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, AttackEvent attack, ICommonSession? session) { var curTime = Timing.CurTime; @@ -471,7 +471,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem throw new NotImplementedException(); } - DoLungeAnimation(user, weaponUid, weapon.Angle, TransformSystem.ToMapCoordinates(GetCoordinates(attack.Coordinates)), weapon.Range, animation, predicted); + DoLungeAnimation(user, weaponUid, weapon.Angle, TransformSystem.ToMapCoordinates(GetCoordinates(attack.Coordinates)), weapon.Range, animation); } var attackEv = new MeleeAttackEvent(weaponUid); @@ -963,7 +963,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem return true; } - private void DoLungeAnimation(EntityUid user, EntityUid weapon, Angle angle, MapCoordinates coordinates, float length, string? animation, bool predicted = true) + private void DoLungeAnimation(EntityUid user, EntityUid weapon, Angle angle, MapCoordinates coordinates, float length, string? animation) { // TODO: Assert that offset eyes are still okay. if (!TryComp(user, out TransformComponent? userXform)) @@ -984,7 +984,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem if (localPos.Length() > visualLength) localPos = localPos.Normalized() * visualLength; - DoLunge(user, weapon, angle, localPos, animation, predicted); + DoLunge(user, weapon, angle, localPos, animation); } public abstract void DoLunge(EntityUid user, EntityUid weapon, Angle angle, Vector2 localPos, string? animation, bool predicted = true); diff --git a/Resources/Locale/en-US/components/force-attack-component.ftl b/Resources/Locale/en-US/components/force-attack-component.ftl deleted file mode 100644 index fc033e1edc..0000000000 --- a/Resources/Locale/en-US/components/force-attack-component.ftl +++ /dev/null @@ -1 +0,0 @@ -force-attack-component-message = Your anger overwhelms you! diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 2954199554..aaf94e0d2d 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -2608,7 +2608,6 @@ raffle: settings: short - type: GhostTakeoverAvailable - - type: ForceAttack - type: entity name: tarantula diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml index 49f53654f2..b3f7749246 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml @@ -386,7 +386,6 @@ solution: bloodstream - type: DrainableSolution solution: bloodstream - - type: ForceAttack - type: entity parent: MarkerBase diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml index ef1203e458..8d8b4f763e 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml @@ -168,7 +168,6 @@ - MindRoleGhostRoleTeamAntagonist raffle: settings: short - - type: ForceAttack - type: entity name: green slime @@ -209,7 +208,6 @@ - MindRoleGhostRoleTeamAntagonist raffle: settings: short - - type: ForceAttack - type: entity name: yellow slime @@ -249,4 +247,3 @@ - MindRoleGhostRoleTeamAntagonist raffle: settings: short - - type: ForceAttack diff --git a/Resources/Prototypes/FeedbackPopup/feedbackpopups.yml b/Resources/Prototypes/FeedbackPopup/feedbackpopups.yml index a71276bffc..d424a24244 100644 --- a/Resources/Prototypes/FeedbackPopup/feedbackpopups.yml +++ b/Resources/Prototypes/FeedbackPopup/feedbackpopups.yml @@ -17,16 +17,3 @@ responseType: "General Feedback" responseLink: "https://forum.spacestation14.com/c/development/feedback/51" showRoundEnd: false - -- type: feedbackPopup - id: ForceVentCrittersToAttackFeedback - popupOrigin: wizden_master - title: "[bold]Played an antagonist vent critter role?[/bold]" - description: >- - If you've played or interacted with a player-controlled antagonist vent critter (spiders, slimes, clown spiders), please leave your feedback on how the new auto-attack feature went. - responseType: "Feedback Thread" - responseLink: "https://forum.spacestation14.com/t/force-vent-critters-to-attack-feedback/26861" - showRoundEnd: true - ruleWhitelist: - components: - - VentHordeRule From aa8c12e6eb92d725e1997e777b822fab2b4ccf2a Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:36:41 -0700 Subject: [PATCH 126/126] asfafasasffas --- .../Radiation/Systems/SharedRadiationSystem.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Content.Shared/Radiation/Systems/SharedRadiationSystem.cs b/Content.Shared/Radiation/Systems/SharedRadiationSystem.cs index ea298d313d..78e061b9c3 100644 --- a/Content.Shared/Radiation/Systems/SharedRadiationSystem.cs +++ b/Content.Shared/Radiation/Systems/SharedRadiationSystem.cs @@ -6,6 +6,11 @@ public abstract partial class SharedRadiationSystem : EntitySystem { [Dependency] protected readonly EntityQuery SourceQuery = default!; + /// + /// Sets the intensity of a to the passed intensity. + /// + /// Radiation source we're attempting to update + /// Intensity we're setting the source to. public void SetIntensity(Entity entity, float intensity) { if (!SourceQuery.Resolve(entity, ref entity.Comp, false)) @@ -15,8 +20,8 @@ public abstract partial class SharedRadiationSystem : EntitySystem UpdateSource((entity, entity.Comp)); } - protected virtual void UpdateSource(Entity entity) - { - - } + /// + /// Updates the radiation source cache. Does nothing on client, see server! + /// + protected virtual void UpdateSource(Entity entity) { } }