diff --git a/.editorconfig b/.editorconfig
index a87e5e0651..fa740a7511 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -351,7 +351,7 @@ resharper_csharp_qualified_using_at_nested_scope = false
resharper_csharp_prefer_qualified_reference = false
resharper_csharp_allow_alias = false
-[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props}]
+[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props,slnx}]
indent_size = 2
[nuget.config]
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 53fad99b1a..caad947fdd 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -14,7 +14,7 @@
Small fixes/refactors are exempt. Media may be used in SS14 progress reports with credit. -->
## Requirements
-
+
- [ ] I have read and am following the [Pull Request and Changelog Guidelines](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html).
- [ ] I have added media to this PR or it does not require an in-game showcase.
diff --git a/.github/workflows/build-docfx.yml b/.github/workflows/build-docfx.yml
index 1f010b7291..dee31e9b31 100644
--- a/.github/workflows/build-docfx.yml
+++ b/.github/workflows/build-docfx.yml
@@ -21,7 +21,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore
diff --git a/.github/workflows/build-map-renderer.yml b/.github/workflows/build-map-renderer.yml
index f93f4b25ae..fb6be1603c 100644
--- a/.github/workflows/build-map-renderer.yml
+++ b/.github/workflows/build-map-renderer.yml
@@ -36,7 +36,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore
diff --git a/.github/workflows/build-test-debug.yml b/.github/workflows/build-test-debug.yml
index 4e391b2aef..ad1fbf29ac 100644
--- a/.github/workflows/build-test-debug.yml
+++ b/.github/workflows/build-test-debug.yml
@@ -36,7 +36,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore
diff --git a/.github/workflows/publish-testing.yml b/.github/workflows/publish-testing.yml
index 7a792ed2df..1b1344315a 100644
--- a/.github/workflows/publish-testing.yml
+++ b/.github/workflows/publish-testing.yml
@@ -20,7 +20,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Get Engine Tag
run: |
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 3d54fef530..e7359c3be8 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -27,7 +27,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Get Engine Tag
run: |
diff --git a/.github/workflows/test-packaging.yml b/.github/workflows/test-packaging.yml
index 325d8d04d9..6cb6d836d5 100644
--- a/.github/workflows/test-packaging.yml
+++ b/.github/workflows/test-packaging.yml
@@ -51,7 +51,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore
diff --git a/.github/workflows/yaml-linter.yml b/.github/workflows/yaml-linter.yml
index c10549bcb4..d8ce557134 100644
--- a/.github/workflows/yaml-linter.yml
+++ b/.github/workflows/yaml-linter.yml
@@ -26,7 +26,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore
- name: Build
diff --git a/.gitignore b/.gitignore
index 2dd5912047..76f5837d75 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+# MSbuild binlog files
+*.binlog
+
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
diff --git a/BuildChecker/git_helper.py b/BuildChecker/git_helper.py
index bd6603bd34..4e42b1e70e 100644
--- a/BuildChecker/git_helper.py
+++ b/BuildChecker/git_helper.py
@@ -11,7 +11,7 @@ import time
from pathlib import Path
from typing import List
-SOLUTION_PATH = Path("..") / "SpaceStation14.sln"
+SOLUTION_PATH = Path("..") / "SpaceStation14.slnx"
# If this doesn't match the saved version we overwrite them all.
CURRENT_HOOKS_VERSION = "4"
QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet"
diff --git a/Content.Benchmarks/Content.Benchmarks.csproj b/Content.Benchmarks/Content.Benchmarks.csproj
index c3b60a1c69..8d4dfa31bd 100644
--- a/Content.Benchmarks/Content.Benchmarks.csproj
+++ b/Content.Benchmarks/Content.Benchmarks.csproj
@@ -1,17 +1,20 @@
-
- $(TargetFramework)
..\bin\Content.Benchmarks\
- false
false
Exe
true
- 12
+ false
+ disable
+
+
+
+
+
@@ -19,10 +22,12 @@
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/Content.Client/Cargo/UI/BountyEntry.xaml.cs b/Content.Client/Cargo/UI/BountyEntry.xaml.cs
index d813f70ff4..bac7d84bf7 100644
--- a/Content.Client/Cargo/UI/BountyEntry.xaml.cs
+++ b/Content.Client/Cargo/UI/BountyEntry.xaml.cs
@@ -7,7 +7,6 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
-using Serilog;
namespace Content.Client.Cargo.UI;
diff --git a/Content.Client/Chemistry/EntitySystems/HyposprayStatusControlSystem.cs b/Content.Client/Chemistry/EntitySystems/HyposprayStatusControlSystem.cs
deleted file mode 100644
index 4dfc8506d2..0000000000
--- a/Content.Client/Chemistry/EntitySystems/HyposprayStatusControlSystem.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Content.Client.Chemistry.UI;
-using Content.Client.Items;
-using Content.Shared.Chemistry.Components;
-using Content.Shared.Chemistry.EntitySystems;
-
-namespace Content.Client.Chemistry.EntitySystems;
-
-public sealed class HyposprayStatusControlSystem : EntitySystem
-{
- [Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
- public override void Initialize()
- {
- base.Initialize();
- Subs.ItemStatus(ent => new HyposprayStatusControl(ent, _solutionContainers));
- }
-}
diff --git a/Content.Client/Chemistry/EntitySystems/InjectorStatusControlSystem.cs b/Content.Client/Chemistry/EntitySystems/InjectorStatusControlSystem.cs
new file mode 100644
index 0000000000..ca8685eb3e
--- /dev/null
+++ b/Content.Client/Chemistry/EntitySystems/InjectorStatusControlSystem.cs
@@ -0,0 +1,20 @@
+using Content.Client.Chemistry.UI;
+using Content.Client.Items;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.EntitySystems;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Chemistry.EntitySystems;
+
+public sealed class InjectorStatusControlSystem : EntitySystem
+{
+ [Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ Subs.ItemStatus(injector => new InjectorStatusControl(injector, _solutionContainers, _prototypeManager));
+ }
+}
diff --git a/Content.Client/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Client/Chemistry/EntitySystems/InjectorSystem.cs
deleted file mode 100644
index 58cb5330a2..0000000000
--- a/Content.Client/Chemistry/EntitySystems/InjectorSystem.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Content.Client.Chemistry.UI;
-using Content.Client.Items;
-using Content.Shared.Chemistry.Components;
-using Content.Shared.Chemistry.EntitySystems;
-
-namespace Content.Client.Chemistry.EntitySystems;
-
-public sealed class InjectorSystem : SharedInjectorSystem
-{
- public override void Initialize()
- {
- base.Initialize();
-
- Subs.ItemStatus(ent => new InjectorStatusControl(ent, SolutionContainer));
- }
-}
diff --git a/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs b/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs
index a669a8da4c..d0a9f5644c 100644
--- a/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs
+++ b/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs
@@ -48,6 +48,10 @@ namespace Content.Client.Chemistry.UI
(uint) _window.BottleDosage.Value, _window.LabelLine));
_window.BufferSortButton.OnPressed += _ => SendMessage(
new ChemMasterSortingTypeCycleMessage());
+ _window.OutputBufferDraw.OnPressed += _ => SendMessage(
+ new ChemMasterOutputDrawSourceMessage(ChemMasterDrawSource.Internal));
+ _window.OutputBeakerDraw.OnPressed += _ => SendMessage(
+ new ChemMasterOutputDrawSourceMessage(ChemMasterDrawSource.External));
for (uint i = 0; i < _window.PillTypeButtons.Length; i++)
{
diff --git a/Content.Client/Chemistry/UI/ChemMasterWindow.xaml b/Content.Client/Chemistry/UI/ChemMasterWindow.xaml
index 5bc640ab44..cc2a199172 100644
--- a/Content.Client/Chemistry/UI/ChemMasterWindow.xaml
+++ b/Content.Client/Chemistry/UI/ChemMasterWindow.xaml
@@ -79,10 +79,13 @@
-
+
+
+
-
-
+
+
+
diff --git a/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs b/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs
index d610f34b29..a8aab1d35f 100644
--- a/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs
+++ b/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs
@@ -150,7 +150,17 @@ namespace Content.Client.Chemistry.UI
// Ensure the Panel Info is updated, including UI elements for Buffer Volume, Output Container and so on
UpdatePanelInfo(castState);
- BufferCurrentVolume.Text = $" {castState.BufferCurrentVolume?.Int() ?? 0}u";
+ switch (castState.DrawSource)
+ {
+ case ChemMasterDrawSource.Internal:
+ SetBufferText(castState.BufferCurrentVolume, "chem-master-output-buffer-draw");
+ break;
+ case ChemMasterDrawSource.External:
+ SetBufferText(castState.InputContainerInfo?.CurrentVolume, "chem-master-output-beaker-draw");
+ break;
+ default:
+ throw new($"Chemmaster {castState.OutputContainerInfo} draw source is not set");
+ }
InputEjectButton.Disabled = castState.InputContainerInfo is null;
OutputEjectButton.Disabled = castState.OutputContainerInfo is null;
@@ -168,9 +178,14 @@ namespace Content.Client.Chemistry.UI
var holdsReagents = output?.Reagents != null;
var pillNumberMax = holdsReagents ? 0 : remainingCapacity;
var bottleAmountMax = holdsReagents ? remainingCapacity : 0;
- var bufferVolume = castState.BufferCurrentVolume?.Int() ?? 0;
+ var outputVolume = castState.DrawSource switch
+ {
+ ChemMasterDrawSource.Internal => castState.BufferCurrentVolume?.Int() ?? 0,
+ ChemMasterDrawSource.External => castState.InputContainerInfo?.CurrentVolume.Int() ?? 0,
+ _ => 0,
+ };
- PillDosage.Value = (int)Math.Min(bufferVolume, castState.PillDosageLimit);
+ PillDosage.Value = (int)Math.Min(outputVolume, castState.PillDosageLimit);
PillTypeButtons[castState.SelectedPillType].Pressed = true;
@@ -186,25 +201,35 @@ namespace Content.Client.Chemistry.UI
// Avoid division by zero
if (PillDosage.Value > 0)
{
- PillNumber.Value = Math.Min(bufferVolume / PillDosage.Value, pillNumberMax);
+ PillNumber.Value = Math.Min(outputVolume / PillDosage.Value, pillNumberMax);
}
else
{
PillNumber.Value = 0;
}
- BottleDosage.Value = Math.Min(bottleAmountMax, bufferVolume);
+ BottleDosage.Value = Math.Min(bottleAmountMax, outputVolume);
}
///
- /// Generate a product label based on reagents in the buffer.
+ /// Generate a product label based on reagents in the buffer or beaker.
///
/// State data sent by the server.
private string GenerateLabel(ChemMasterBoundUserInterfaceState state)
{
- if (state.BufferCurrentVolume == 0)
+ if (
+ state.BufferCurrentVolume == 0 && state.DrawSource == ChemMasterDrawSource.Internal ||
+ state.InputContainerInfo?.CurrentVolume == 0 && state.DrawSource == ChemMasterDrawSource.External ||
+ state.InputContainerInfo?.Reagents == null
+ )
return "";
- var reagent = state.BufferReagents.OrderBy(r => r.Quantity).First().Reagent;
+ var reagent = (state.DrawSource switch
+ {
+ ChemMasterDrawSource.Internal => state.BufferReagents,
+ ChemMasterDrawSource.External => state.InputContainerInfo.Reagents ?? [],
+ _ => throw new($"Chemmaster {state.OutputContainerInfo} draw source is not set"),
+ }).MinBy(r => r.Quantity)
+ .Reagent;
_prototypeManager.TryIndex(reagent.Prototype, out ReagentPrototype? proto);
return proto?.LocalizedName ?? "";
}
@@ -233,6 +258,8 @@ namespace Content.Client.Chemistry.UI
_ => Loc.GetString("chem-master-window-sort-type-none")
};
+ OutputBufferDraw.Pressed = state.DrawSource == ChemMasterDrawSource.Internal;
+ OutputBeakerDraw.Pressed = state.DrawSource == ChemMasterDrawSource.External;
if (!state.BufferReagents.Any())
{
@@ -414,6 +441,12 @@ namespace Content.Client.Chemistry.UI
get => LabelLineEdit.Text;
set => LabelLineEdit.Text = value;
}
+
+ private void SetBufferText(FixedPoint2? volume, string text)
+ {
+ BufferCurrentVolume.Text = $" {volume ?? FixedPoint2.Zero}u";
+ DrawSource.Text = Loc.GetString(text);
+ }
}
public sealed class ReagentButton : Button
diff --git a/Content.Client/Chemistry/UI/HyposprayStatusControl.cs b/Content.Client/Chemistry/UI/HyposprayStatusControl.cs
deleted file mode 100644
index a564bcefc6..0000000000
--- a/Content.Client/Chemistry/UI/HyposprayStatusControl.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using Content.Client.Message;
-using Content.Client.Stylesheets;
-using Content.Shared.Chemistry.Components;
-using Content.Shared.Chemistry.EntitySystems;
-using Content.Shared.FixedPoint;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Shared.Timing;
-
-namespace Content.Client.Chemistry.UI;
-
-public sealed class HyposprayStatusControl : Control
-{
- private readonly Entity _parent;
- private readonly RichTextLabel _label;
- private readonly SharedSolutionContainerSystem _solutionContainers;
-
- private FixedPoint2 PrevVolume;
- private FixedPoint2 PrevMaxVolume;
- private bool PrevOnlyAffectsMobs;
-
- public HyposprayStatusControl(Entity parent, SharedSolutionContainerSystem solutionContainers)
- {
- _parent = parent;
- _solutionContainers = solutionContainers;
- _label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } };
- AddChild(_label);
- }
-
- protected override void FrameUpdate(FrameEventArgs args)
- {
- base.FrameUpdate(args);
-
- if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution))
- return;
-
- // only updates the UI if any of the details are different than they previously were
- if (PrevVolume == solution.Volume
- && PrevMaxVolume == solution.MaxVolume
- && PrevOnlyAffectsMobs == _parent.Comp.OnlyAffectsMobs)
- return;
-
- PrevVolume = solution.Volume;
- PrevMaxVolume = solution.MaxVolume;
- PrevOnlyAffectsMobs = _parent.Comp.OnlyAffectsMobs;
-
- var modeStringLocalized = Loc.GetString((_parent.Comp.OnlyAffectsMobs && _parent.Comp.CanContainerDraw) switch
- {
- false => "hypospray-all-mode-text",
- true => "hypospray-mobs-only-mode-text",
- });
-
- _label.SetMarkup(Loc.GetString("hypospray-volume-label",
- ("currentVolume", solution.Volume),
- ("totalVolume", solution.MaxVolume),
- ("modeString", modeStringLocalized)));
- }
-}
diff --git a/Content.Client/Chemistry/UI/InjectorStatusControl.cs b/Content.Client/Chemistry/UI/InjectorStatusControl.cs
index 24f988bd35..0c57da7813 100644
--- a/Content.Client/Chemistry/UI/InjectorStatusControl.cs
+++ b/Content.Client/Chemistry/UI/InjectorStatusControl.cs
@@ -2,26 +2,32 @@ using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Chemistry.UI;
public sealed class InjectorStatusControl : Control
{
+ private readonly IPrototypeManager _prototypeManager;
+
private readonly Entity _parent;
private readonly SharedSolutionContainerSystem _solutionContainers;
private readonly RichTextLabel _label;
- private FixedPoint2 PrevVolume;
- private FixedPoint2 PrevMaxVolume;
- private FixedPoint2 PrevTransferAmount;
- private InjectorToggleMode PrevToggleState;
+ private FixedPoint2 _prevVolume;
+ private FixedPoint2 _prevMaxVolume;
+ private FixedPoint2? _prevTransferAmount;
+ private InjectorBehavior _prevBehavior;
- public InjectorStatusControl(Entity parent, SharedSolutionContainerSystem solutionContainers)
+ public InjectorStatusControl(Entity parent, SharedSolutionContainerSystem solutionContainers, IPrototypeManager prototypeManager)
{
+ _prototypeManager = prototypeManager;
+
_parent = parent;
_solutionContainers = solutionContainers;
_label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } };
@@ -32,33 +38,38 @@ public sealed class InjectorStatusControl : Control
{
base.FrameUpdate(args);
- if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution))
+ if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution)
+ || !_prototypeManager.Resolve(_parent.Comp.ActiveModeProtoId, out var activeMode))
return;
// only updates the UI if any of the details are different than they previously were
- if (PrevVolume == solution.Volume
- && PrevMaxVolume == solution.MaxVolume
- && PrevTransferAmount == _parent.Comp.CurrentTransferAmount
- && PrevToggleState == _parent.Comp.ToggleState)
+ if (_prevVolume == solution.Volume
+ && _prevMaxVolume == solution.MaxVolume
+ && _prevTransferAmount == _parent.Comp.CurrentTransferAmount
+ && _prevBehavior == activeMode.Behavior)
return;
- PrevVolume = solution.Volume;
- PrevMaxVolume = solution.MaxVolume;
- PrevTransferAmount = _parent.Comp.CurrentTransferAmount;
- PrevToggleState = _parent.Comp.ToggleState;
+ _prevVolume = solution.Volume;
+ _prevMaxVolume = solution.MaxVolume;
+ _prevTransferAmount = _parent.Comp.CurrentTransferAmount;
+ _prevBehavior = activeMode.Behavior;
// Update current volume and injector state
- var modeStringLocalized = Loc.GetString(_parent.Comp.ToggleState switch
+ // Seeing transfer volume is only important for injectors that can change it.
+ if (activeMode.TransferAmounts.Count > 1 && _parent.Comp.CurrentTransferAmount.HasValue)
{
- InjectorToggleMode.Draw => "injector-draw-text",
- InjectorToggleMode.Inject => "injector-inject-text",
- _ => "injector-invalid-injector-toggle-mode"
- });
-
- _label.SetMarkup(Loc.GetString("injector-volume-label",
- ("currentVolume", solution.Volume),
- ("totalVolume", solution.MaxVolume),
- ("modeString", modeStringLocalized),
- ("transferVolume", _parent.Comp.CurrentTransferAmount)));
+ _label.SetMarkup(Loc.GetString("injector-volume-transfer-label",
+ ("currentVolume", solution.Volume),
+ ("totalVolume", solution.MaxVolume),
+ ("modeString", Loc.GetString(activeMode.Name)),
+ ("transferVolume", _parent.Comp.CurrentTransferAmount.Value)));
+ }
+ else
+ {
+ _label.SetMarkup(Loc.GetString("injector-volume-label",
+ ("currentVolume", solution.Volume),
+ ("totalVolume", solution.MaxVolume),
+ ("modeString", Loc.GetString(activeMode.Name))));
+ }
}
}
diff --git a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs
index d5fee2bdda..6041b405c9 100644
--- a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs
+++ b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs
@@ -30,7 +30,10 @@ namespace Content.Client.Construction.UI
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
+
private readonly SpriteSystem _spriteSystem;
+ private readonly ISawmill _sawmill;
private readonly IConstructionMenuView _constructionView;
private readonly EntityWhitelistSystem _whitelistSystem;
@@ -90,6 +93,7 @@ namespace Content.Client.Construction.UI
_constructionView = new ConstructionMenu();
_whitelistSystem = _entManager.System();
_spriteSystem = _entManager.System();
+ _sawmill = _logManager.GetSawmill("construction.ui");
// This is required so that if we load after the system is initialized, we can bind to it immediately
if (_systemManager.TryGetEntitySystem(out var constructionSystem))
@@ -284,7 +288,7 @@ namespace Content.Client.Construction.UI
if (!_constructionSystem!.TryGetRecipePrototype(recipe.ID, out var targetProtoId))
{
- Logger.Error("Cannot find the target prototype in the recipe cache with the id \"{0}\" of {1}.",
+ _sawmill.Error("Cannot find the target prototype in the recipe cache with the id \"{0}\" of {1}.",
recipe.ID,
nameof(ConstructionPrototype));
continue;
diff --git a/Content.Client/Content.Client.csproj b/Content.Client/Content.Client.csproj
index d8855ce508..e849d9313b 100644
--- a/Content.Client/Content.Client.csproj
+++ b/Content.Client/Content.Client.csproj
@@ -1,26 +1,24 @@
-
- $(TargetFramework)
- 12
- false
false
..\bin\Content.Client\
Exe
RA0032;nullable
- enable
- Debug;Release;Tools;DebugOpt
- AnyCPU
+
+
+
+
+
+
+
+
+
-
-
-
-
diff --git a/Content.Client/Fluids/PuddleSystem.cs b/Content.Client/Fluids/PuddleSystem.cs
index 1ab1ded3e8..ab96d24d85 100644
--- a/Content.Client/Fluids/PuddleSystem.cs
+++ b/Content.Client/Fluids/PuddleSystem.cs
@@ -73,7 +73,19 @@ public sealed class PuddleSystem : SharedPuddleSystem
// Maybe someday we'll have clientside prediction for entity spawning, but not today.
// Until then, these methods do nothing on the client.
///
- public override bool TrySplashSpillAt(EntityUid uid, EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true, EntityUid? user = null)
+ public override bool TrySplashSpillAt(Entity entity, EntityCoordinates coordinates, out EntityUid puddleUid, out Solution solution, bool sound = true, EntityUid? user = null)
+ {
+ puddleUid = EntityUid.Invalid;
+ solution = new Solution();
+ return false;
+ }
+
+ public override bool TrySplashSpillAt(EntityUid entity,
+ EntityCoordinates coordinates,
+ Solution spilled,
+ out EntityUid puddleUid,
+ bool sound = true,
+ EntityUid? user = null)
{
puddleUid = EntityUid.Invalid;
return false;
diff --git a/Content.Client/Guidebook/Controls/GuidebookRichPrototypeLink.cs b/Content.Client/Guidebook/Controls/GuidebookRichPrototypeLink.cs
index b54dd8b701..1e8b524997 100644
--- a/Content.Client/Guidebook/Controls/GuidebookRichPrototypeLink.cs
+++ b/Content.Client/Guidebook/Controls/GuidebookRichPrototypeLink.cs
@@ -42,7 +42,7 @@ public sealed class GuidebookRichPrototypeLink : Control, IPrototypeLinkControl
public void SetMessage(FormattedMessage message)
{
_message = message;
- _richTextLabel.SetMessage(_message);
+ _richTextLabel.SetMessage(_message, tagsAllowed: null);
}
public IPrototype? LinkedPrototype { get; set; }
diff --git a/Content.Client/Guidebook/DocumentParsingManager.static.cs b/Content.Client/Guidebook/DocumentParsingManager.static.cs
index 3e37942381..c702ac97ad 100644
--- a/Content.Client/Guidebook/DocumentParsingManager.static.cs
+++ b/Content.Client/Guidebook/DocumentParsingManager.static.cs
@@ -82,7 +82,7 @@ public sealed partial class DocumentParsingManager
}
msg.Pop();
- rt.SetMessage(msg);
+ rt.SetMessage(msg, tagsAllowed: null);
return rt;
},
TextParser)
diff --git a/Content.Client/Info/InfoSection.xaml.cs b/Content.Client/Info/InfoSection.xaml.cs
index 9e10a4d7b4..95a74c72c7 100644
--- a/Content.Client/Info/InfoSection.xaml.cs
+++ b/Content.Client/Info/InfoSection.xaml.cs
@@ -18,7 +18,7 @@ public sealed partial class InfoSection : BoxContainer
{
TitleLabel.Text = title;
if (markup)
- Content.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()));
+ Content.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()), tagsAllowed: null);
else
Content.SetMessage(text);
}
diff --git a/Content.Client/Info/ServerInfo.cs b/Content.Client/Info/ServerInfo.cs
index 901fc91337..a28a3d4a6e 100644
--- a/Content.Client/Info/ServerInfo.cs
+++ b/Content.Client/Info/ServerInfo.cs
@@ -24,7 +24,7 @@ namespace Content.Client.Info
}
public void SetInfoBlob(string markup)
{
- _richTextLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(markup));
+ _richTextLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(markup), tagsAllowed: null);
}
}
}
diff --git a/Content.Client/Kitchen/EntitySystems/ReagentGrinderSystem.cs b/Content.Client/Kitchen/EntitySystems/ReagentGrinderSystem.cs
new file mode 100644
index 0000000000..5eebd4a0fb
--- /dev/null
+++ b/Content.Client/Kitchen/EntitySystems/ReagentGrinderSystem.cs
@@ -0,0 +1,8 @@
+using Content.Shared.Kitchen.EntitySystems;
+using JetBrains.Annotations;
+
+namespace Content.Client.Kitchen.EntitySystems;
+
+[UsedImplicitly]
+public sealed class ReagentGrinderSystem : SharedReagentGrinderSystem;
+
diff --git a/Content.Client/Launcher/LauncherConnecting.cs b/Content.Client/Launcher/LauncherConnecting.cs
index 33d31cc52d..9b9c472781 100644
--- a/Content.Client/Launcher/LauncherConnecting.cs
+++ b/Content.Client/Launcher/LauncherConnecting.cs
@@ -20,8 +20,10 @@ namespace Content.Client.Launcher
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IClipboardManager _clipboard = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
private LauncherConnectingGui? _control;
+ private ISawmill _sawmill = default!;
private Page _currentPage;
private string? _connectFailReason;
@@ -61,6 +63,8 @@ namespace Content.Client.Launcher
{
_control = new LauncherConnectingGui(this, _random, _prototypeManager, _cfg, _clipboard);
+ _sawmill = _logManager.GetSawmill("launcher-ui");
+
_userInterfaceManager.StateRoot.AddChild(_control);
_clientNetManager.ConnectFailed += OnConnectFailed;
@@ -115,12 +119,12 @@ namespace Content.Client.Launcher
}
else
{
- Logger.InfoS("launcher-ui", $"Redial not possible, no Ss14Address");
+ _sawmill.Info($"Redial not possible, no Ss14Address");
}
}
catch (Exception ex)
{
- Logger.ErrorS("launcher-ui", $"Redial exception: {ex}");
+ _sawmill.Error($"Redial exception: {ex}");
}
return false;
}
diff --git a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs
index 9b1e7d50f8..988ece124f 100644
--- a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs
+++ b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs
@@ -1,6 +1,5 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
-using Prometheus;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
diff --git a/Content.Client/Shuttles/BUI/IFFConsoleBoundUserInterface.cs b/Content.Client/Shuttles/BUI/IFFConsoleBoundUserInterface.cs
index 8d84abed8a..704307f06b 100644
--- a/Content.Client/Shuttles/BUI/IFFConsoleBoundUserInterface.cs
+++ b/Content.Client/Shuttles/BUI/IFFConsoleBoundUserInterface.cs
@@ -23,7 +23,6 @@ public sealed class IFFConsoleBoundUserInterface : BoundUserInterface
_window = this.CreateWindowCenteredLeft();
_window.ShowIFF += SendIFFMessage;
- _window.ShowVessel += SendVesselMessage;
}
protected override void UpdateState(BoundUserInterfaceState state)
@@ -44,14 +43,6 @@ public sealed class IFFConsoleBoundUserInterface : BoundUserInterface
});
}
- private void SendVesselMessage(bool obj)
- {
- SendMessage(new IFFShowVesselMessage()
- {
- Show = obj,
- });
- }
-
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
diff --git a/Content.Client/Shuttles/UI/IFFConsoleWindow.xaml b/Content.Client/Shuttles/UI/IFFConsoleWindow.xaml
index dab11a7b62..90684889a6 100644
--- a/Content.Client/Shuttles/UI/IFFConsoleWindow.xaml
+++ b/Content.Client/Shuttles/UI/IFFConsoleWindow.xaml
@@ -9,12 +9,6 @@
-
-
-
-
-
-
diff --git a/Content.Client/Shuttles/UI/IFFConsoleWindow.xaml.cs b/Content.Client/Shuttles/UI/IFFConsoleWindow.xaml.cs
index f14ef22c3c..d61486d2d8 100644
--- a/Content.Client/Shuttles/UI/IFFConsoleWindow.xaml.cs
+++ b/Content.Client/Shuttles/UI/IFFConsoleWindow.xaml.cs
@@ -13,9 +13,7 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
IComputerWindow
{
private readonly ButtonGroup _showIFFButtonGroup = new();
- private readonly ButtonGroup _showVesselButtonGroup = new();
public event Action? ShowIFF;
- public event Action? ShowVessel;
public IFFConsoleWindow()
{
@@ -24,11 +22,6 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
ShowIFFOnButton.Group = _showIFFButtonGroup;
ShowIFFOnButton.OnPressed += args => ShowIFFPressed(true);
ShowIFFOffButton.OnPressed += args => ShowIFFPressed(false);
-
- ShowVesselOffButton.Group = _showVesselButtonGroup;
- ShowVesselOnButton.Group = _showVesselButtonGroup;
- ShowVesselOnButton.OnPressed += args => ShowVesselPressed(true);
- ShowVesselOffButton.OnPressed += args => ShowVesselPressed(false);
}
private void ShowIFFPressed(bool pressed)
@@ -36,19 +29,14 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
ShowIFF?.Invoke(pressed);
}
- private void ShowVesselPressed(bool pressed)
- {
- ShowVessel?.Invoke(pressed);
- }
-
public void UpdateState(IFFConsoleBoundUserInterfaceState state)
{
- if ((state.AllowedFlags & IFFFlags.HideLabel) != 0x0)
+ if ((state.AllowedFlags & IFFFlags.HideLabel) != 0x0 || (state.AllowedFlags & IFFFlags.Hide) != 0x0)
{
ShowIFFOffButton.Disabled = false;
ShowIFFOnButton.Disabled = false;
- if ((state.Flags & IFFFlags.HideLabel) != 0x0)
+ if ((state.Flags & IFFFlags.HideLabel) != 0x0 || (state.Flags & IFFFlags.Hide) != 0x0)
{
ShowIFFOffButton.Pressed = true;
}
@@ -62,25 +50,5 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
ShowIFFOffButton.Disabled = true;
ShowIFFOnButton.Disabled = true;
}
-
- if ((state.AllowedFlags & IFFFlags.Hide) != 0x0)
- {
- ShowVesselOffButton.Disabled = false;
- ShowVesselOnButton.Disabled = false;
-
- if ((state.Flags & IFFFlags.Hide) != 0x0)
- {
- ShowVesselOffButton.Pressed = true;
- }
- else
- {
- ShowVesselOnButton.Pressed = true;
- }
- }
- else
- {
- ShowVesselOffButton.Disabled = true;
- ShowVesselOnButton.Disabled = true;
- }
}
}
diff --git a/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs b/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs
index 0acc3a2646..eee45bacc8 100644
--- a/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs
+++ b/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs
@@ -21,7 +21,7 @@ public sealed partial class BorgMenu : FancyWindow
[Dependency] private readonly IEntityManager _entity = default!;
private readonly NameModifierSystem _nameModifier;
private readonly PowerCellSystem _powerCell;
- private readonly PredictedBatterySystem _battery;
+ private readonly SharedBatterySystem _battery;
public Action? BrainButtonPressed;
public Action? EjectBatteryButtonPressed;
@@ -44,7 +44,7 @@ public sealed partial class BorgMenu : FancyWindow
_nameModifier = _entity.System();
_powerCell = _entity.System();
- _battery = _entity.System();
+ _battery = _entity.System();
_maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength);
diff --git a/Content.Client/Silicons/Borgs/BorgModuleControl.xaml b/Content.Client/Silicons/Borgs/BorgModuleControl.xaml
index 2f6e25f983..1be7cea8d9 100644
--- a/Content.Client/Silicons/Borgs/BorgModuleControl.xaml
+++ b/Content.Client/Silicons/Borgs/BorgModuleControl.xaml
@@ -3,10 +3,10 @@
StyleClasses="PanelLight"
Margin="5 5 5 0">
+
-
diff --git a/Content.Client/Silicons/Borgs/BorgSystem.cs b/Content.Client/Silicons/Borgs/BorgSystem.cs
index a4ca3725dc..517027d082 100644
--- a/Content.Client/Silicons/Borgs/BorgSystem.cs
+++ b/Content.Client/Silicons/Borgs/BorgSystem.cs
@@ -18,7 +18,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
- [Dependency] private readonly PredictedBatterySystem _battery = default!;
+ [Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _player = default!;
diff --git a/Content.Client/UserInterface/Controls/ListContainer.cs b/Content.Client/UserInterface/Controls/ListContainer.cs
index 53dddf110c..cf9ad681e5 100644
--- a/Content.Client/UserInterface/Controls/ListContainer.cs
+++ b/Content.Client/UserInterface/Controls/ListContainer.cs
@@ -28,7 +28,16 @@ public class ListContainer : Control
/// Called when creating a button on the UI.
/// The provided is the generated button that Controls should be parented to.
///
- public Action? GenerateItem;
+ public Action? GenerateItem
+ {
+ get => _generateItem;
+ set {
+ _generateItem = value;
+ // Invalidate _itemHeight so we recalculate the size of children the next
+ // time PopulateList() is called
+ _itemHeight = 0;
+ }
+ }
///
public Action? ItemPressed;
@@ -59,6 +68,7 @@ public class ListContainer : Control
private bool _updateChildren = false;
private bool _suppressScrollValueChanged;
private ButtonGroup? _buttonGroup;
+ public Action? _generateItem;
public int ScrollSpeedY { get; set; } = 50;
diff --git a/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs b/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs
index c389cf4dfb..6e8a41de7c 100644
--- a/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs
+++ b/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs
@@ -117,7 +117,7 @@ public partial class ChatBox : UIWidget
formatted.PushColor(color);
formatted.AddMarkupOrThrow(message);
formatted.Pop();
- Contents.AddMessage(formatted);
+ Contents.AddMessage(formatted, tagsAllowed: null);
}
public void Focus(ChatSelectChannel? channel = null)
diff --git a/Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs b/Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs
index e76ca1cf8f..b2b374cac5 100644
--- a/Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs
+++ b/Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs
@@ -26,6 +26,8 @@ public sealed class VoiceMaskBoundUserInterface : BoundUserInterface
_window.OnNameChange += OnNameSelected;
_window.OnVerbChange += verb => SendMessage(new VoiceMaskChangeVerbMessage(verb));
+ _window.OnToggle += OnToggle;
+ _window.OnAccentToggle += OnAccentToggle;
}
private void OnNameSelected(string name)
@@ -33,6 +35,16 @@ public sealed class VoiceMaskBoundUserInterface : BoundUserInterface
SendMessage(new VoiceMaskChangeNameMessage(name));
}
+ private void OnToggle()
+ {
+ SendMessage(new VoiceMaskToggleMessage());
+ }
+
+ private void OnAccentToggle()
+ {
+ SendMessage(new VoiceMaskAccentToggleMessage());
+ }
+
protected override void UpdateState(BoundUserInterfaceState state)
{
if (state is not VoiceMaskBuiState cast || _window == null)
@@ -40,7 +52,7 @@ public sealed class VoiceMaskBoundUserInterface : BoundUserInterface
return;
}
- _window.UpdateState(cast.Name, cast.Verb);
+ _window.UpdateState(cast.Name, cast.Verb, cast.Active, cast.AccentHide);
}
protected override void Dispose(bool disposing)
diff --git a/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml b/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml
index e23aca1239..18416757b9 100644
--- a/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml
+++ b/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml
@@ -12,5 +12,7 @@
+
+
diff --git a/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs b/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs
index 7ca4dd4b95..a5e7036283 100644
--- a/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs
+++ b/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs
@@ -12,6 +12,8 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
{
public Action? OnNameChange;
public Action? OnVerbChange;
+ public Action? OnToggle;
+ public Action? OnAccentToggle;
private List<(string, string)> _verbs = new();
@@ -31,6 +33,9 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
OnVerbChange?.Invoke((string?) args.Button.GetItemMetadata(args.Id));
SpeechVerbSelector.SelectId(args.Id);
};
+
+ ToggleButton.OnPressed += args => OnToggle?.Invoke();
+ ToggleAccentButton.OnPressed += args => OnAccentToggle?.Invoke();
}
public void ReloadVerbs(IPrototypeManager proto)
@@ -64,10 +69,12 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
SpeechVerbSelector.SelectId(id);
}
- public void UpdateState(string name, string? verb)
+ public void UpdateState(string name, string? verb, bool active, bool accentHide)
{
NameSelector.Text = name;
_verb = verb;
+ ToggleButton.Pressed = active;
+ ToggleAccentButton.Pressed = accentHide;
for (int id = 0; id < SpeechVerbSelector.ItemCount; id++)
{
diff --git a/Content.IntegrationTests/Content.IntegrationTests.csproj b/Content.IntegrationTests/Content.IntegrationTests.csproj
index 2e922d2509..8c62806f74 100644
--- a/Content.IntegrationTests/Content.IntegrationTests.csproj
+++ b/Content.IntegrationTests/Content.IntegrationTests.csproj
@@ -1,12 +1,10 @@
-
- $(TargetFramework)
..\bin\Content.IntegrationTests\
- false
false
- 12
+ disable
+
@@ -17,12 +15,12 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/Content.IntegrationTests/Tests/Atmos/AtmosMonitoringTest.cs b/Content.IntegrationTests/Tests/Atmos/AtmosMonitoringTest.cs
new file mode 100644
index 0000000000..83d7b9be53
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Atmos/AtmosMonitoringTest.cs
@@ -0,0 +1,130 @@
+using System.Numerics;
+using Content.Server.Atmos.Monitor.Components;
+using Content.Shared.Atmos;
+using Robust.Shared.Console;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.IntegrationTests.Tests.Atmos;
+
+///
+/// Test for determining that an AtmosMonitoringComponent/System correctly references
+/// the GasMixture of the tile it is on if the tile's GasMixture ever changes.
+///
+[TestOf(typeof(Atmospherics))]
+public sealed class AtmosMonitoringTest : AtmosTest
+{
+ // We can just reuse the dP test, I just want a grid.
+ protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
+
+ private readonly EntProtoId _airSensorProto = new("AirSensor");
+ private readonly EntProtoId _wallProto = new("WallSolid");
+
+ ///
+ /// Tests if the monitor properly nulls out its reference to the tile mixture
+ /// when a wall is placed on top of it, and restores the reference when the wall is removed.
+ ///
+ [Test]
+ public async Task NullOutTileAtmosphereGasMixture()
+ {
+ // run an atmos update to initialize everything For Real surely
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ var gridNetEnt = SEntMan.GetNetEntity(RelevantAtmos.Owner);
+ TargetCoords = new NetCoordinates(gridNetEnt, Vector2.Zero);
+ var netEnt = await Spawn(_airSensorProto);
+ var airSensorUid = SEntMan.GetEntity(netEnt);
+ Transform.TryGetGridTilePosition(airSensorUid, out var vec);
+
+ // run another one to ensure that the ref to the GasMixture was picked up
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ // should be in the middle
+ Assert.That(vec,
+ Is.EqualTo(Vector2i.Zero),
+ "Air sensor not in expected position on grid (0, 0)");
+
+ var atmosMonitor = SEntMan.GetComponent(airSensorUid);
+ var tileMixture = SAtmos.GetTileMixture(airSensorUid);
+
+ Assert.That(tileMixture,
+ Is.SameAs(atmosMonitor.TileGas),
+ "Atmos monitor's TileGas does not match actual tile mixture after spawn.");
+
+ // ok now spawn a wall or something on top of it
+ var wall = await Spawn(_wallProto);
+ var wallUid = SEntMan.GetEntity(wall);
+
+ // ensure that atmospherics registers the change - the gas mixture should no longer exist
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ // the monitor's ref to the gas should be null now
+ Assert.That(atmosMonitor.TileGas,
+ Is.Null,
+ "Atmos monitor's TileGas is not null after wall placed on top. Possible dead reference.");
+ // the actual mixture on the tile should be null now too
+ var nullTileMixture = SAtmos.GetTileMixture(airSensorUid);
+ Assert.That(nullTileMixture, Is.Null, "Tile mixture is not null after wall placed on top.");
+
+ // ok now delete the wall
+ await Delete(wallUid);
+
+ // ensure that atmospherics registers the change - the gas mixture should be back
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ // gas mixture should now exist again
+ var newTileMixture = SAtmos.GetTileMixture(airSensorUid);
+ Assert.That(newTileMixture, Is.Not.Null, "Tile mixture is null after wall removed.");
+ // monitor's ref to the gas should be back too
+ Assert.That(atmosMonitor.TileGas,
+ Is.SameAs(newTileMixture),
+ "Atmos monitor's TileGas does not match actual tile mixture after wall removed.");
+ }
+
+ ///
+ /// Tests if the monitor properly updates its reference to the tile mixture
+ /// when the FixGridAtmos command is called.
+ ///
+ [Test]
+ public async Task FixGridAtmosReplaceMixtureOnTileChange()
+ {
+ // run an atmos update to initialize everything For Real surely
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ var gridNetEnt = SEntMan.GetNetEntity(RelevantAtmos.Owner);
+ TargetCoords = new NetCoordinates(gridNetEnt, Vector2.Zero);
+ var netEnt = await Spawn(_airSensorProto);
+ var airSensorUid = SEntMan.GetEntity(netEnt);
+ Transform.TryGetGridTilePosition(airSensorUid, out var vec);
+
+ // run another one to ensure that the ref to the GasMixture was picked up
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ // should be in the middle
+ Assert.That(vec,
+ Is.EqualTo(Vector2i.Zero),
+ "Air sensor not in expected position on grid (0, 0)");
+
+ var atmosMonitor = SEntMan.GetComponent(airSensorUid);
+ var tileMixture = SAtmos.GetTileMixture(airSensorUid);
+
+ Assert.That(tileMixture,
+ Is.SameAs(atmosMonitor.TileGas),
+ "Atmos monitor's TileGas does not match actual tile mixture after spawn.");
+
+ SAtmos.RebuildGridAtmosphere((ProcessEnt.Owner, ProcessEnt.Comp1, ProcessEnt.Comp3));
+
+ // EXTREMELY IMPORTANT: The reference to the tile mixture on the tile should be completely different.
+ var newTileMixture = SAtmos.GetTileMixture(airSensorUid);
+ Assert.That(newTileMixture,
+ Is.Not.SameAs(tileMixture),
+ "Tile mixture is the same instance after fixgridatmos was ran. It should be a new instance.");
+
+ // The monitor's ref to the tile mixture should have updated too.
+ Assert.That(atmosMonitor.TileGas,
+ Is.SameAs(newTileMixture),
+ "Atmos monitor's TileGas does not match actual tile mixture after fixgridatmos was ran.");
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Atmos/AtmosTest.cs b/Content.IntegrationTests/Tests/Atmos/AtmosTest.cs
index d50c4a200b..d3bdc91cda 100644
--- a/Content.IntegrationTests/Tests/Atmos/AtmosTest.cs
+++ b/Content.IntegrationTests/Tests/Atmos/AtmosTest.cs
@@ -20,7 +20,7 @@ public abstract class AtmosTest : InteractionTest
protected AtmosphereSystem SAtmos = default!;
protected EntityLookupSystem LookupSystem = default!;
- protected Entity RelevantAtmos = default!;
+ protected Entity RelevantAtmos;
///
/// Used in . Resolved during test setup.
@@ -40,14 +40,35 @@ public abstract class AtmosTest : InteractionTest
SAtmos = SEntMan.System();
LookupSystem = SEntMan.System();
- RelevantAtmos = (MapData.Grid, SEntMan.GetComponent(MapData.Grid));
+ SEntMan.TryGetComponent(MapData.Grid, out var gridAtmosComp);
+ SEntMan.TryGetComponent(MapData.Grid, out var overlayComp);
+ SEntMan.TryGetComponent(MapData.Grid, out var mapGridComp);
+ var xform = SEntMan.GetComponent(MapData.Grid);
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(gridAtmosComp,
+ Is.Not.Null,
+ "Loaded map doesn't have a GridAtmosphereComponent on its grid. " +
+ "Did you forget to override TestMapPath with a proper atmospherics testing map?");
+ Assert.That(overlayComp,
+ Is.Not.Null,
+ "Loaded map doesn't have a GasTileOverlayComponent on its grid. " +
+ "Did you forget to override TestMapPath with a proper atmospherics testing map?");
+ Assert.That(mapGridComp,
+ Is.Not.Null,
+ "Loaded map doesn't have a MapGridComponent on its grid. " +
+ "Did you forget to override TestMapPath with a proper atmospherics testing map?");
+ }
+
+ RelevantAtmos = (MapData.Grid, gridAtmosComp);
ProcessEnt = new Entity(
MapData.Grid.Owner,
- SEntMan.GetComponent(MapData.Grid.Owner),
- SEntMan.GetComponent(MapData.Grid.Owner),
- SEntMan.GetComponent(MapData.Grid.Owner),
- SEntMan.GetComponent(MapData.Grid.Owner));
+ gridAtmosComp,
+ overlayComp,
+ mapGridComp,
+ xform);
}
///
diff --git a/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs b/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
index c3b3877c98..d4283568e8 100644
--- a/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
+++ b/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
@@ -1,15 +1,11 @@
-using System.Linq;
using System.Numerics;
using Content.Server.Atmos;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
-using Robust.Shared.EntitySerialization;
-using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Tests.Atmos;
@@ -20,7 +16,7 @@ namespace Content.IntegrationTests.Tests.Atmos;
///
[TestFixture]
[TestOf(typeof(DeltaPressureSystem))]
-public sealed class DeltaPressureTest
+public sealed class DeltaPressureTest : AtmosTest
{
#region Prototypes
@@ -92,7 +88,7 @@ public sealed class DeltaPressureTest
#endregion
- private readonly ResPath _testMap = new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
+ protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
// TODO ATMOS TESTS
// - Check for directional windows (partial airtight ents) properly computing pressure differences
@@ -111,40 +107,15 @@ public sealed class DeltaPressureTest
[Test]
public async Task ProcessingListAutoJoinTest()
{
- await using var pair = await PoolManager.GetServerClient();
- var server = pair.Server;
-
- var entMan = server.EntMan;
- var mapLoader = entMan.System();
- var atmosphereSystem = entMan.System();
- var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
-
- Entity grid = default;
- Entity dpEnt;
-
- // Load our test map in and assert that it exists.
- await server.WaitPost(() =>
+ await Server.WaitAssertion(() =>
{
-#pragma warning disable NUnit2045
- Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
- $"Failed to load map {_testMap}.");
- Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
-#pragma warning restore NUnit2045
+ var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(ProcessEnt, Vector2.Zero));
+ var dpEnt = new Entity(uid, SEntMan.GetComponent(uid));
- grid = gridSet.First();
+ Assert.That(SAtmos.IsDeltaPressureEntityInList(RelevantAtmos, dpEnt), "Entity was not in processing list when it should have automatically joined!");
+ SEntMan.DeleteEntity(uid);
+ Assert.That(!SAtmos.IsDeltaPressureEntityInList(RelevantAtmos, dpEnt), "Entity was still in processing list after deletion!");
});
-
- await server.WaitAssertion(() =>
- {
- var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
- dpEnt = new Entity(uid, entMan.GetComponent(uid));
-
- Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have automatically joined!");
- entMan.DeleteEntity(uid);
- Assert.That(!atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was still in processing list after deletion!");
- });
-
- await pair.CleanReturnAsync();
}
///
@@ -154,45 +125,27 @@ public sealed class DeltaPressureTest
[Test]
public async Task ProcessingDeltaStandbyTest()
{
- await using var pair = await PoolManager.GetServerClient();
- var server = pair.Server;
-
- var entMan = server.EntMan;
- var mapLoader = entMan.System();
- var atmosphereSystem = entMan.System();
- var transformSystem = entMan.System();
- var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
-
- Entity grid = default;
Entity dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
// Load our test map in and assert that it exists.
- await server.WaitPost(() =>
+ await Server.WaitPost(() =>
{
-#pragma warning disable NUnit2045
- Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
- $"Failed to load map {_testMap}.");
- Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
-#pragma warning restore NUnit2045
-
- grid = gridSet.First();
- var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
- dpEnt = new Entity(uid, entMan.GetComponent(uid));
- Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
+ var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(ProcessEnt, Vector2.Zero));
+ dpEnt = new Entity(uid, SEntMan.GetComponent(uid));
+ Assert.That(SAtmos.IsDeltaPressureEntityInList(ProcessEnt, dpEnt), "Entity was not in processing list when it should have been added!");
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
- await server.WaitPost(() =>
+ await Server.WaitPost(() =>
{
- var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
- var gridAtmosComp = entMan.GetComponent(grid);
+ var indices = Transform.GetGridOrMapTilePosition(dpEnt);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
- tile = gridAtmosComp.Tiles[offsetIndices];
+ tile = RelevantAtmos.Comp.Tiles[offsetIndices];
Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
@@ -202,19 +155,17 @@ public sealed class DeltaPressureTest
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
- await server.WaitRunTicks(30);
+ await Server.WaitRunTicks(30);
// Entity should exist, if it took one tick of damage then it should be instantly destroyed.
- await server.WaitAssertion(() =>
+ await Server.WaitAssertion(() =>
{
- Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold pressure from {direction} side!");
+ Assert.That(!SEntMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold pressure from {direction} side!");
tile.Air!.Clear();
});
- await server.WaitRunTicks(30);
+ await Server.WaitRunTicks(30);
}
-
- await pair.CleanReturnAsync();
}
///
@@ -224,47 +175,43 @@ public sealed class DeltaPressureTest
[Test]
public async Task ProcessingDeltaDamageTest()
{
- await using var pair = await PoolManager.GetServerClient();
- var server = pair.Server;
-
- var entMan = server.EntMan;
- var mapLoader = entMan.System();
- var atmosphereSystem = entMan.System();
- var transformSystem = entMan.System();
- var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
-
- Entity grid = default;
Entity dpEnt = default;
- TileAtmosphere tile = null!;
AtmosDirection direction = default;
// Load our test map in and assert that it exists.
- await server.WaitPost(() =>
+ await Server.WaitPost(() =>
{
-#pragma warning disable NUnit2045
- Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
- $"Failed to load map {_testMap}.");
- Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
-#pragma warning restore NUnit2045
-
- grid = gridSet.First();
+ SAtmos.SetAtmosphereSimulation(ProcessEnt, false);
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
- await server.WaitPost(() =>
+ /*
+ RUNNING REGULAR TICKS USING WaitRunTicks AND GUESSING AS TO HOW MANY ATMOS SIMULATION TICKS ARE HAPPENING
+ WILL CAUSE A RACE CONDITION THAT IS A PAIN IN THE ASS TO DEBUG
+
+ AN ENTITY MAY BE REMOVED AND ADDED BETWEEN A SUBTICK. IF LINDA PROCESSING IS ENABLED IT MIGHT CAUSE
+ AN EQUALIZATION TO PUT AIR IN OTHER TILES IN THE SMALL WINDOW WHERE THE TILE IS NOT AIRTIGHT
+ WHICH WILL THROW OFF DELTAS
+ */
+
+ await Server.WaitPost(() =>
+ {
+ SAtmos.RunProcessingFull(ProcessEnt,ProcessEnt.Owner, SAtmos.AtmosTickRate);
+ });
+
+ await Server.WaitPost(() =>
{
// Need to spawn an entity each run to ensure it works for all directions.
- var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
- dpEnt = new Entity(uid, entMan.GetComponent(uid));
- Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
+ var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(ProcessEnt.Owner, Vector2.Zero));
+ dpEnt = new Entity(uid, SEntMan.GetComponent(uid));
+ Assert.That(SAtmos.IsDeltaPressureEntityInList(ProcessEnt.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
- var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
- var gridAtmosComp = entMan.GetComponent(grid);
+ var indices = Transform.GetGridOrMapTilePosition(dpEnt);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
- tile = gridAtmosComp.Tiles[offsetIndices];
+ var tile = ProcessEnt.Comp1.Tiles[offsetIndices];
Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
@@ -274,19 +221,29 @@ public sealed class DeltaPressureTest
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
- await server.WaitRunTicks(30);
-
- // Entity should exist, if it took one tick of damage then it should be instantly destroyed.
- await server.WaitAssertion(() =>
+ // get jiggy with it! hit that dance white boy!
+ await Server.WaitPost(() =>
{
- Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold pressure from {direction} side!");
- tile.Air!.Clear();
+ SAtmos.RunProcessingFull(ProcessEnt,ProcessEnt.Owner, SAtmos.AtmosTickRate);
});
- await server.WaitRunTicks(30);
- }
+ // need to run some ticks as deleted entities are queued for removal
+ // and not removed instantly
+ await Server.WaitRunTicks(30);
- await pair.CleanReturnAsync();
+ // Entity shouldn't exist, if it took one tick of damage then it should be instantly destroyed.
+ await Server.WaitAssertion(() =>
+ {
+ Assert.That(SEntMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold pressure from {direction} side!");
+
+ // Double whammy: in case any unintended gas leak occured due to a race condition,
+ // clear out all the tiles.
+ foreach (var mix in SAtmos.GetAllMixtures(ProcessEnt))
+ {
+ mix.Clear();
+ }
+ });
+ }
}
///
@@ -296,39 +253,23 @@ public sealed class DeltaPressureTest
[Test]
public async Task ProcessingAbsoluteStandbyTest()
{
- await using var pair = await PoolManager.GetServerClient();
- var server = pair.Server;
-
- var entMan = server.EntMan;
- var mapLoader = entMan.System();
- var atmosphereSystem = entMan.System();
- var transformSystem = entMan.System();
- var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
-
- Entity grid = default;
Entity dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
- await server.WaitPost(() =>
+ await Server.WaitPost(() =>
{
-#pragma warning disable NUnit2045
- Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
- $"Failed to load map {_testMap}.");
- Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
-#pragma warning restore NUnit2045
- grid = gridSet.First();
- var uid = entMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(grid.Owner, Vector2.Zero));
- dpEnt = new Entity(uid, entMan.GetComponent(uid));
- Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
+ var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(ProcessEnt.Owner, Vector2.Zero));
+ dpEnt = new Entity(uid, SEntMan.GetComponent(uid));
+ Assert.That(SAtmos.IsDeltaPressureEntityInList(ProcessEnt.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
- await server.WaitPost(() =>
+ await Server.WaitPost(() =>
{
- var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
- var gridAtmosComp = entMan.GetComponent(grid);
+ var indices = Transform.GetGridOrMapTilePosition(dpEnt);
+ var gridAtmosComp = SEntMan.GetComponent(ProcessEnt);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
@@ -340,18 +281,16 @@ public sealed class DeltaPressureTest
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
- await server.WaitRunTicks(30);
+ await Server.WaitRunTicks(30);
- await server.WaitAssertion(() =>
+ await Server.WaitAssertion(() =>
{
- Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold absolute pressure from {direction} side!");
+ Assert.That(!SEntMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold absolute pressure from {direction} side!");
tile.Air!.Clear();
});
- await server.WaitRunTicks(30);
+ await Server.WaitRunTicks(30);
}
-
- await pair.CleanReturnAsync();
}
///
@@ -361,41 +300,21 @@ public sealed class DeltaPressureTest
[Test]
public async Task ProcessingAbsoluteDamageTest()
{
- await using var pair = await PoolManager.GetServerClient();
- var server = pair.Server;
-
- var entMan = server.EntMan;
- var mapLoader = entMan.System();
- var atmosphereSystem = entMan.System();
- var transformSystem = entMan.System();
- var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
-
- Entity grid = default;
Entity dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
- await server.WaitPost(() =>
- {
-#pragma warning disable NUnit2045
- Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
- $"Failed to load map {_testMap}.");
- Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
-#pragma warning restore NUnit2045
- grid = gridSet.First();
- });
-
for (var i = 0; i < Atmospherics.Directions; i++)
{
- await server.WaitPost(() =>
+ await Server.WaitPost(() =>
{
// Spawn fresh entity each iteration to verify all directions work
- var uid = entMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(grid.Owner, Vector2.Zero));
- dpEnt = new Entity(uid, entMan.GetComponent(uid));
- Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
+ var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(ProcessEnt.Owner, Vector2.Zero));
+ dpEnt = new Entity(uid, SEntMan.GetComponent(uid));
+ Assert.That(SAtmos.IsDeltaPressureEntityInList(ProcessEnt.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
- var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
- var gridAtmosComp = entMan.GetComponent(grid);
+ var indices = Transform.GetGridOrMapTilePosition(dpEnt);
+ var gridAtmosComp = SEntMan.GetComponent(ProcessEnt);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
@@ -408,17 +327,15 @@ public sealed class DeltaPressureTest
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
- await server.WaitRunTicks(30);
+ await Server.WaitRunTicks(30);
- await server.WaitAssertion(() =>
+ await Server.WaitAssertion(() =>
{
- Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold absolute pressure from {direction} side!");
+ Assert.That(SEntMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold absolute pressure from {direction} side!");
tile.Air!.Clear();
});
- await server.WaitRunTicks(30);
+ await Server.WaitRunTicks(30);
}
-
- await pair.CleanReturnAsync();
}
}
diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs
index b90e1bd13c..bbc8b67c53 100644
--- a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs
+++ b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs
@@ -31,6 +31,8 @@ namespace Content.IntegrationTests.Tests.Buckle
- type: Hands
- type: ComplexInteraction
- type: InputMover
+ - type: Physics
+ bodyType: KinematicController
- type: Body
prototype: Human
- type: StandingState
diff --git a/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs b/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs
index a77761a7d1..d430325e31 100644
--- a/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs
+++ b/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs
@@ -54,7 +54,7 @@ public sealed class ObjectiveCommandsTest
});
Assert.That(mindEnt, Is.Not.Null);
- var mindComp = mindEnt.Value.Comp;
+ var mindComp = mindEnt!.Value.Comp;
Assert.That(mindComp.Objectives, Is.Empty, "Dummy player started with objectives.");
await pair.WaitCommand($"addobjective {playerSession.Name} {ObjectiveProtoId}");
diff --git a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
index 72e8901631..20b0877548 100644
--- a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
+++ b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
@@ -164,7 +164,7 @@ namespace Content.IntegrationTests.Tests.Damageable
var damageToDeal = FixedPoint2.New(types.Count * 5);
DamageSpecifier damage = new(group3, damageToDeal);
- sDamageableSystem.TryChangeDamage(uid, damage, true);
+ sDamageableSystem.ChangeDamage(uid, damage, true);
Assert.Multiple(() =>
{
@@ -178,7 +178,7 @@ namespace Content.IntegrationTests.Tests.Damageable
});
// Heal
- sDamageableSystem.TryChangeDamage(uid, -damage);
+ sDamageableSystem.ChangeDamage(uid, -damage);
Assert.Multiple(() =>
{
@@ -197,7 +197,7 @@ namespace Content.IntegrationTests.Tests.Damageable
Assert.That(types, Has.Count.EqualTo(3));
damage = new DamageSpecifier(group3, 14);
- sDamageableSystem.TryChangeDamage(uid, damage, true);
+ sDamageableSystem.ChangeDamage(uid, damage, true);
Assert.Multiple(() =>
{
@@ -209,7 +209,7 @@ namespace Content.IntegrationTests.Tests.Damageable
});
// Heal
- sDamageableSystem.TryChangeDamage(uid, -damage);
+ sDamageableSystem.ChangeDamage(uid, -damage);
Assert.Multiple(() =>
{
@@ -225,7 +225,7 @@ namespace Content.IntegrationTests.Tests.Damageable
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
});
damage = new DamageSpecifier(group1, FixedPoint2.New(10)) + new DamageSpecifier(type2b, FixedPoint2.New(10));
- sDamageableSystem.TryChangeDamage(uid, damage, true);
+ sDamageableSystem.ChangeDamage(uid, damage, true);
Assert.Multiple(() =>
{
@@ -245,9 +245,9 @@ namespace Content.IntegrationTests.Tests.Damageable
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
// Test 'wasted' healing
- sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(type3a, 5));
- sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(type3b, 7));
- sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(group3, -11));
+ sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(type3a, 5));
+ sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(type3b, 7));
+ sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(group3, -11));
Assert.Multiple(() =>
{
@@ -257,11 +257,11 @@ namespace Content.IntegrationTests.Tests.Damageable
});
// Test Over-Healing
- sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(group3, FixedPoint2.New(-100)));
+ sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(group3, FixedPoint2.New(-100)));
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
// Test that if no health change occurred, returns false
- sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(group3, -100));
+ sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(group3, -100));
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
});
await pair.CleanReturnAsync();
diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs b/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs
index 6f33188813..513049bcad 100644
--- a/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs
+++ b/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs
@@ -112,7 +112,7 @@ public sealed partial class MindTests
Assert.That(entMan.EntityExists(attachedEntity), Is.True);
Assert.That(attachedEntity, Is.Not.EqualTo(playerEnt));
Assert.That(entMan.HasComponent(attachedEntity));
- var transform = entMan.GetComponent(attachedEntity.Value);
+ var transform = entMan.GetComponent(attachedEntity!.Value);
Assert.That(transform.MapID, Is.Not.EqualTo(MapId.Nullspace));
Assert.That(transform.MapID, Is.Not.EqualTo(testMap.MapId));
#pragma warning restore NUnit2045
@@ -175,7 +175,7 @@ public sealed partial class MindTests
Assert.That(player.AttachedEntity, Is.Not.Null);
Assert.That(entMan.EntityExists(player.AttachedEntity));
#pragma warning restore NUnit2045
- var originalEntity = player.AttachedEntity.Value;
+ var originalEntity = player.AttachedEntity!.Value;
EntityUid ghost = default!;
await server.WaitAssertion(() =>
@@ -248,7 +248,7 @@ public sealed partial class MindTests
var mindId = player.ContentData()?.Mind;
Assert.That(mindId, Is.Not.Null);
- var mind = entMan.GetComponent(mindId.Value);
+ var mind = entMan.GetComponent(mindId!.Value);
Assert.That(mind.VisitingEntity, Is.Null);
await pair.CleanReturnAsync();
diff --git a/Content.IntegrationTests/Tests/Power/PowerStateTest.cs b/Content.IntegrationTests/Tests/Power/PowerStateTest.cs
new file mode 100644
index 0000000000..dec398212d
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Power/PowerStateTest.cs
@@ -0,0 +1,186 @@
+using Content.Shared.Coordinates;
+using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+
+namespace Content.IntegrationTests.Tests.Power;
+
+[TestFixture]
+public sealed class PowerStateTest
+{
+ [TestPrototypes]
+ private const string Prototypes = @"
+- type: entity
+ id: PowerStateApcReceiverDummy
+ components:
+ - type: ApcPowerReceiver
+ - type: ExtensionCableReceiver
+ - type: Transform
+ anchored: true
+ - type: PowerState
+ isWorking: false
+ idlePowerDraw: 10
+ workingPowerDraw: 50
+";
+
+ ///
+ /// Asserts that switching from idle to working updates the power receiver load to the working draw.
+ ///
+ [Test]
+ public async Task SetWorkingState_IdleToWorking_UpdatesLoad()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var mapManager = server.ResolveDependency();
+ var entManager = server.ResolveDependency();
+ var mapSys = entManager.System();
+
+ await server.WaitAssertion(() =>
+ {
+ mapSys.CreateMap(out var mapId);
+ var grid = mapManager.CreateGridEntity(mapId);
+
+ mapSys.SetTile(grid, Vector2i.Zero, new Tile(1));
+
+ var ent = entManager.SpawnEntity("PowerStateApcReceiverDummy", grid.Owner.ToCoordinates());
+
+ var receiver = entManager.GetComponent(ent);
+ var powerState = entManager.GetComponent(ent);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.False);
+ Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
+ });
+
+ var system = entManager.System();
+ system.SetWorkingState((ent, powerState), true);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.True);
+ Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f));
+ });
+ });
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Asserts that switching from working to idle updates the power receiver load to the idle draw.
+ ///
+ [Test]
+ public async Task SetWorkingState_WorkingToIdle_UpdatesLoad()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var mapManager = server.ResolveDependency();
+ var entManager = server.ResolveDependency();
+ var mapSys = entManager.System();
+
+ await server.WaitAssertion(() =>
+ {
+ mapSys.CreateMap(out var mapId);
+ var grid = mapManager.CreateGridEntity(mapId);
+
+ mapSys.SetTile(grid, Vector2i.Zero, new Tile(1));
+
+ var ent = entManager.SpawnEntity("PowerStateApcReceiverDummy", grid.Owner.ToCoordinates());
+
+ var receiver = entManager.GetComponent(ent);
+ var powerState = entManager.GetComponent(ent);
+ var system = entManager.System();
+ Entity newEnt = (ent, powerState);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.False);
+ Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
+ });
+
+ system.SetWorkingState(newEnt, true);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.True);
+ Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f));
+ });
+
+ system.SetWorkingState(newEnt, false);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.False);
+ Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
+ });
+ });
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Asserts that setting the working state to the current state does not change the power receiver load.
+ ///
+ [Test]
+ public async Task SetWorkingState_AlreadyInState_NoChange()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var mapManager = server.ResolveDependency();
+ var entManager = server.ResolveDependency();
+ var mapSys = entManager.System();
+
+ await server.WaitAssertion(() =>
+ {
+ mapSys.CreateMap(out var mapId);
+ var grid = mapManager.CreateGridEntity(mapId);
+
+ mapSys.SetTile(grid, Vector2i.Zero, new Tile(1));
+
+ var ent = entManager.SpawnEntity("PowerStateApcReceiverDummy", grid.Owner.ToCoordinates());
+
+ var receiver = entManager.GetComponent(ent);
+ var powerState = entManager.GetComponent(ent);
+ var system = entManager.System();
+ Entity valueTuple = (ent, powerState);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.False);
+ Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
+ });
+
+ system.SetWorkingState(valueTuple, false);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.False);
+ Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
+ });
+
+ system.SetWorkingState(valueTuple, true);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.True);
+ Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f));
+ });
+
+ system.SetWorkingState(valueTuple, true);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.True);
+ 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 85bd366697..a28f646ef8 100644
--- a/Content.IntegrationTests/Tests/Power/PowerTest.cs
+++ b/Content.IntegrationTests/Tests/Power/PowerTest.cs
@@ -54,6 +54,7 @@ namespace Content.IntegrationTests.Tests.Power
nodeGroupID: HVPower
- type: PowerNetworkBattery
- type: Battery
+ netsync: false
- type: BatteryCharger
- type: entity
@@ -68,6 +69,7 @@ namespace Content.IntegrationTests.Tests.Power
nodeGroupID: HVPower
- type: PowerNetworkBattery
- type: Battery
+ netsync: false
- type: BatteryDischarger
- type: entity
@@ -85,6 +87,7 @@ namespace Content.IntegrationTests.Tests.Power
nodeGroupID: HVPower
- type: PowerNetworkBattery
- type: Battery
+ netsync: false
- type: BatteryDischarger
node: output
- type: BatteryCharger
@@ -110,6 +113,7 @@ namespace Content.IntegrationTests.Tests.Power
maxSupply: 1000
supplyRampTolerance: 1000
- type: Battery
+ netsync: false
maxCharge: 1000
startingCharge: 1000
- type: Transform
@@ -119,6 +123,7 @@ namespace Content.IntegrationTests.Tests.Power
id: ApcDummy
components:
- type: Battery
+ netsync: false
maxCharge: 10000
startingCharge: 10000
- type: PowerNetworkBattery
@@ -380,6 +385,8 @@ namespace Content.IntegrationTests.Tests.Power
const float startingCharge = 100_000;
PowerNetworkBatteryComponent netBattery = default!;
+ EntityUid generatorEnt = default!;
+ EntityUid consumerEnt = default!;
BatteryComponent battery = default!;
PowerConsumerComponent consumer = default!;
@@ -395,8 +402,8 @@ namespace Content.IntegrationTests.Tests.Power
entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
}
- var generatorEnt = entityManager.SpawnEntity("DischargingBatteryDummy", grid.Owner.ToCoordinates());
- var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 2));
+ generatorEnt = entityManager.SpawnEntity("DischargingBatteryDummy", grid.Owner.ToCoordinates());
+ consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 2));
netBattery = entityManager.GetComponent(generatorEnt);
battery = entityManager.GetComponent(generatorEnt);
@@ -441,7 +448,8 @@ namespace Content.IntegrationTests.Tests.Power
// Trivial integral to calculate expected power spent.
const double spentExpected = (200 + 100) / 2.0 * 0.25;
- Assert.That(battery.CurrentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
+ var currentCharge = batterySys.GetCharge((generatorEnt, battery));
+ Assert.That(currentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
});
});
@@ -460,7 +468,8 @@ namespace Content.IntegrationTests.Tests.Power
// Trivial integral to calculate expected power spent.
const double spentExpected = (400 + 100) / 2.0 * 0.75 + 400 * 0.25;
- Assert.That(battery.CurrentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
+ var currentCharge = batterySys.GetCharge((generatorEnt, battery));
+ Assert.That(currentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
});
});
@@ -576,6 +585,8 @@ namespace Content.IntegrationTests.Tests.Power
var entityManager = server.ResolveDependency();
var batterySys = entityManager.System();
var mapSys = entityManager.System();
+ EntityUid generatorEnt = default!;
+ EntityUid batteryEnt = default!;
PowerSupplierComponent supplier = default!;
BatteryComponent battery = default!;
@@ -591,8 +602,8 @@ namespace Content.IntegrationTests.Tests.Power
entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
}
- var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates());
- var batteryEnt = entityManager.SpawnEntity("ChargingBatteryDummy", grid.Owner.ToCoordinates(0, 2));
+ generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates());
+ batteryEnt = entityManager.SpawnEntity("ChargingBatteryDummy", grid.Owner.ToCoordinates(0, 2));
supplier = entityManager.GetComponent(generatorEnt);
var netBattery = entityManager.GetComponent(batteryEnt);
@@ -615,7 +626,8 @@ namespace Content.IntegrationTests.Tests.Power
{
// half a second @ 500 W = 250
// 50% efficiency, so 125 J stored total.
- Assert.That(battery.CurrentCharge, Is.EqualTo(125).Within(0.1));
+ var currentCharge = batterySys.GetCharge((batteryEnt, battery));
+ Assert.That(currentCharge, Is.EqualTo(125).Within(0.1));
Assert.That(supplier.CurrentSupply, Is.EqualTo(500).Within(0.1));
});
});
@@ -633,6 +645,9 @@ namespace Content.IntegrationTests.Tests.Power
var gameTiming = server.ResolveDependency();
var batterySys = entityManager.System();
var mapSys = entityManager.System();
+ EntityUid batteryEnt = default!;
+ EntityUid supplyEnt = default!;
+ EntityUid consumerEnt = default!;
PowerConsumerComponent consumer = default!;
PowerSupplierComponent supplier = default!;
PowerNetworkBatteryComponent netBattery = default!;
@@ -653,9 +668,9 @@ namespace Content.IntegrationTests.Tests.Power
var terminal = entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 1));
entityManager.GetComponent(terminal).LocalRotation = Angle.FromDegrees(180);
- var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
- var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
- var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
+ batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
+ supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
+ consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
consumer = entityManager.GetComponent(consumerEnt);
supplier = entityManager.GetComponent(supplyEnt);
@@ -694,7 +709,8 @@ namespace Content.IntegrationTests.Tests.Power
Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(200).Within(0.1));
const int expectedSpent = 200;
- Assert.That(battery.CurrentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
+ var currentCharge = batterySys.GetCharge((batteryEnt, battery));
+ Assert.That(currentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
});
});
@@ -711,6 +727,9 @@ namespace Content.IntegrationTests.Tests.Power
var gameTiming = server.ResolveDependency();
var batterySys = entityManager.System();
var mapSys = entityManager.System();
+ EntityUid batteryEnt = default!;
+ EntityUid supplyEnt = default!;
+ EntityUid consumerEnt = default!;
PowerConsumerComponent consumer = default!;
PowerSupplierComponent supplier = default!;
PowerNetworkBatteryComponent netBattery = default!;
@@ -731,9 +750,9 @@ namespace Content.IntegrationTests.Tests.Power
var terminal = entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 1));
entityManager.GetComponent(terminal).LocalRotation = Angle.FromDegrees(180);
- var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
- var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
- var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
+ batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
+ supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
+ consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
consumer = entityManager.GetComponent(consumerEnt);
supplier = entityManager.GetComponent(supplyEnt);
@@ -772,7 +791,8 @@ namespace Content.IntegrationTests.Tests.Power
Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(400).Within(0.1));
const int expectedSpent = 400;
- Assert.That(battery.CurrentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
+ var currentCharge = batterySys.GetCharge((batteryEnt, battery));
+ Assert.That(currentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
});
});
@@ -1223,6 +1243,9 @@ namespace Content.IntegrationTests.Tests.Power
var entityManager = server.ResolveDependency();
var batterySys = entityManager.System();
var mapSys = entityManager.System();
+ EntityUid generatorEnt = default!;
+ EntityUid substationEnt = default!;
+ EntityUid apcEnt = default!;
PowerNetworkBatteryComponent substationNetBattery = default!;
BatteryComponent apcBattery = default!;
@@ -1242,9 +1265,9 @@ namespace Content.IntegrationTests.Tests.Power
entityManager.SpawnEntity("CableMV", grid.Owner.ToCoordinates(0, 1));
entityManager.SpawnEntity("CableMV", grid.Owner.ToCoordinates(0, 2));
- var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
- var substationEnt = entityManager.SpawnEntity("SubstationDummy", grid.Owner.ToCoordinates(0, 1));
- var apcEnt = entityManager.SpawnEntity("ApcDummy", grid.Owner.ToCoordinates(0, 2));
+ generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
+ substationEnt = entityManager.SpawnEntity("SubstationDummy", grid.Owner.ToCoordinates(0, 1));
+ apcEnt = entityManager.SpawnEntity("ApcDummy", grid.Owner.ToCoordinates(0, 2));
var generatorSupplier = entityManager.GetComponent(generatorEnt);
substationNetBattery = entityManager.GetComponent(substationEnt);
@@ -1262,8 +1285,9 @@ namespace Content.IntegrationTests.Tests.Power
{
Assert.Multiple(() =>
{
+ var currentCharge = batterySys.GetCharge((apcEnt, apcBattery));
Assert.That(substationNetBattery.CurrentSupply, Is.GreaterThan(0)); //substation should be providing power
- Assert.That(apcBattery.CurrentCharge, Is.GreaterThan(0)); //apc battery should have gained charge
+ Assert.That(currentCharge, Is.GreaterThan(0)); //apc battery should have gained charge
});
});
diff --git a/Content.IntegrationTests/Tests/Power/StationPowerTests.cs b/Content.IntegrationTests/Tests/Power/StationPowerTests.cs
index 542a4645c6..9d79abf480 100644
--- a/Content.IntegrationTests/Tests/Power/StationPowerTests.cs
+++ b/Content.IntegrationTests/Tests/Power/StationPowerTests.cs
@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using Content.Server.GameTicking;
using Content.Server.Power.Components;
+using Content.Server.Power.EntitySystems;
using Content.Server.Power.NodeGroups;
using Content.Server.Power.Pow3r;
using Content.Shared.Maps;
@@ -48,6 +49,7 @@ public sealed class StationPowerTests
var entMan = server.EntMan;
var protoMan = server.ProtoMan;
var ticker = entMan.System();
+ var batterySys = entMan.System();
// Load the map
await server.WaitAssertion(() =>
@@ -71,7 +73,8 @@ public sealed class StationPowerTests
if (node.NodeGroup is not IBasePowerNet group)
continue;
networks.TryGetValue(group.NetworkNode, out var charge);
- networks[group.NetworkNode] = charge + battery.CurrentCharge;
+ var currentCharge = batterySys.GetCharge((uid, battery));
+ networks[group.NetworkNode] = charge + currentCharge;
}
var totalStartingCharge = networks.MaxBy(n => n.Value).Value;
diff --git a/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs b/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs
index ca70120ee9..cc440d0ccb 100644
--- a/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs
+++ b/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs
@@ -14,7 +14,7 @@ public sealed class AdminTest : ToolshedTest
var toolMan = Server.ResolveDependency();
var admin = Server.ResolveDependency();
var ignored = new HashSet()
- {typeof(LocTest).Assembly, typeof(Robust.UnitTesting.Shared.Toolshed.LocTest).Assembly};
+ {typeof(LocTest).Assembly};
await Server.WaitAssertion(() =>
{
diff --git a/Content.IntegrationTests/Tests/Toolshed/LocTest.cs b/Content.IntegrationTests/Tests/Toolshed/LocTest.cs
index fb210eba50..849f9e55d3 100644
--- a/Content.IntegrationTests/Tests/Toolshed/LocTest.cs
+++ b/Content.IntegrationTests/Tests/Toolshed/LocTest.cs
@@ -19,7 +19,7 @@ public sealed class LocTest : ToolshedTest
var locStrings = new HashSet();
var ignored = new HashSet()
- {typeof(LocTest).Assembly, typeof(Robust.UnitTesting.Shared.Toolshed.LocTest).Assembly};
+ {typeof(LocTest).Assembly};
await Server.WaitAssertion(() =>
{
diff --git a/Content.MapRenderer/Content.MapRenderer.csproj b/Content.MapRenderer/Content.MapRenderer.csproj
index 4320717732..98fb446bd5 100644
--- a/Content.MapRenderer/Content.MapRenderer.csproj
+++ b/Content.MapRenderer/Content.MapRenderer.csproj
@@ -3,19 +3,27 @@
Exe
..\bin\Content.MapRenderer\
false
- enable
true
+ false
+
+
-
+
+
+
+
+
+
+
diff --git a/Content.Packaging/Content.Packaging.csproj b/Content.Packaging/Content.Packaging.csproj
index 9823e40b8e..d278b48734 100644
--- a/Content.Packaging/Content.Packaging.csproj
+++ b/Content.Packaging/Content.Packaging.csproj
@@ -2,13 +2,13 @@
Exe
enable
- enable
True
-
-
-
+
+
+
+
diff --git a/Content.Packaging/DepsHandler.cs b/Content.Packaging/DepsHandler.cs
new file mode 100644
index 0000000000..9907b97ba9
--- /dev/null
+++ b/Content.Packaging/DepsHandler.cs
@@ -0,0 +1,80 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Content.Packaging;
+
+///
+/// Helper class for working with .deps.json files.
+///
+public sealed class DepsHandler
+{
+ public readonly Dictionary Libraries = new();
+
+ public DepsHandler(DepsData data)
+ {
+ if (data.Targets.Count != 1)
+ throw new Exception("Expected exactly one target");
+
+ var target = data.Targets.Single().Value;
+
+ foreach (var (libNameAndVersion, libInfo) in target)
+ {
+ var split = libNameAndVersion.Split('/', 2);
+
+ Libraries.Add(split[0], libInfo);
+ }
+ }
+
+ public static DepsHandler Load(string depsFile)
+ {
+ using var f = File.OpenRead(depsFile);
+ var depsData = JsonSerializer.Deserialize(f) ?? throw new InvalidOperationException("Deps are null!");
+
+ return new DepsHandler(depsData);
+ }
+
+ public HashSet RecursiveGetLibrariesFrom(string start)
+ {
+ var found = new HashSet();
+
+ RecursiveAddLibraries(start, found);
+
+ return found;
+ }
+
+ private void RecursiveAddLibraries(string start, HashSet set)
+ {
+ if (!set.Add(start))
+ return;
+
+ var lib = Libraries[start];
+ if (lib.Dependencies == null)
+ return;
+
+ foreach (var dep in lib.Dependencies.Keys)
+ {
+ RecursiveAddLibraries(dep, set);
+ }
+ }
+
+ public sealed class DepsData
+ {
+ [JsonInclude, JsonPropertyName("targets")]
+ public required Dictionary> Targets;
+ }
+
+ public sealed class LibraryInfo
+ {
+ [JsonInclude, JsonPropertyName("dependencies")]
+ public Dictionary? Dependencies;
+
+ [JsonInclude, JsonPropertyName("runtime")]
+ public Dictionary? Runtime;
+
+ // Paths are like lib/netstandard2.0/JetBrains.Annotations.dll
+ public IEnumerable GetDllNames()
+ {
+ return Runtime == null ? [] : Runtime.Keys.Select(p => p.Split('/')[^1]);
+ }
+ }
+}
diff --git a/Content.Packaging/ServerPackaging.cs b/Content.Packaging/ServerPackaging.cs
index 2715e7a900..8e559d5a65 100644
--- a/Content.Packaging/ServerPackaging.cs
+++ b/Content.Packaging/ServerPackaging.cs
@@ -37,25 +37,9 @@ public static class ServerPackaging
.Select(o => o.Rid)
.ToList();
- private static readonly List ServerContentAssemblies = new()
- {
- "Content.Server.Database",
- "Content.Server",
- "Content.Shared",
- "Content.Shared.Database",
- };
-
- private static readonly List ServerExtraAssemblies = new()
- {
- // Python script had Npgsql. though we want Npgsql.dll as well soooo
- "Npgsql",
- "Microsoft",
- "NetCord",
- };
-
private static readonly List ServerNotExtraAssemblies = new()
{
- "Microsoft.CodeAnalysis",
+ "JetBrains.Annotations",
};
private static readonly HashSet BinSkipFolders = new()
@@ -181,23 +165,13 @@ public static class ServerPackaging
var inputPassCore = graph.InputCore;
var inputPassResources = graph.InputResources;
- var contentAssemblies = new List(ServerContentAssemblies);
// Additional assemblies that need to be copied such as EFCore.
var sourcePath = Path.Combine(contentDir, "bin", "Content.Server");
- // Should this be an asset pass?
- // For future archaeologists I just want audio rework to work and need the audio pass so
- // just porting this as is from python.
- foreach (var fullPath in Directory.EnumerateFiles(sourcePath, "*.*", SearchOption.AllDirectories))
- {
- var fileName = Path.GetFileNameWithoutExtension(fullPath);
+ var deps = DepsHandler.Load(Path.Combine(sourcePath, "Content.Server.deps.json"));
- if (!ServerNotExtraAssemblies.Any(o => fileName.StartsWith(o)) && ServerExtraAssemblies.Any(o => fileName.StartsWith(o)))
- {
- contentAssemblies.Add(fileName);
- }
- }
+ var contentAssemblies = GetContentAssemblyNamesToCopy(deps);
await RobustSharedPackaging.DoResourceCopy(
Path.Combine("RobustToolbox", "bin", "Server",
@@ -229,5 +203,21 @@ public static class ServerPackaging
inputPassResources.InjectFinished();
}
+ // This returns both content assemblies (e.g. Content.Server.dll) and dependencies (e.g. Npgsql)
+ private static IEnumerable GetContentAssemblyNamesToCopy(DepsHandler deps)
+ {
+ var depsContent = deps.RecursiveGetLibrariesFrom("Content.Server").SelectMany(GetLibraryNames);
+ var depsRobust = deps.RecursiveGetLibrariesFrom("Robust.Server").SelectMany(GetLibraryNames);
+
+ var depsContentExclusive = depsContent.Except(depsRobust).ToHashSet();
+
+ // Remove .dll suffix and apply filtering.
+ var names = depsContentExclusive.Select(p => p[..^4]).Where(p => !ServerNotExtraAssemblies.Any(p.StartsWith));
+
+ return names;
+
+ IEnumerable GetLibraryNames(string library) => deps.Libraries[library].GetDllNames();
+ }
+
private readonly record struct PlatformReg(string Rid, string TargetOs, bool BuildByDefault);
}
diff --git a/Content.PatreonParser/Content.PatreonParser.csproj b/Content.PatreonParser/Content.PatreonParser.csproj
index 1724ec0cea..bba47a062d 100644
--- a/Content.PatreonParser/Content.PatreonParser.csproj
+++ b/Content.PatreonParser/Content.PatreonParser.csproj
@@ -2,7 +2,7 @@
Exe
- net9.0
+ net10.0
enable
enable
true
diff --git a/Content.Replay/Content.Replay.csproj b/Content.Replay/Content.Replay.csproj
index 28cfc7f666..5008214522 100644
--- a/Content.Replay/Content.Replay.csproj
+++ b/Content.Replay/Content.Replay.csproj
@@ -1,26 +1,23 @@
- $(TargetFramework)
- 12
- false
false
..\bin\Content.Replay\
Exe
RA0032;nullable
- enable
+
+
-
-
-
-
+
+
+
diff --git a/Content.Server.Database/Content.Server.Database.csproj b/Content.Server.Database/Content.Server.Database.csproj
index d98d0642db..22b718b363 100644
--- a/Content.Server.Database/Content.Server.Database.csproj
+++ b/Content.Server.Database/Content.Server.Database.csproj
@@ -1,16 +1,13 @@
-
- $(TargetFramework)
- 12
- false
false
..\bin\Content.Server.Database\
true
- enable
RA0003
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
index 777d40e110..786a4294bf 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
@@ -31,6 +31,7 @@ public sealed partial class AdminVerbSystem
private static readonly EntProtoId DefaultChangelingRule = "Changeling";
private static readonly EntProtoId ParadoxCloneRuleId = "ParadoxCloneSpawn";
private static readonly EntProtoId DefaultWizardRule = "Wizard";
+ private static readonly EntProtoId DefaultNinjaRule = "NinjaSpawn";
private static readonly ProtoId PirateGearId = "PirateGear";
// All antag verbs have names so invokeverb works.
@@ -207,6 +208,21 @@ public sealed partial class AdminVerbSystem
};
args.Verbs.Add(wizard);
+ var ninjaName = Loc.GetString("admin-verb-text-make-space-ninja");
+ Verb ninja = new()
+ {
+ Text = ninjaName,
+ Category = VerbCategory.Antag,
+ Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Weapons/Melee/energykatana.rsi"), "icon"),
+ Act = () =>
+ {
+ _antag.ForceMakeAntag(targetPlayer, DefaultNinjaRule);
+ },
+ Impact = LogImpact.High,
+ Message = string.Join(": ", ninjaName, Loc.GetString("admin-verb-make-space-ninja")),
+ };
+ args.Verbs.Add(ninja);
+
if (HasComp(args.Target)) // only humanoids can be cloned
args.Verbs.Add(paradox);
}
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs
index bd214578ea..f30e0ef728 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs
@@ -4,7 +4,6 @@ using System.Numerics;
using Content.Server.Cargo.Components;
using Content.Server.Doors.Systems;
using Content.Server.Hands.Systems;
-using Content.Server.Power.EntitySystems;
using Content.Server.Stack;
using Content.Server.Station.Systems;
using Content.Server.Weapons.Ranged.Systems;
@@ -51,8 +50,7 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly AdminTestArenaSystem _adminTestArenaSystem = default!;
[Dependency] private readonly StationJobsSystem _stationJobsSystem = default!;
[Dependency] private readonly JointSystem _jointSystem = default!;
- [Dependency] private readonly BatterySystem _batterySystem = default!;
- [Dependency] private readonly PredictedBatterySystem _predictedBatterySystem = default!;
+ [Dependency] private readonly SharedBatterySystem _batterySystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly GunSystem _gun = default!;
@@ -161,57 +159,6 @@ public sealed partial class AdminVerbSystem
args.Verbs.Add(makeVulnerable);
}
- if (TryComp(args.Target, out var pBattery))
- {
- Verb refillBattery = new()
- {
- Text = Loc.GetString("admin-verbs-refill-battery"),
- Category = VerbCategory.Tricks,
- Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/fill_battery.png")),
- Act = () =>
- {
- _predictedBatterySystem.SetCharge((args.Target, pBattery), pBattery.MaxCharge);
- },
- Impact = LogImpact.Medium,
- Message = Loc.GetString("admin-trick-refill-battery-description"),
- Priority = (int)TricksVerbPriorities.RefillBattery,
- };
- args.Verbs.Add(refillBattery);
-
- Verb drainBattery = new()
- {
- Text = Loc.GetString("admin-verbs-drain-battery"),
- Category = VerbCategory.Tricks,
- Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/drain_battery.png")),
- Act = () =>
- {
- _predictedBatterySystem.SetCharge((args.Target, pBattery), 0);
- },
- Impact = LogImpact.Medium,
- Priority = (int)TricksVerbPriorities.DrainBattery,
- };
- args.Verbs.Add(drainBattery);
-
- Verb infiniteBattery = new()
- {
- Text = Loc.GetString("admin-verbs-infinite-battery"),
- Category = VerbCategory.Tricks,
- Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/infinite_battery.png")),
- Act = () =>
- {
- var recharger = EnsureComp(args.Target);
- recharger.AutoRechargeRate = pBattery.MaxCharge; // Instant refill.
- recharger.AutoRechargePauseTime = TimeSpan.Zero; // No delay.
- Dirty(args.Target, recharger);
- _predictedBatterySystem.RefreshChargeRate((args.Target, pBattery));
- },
- Impact = LogImpact.Medium,
- Message = Loc.GetString("admin-trick-infinite-battery-object-description"),
- Priority = (int)TricksVerbPriorities.InfiniteBattery,
- };
- args.Verbs.Add(infiniteBattery);
- }
-
if (TryComp(args.Target, out var battery))
{
Verb refillBattery = new()
@@ -254,6 +201,8 @@ public sealed partial class AdminVerbSystem
var recharger = EnsureComp(args.Target);
recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill.
recharger.AutoRechargePauseTime = TimeSpan.Zero; // No delay.
+ Dirty(args.Target, recharger);
+ _batterySystem.RefreshChargeRate((args.Target, battery));
},
Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-infinite-battery-object-description"),
diff --git a/Content.Server/Anomaly/AnomalySystem.Generator.cs b/Content.Server/Anomaly/AnomalySystem.Generator.cs
index 46ad9278f8..09af0419bb 100644
--- a/Content.Server/Anomaly/AnomalySystem.Generator.cs
+++ b/Content.Server/Anomaly/AnomalySystem.Generator.cs
@@ -100,7 +100,7 @@ public sealed partial class AnomalySystem
// no air-blocked areas.
if (_atmosphere.IsTileSpace(grid, xform.MapUid, tile) ||
- _atmosphere.IsTileAirBlocked(grid, tile, mapGridComp: gridComp))
+ _atmosphere.IsTileAirBlockedCached(grid, tile))
{
continue;
}
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs
index 96a5c69457..5cce6075b7 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs
@@ -306,6 +306,8 @@ public partial class AtmosphereSystem
/// The directions to check for air-blockage.
/// Optional map grid component associated with the grid.
/// True if the tile is air-blocked in the specified directions, false otherwise.
+ /// This rebuilds airtight data on-the-fly. You should only use this if you've just
+ /// invalidated airtight data, and you cannot wait one atmostick to revalidate it.
[PublicAPI]
public bool IsTileAirBlocked(EntityUid gridUid,
Vector2i tile,
@@ -315,11 +317,35 @@ public partial class AtmosphereSystem
if (!Resolve(gridUid, ref mapGridComp, false))
return false;
- // TODO ATMOS: This reconstructs the data instead of getting the cached version. Might want to include a method to get the cached version later.
var data = GetAirtightData(gridUid, mapGridComp, tile);
return data.BlockedDirections.IsFlagSet(directions);
}
+ ///
+ /// Checks if a tile on a grid is air-blocked in the specified directions, using cached data.
+ ///
+ /// The grid to check.
+ /// The tile on the grid to check.
+ /// The directions to check for air-blockage.
+ /// True if the tile is air-blocked in the specified directions, false otherwise.
+ /// Returns data that is currently cached by Atmospherics.
+ /// You should always use this method over as it's more performant.
+ /// If you need to get up-to-date data because you've just invalidated airtight data,
+ /// use .
+ [PublicAPI]
+ public bool IsTileAirBlockedCached(Entity grid,
+ Vector2i tile,
+ AtmosDirection directions = AtmosDirection.All)
+ {
+ if (!_atmosQuery.Resolve(grid, ref grid.Comp, false))
+ return false;
+
+ if (!grid.Comp.Tiles.TryGetValue(tile, out var atmosTile))
+ return false;
+
+ return atmosTile.AirtightData.BlockedDirections.IsFlagSet(directions);
+ }
+
///
/// Checks if a tile on a grid or map is space as defined by a tile's definition of space.
/// Some tiles can hold back space and others cannot - for example, plating can hold
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Commands.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Commands.cs
index 246c3a571f..11ebfe2066 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Commands.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Commands.cs
@@ -19,7 +19,9 @@ public sealed partial class AtmosphereSystem
// Fix Grid Atmos command.
_consoleHost.RegisterCommand("fixgridatmos",
"Makes every tile on a grid have a roundstart gas mix.",
- "fixgridatmos ", FixGridAtmosCommand, FixGridAtmosCommandCompletions);
+ "fixgridatmos ",
+ FixGridAtmosCommand,
+ FixGridAtmosCommandCompletions);
}
private void ShutdownCommands()
@@ -36,42 +38,6 @@ public sealed partial class AtmosphereSystem
return;
}
- var mixtures = new GasMixture[9];
- for (var i = 0; i < mixtures.Length; i++)
- mixtures[i] = new GasMixture(Atmospherics.CellVolume) { Temperature = Atmospherics.T20C };
-
- // 0: Air
- mixtures[0].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesStandard);
- mixtures[0].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesStandard);
-
- // 1: Vaccum
-
- // 2: Oxygen (GM)
- mixtures[2].AdjustMoles(Gas.Oxygen, Atmospherics.MolesCellGasMiner);
-
- // 3: Nitrogen (GM)
- mixtures[3].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellGasMiner);
-
- // 4: Plasma (GM)
- mixtures[4].AdjustMoles(Gas.Plasma, Atmospherics.MolesCellGasMiner);
-
- // 5: Instant Plasmafire (r)
- mixtures[5].AdjustMoles(Gas.Oxygen, Atmospherics.MolesCellGasMiner);
- mixtures[5].AdjustMoles(Gas.Plasma, Atmospherics.MolesCellGasMiner);
- mixtures[5].Temperature = 5000f;
-
- // 6: (Walk-In) Freezer
- mixtures[6].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesFreezer);
- mixtures[6].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesFreezer);
- mixtures[6].Temperature = Atmospherics.FreezerTemp; // Little colder than an actual freezer but gives a grace period to get e.g. themomachines set up, should keep warm for a few door openings
-
- // 7: Nitrogen (101kpa) for vox rooms
- mixtures[7].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellStandard);
-
- // 8: Air (GM)
- mixtures[8].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesGasMiner);
- mixtures[8].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesGasMiner);
-
foreach (var arg in args)
{
if (!NetEntity.TryParse(arg, out var netEntity) || !TryGetEntity(netEntity, out var euid))
@@ -92,34 +58,82 @@ public sealed partial class AtmosphereSystem
continue;
}
- // Force Invalidate & update air on all tiles
- Entity grid =
- new(euid.Value, gridAtmosphere, Comp(euid.Value), gridComp, Transform(euid.Value));
-
- RebuildGridTiles(grid);
-
- var query = GetEntityQuery();
- foreach (var (indices, tile) in gridAtmosphere.Tiles.ToArray())
- {
- if (tile.Air is not {Immutable: false} air)
- continue;
-
- air.Clear();
- var mixtureId = 0;
- var enumerator = _mapSystem.GetAnchoredEntitiesEnumerator(grid, grid, indices);
- while (enumerator.MoveNext(out var entUid))
- {
- if (query.TryComp(entUid, out var marker))
- mixtureId = marker.Mode;
- }
-
- var mixture = mixtures[mixtureId];
- Merge(air, mixture);
- air.Temperature = mixture.Temperature;
- }
+ RebuildGridAtmosphere((euid.Value, gridAtmosphere, gridComp));
}
}
+ ///
+ /// Rebuilds all s on a grid to have roundstart gas mixes.
+ ///
+ /// Please be responsible with this method. Used only by tests and fixgridatmos.
+ public void RebuildGridAtmosphere(Entity ent)
+ {
+ var mixtures = new GasMixture[9];
+ for (var i = 0; i < mixtures.Length; i++)
+ {
+ mixtures[i] = new GasMixture(Atmospherics.CellVolume) { Temperature = Atmospherics.T20C };
+ }
+
+ // 0: Air
+ mixtures[0].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesStandard);
+ mixtures[0].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesStandard);
+
+ // 1: Vaccum
+
+ // 2: Oxygen (GM)
+ mixtures[2].AdjustMoles(Gas.Oxygen, Atmospherics.MolesCellGasMiner);
+
+ // 3: Nitrogen (GM)
+ mixtures[3].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellGasMiner);
+
+ // 4: Plasma (GM)
+ mixtures[4].AdjustMoles(Gas.Plasma, Atmospherics.MolesCellGasMiner);
+
+ // 5: Instant Plasmafire (r)
+ mixtures[5].AdjustMoles(Gas.Oxygen, Atmospherics.MolesCellGasMiner);
+ mixtures[5].AdjustMoles(Gas.Plasma, Atmospherics.MolesCellGasMiner);
+ mixtures[5].Temperature = 5000f;
+
+ // 6: (Walk-In) Freezer
+ mixtures[6].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesFreezer);
+ mixtures[6].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesFreezer);
+ mixtures[6].Temperature = Atmospherics.FreezerTemp; // Little colder than an actual freezer but gives a grace period to get e.g. themomachines set up, should keep warm for a few door openings
+
+ // 7: Nitrogen (101kpa) for vox rooms
+ mixtures[7].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellStandard);
+
+ // 8: Air (GM)
+ mixtures[8].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesGasMiner);
+ mixtures[8].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesGasMiner);
+
+
+ // Force Invalidate & update air on all tiles
+ Entity grid =
+ new(ent.Owner, ent.Comp1, Comp(ent), ent.Comp2, Transform(ent));
+
+ RebuildGridTiles(grid);
+
+ var query = GetEntityQuery();
+ foreach (var (indices, tile) in ent.Comp1.Tiles.ToArray())
+ {
+ if (tile.Air is not {Immutable: false} air)
+ continue;
+
+ air.Clear();
+ var mixtureId = 0;
+ var enumerator = _mapSystem.GetAnchoredEntitiesEnumerator(grid, grid, indices);
+ while (enumerator.MoveNext(out var entUid))
+ {
+ if (query.TryComp(entUid, out var marker))
+ mixtureId = marker.Mode;
+ }
+
+ var mixture = mixtures[mixtureId];
+ Merge(air, mixture);
+ air.Temperature = mixture.Temperature;
+ }
+ }
+
///
/// Clears & re-creates all references to s stored on a grid.
///
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
index 0c6a3a7daa..95d56c9ca6 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
@@ -3,6 +3,7 @@ using System.Runtime.CompilerServices;
using Content.Server.Atmos.Reactions;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Reactions;
+using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
@@ -477,6 +478,26 @@ namespace Content.Server.Atmos.EntitySystems
return reaction;
}
+ ///
+ /// Adds an array of moles to a .
+ /// Guards against negative moles by clamping to zero.
+ ///
+ /// The to add moles to.
+ /// The of moles to add.
+ /// Thrown when the length of the
+ /// is not the same as the length of the gas array.
+ [PublicAPI]
+ public static void AddMolsToMixture(GasMixture mixture, ReadOnlySpan molsToAdd)
+ {
+ // Span length should be as long as the length of the gas array.
+ // Technically this is a redundant check because NumericsHelpers will do the same thing,
+ // but eh.
+ ArgumentOutOfRangeException.ThrowIfNotEqual(mixture.Moles.Length, molsToAdd.Length, nameof(mixture.Moles.Length));
+
+ NumericsHelpers.Add(mixture.Moles, molsToAdd);
+ NumericsHelpers.Max(mixture.Moles, 0f);
+ }
+
public enum GasCompareResult
{
NoExchange = -2,
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs
index c0f081f9ba..d562fe5111 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs
@@ -265,6 +265,7 @@ namespace Content.Server.Atmos.EntitySystems
tile.ArchivedCycle = 0;
tile.LastShare = 0f;
tile.Hotspot = new Hotspot();
+ NotifyDeviceTileChanged((ent.Owner, ent.Comp1, ent.Comp3), tile.GridIndices);
return;
}
@@ -275,6 +276,10 @@ namespace Content.Server.Atmos.EntitySystems
if (data.FixVacuum)
GridFixTileVacuum(tile);
+
+ // Since we assigned the tile a new GasMixture we need to tell any devices
+ // on this tile that the reference has changed.
+ NotifyDeviceTileChanged((ent.Owner, ent.Comp1, ent.Comp3), tile.GridIndices);
}
private void QueueRunTiles(
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Utils.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Utils.cs
index a402cf20f3..9b53d0d16c 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Utils.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Utils.cs
@@ -1,10 +1,8 @@
using System.Runtime.CompilerServices;
using Content.Server.Atmos.Components;
-using Content.Server.Maps;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
-using Content.Shared.Maps;
-using Robust.Shared.Map;
+using Content.Shared.Atmos.Piping.Components;
using Robust.Shared.Map.Components;
namespace Content.Server.Atmos.EntitySystems;
@@ -176,4 +174,21 @@ public partial class AtmosphereSystem
_tile.PryTile(tileRef);
}
+
+ ///
+ /// Notifies all subscribing entities on a particular tile that the tile has changed.
+ /// Atmos devices may store references to tiles, so this is used to properly resync devices
+ /// after a significant atmos change on that tile, for example a tile getting a new .
+ ///
+ /// The grid atmosphere entity.
+ /// The tile to check for devices on.
+ private void NotifyDeviceTileChanged(Entity ent, Vector2i tile)
+ {
+ var inTile = _mapSystem.GetAnchoredEntities(ent.Owner, ent.Comp2, tile);
+ var ev = new AtmosDeviceTileChangedEvent();
+ foreach (var uid in inTile)
+ {
+ RaiseLocalEvent(uid, ref ev);
+ }
+ }
}
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
index 8120caca4e..df380912b6 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
@@ -64,8 +64,8 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
InitializeGridAtmosphere();
InitializeMap();
- _mapAtmosQuery = GetEntityQuery();
_atmosQuery = GetEntityQuery();
+ _mapAtmosQuery = GetEntityQuery();
_airtightQuery = GetEntityQuery();
_firelockQuery = GetEntityQuery();
diff --git a/Content.Server/Atmos/EntitySystems/HeatExchangerSystem.cs b/Content.Server/Atmos/EntitySystems/HeatExchangerSystem.cs
index b3644e88b7..8183bb99e4 100644
--- a/Content.Server/Atmos/EntitySystems/HeatExchangerSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/HeatExchangerSystem.cs
@@ -43,7 +43,7 @@ public sealed class HeatExchangerSystem : EntitySystem
// make sure that the tile the device is on isn't blocked by a wall or something similar.
if (args.Grid is {} grid
&& _transform.TryGetGridTilePosition(uid, out var tile)
- && _atmosphereSystem.IsTileAirBlocked(grid, tile))
+ && _atmosphereSystem.IsTileAirBlockedCached(grid, tile))
{
return;
}
diff --git a/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs b/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs
index 452b300331..7333b7b814 100644
--- a/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs
+++ b/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs
@@ -57,6 +57,13 @@ public sealed class AtmosMonitorSystem : EntitySystem
SubscribeLocalEvent(OnPacketRecv);
SubscribeLocalEvent(OnAtmosDeviceLeaveAtmosphere);
SubscribeLocalEvent(OnAtmosDeviceEnterAtmosphere);
+ SubscribeLocalEvent(OnAtmosDeviceTileChangedEvent);
+ }
+
+ private void OnAtmosDeviceTileChangedEvent(Entity ent, ref AtmosDeviceTileChangedEvent args)
+ {
+ if (!ent.Comp.MonitorsPipeNet)
+ ent.Comp.TileGas = _atmosphereSystem.GetContainingMixture(ent.Owner, true);
}
private void OnAtmosDeviceLeaveAtmosphere(EntityUid uid, AtmosMonitorComponent atmosMonitor, ref AtmosDeviceDisabledEvent args)
diff --git a/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs b/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs
index c2c2a8365c..72941a1665 100644
--- a/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs
+++ b/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs
@@ -119,9 +119,9 @@ public sealed partial class CargoSystem
label.Id = bounty.Id;
label.AssociatedStationId = stationId;
var msg = new FormattedMessage();
- msg.AddText(Loc.GetString("bounty-manifest-header", ("id", bounty.Id)));
+ msg.AddMarkupOrThrow(Loc.GetString("bounty-manifest-header", ("id", bounty.Id)));
msg.PushNewline();
- msg.AddText(Loc.GetString("bounty-manifest-list-start"));
+ msg.AddMarkupOrThrow(Loc.GetString("bounty-manifest-list-start"));
msg.PushNewline();
foreach (var entry in prototype.Entries)
{
diff --git a/Content.Server/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs b/Content.Server/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs
index 66934c8a87..a544ba66e0 100644
--- a/Content.Server/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs
+++ b/Content.Server/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs
@@ -77,11 +77,11 @@ public sealed class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSystem
printed.Task = item;
var msg = new FormattedMessage();
- msg.AddText(Loc.GetString("nano-task-printed-description", ("description", item.Description)));
+ msg.AddMarkupOrThrow(Loc.GetString("nano-task-printed-description", ("description", FormattedMessage.EscapeText(item.Description))));
msg.PushNewline();
- msg.AddText(Loc.GetString("nano-task-printed-requester", ("requester", item.TaskIsFor)));
+ msg.AddMarkupOrThrow(Loc.GetString("nano-task-printed-requester", ("requester", FormattedMessage.EscapeText(item.TaskIsFor))));
msg.PushNewline();
- msg.AddText(item.Priority switch {
+ msg.AddMarkupOrThrow(item.Priority switch {
NanoTaskPriority.High => Loc.GetString("nano-task-printed-high-priority"),
NanoTaskPriority.Medium => Loc.GetString("nano-task-printed-medium-priority"),
NanoTaskPriority.Low => Loc.GetString("nano-task-printed-low-priority"),
diff --git a/Content.Server/Chat/Managers/ChatManager.cs b/Content.Server/Chat/Managers/ChatManager.cs
index f90e286d9e..0843f80998 100644
--- a/Content.Server/Chat/Managers/ChatManager.cs
+++ b/Content.Server/Chat/Managers/ChatManager.cs
@@ -45,6 +45,9 @@ internal sealed partial class ChatManager : IChatManager
[Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!;
[Dependency] private readonly ISharedPlayerManager _player = default!;
[Dependency] private readonly DiscordChatLink _discordLink = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
+
+ private ISawmill _sawmill = default!;
///
/// The maximum length a player-sent message can be sent
@@ -64,6 +67,8 @@ internal sealed partial class ChatManager : IChatManager
_configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
_configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
+ _sawmill = _logManager.GetSawmill("SERVER");
+
RegisterRateLimits();
}
@@ -111,7 +116,7 @@ internal sealed partial class ChatManager : IChatManager
{
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", FormattedMessage.EscapeText(message)));
ChatMessageToAll(ChatChannel.Server, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride);
- Logger.InfoS("SERVER", message);
+ _sawmill.Info(message);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Server announcement: {message}");
}
diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs
index 581b678b84..9ea04ca3bc 100644
--- a/Content.Server/Chat/Systems/ChatSystem.cs
+++ b/Content.Server/Chat/Systems/ChatSystem.cs
@@ -736,7 +736,7 @@ public sealed partial class ChatSystem : SharedChatSystem
public string TransformSpeech(EntityUid sender, string message)
{
var ev = new TransformSpeechEvent(sender, message);
- RaiseLocalEvent(ev);
+ RaiseLocalEvent(sender, ev, true);
return ev.Message;
}
diff --git a/Content.Server/Chemistry/Components/ChemMasterComponent.cs b/Content.Server/Chemistry/Components/ChemMasterComponent.cs
index 0309d07ed9..398fddae0c 100644
--- a/Content.Server/Chemistry/Components/ChemMasterComponent.cs
+++ b/Content.Server/Chemistry/Components/ChemMasterComponent.cs
@@ -26,5 +26,11 @@ namespace Content.Server.Chemistry.Components
[DataField("clickSound"), ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier ClickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
+
+ ///
+ /// Which source the chem master should draw from when making pills/bottles.
+ ///
+ [DataField]
+ public ChemMasterDrawSource DrawSource = ChemMasterDrawSource.Internal;
}
}
diff --git a/Content.Server/Chemistry/EntitySystems/ChemMasterSystem.cs b/Content.Server/Chemistry/EntitySystems/ChemMasterSystem.cs
index 64350cbda1..7252a59f60 100644
--- a/Content.Server/Chemistry/EntitySystems/ChemMasterSystem.cs
+++ b/Content.Server/Chemistry/EntitySystems/ChemMasterSystem.cs
@@ -57,6 +57,7 @@ namespace Content.Server.Chemistry.EntitySystems
SubscribeLocalEvent(OnReagentButtonMessage);
SubscribeLocalEvent(OnCreatePillsMessage);
SubscribeLocalEvent(OnOutputToBottleMessage);
+ SubscribeLocalEvent(OnSetDrawSourceMessage);
}
private void SubscribeUpdateUiState(Entity ent, ref T ev)
@@ -77,7 +78,7 @@ namespace Content.Server.Chemistry.EntitySystems
var state = new ChemMasterBoundUserInterfaceState(
chemMaster.Mode, chemMaster.SortingType, BuildInputContainerInfo(inputContainer), BuildOutputContainerInfo(outputContainer),
- bufferReagents, bufferCurrentVolume, chemMaster.PillType, chemMaster.PillDosageLimit, updateLabel);
+ bufferReagents, bufferCurrentVolume, chemMaster.PillType, chemMaster.PillDosageLimit, updateLabel, chemMaster.DrawSource);
_userInterfaceSystem.SetUiState(owner, ChemMasterUiKey.Key, state);
}
@@ -135,6 +136,17 @@ namespace Content.Server.Chemistry.EntitySystems
ClickSound(chemMaster);
}
+ private void OnSetDrawSourceMessage(Entity chemMaster, ref ChemMasterOutputDrawSourceMessage message)
+ {
+ //Ensure draw source is valid, either from the internal buffer or the inserted beaker
+ if (!Enum.IsDefined(message.DrawSource))
+ return;
+
+ chemMaster.Comp.DrawSource = message.DrawSource;
+ UpdateUiState(chemMaster);
+ ClickSound(chemMaster);
+ }
+
private void TransferReagents(Entity chemMaster, ReagentId id, FixedPoint2 amount, bool fromBuffer)
{
var container = _itemSlotsSystem.GetItemOrNull(chemMaster, SharedChemMaster.InputSlotName);
@@ -208,9 +220,9 @@ namespace Content.Server.Chemistry.EntitySystems
return;
var needed = message.Dosage * message.Number;
- if (!WithdrawFromBuffer(chemMaster, needed, user, out var withdrawal))
- return;
+ if (!WithdrawFromSource(chemMaster, needed, user, out var withdrawal))
+ return;
_labelSystem.Label(container, message.Label);
for (var i = 0; i < message.Number; i++)
@@ -219,7 +231,10 @@ namespace Content.Server.Chemistry.EntitySystems
_storageSystem.Insert(container, item, out _, user: user, storage);
_labelSystem.Label(item, message.Label);
- _solutionContainerSystem.EnsureSolutionEntity(item, SharedChemMaster.PillSolutionName,out var itemSolution ,message.Dosage);
+ _solutionContainerSystem.EnsureSolutionEntity(item,
+ SharedChemMaster.PillSolutionName,
+ out var itemSolution,
+ message.Dosage);
if (!itemSolution.HasValue)
return;
@@ -256,7 +271,7 @@ namespace Content.Server.Chemistry.EntitySystems
if (message.Label.Length > SharedChemMaster.LabelMaxLength)
return;
- if (!WithdrawFromBuffer(chemMaster, message.Dosage, user, out var withdrawal))
+ if (!WithdrawFromSource(chemMaster, message.Dosage, user, out var withdrawal))
return;
_labelSystem.Label(container, message.Label);
@@ -270,34 +285,77 @@ namespace Content.Server.Chemistry.EntitySystems
ClickSound(chemMaster);
}
- private bool WithdrawFromBuffer(
+ private bool WithdrawFromSource(
Entity chemMaster,
- FixedPoint2 neededVolume, EntityUid? user,
+ FixedPoint2 neededVolume,
+ EntityUid? user,
[NotNullWhen(returnValue: true)] out Solution? outputSolution)
{
outputSolution = null;
- if (!_solutionContainerSystem.TryGetSolution(chemMaster.Owner, SharedChemMaster.BufferSolutionName, out _, out var solution))
- {
- return false;
- }
+ Solution? solution;
+ Entity? soln = null;
- if (solution.Volume == 0)
+ switch (chemMaster.Comp.DrawSource)
{
- if (user.HasValue)
- _popupSystem.PopupCursor(Loc.GetString("chem-master-window-buffer-empty-text"), user.Value);
- return false;
- }
+ case ChemMasterDrawSource.Internal:
+ if (!_solutionContainerSystem.TryGetSolution(chemMaster.Owner, SharedChemMaster.BufferSolutionName, out _, out solution))
+ return false;
- // ReSharper disable once InvertIf
- if (neededVolume > solution.Volume)
- {
- if (user.HasValue)
- _popupSystem.PopupCursor(Loc.GetString("chem-master-window-buffer-low-text"), user.Value);
- return false;
+ if (solution.Volume == 0)
+ {
+ if (user is { } uid)
+ _popupSystem.PopupCursor(Loc.GetString("chem-master-window-buffer-empty-text"), uid);
+
+ return false;
+ }
+ if (neededVolume > solution.Volume)
+ {
+ if (user is { } uid)
+ _popupSystem.PopupCursor(Loc.GetString("chem-master-window-buffer-low-text"), uid);
+
+ return false;
+ }
+
+ break;
+
+ case ChemMasterDrawSource.External:
+ if (_itemSlotsSystem.GetItemOrNull(chemMaster, SharedChemMaster.InputSlotName) is not {} container)
+ {
+ if (user.HasValue)
+ _popupSystem.PopupCursor(Loc.GetString("chem-master-window-no-beaker-text"), user.Value);
+ return false;
+ }
+
+ if (!_solutionContainerSystem.TryGetFitsInDispenser(container, out soln, out solution))
+ return false;
+
+ if (solution.Volume == 0)
+ {
+ if (user is { } uid)
+ _popupSystem.PopupCursor(Loc.GetString("chem-master-window-beaker-empty-text"), uid);
+
+ return false;
+ }
+ if (neededVolume > solution.Volume)
+ {
+ if (user is { } uid)
+ _popupSystem.PopupCursor(Loc.GetString("chem-master-window-beaker-low-text"), uid);
+
+ return false;
+ }
+
+ break;
+
+ default:
+ return false;
}
outputSolution = solution.SplitSolution(neededVolume);
+
+ if (soln.HasValue)
+ _solutionContainerSystem.UpdateChemicals(soln.Value);
+
return true;
}
diff --git a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
deleted file mode 100644
index 6088d01c59..0000000000
--- a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-
-using Content.Shared.Chemistry.EntitySystems;
-
-namespace Content.Server.Chemistry.EntitySystems;
-
-public sealed class InjectorSystem : SharedInjectorSystem;
diff --git a/Content.Server/Codewords/CodewordFactionPrototype.cs b/Content.Server/Codewords/CodewordFactionPrototype.cs
index 72d24b1dcd..62552c1966 100644
--- a/Content.Server/Codewords/CodewordFactionPrototype.cs
+++ b/Content.Server/Codewords/CodewordFactionPrototype.cs
@@ -10,11 +10,11 @@ public sealed partial class CodewordFactionPrototype : IPrototype
{
///
[IdDataField]
- public string ID { get; } = default!;
+ public string ID { get; private set; } = default!;
///
/// The generator to use for this faction.
///
[DataField(required:true)]
- public ProtoId Generator { get; } = default!;
+ public ProtoId Generator { get; private set; } = default!;
}
diff --git a/Content.Server/Codewords/CodewordGeneratorPrototype.cs b/Content.Server/Codewords/CodewordGeneratorPrototype.cs
index 15e50ebf73..b17a40fc6a 100644
--- a/Content.Server/Codewords/CodewordGeneratorPrototype.cs
+++ b/Content.Server/Codewords/CodewordGeneratorPrototype.cs
@@ -11,13 +11,13 @@ public sealed partial class CodewordGeneratorPrototype : IPrototype
{
///
[IdDataField]
- public string ID { get; } = default!;
+ public string ID { get; private set; } = default!;
///
/// List of datasets to use for word generation. All values will be concatenated into one list and then randomly chosen from
///
[DataField]
- public List> Words { get; } =
+ public List> Words { get; private set; } =
[
"Adjectives",
"Verbs",
diff --git a/Content.Server/Construction/Completions/BuildMech.cs b/Content.Server/Construction/Completions/BuildMech.cs
index 5f1e40c347..c0b5921db9 100644
--- a/Content.Server/Construction/Completions/BuildMech.cs
+++ b/Content.Server/Construction/Completions/BuildMech.cs
@@ -48,7 +48,7 @@ public sealed partial class BuildMech : IGraphAction
var cell = container.ContainedEntities[0];
- if (!entityManager.TryGetComponent(cell, out var batteryComponent))
+ if (!entityManager.TryGetComponent(cell, out var batteryComponent))
{
Logger.Warning($"Mech construct entity {uid} had an invalid entity in container \"{Container}\"! Aborting build mech action.");
return;
diff --git a/Content.Server/Content.Server.csproj b/Content.Server/Content.Server.csproj
index e2b65b873c..10d4bd56ab 100644
--- a/Content.Server/Content.Server.csproj
+++ b/Content.Server/Content.Server.csproj
@@ -1,31 +1,30 @@
-
- $(TargetFramework)
- 12
- false
false
..\bin\Content.Server\
true
Exe
1998
RA0032;nullable
- enable
true
+
+
+
-
-
-
-
+
+
+
+
+
diff --git a/Content.Server/Destructible/Thresholds/Behaviors/SpillBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/SpillBehavior.cs
index ff050ce2cd..ad34f05edd 100644
--- a/Content.Server/Destructible/Thresholds/Behaviors/SpillBehavior.cs
+++ b/Content.Server/Destructible/Thresholds/Behaviors/SpillBehavior.cs
@@ -1,42 +1,38 @@
-using Content.Shared.Chemistry.EntitySystems;
using Content.Server.Fluids.EntitySystems;
-using Content.Shared.Fluids.Components;
+using Content.Shared.Chemistry.EntitySystems;
using JetBrains.Annotations;
-namespace Content.Server.Destructible.Thresholds.Behaviors
+namespace Content.Server.Destructible.Thresholds.Behaviors;
+
+[UsedImplicitly]
+[DataDefinition]
+public sealed partial class SpillBehavior : IThresholdBehavior
{
- [UsedImplicitly]
- [DataDefinition]
- public sealed partial class SpillBehavior : IThresholdBehavior
+ ///
+ /// Optional fallback solution name if SpillableComponent is not present.
+ ///
+ [DataField]
+ public string? Solution;
+
+ ///
+ /// When triggered, spills the entity's solution onto the ground.
+ /// Will first try to use the solution from a SpillableComponent if present,
+ /// otherwise falls back to the solution specified in the behavior's data fields.
+ /// The solution is properly drained/split before spilling to prevent double-spilling with other behaviors.
+ ///
+ /// Entity whose solution will be spilled
+ /// System calling this behavior
+ /// Optional entity that caused this behavior to trigger
+ public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null)
{
- [DataField]
- public string? Solution;
+ var puddleSystem = system.EntityManager.System();
+ var solutionContainer = system.EntityManager.System();
+ var coordinates = system.EntityManager.GetComponent(owner).Coordinates;
- ///
- /// If there is a SpillableComponent on EntityUidowner use it to create a puddle/smear.
- /// Or whatever solution is specified in the behavior itself.
- /// If none are available do nothing.
- ///
- /// Entity on which behavior is executed
- /// system calling the behavior
- ///
- public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null)
- {
- var solutionContainerSystem = system.EntityManager.System();
- var spillableSystem = system.EntityManager.System();
-
- var coordinates = system.EntityManager.GetComponent(owner).Coordinates;
-
- if (system.EntityManager.TryGetComponent(owner, out SpillableComponent? spillableComponent) &&
- solutionContainerSystem.TryGetSolution(owner, spillableComponent.SolutionName, out _, out var compSolution))
- {
- spillableSystem.TrySplashSpillAt(owner, coordinates, compSolution, out _, false, user: cause);
- }
- else if (Solution != null &&
- solutionContainerSystem.TryGetSolution(owner, Solution, out _, out var behaviorSolution))
- {
- spillableSystem.TrySplashSpillAt(owner, coordinates, behaviorSolution, out _, user: cause);
- }
- }
+ // Spill the solution that was drained/split
+ if (solutionContainer.TryGetSolution(owner, Solution, out _, out var solution))
+ puddleSystem.TrySplashSpillAt(owner, coordinates, solution, out _, false, cause);
+ else
+ puddleSystem.TrySplashSpillAt(owner, coordinates, out _, out _, false, cause);
}
}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffectSystem.cs
index d5589966b1..1f8f3fc946 100644
--- a/Content.Server/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffectSystem.cs
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffectSystem.cs
@@ -33,8 +33,9 @@ public sealed partial class PlantMutateChemicalsEntityEffectSystem : EntityEffec
}
else
{
- seedChemQuantity.Min = FixedPoint2.Epsilon;
- seedChemQuantity.Max = FixedPoint2.Zero + amount;
+ //Set the minimum to a fifth of the quantity to give some level of bad luck protection
+ seedChemQuantity.Min = FixedPoint2.Clamp(pick.Quantity / 5f, FixedPoint2.Epsilon, 1f);
+ seedChemQuantity.Max = seedChemQuantity.Min + amount;
seedChemQuantity.Inherent = false;
}
var potencyDivisor = 100f / seedChemQuantity.Max;
diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs
index 01526d4ee5..e632d6efb3 100644
--- a/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs
+++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs
@@ -29,23 +29,14 @@ public sealed partial class PuddleSystem
private void SpillOnLand(Entity entity, ref LandEvent args)
{
- if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution))
+ if (!entity.Comp.SpillWhenThrown || Openable.IsClosed(entity.Owner))
return;
- if (Openable.IsClosed(entity.Owner))
- return;
-
- if (!entity.Comp.SpillWhenThrown)
- return;
-
- if (args.User != null)
+ if (TrySplashSpillAt(entity.Owner, Transform(entity).Coordinates, out _, out var solution) && args.User != null)
{
AdminLogger.Add(LogType.Landed,
$"{ToPrettyString(entity.Owner):entity} spilled a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution} on landing");
}
-
- var drainedSolution = _solutionContainerSystem.Drain(entity.Owner, soln.Value, solution.Volume);
- TrySplashSpillAt(entity.Owner, Transform(entity).Coordinates, drainedSolution, out _);
}
private void OnDoAfter(Entity entity, ref SpillDoAfterEvent args)
diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs
index 2f966354ec..aca8c5383d 100644
--- a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs
+++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs
@@ -369,7 +369,40 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
// TODO: This can be predicted once https://github.com/space-wizards/RobustToolbox/pull/5849 is merged
///
- public override bool TrySplashSpillAt(EntityUid uid,
+ public override bool TrySplashSpillAt(Entity entity,
+ EntityCoordinates coordinates,
+ out EntityUid puddleUid,
+ out Solution spilled,
+ bool sound = true,
+ EntityUid? user = null)
+ {
+ puddleUid = EntityUid.Invalid;
+ spilled = new Solution();
+
+ if (!Resolve(entity, ref entity.Comp))
+ return false;
+
+ if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var solution))
+ return false;
+
+ spilled = solution.Value.Comp.Solution;
+
+ return TrySplashSpillAt(entity, coordinates, solution.Value, out puddleUid, sound, user);
+ }
+
+ private bool TrySplashSpillAt(EntityUid entity,
+ EntityCoordinates coordinates,
+ Entity solution,
+ out EntityUid puddleUid,
+ bool sound = true,
+ EntityUid? user = null)
+ {
+ var result = TrySplashSpillAt(entity, coordinates, solution.Comp.Solution, out puddleUid, sound, user);
+ _solutionContainerSystem.UpdateChemicals(solution);
+ return result;
+ }
+
+ public override bool TrySplashSpillAt(EntityUid entity,
EntityCoordinates coordinates,
Solution solution,
out EntityUid puddleUid,
@@ -381,6 +414,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
if (solution.Volume == 0)
return false;
+ var spilled = solution.SplitSolution(solution.Volume);
var targets = new List();
var reactive = new HashSet>();
_lookup.GetEntitiesInRange(coordinates, 1.0f, reactive);
@@ -392,26 +426,28 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
var owner = ent.Owner;
// between 5 and 30%
- var splitAmount = solution.Volume * _random.NextFloat(0.05f, 0.30f);
- var splitSolution = solution.SplitSolution(splitAmount);
+ var splitAmount = spilled.Volume * _random.NextFloat(0.05f, 0.30f);
+ var splitSolution = spilled.SplitSolution(splitAmount);
if (user != null)
{
AdminLogger.Add(LogType.Landed,
- $"{ToPrettyString(user.Value):user} threw {ToPrettyString(uid):entity} which splashed a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution} onto {ToPrettyString(owner):target}");
+ $"{ToPrettyString(user.Value):user} threw {ToPrettyString(entity):entity} which splashed a solution {SharedSolutionContainerSystem.ToPrettyString(spilled):solution} onto {ToPrettyString(owner):target}");
}
targets.Add(owner);
Reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch);
- Popups.PopupEntity(
- Loc.GetString("spill-land-spilled-on-other", ("spillable", uid),
- ("target", Identity.Entity(owner, EntityManager))), owner, PopupType.SmallCaution);
+ Popups.PopupEntity(Loc.GetString("spill-land-spilled-on-other",
+ ("spillable", entity),
+ ("target", Identity.Entity(owner, EntityManager))),
+ owner,
+ PopupType.SmallCaution);
}
- _color.RaiseEffect(solution.GetColor(_prototypeManager), targets,
- Filter.Pvs(uid, entityManager: EntityManager));
+ _color.RaiseEffect(spilled.GetColor(_prototypeManager), targets,
+ Filter.Pvs(entity, entityManager: EntityManager));
- return TrySpillAt(coordinates, solution, out puddleUid, sound);
+ return TrySpillAt(coordinates, spilled, out puddleUid, sound);
}
///
diff --git a/Content.Server/GameTicking/Commands/JoinGameCommand.cs b/Content.Server/GameTicking/Commands/JoinGameCommand.cs
index a32a2f9495..3f5e294754 100644
--- a/Content.Server/GameTicking/Commands/JoinGameCommand.cs
+++ b/Content.Server/GameTicking/Commands/JoinGameCommand.cs
@@ -17,6 +17,9 @@ namespace Content.Server.GameTicking.Commands
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
+
+ private readonly ISawmill _sawmill;
public string Command => "joingame";
public string Description => "";
@@ -25,7 +28,10 @@ namespace Content.Server.GameTicking.Commands
public JoinGameCommand()
{
IoCManager.InjectDependencies(this);
+
+ _sawmill = _logManager.GetSawmill("security");
}
+
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
@@ -46,8 +52,8 @@ namespace Content.Server.GameTicking.Commands
if (ticker.PlayerGameStatuses.TryGetValue(player.UserId, out var status) && status == PlayerGameStatus.JoinedGame)
{
- Logger.InfoS("security", $"{player.Name} ({player.UserId}) attempted to latejoin while in-game.");
- shell.WriteError($"{player.Name} is not in the lobby. This incident will be reported.");
+ _sawmill.Info($"{player.Name} ({player.UserId}) attempted to latejoin while in-game.");
+ shell.WriteError($"{player.Name} is not in the lobby. This incident will be reported.");
return;
}
diff --git a/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs b/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs
index 10ec2a5e9b..5c02dea513 100644
--- a/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs
+++ b/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs
@@ -2,7 +2,6 @@
using Content.Server.Administration;
using Content.Server.GameTicking.Presets;
using Content.Shared.Administration;
-using Linguini.Shared.Util;
using Robust.Shared.Console;
namespace Content.Server.GameTicking.Commands
@@ -18,7 +17,7 @@ namespace Content.Server.GameTicking.Commands
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
- if (!args.Length.InRange(1, 3))
+ if (args.Length is < 1 or > 3)
{
shell.WriteError(Loc.GetString("shell-need-between-arguments", ("lower", 1), ("upper", 3), ("currentAmount", args.Length)));
return;
diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs
index cd93eae502..8f1f08269b 100644
--- a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs
+++ b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs
@@ -116,7 +116,7 @@ public abstract partial class GameRuleSystem where T: IComponent
tile = new Vector2i(randomX, randomY);
if (_atmosphere.IsTileSpace(targetGrid, Transform(targetGrid).MapUid, tile)
- || _atmosphere.IsTileAirBlocked(targetGrid, tile, mapGridComp: gridComp))
+ || _atmosphere.IsTileAirBlockedCached(targetGrid, tile))
{
continue;
}
diff --git a/Content.Server/Ghost/Roles/ToggleableGhostRoleSystem.cs b/Content.Server/Ghost/Roles/ToggleableGhostRoleSystem.cs
index 7bb25a63fb..13f90d8bd1 100644
--- a/Content.Server/Ghost/Roles/ToggleableGhostRoleSystem.cs
+++ b/Content.Server/Ghost/Roles/ToggleableGhostRoleSystem.cs
@@ -49,15 +49,23 @@ public sealed class ToggleableGhostRoleSystem : EntitySystem
UpdateAppearance(uid, ToggleableGhostRoleStatus.Searching);
- var ghostRole = EnsureComp(uid);
- EnsureComp(uid);
+ ActivateGhostRole((uid, component));
+ }
- //GhostRoleComponent inherits custom settings from the ToggleableGhostRoleComponent
- ghostRole.RoleName = Loc.GetString(component.RoleName);
- ghostRole.RoleDescription = Loc.GetString(component.RoleDescription);
- ghostRole.RoleRules = Loc.GetString(component.RoleRules);
- ghostRole.JobProto = component.JobProto;
- ghostRole.MindRoles = component.MindRoles;
+ public void ActivateGhostRole(Entity ent)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return;
+
+ var ghostRole = EnsureComp(ent);
+ EnsureComp(ent);
+
+ // GhostRoleComponent inherits custom settings from the ToggleableGhostRoleComponent
+ ghostRole.RoleName = Loc.GetString(ent.Comp.RoleName);
+ ghostRole.RoleDescription = Loc.GetString(ent.Comp.RoleDescription);
+ ghostRole.RoleRules = Loc.GetString(ent.Comp.RoleRules);
+ ghostRole.JobProto = ent.Comp.JobProto;
+ ghostRole.MindRoles = ent.Comp.MindRoles;
}
private void OnExamined(EntityUid uid, ToggleableGhostRoleComponent component, ExaminedEvent args)
diff --git a/Content.Server/Implants/ImplanterSystem.cs b/Content.Server/Implants/ImplanterSystem.cs
index 48e83e4878..08b744ee09 100644
--- a/Content.Server/Implants/ImplanterSystem.cs
+++ b/Content.Server/Implants/ImplanterSystem.cs
@@ -88,7 +88,7 @@ public sealed partial class ImplanterSystem : SharedImplanterSystem
if (!_doAfter.TryStartDoAfter(args))
return;
- _popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, user);
+ _popup.PopupEntity(Loc.GetString("injector-component-needle-injecting-user"), target, user);
var userName = Identity.Entity(user, EntityManager);
_popup.PopupEntity(Loc.GetString("implanter-component-implanting-target", ("user", userName)), user, target, PopupType.LargeCaution);
@@ -112,7 +112,7 @@ public sealed partial class ImplanterSystem : SharedImplanterSystem
};
if (_doAfter.TryStartDoAfter(args))
- _popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, user);
+ _popup.PopupEntity(Loc.GetString("injector-component-needle-injecting-user"), target, user);
}
diff --git a/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs b/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs
deleted file mode 100644
index 5bbbe2dc8d..0000000000
--- a/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using Content.Shared.Kitchen;
-using Content.Server.Kitchen.EntitySystems;
-using Robust.Shared.Audio;
-
-namespace Content.Server.Kitchen.Components
-{
- ///
- /// The combo reagent grinder/juicer. The reason why grinding and juicing are seperate is simple,
- /// think of grinding as a utility to break an object down into its reagents. Think of juicing as
- /// converting something into its single juice form. E.g, grind an apple and get the nutriment and sugar
- /// it contained, juice an apple and get "apple juice".
- ///
- [Access(typeof(ReagentGrinderSystem)), RegisterComponent]
- public sealed partial class ReagentGrinderComponent : Component
- {
- [DataField]
- public int StorageMaxEntities = 6;
-
- [DataField]
- public TimeSpan WorkTime = TimeSpan.FromSeconds(3.5); // Roughly matches the grind/juice sounds.
-
- [DataField]
- public float WorkTimeMultiplier = 1;
-
- [DataField]
- public SoundSpecifier ClickSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
-
- [DataField]
- public SoundSpecifier GrindSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/blender.ogg");
-
- [DataField]
- public SoundSpecifier JuiceSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/juicer.ogg");
-
- [DataField]
- public GrinderAutoMode AutoMode = GrinderAutoMode.Off;
-
- public EntityUid? AudioStream;
- }
-
- [Access(typeof(ReagentGrinderSystem)), RegisterComponent]
- public sealed partial class ActiveReagentGrinderComponent : Component
- {
- ///
- /// Remaining time until the grinder finishes grinding/juicing.
- ///
- [ViewVariables]
- public TimeSpan EndTime;
-
- [ViewVariables]
- public GrinderProgram Program;
- }
-}
diff --git a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs
index b850bc87fa..a709f4b8d9 100644
--- a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs
+++ b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs
@@ -1,5 +1,3 @@
-using Content.Server.Kitchen.Components;
-using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Stack;
using Content.Shared.Chemistry.EntitySystems;
@@ -20,15 +18,15 @@ using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
using System.Linq;
-using Content.Server.Construction.Completions;
using Content.Server.Jittering;
using Content.Shared.Jittering;
+using Content.Shared.Kitchen.EntitySystems;
using Content.Shared.Power;
namespace Content.Server.Kitchen.EntitySystems
{
[UsedImplicitly]
- internal sealed class ReagentGrinderSystem : EntitySystem
+ internal sealed class ReagentGrinderSystem : SharedReagentGrinderSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainersSystem = default!;
@@ -67,6 +65,8 @@ namespace Content.Server.Kitchen.EntitySystems
{
entity.Comp.AutoMode = (GrinderAutoMode) (((byte) entity.Comp.AutoMode + 1) % Enum.GetValues(typeof(GrinderAutoMode)).Length);
+ Dirty(entity);
+
UpdateUiState(entity);
}
@@ -90,12 +90,7 @@ namespace Content.Server.Kitchen.EntitySystems
foreach (var item in inputContainer.ContainedEntities.ToList())
{
- var solution = active.Program switch
- {
- GrinderProgram.Grind => GetGrindSolution(item),
- GrinderProgram.Juice => CompOrNull(item)?.JuiceSolution,
- _ => null,
- };
+ var solution = GetGrinderSolution(item, active.Program);
if (solution is null)
continue;
@@ -218,8 +213,8 @@ namespace Content.Server.Kitchen.EntitySystems
&& _solutionContainersSystem.TryGetFitsInDispenser(outputContainer.Value, out _, out containerSolution)
&& inputContainer.ContainedEntities.Count > 0)
{
- canGrind = inputContainer.ContainedEntities.All(CanGrind);
- canJuice = inputContainer.ContainedEntities.All(CanJuice);
+ canGrind = inputContainer.ContainedEntities.All(x => CanGrind(x));
+ canJuice = inputContainer.ContainedEntities.All(x => CanJuice(x));
}
var state = new ReagentGrinderInterfaceState(
@@ -293,10 +288,10 @@ namespace Content.Server.Kitchen.EntitySystems
SoundSpecifier? sound;
switch (program)
{
- case GrinderProgram.Grind when inputContainer.ContainedEntities.All(CanGrind):
+ case GrinderProgram.Grind when inputContainer.ContainedEntities.All(x => CanGrind(x)):
sound = reagentGrinder.GrindSound;
break;
- case GrinderProgram.Juice when inputContainer.ContainedEntities.All(CanJuice):
+ case GrinderProgram.Juice when inputContainer.ContainedEntities.All(x => CanJuice(x)):
sound = reagentGrinder.JuiceSound;
break;
default:
@@ -317,29 +312,5 @@ namespace Content.Server.Kitchen.EntitySystems
{
_audioSystem.PlayPvs(reagentGrinder.Comp.ClickSound, reagentGrinder.Owner, AudioParams.Default.WithVolume(-2f));
}
-
- private Solution? GetGrindSolution(EntityUid uid)
- {
- if (TryComp(uid, out var extractable)
- && extractable.GrindableSolution is not null
- && _solutionContainersSystem.TryGetSolution(uid, extractable.GrindableSolution, out _, out var solution))
- {
- return solution;
- }
- else
- return null;
- }
-
- private bool CanGrind(EntityUid uid)
- {
- var solutionName = CompOrNull(uid)?.GrindableSolution;
-
- return solutionName is not null && _solutionContainersSystem.TryGetSolution(uid, solutionName, out _, out _);
- }
-
- private bool CanJuice(EntityUid uid)
- {
- return CompOrNull(uid)?.JuiceSolution is not null;
- }
}
}
diff --git a/Content.Server/Light/EntitySystems/EmergencyLightSystem.cs b/Content.Server/Light/EntitySystems/EmergencyLightSystem.cs
index eddef87853..824afbc789 100644
--- a/Content.Server/Light/EntitySystems/EmergencyLightSystem.cs
+++ b/Content.Server/Light/EntitySystems/EmergencyLightSystem.cs
@@ -153,7 +153,7 @@ public sealed class EmergencyLightSystem : SharedEmergencyLightSystem
}
else
{
- _battery.SetCharge((entity.Owner, battery), battery.CurrentCharge + entity.Comp.ChargingWattage * frameTime * entity.Comp.ChargingEfficiency);
+ _battery.ChangeCharge((entity.Owner, battery), entity.Comp.ChargingWattage * frameTime * entity.Comp.ChargingEfficiency);
if (_battery.IsFull((entity.Owner, battery)))
{
if (TryComp(entity.Owner, out var receiver))
diff --git a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs
index da024ee075..e4e947b548 100644
--- a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs
+++ b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs
@@ -24,7 +24,7 @@ namespace Content.Server.Light.EntitySystems
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
- [Dependency] private readonly PredictedBatterySystem _battery = default!;
+ [Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPointLightSystem _lights = default!;
@@ -147,7 +147,7 @@ namespace Content.Server.Light.EntitySystems
}
// TODO: Very important: Make this charge rate based instead of instantly removing charge each update step.
- // See PredictedBatteryComponent
+ // See BatteryComponent
public override void Update(float frameTime)
{
var toRemove = new RemQueue>();
diff --git a/Content.Server/Mech/Systems/MechSystem.cs b/Content.Server/Mech/Systems/MechSystem.cs
index 80169cb2ed..f4b7ee48a8 100644
--- a/Content.Server/Mech/Systems/MechSystem.cs
+++ b/Content.Server/Mech/Systems/MechSystem.cs
@@ -1,4 +1,3 @@
-using System.Linq;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Systems;
using Content.Server.Mech.Components;
@@ -6,6 +5,7 @@ using Content.Shared.ActionBlocker;
using Content.Shared.Damage.Systems;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
+using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Mech;
using Content.Shared.Mech.Components;
@@ -25,6 +25,7 @@ using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
+using System.Linq;
namespace Content.Server.Mech.Systems;
@@ -33,7 +34,7 @@ public sealed partial class MechSystem : SharedMechSystem
{
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
- [Dependency] private readonly PredictedBatterySystem _battery = default!;
+ [Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
@@ -88,7 +89,7 @@ public sealed partial class MechSystem : SharedMechSystem
if (TryComp(uid, out var panel) && !panel.Open)
return;
- if (component.BatterySlot.ContainedEntity == null && TryComp(args.Used, out var battery))
+ if (component.BatterySlot.ContainedEntity == null && TryComp(args.Used, out var battery))
{
InsertBattery(uid, args.Used, component, battery);
_actionBlocker.UpdateCanMove(uid);
@@ -109,7 +110,7 @@ public sealed partial class MechSystem : SharedMechSystem
private void OnInsertBattery(EntityUid uid, MechComponent component, EntInsertedIntoContainerMessage args)
{
- if (args.Container != component.BatterySlot || !TryComp(args.Entity, out var battery))
+ if (args.Container != component.BatterySlot || !TryComp(args.Entity, out var battery))
return;
component.Energy = _battery.GetCharge((args.Entity, battery));
@@ -219,7 +220,7 @@ public sealed partial class MechSystem : SharedMechSystem
{
BreakOnMove = true,
};
- _popup.PopupEntity(Loc.GetString("mech-eject-pilot-alert", ("item", uid), ("user", args.User)), uid, PopupType.Large);
+ _popup.PopupEntity(Loc.GetString("mech-eject-pilot-alert", ("item", uid), ("user", Identity.Entity(args.User, EntityManager))), uid, PopupType.Large);
_doAfter.TryStartDoAfter(doAfterEventArgs);
}
@@ -235,7 +236,7 @@ public sealed partial class MechSystem : SharedMechSystem
if (_whitelistSystem.IsWhitelistFail(component.PilotWhitelist, args.User))
{
- _popup.PopupEntity(Loc.GetString("mech-no-enter", ("item", uid)), args.User);
+ _popup.PopupEntity(Loc.GetString("mech-no-enter", ("item", uid)), Identity.Entity(args.User, EntityManager));
return;
}
@@ -337,7 +338,7 @@ public sealed partial class MechSystem : SharedMechSystem
if (battery == null)
return false;
- if (!TryComp(battery, out var batteryComp))
+ if (!TryComp(battery, out var batteryComp))
return false;
_battery.SetCharge((battery.Value, batteryComp), _battery.GetCharge((battery.Value, batteryComp)) + delta.Float());
@@ -353,7 +354,7 @@ public sealed partial class MechSystem : SharedMechSystem
return true;
}
- public void InsertBattery(EntityUid uid, EntityUid toInsert, MechComponent? component = null, PredictedBatteryComponent? battery = null)
+ public void InsertBattery(EntityUid uid, EntityUid toInsert, MechComponent? component = null, BatteryComponent? battery = null)
{
if (!Resolve(uid, ref component, false))
return;
diff --git a/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs b/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs
index f6d9d7bec7..98fae3ae13 100644
--- a/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs
+++ b/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs
@@ -1,6 +1,5 @@
using Content.Server.Ninja.Events;
using Content.Server.Power.Components;
-using Content.Server.Power.EntitySystems;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.Ninja.Components;
@@ -17,8 +16,7 @@ namespace Content.Server.Ninja.Systems;
///
public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
{
- [Dependency] private readonly BatterySystem _battery = default!;
- [Dependency] private readonly PredictedBatterySystem _predictedBattery = default!;
+ [Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -80,20 +78,20 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
protected override bool TryDrainPower(Entity ent, EntityUid target)
{
var (uid, comp) = ent;
- if (comp.BatteryUid == null || !TryComp(comp.BatteryUid.Value, out var battery))
+ if (comp.BatteryUid == null || !TryComp(comp.BatteryUid.Value, out var battery))
return false;
if (!TryComp(target, out var targetBattery) || !TryComp(target, out var pnb))
return false;
- if (MathHelper.CloseToPercent(targetBattery.CurrentCharge, 0))
+ var available = _battery.GetCharge((target, targetBattery));
+ if (MathHelper.CloseToPercent(available, 0))
{
_popup.PopupEntity(Loc.GetString("battery-drainer-empty", ("battery", target)), uid, uid, PopupType.Medium);
return false;
}
- var available = targetBattery.CurrentCharge;
- var required = battery.MaxCharge - _predictedBattery.GetCharge((comp.BatteryUid.Value, battery));
+ var required = battery.MaxCharge - _battery.GetCharge((comp.BatteryUid.Value, battery));
// higher tier storages can charge more
var maxDrained = pnb.MaxSupply * comp.DrainTime;
var input = Math.Min(Math.Min(available, required / comp.DrainEfficiency), maxDrained);
@@ -101,15 +99,13 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
return false;
var output = input * comp.DrainEfficiency;
- // PowerCells use PredictedBatteryComponent
- // SMES, substations and APCs use BatteryComponent
- _predictedBattery.ChangeCharge((comp.BatteryUid.Value, battery), output);
+ _battery.ChangeCharge((comp.BatteryUid.Value, battery), output);
// TODO: create effect message or something
Spawn("EffectSparks", Transform(target).Coordinates);
_audio.PlayPvs(comp.SparkSound, target);
_popup.PopupEntity(Loc.GetString("battery-drainer-success", ("battery", target)), uid, uid);
// repeat the doafter until battery is full
- return !_predictedBattery.IsFull((comp.BatteryUid.Value, battery));
+ return !_battery.IsFull((comp.BatteryUid.Value, battery));
}
}
diff --git a/Content.Server/Ninja/Systems/ItemCreatorSystem.cs b/Content.Server/Ninja/Systems/ItemCreatorSystem.cs
index 227fdea789..cca80f2e42 100644
--- a/Content.Server/Ninja/Systems/ItemCreatorSystem.cs
+++ b/Content.Server/Ninja/Systems/ItemCreatorSystem.cs
@@ -9,7 +9,7 @@ namespace Content.Server.Ninja.Systems;
public sealed class ItemCreatorSystem : SharedItemCreatorSystem
{
- [Dependency] private readonly PredictedBatterySystem _battery = default!;
+ [Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
diff --git a/Content.Server/Ninja/Systems/NinjaSuitSystem.cs b/Content.Server/Ninja/Systems/NinjaSuitSystem.cs
index 8ebd56ea7c..f6c4643cad 100644
--- a/Content.Server/Ninja/Systems/NinjaSuitSystem.cs
+++ b/Content.Server/Ninja/Systems/NinjaSuitSystem.cs
@@ -62,7 +62,7 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery))
return;
- if (!TryComp(args.EntityUid, out var inserting))
+ if (!TryComp(args.EntityUid, out var inserting))
{
args.Cancel();
return;
@@ -88,11 +88,11 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
}
// this function assigns a score to a power cell depending on the capacity, to be used when comparing which cell is better.
- private float GetCellScore(EntityUid uid, PredictedBatteryComponent battcomp)
+ private float GetCellScore(EntityUid uid, BatteryComponent battcomp)
{
// if a cell is able to automatically recharge, boost the score drastically depending on the recharge rate,
// this is to ensure a ninja can still upgrade to a micro reactor cell even if they already have a medium or high.
- if (TryComp(uid, out var selfcomp))
+ if (TryComp(uid, out var selfcomp))
return battcomp.MaxCharge + selfcomp.AutoRechargeRate * AutoRechargeValue;
return battcomp.MaxCharge;
}
diff --git a/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs b/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs
index f2fc9abfcc..4f3a1873f5 100644
--- a/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs
+++ b/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs
@@ -24,7 +24,7 @@ namespace Content.Server.Ninja.Systems;
public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
{
[Dependency] private readonly AlertsSystem _alerts = default!;
- [Dependency] private readonly PredictedBatterySystem _battery = default!;
+ [Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly CodeConditionSystem _codeCondition = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
@@ -91,7 +91,7 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
///
/// Get the battery component in a ninja's suit, if it's worn.
///
- public bool GetNinjaBattery(EntityUid user, [NotNullWhen(true)] out EntityUid? batteryUid, [NotNullWhen(true)] out PredictedBatteryComponent? batteryComp)
+ public bool GetNinjaBattery(EntityUid user, [NotNullWhen(true)] out EntityUid? batteryUid, [NotNullWhen(true)] out BatteryComponent? batteryComp)
{
if (TryComp(user, out var ninja)
&& ninja.Suit != null
diff --git a/Content.Server/Ninja/Systems/StunProviderSystem.cs b/Content.Server/Ninja/Systems/StunProviderSystem.cs
index 49d7ab5e85..387844b6b1 100644
--- a/Content.Server/Ninja/Systems/StunProviderSystem.cs
+++ b/Content.Server/Ninja/Systems/StunProviderSystem.cs
@@ -17,7 +17,7 @@ namespace Content.Server.Ninja.Systems;
///
public sealed class StunProviderSystem : SharedStunProviderSystem
{
- [Dependency] private readonly PredictedBatterySystem _battery = default!;
+ [Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
diff --git a/Content.Server/Physics/Controllers/MoverController.cs b/Content.Server/Physics/Controllers/MoverController.cs
index 5c87de1863..f933c72103 100644
--- a/Content.Server/Physics/Controllers/MoverController.cs
+++ b/Content.Server/Physics/Controllers/MoverController.cs
@@ -1,18 +1,16 @@
+using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Systems;
-using Content.Shared.Friction;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.Shuttles.Components;
using Content.Shared.Shuttles.Systems;
using Prometheus;
-using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
-using DroneConsoleComponent = Content.Server.Shuttles.DroneConsoleComponent;
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
-using Robust.Shared.Map.Components;
+using DroneConsoleComponent = Content.Server.Shuttles.DroneConsoleComponent;
namespace Content.Server.Physics.Controllers;
@@ -20,20 +18,111 @@ public sealed class MoverController : SharedMoverController
{
private static readonly Gauge ActiveMoverGauge = Metrics.CreateGauge(
"physics_active_mover_count",
- "Active amount of InputMovers being processed by MoverController");
+ "Amount of ActiveInputMovers being processed by MoverController");
[Dependency] private readonly ThrusterSystem _thruster = default!;
- [Dependency] private readonly SharedTransformSystem _xformSystem = default!;
private Dictionary)> _shuttlePilots = new();
+ private EntityQuery _activeQuery;
+ private EntityQuery _droneQuery;
+ private EntityQuery _shuttleQuery;
+
+ // Not needed for persistence; just used to save an alloc
+ private readonly HashSet _seenMovers = [];
+ private readonly HashSet _seenRelayMovers = [];
+ private readonly List> _moversToUpdate = [];
+
public override void Initialize()
{
base.Initialize();
+
+ SubscribeLocalEvent(OnEntityPaused);
+ SubscribeLocalEvent(OnEntityUnpaused);
+
SubscribeLocalEvent(OnRelayPlayerAttached);
SubscribeLocalEvent(OnRelayPlayerDetached);
SubscribeLocalEvent(OnPlayerAttached);
SubscribeLocalEvent(OnPlayerDetached);
+
+ _activeQuery = GetEntityQuery();
+ _droneQuery = GetEntityQuery();
+ _shuttleQuery = GetEntityQuery();
+ }
+
+ private void OnEntityPaused(Entity ent, ref EntityPausedEvent args)
+ {
+ // Become unactive [sic] if we don't have PhysicsComp.IgnorePaused
+ if (PhysicsQuery.TryComp(ent, out var phys) && phys.IgnorePaused)
+ return;
+ RemCompDeferred(ent);
+ }
+
+ private void OnEntityUnpaused(Entity ent, ref EntityUnpausedEvent args)
+ {
+ UpdateMoverStatus((ent, ent.Comp));
+ }
+
+ protected override void OnMoverStartup(Entity ent, ref ComponentStartup args)
+ {
+ base.OnMoverStartup(ent, ref args);
+ UpdateMoverStatus((ent, ent.Comp));
+ }
+
+ protected override void OnTargetRelayShutdown(Entity ent, ref ComponentShutdown args)
+ {
+ base.OnTargetRelayShutdown(ent, ref args);
+ UpdateMoverStatus((ent, null, ent.Comp));
+ }
+
+ protected override void UpdateMoverStatus(Entity ent)
+ {
+ // Track that we aren't in a loop of movement relayers
+ _seenMovers.Clear();
+ while (true)
+ {
+ if (!MoverQuery.Resolve(ent, ref ent.Comp1, logMissing: false))
+ {
+ RemCompDeferred(ent);
+ break;
+ }
+
+ var meta = MetaData(ent);
+ if (Terminating(ent, meta))
+ break;
+
+ ActiveInputMoverComponent? activeMover = null;
+ if (!meta.EntityPaused
+ || PhysicsQuery.TryComp(ent, out var phys) && phys.IgnorePaused)
+ activeMover = EnsureComp(ent);
+
+ // If we're a relay target, make sure our drivers are InputMovers
+ if (RelayTargetQuery.Resolve(ent, ref ent.Comp2, logMissing: false)
+ // In case we're called from ComponentShutdown:
+ && ent.Comp2.LifeStage <= ComponentLifeStage.Running
+ && Exists(ent.Comp2.Source)
+ && !_seenMovers.Contains(ent.Comp2.Source))
+ {
+ if (ent.Comp2.Source == ent.Owner)
+ {
+ Log.Error($"Entity {ToPrettyString(ent)} is attempting to relay movement to itself!");
+ break;
+ }
+
+ if (activeMover is not null)
+ activeMover.RelayedFrom = ent.Comp2.Source;
+
+ ent = ent.Comp2.Source;
+ _seenMovers.Add(ent);
+ continue;
+ }
+
+ // No longer a well-defined relay target
+ if (activeMover is not null)
+ activeMover.RelayedFrom = null;
+
+ break;
+ }
}
private void OnRelayPlayerAttached(Entity entity, ref PlayerAttachedEvent args)
@@ -63,48 +152,70 @@ public sealed class MoverController : SharedMoverController
return true;
}
- private HashSet _moverAdded = new();
- private List> _movers = new();
-
- private void InsertMover(Entity source)
- {
- if (TryComp(source, out MovementRelayTargetComponent? relay))
- {
- if (TryComp(relay.Source, out InputMoverComponent? relayMover))
- {
- InsertMover((relay.Source, relayMover));
- }
- }
-
- // Already added
- if (!_moverAdded.Add(source.Owner))
- return;
-
- _movers.Add(source);
- }
-
public override void UpdateBeforeSolve(bool prediction, float frameTime)
{
base.UpdateBeforeSolve(prediction, frameTime);
- _moverAdded.Clear();
- _movers.Clear();
- var inputQueryEnumerator = AllEntityQuery();
+ // We use _seenMovers here as well as in UpdateMoverStatus—this means we
+ // cannot have any events get fired while we use it in this while loop.
+ _seenMovers.Clear();
+ _moversToUpdate.Clear();
- // Need to order mob movement so that movers don't run before their relays.
- while (inputQueryEnumerator.MoveNext(out var uid, out var mover))
+ // Don't use EntityQueryEnumerator because admin ghosts have to move on
+ // paused maps. Pausing movers is handled via ActiveInputMoverComponent.
+ var inputQueryEnumerator = AllEntityQuery();
+ while (inputQueryEnumerator.MoveNext(out var uid, out var activeComp, out var moverComp))
{
- InsertMover((uid, mover));
+ _seenRelayMovers.Clear(); // O(1) if already empty
+ QueueRelaySources(activeComp.RelayedFrom);
+
+ // If it's already inserted, that's fine—that means it'll still be
+ // handled before its child movers
+ AddMover((uid, moverComp));
}
- foreach (var mover in _movers)
- {
- HandleMobMovement(mover, frameTime);
- }
+ ActiveMoverGauge.Set(_moversToUpdate.Count);
- ActiveMoverGauge.Set(_movers.Count);
+ foreach (var ent in _moversToUpdate)
+ {
+ HandleMobMovement(ent, frameTime);
+ }
HandleShuttleMovement(frameTime);
+ return;
+
+ // When we insert a chain of relay sources we have to flip its ordering
+ // It's going to be extremely uncommon for a relay chain to be more than
+ // one entity so we just recurse as needed.
+ void QueueRelaySources(EntityUid? next)
+ {
+ // We only care if it's still a mover
+ if (!_activeQuery.TryComp(next, out var nextActive)
+ || !MoverQuery.TryComp(next, out var nextMover)
+ || !_seenRelayMovers.Add(next.Value))
+ return;
+
+ Debug.Assert(next.Value != nextActive.RelayedFrom);
+
+ // While it is (as of writing) currently true that this recursion
+ // should always terminate due to RelayedFrom always being written
+ // in a way that tracks if it's made a loop, we still take the extra
+ // memory (and small time cost) of making sure via _seenRelayMovers.
+ QueueRelaySources(nextActive.RelayedFrom);
+ AddMover((next.Value, nextMover));
+ }
+
+ // Track inserts so we have ~ O(1) inserts without duplicates. Hopefully
+ // it doesn't matter that both _seenMovers and _moversToUpdate are never
+ // trimmed? They should be pretty memory light anyway, and in general
+ // it'll be rare for there to be a decrease in movers.
+ void AddMover(Entity entity)
+ {
+ if (!_seenMovers.Add(entity))
+ return;
+
+ _moversToUpdate.Add(entity);
+ }
}
public (Vector2 Strafe, float Rotation, float Brakes) GetPilotVelocityInput(PilotComponent component)
@@ -152,7 +263,7 @@ public sealed class MoverController : SharedMoverController
protected override void HandleShuttleInput(EntityUid uid, ShuttleButtons button, ushort subTick, bool state)
{
- if (!TryComp(uid, out var pilot) || pilot.Console == null)
+ if (!PilotQuery.TryComp(uid, out var pilot) || pilot.Console == null)
return;
ResetSubtick(pilot);
@@ -263,27 +374,25 @@ public sealed class MoverController : SharedMoverController
// We just mark off their movement and the shuttle itself does its own movement
var activePilotQuery = EntityQueryEnumerator();
- var shuttleQuery = GetEntityQuery();
while (activePilotQuery.MoveNext(out var uid, out var pilot, out var mover))
{
var consoleEnt = pilot.Console;
// TODO: This is terrible. Just make a new mover and also make it remote piloting + device networks
- if (TryComp(consoleEnt, out var cargoConsole))
- {
+ if (_droneQuery.TryComp(consoleEnt, out var cargoConsole))
consoleEnt = cargoConsole.Entity;
- }
- if (!TryComp(consoleEnt, out TransformComponent? xform)) continue;
+ if (!XformQuery.TryComp(consoleEnt, out var xform))
+ continue;
var gridId = xform.GridUid;
// This tries to see if the grid is a shuttle and if the console should work.
- if (!TryComp(gridId, out var _) ||
- !shuttleQuery.TryGetComponent(gridId, out var shuttleComponent) ||
+ if (!MapGridQuery.HasComp(gridId) ||
+ !_shuttleQuery.TryGetComponent(gridId, out var shuttleComponent) ||
!shuttleComponent.Enabled)
continue;
- if (!newPilots.TryGetValue(gridId!.Value, out var pilots))
+ if (!newPilots.TryGetValue(gridId.Value, out var pilots))
{
pilots = (shuttleComponent, new List<(EntityUid, PilotComponent, InputMoverComponent, TransformComponent)>());
newPilots[gridId.Value] = pilots;
@@ -305,13 +414,12 @@ public sealed class MoverController : SharedMoverController
// Collate all of the linear / angular velocites for a shuttle
// then do the movement input once for it.
- var xformQuery = GetEntityQuery();
foreach (var (shuttleUid, (shuttle, pilots)) in _shuttlePilots)
{
- if (Paused(shuttleUid) || CanPilot(shuttleUid) || !TryComp(shuttleUid, out var body))
+ if (Paused(shuttleUid) || CanPilot(shuttleUid) || !PhysicsQuery.TryComp(shuttleUid, out var body))
continue;
- var shuttleNorthAngle = _xformSystem.GetWorldRotation(shuttleUid, xformQuery);
+ var shuttleNorthAngle = TransformSystem.GetWorldRotation(shuttleUid, XformQuery);
// Collate movement linear and angular inputs together
var linearInput = Vector2.Zero;
@@ -321,7 +429,7 @@ public sealed class MoverController : SharedMoverController
var brakeCount = 0;
var angularCount = 0;
- foreach (var (pilotUid, pilot, _, consoleXform) in pilots)
+ foreach (var (_, pilot, _, consoleXform) in pilots)
{
var (strafe, rotation, brakes) = GetPilotVelocityInput(pilot);
@@ -571,9 +679,9 @@ public sealed class MoverController : SharedMoverController
private bool CanPilot(EntityUid shuttleUid)
{
- return TryComp(shuttleUid, out var ftl)
+ return FTLQuery.TryComp(shuttleUid, out var ftl)
&& (ftl.State & (FTLState.Starting | FTLState.Travelling | FTLState.Arriving)) != 0x0
- || HasComp(shuttleUid);
+ || PreventPilotQuery.HasComp(shuttleUid);
}
}
diff --git a/Content.Server/Players/JobWhitelist/JobWhitelistManager.cs b/Content.Server/Players/JobWhitelist/JobWhitelistManager.cs
index c47ffa691f..635e99666f 100644
--- a/Content.Server/Players/JobWhitelist/JobWhitelistManager.cs
+++ b/Content.Server/Players/JobWhitelist/JobWhitelistManager.cs
@@ -10,7 +10,6 @@ using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
-using Serilog;
namespace Content.Server.Players.JobWhitelist;
@@ -22,8 +21,10 @@ public sealed class JobWhitelistManager : IPostInjectInit
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
[Dependency] private readonly UserDbDataManager _userDb = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
private readonly Dictionary> _whitelists = new();
+ private ISawmill _sawmill = default!;
public void Initialize()
{
@@ -79,7 +80,7 @@ public sealed class JobWhitelistManager : IPostInjectInit
{
if (!_whitelists.TryGetValue(player, out var whitelists))
{
- Log.Error("Unable to check if player {Player} is whitelisted for {Job}. Stack trace:\\n{StackTrace}",
+ _sawmill.Error("Unable to check if player {Player} is whitelisted for {Job}. Stack trace:\\n{StackTrace}",
player,
job,
Environment.StackTrace);
@@ -113,5 +114,6 @@ public sealed class JobWhitelistManager : IPostInjectInit
_userDb.AddOnLoadPlayer(LoadData);
_userDb.AddOnFinishLoad(FinishLoad);
_userDb.AddOnPlayerDisconnect(ClientDisconnected);
+ _sawmill = _logManager.GetSawmill("job_whitelist");
}
}
diff --git a/Content.Server/Power/EntitySystems/BatteryInterfaceSystem.cs b/Content.Server/Power/EntitySystems/BatteryInterfaceSystem.cs
index 83ff28646d..d2c95065db 100644
--- a/Content.Server/Power/EntitySystems/BatteryInterfaceSystem.cs
+++ b/Content.Server/Power/EntitySystems/BatteryInterfaceSystem.cs
@@ -3,6 +3,7 @@ using Content.Server.Power.Components;
using Content.Shared.Database;
using Content.Shared.Power;
using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
using Robust.Server.GameObjects;
namespace Content.Server.Power.EntitySystems;
@@ -24,6 +25,7 @@ public sealed class BatteryInterfaceSystem : EntitySystem
{
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = null!;
+ [Dependency] private readonly SharedBatterySystem _battery = null!;
public override void Initialize()
{
@@ -48,7 +50,7 @@ public sealed class BatteryInterfaceSystem : EntitySystem
var netBattery = Comp(ent);
netBattery.CanCharge = args.On;
- _adminLog.Add(LogType.Action,$"{ToPrettyString(args.Actor):actor} set input breaker to {args.On} on {ToPrettyString(ent):target}");
+ _adminLog.Add(LogType.Action, $"{ToPrettyString(args.Actor):actor} set input breaker to {args.On} on {ToPrettyString(ent):target}");
}
private void HandleSetOutputBreaker(Entity ent, ref BatterySetOutputBreakerMessage args)
@@ -56,7 +58,7 @@ public sealed class BatteryInterfaceSystem : EntitySystem
var netBattery = Comp(ent);
netBattery.CanDischarge = args.On;
- _adminLog.Add(LogType.Action,$"{ToPrettyString(args.Actor):actor} set output breaker to {args.On} on {ToPrettyString(ent):target}");
+ _adminLog.Add(LogType.Action, $"{ToPrettyString(args.Actor):actor} set output breaker to {args.On} on {ToPrettyString(ent):target}");
}
private void HandleSetChargeRate(Entity ent, ref BatterySetChargeRateMessage args)
@@ -90,13 +92,14 @@ public sealed class BatteryInterfaceSystem : EntitySystem
if (!_uiSystem.IsUiOpen(uid, BatteryUiKey.Key))
return;
+ var currentCharge = _battery.GetCharge((uid, battery));
_uiSystem.SetUiState(
uid,
BatteryUiKey.Key,
new BatteryBuiState
{
Capacity = battery.MaxCharge,
- Charge = battery.CurrentCharge,
+ Charge = currentCharge,
CanCharge = netBattery.CanCharge,
CanDischarge = netBattery.CanDischarge,
CurrentReceiving = netBattery.CurrentReceiving,
diff --git a/Content.Server/Power/EntitySystems/BatterySystem.API.cs b/Content.Server/Power/EntitySystems/BatterySystem.API.cs
deleted file mode 100644
index 5995758d3d..0000000000
--- a/Content.Server/Power/EntitySystems/BatterySystem.API.cs
+++ /dev/null
@@ -1,155 +0,0 @@
-using Content.Shared.Power;
-using Content.Shared.Power.Components;
-using Content.Shared.Power.EntitySystems;
-
-namespace Content.Server.Power.EntitySystems;
-
-///
-/// Responsible for .
-/// Unpredicted equivalent of .
-/// If you make changes to this make sure to keep the two consistent.
-///
-public sealed partial class BatterySystem
-{
- public override float ChangeCharge(Entity ent, float amount)
- {
- if (!Resolve(ent, ref ent.Comp))
- return 0;
-
- var newValue = Math.Clamp(ent.Comp.CurrentCharge + amount, 0, ent.Comp.MaxCharge);
- var delta = newValue - ent.Comp.CurrentCharge;
-
- if (delta == 0f)
- return delta;
-
- ent.Comp.CurrentCharge = newValue;
-
- TrySetChargeCooldown(ent.Owner);
-
- var ev = new ChargeChangedEvent(ent.Comp.CurrentCharge, delta, ent.Comp.MaxCharge);
- RaiseLocalEvent(ent, ref ev);
- return delta;
- }
-
- public override float UseCharge(Entity ent, float amount)
- {
- if (amount <= 0f || !Resolve(ent, ref ent.Comp) || ent.Comp.CurrentCharge == 0)
- return 0f;
-
- return ChangeCharge(ent, -amount);
- }
-
- public override bool TryUseCharge(Entity ent, float amount)
- {
- if (!Resolve(ent, ref ent.Comp, false) || amount > ent.Comp.CurrentCharge)
- return false;
-
- UseCharge(ent, amount);
- return true;
- }
-
- public override void SetCharge(Entity ent, float value)
- {
- if (!Resolve(ent, ref ent.Comp))
- return;
-
- var oldCharge = ent.Comp.CurrentCharge;
- ent.Comp.CurrentCharge = MathHelper.Clamp(value, 0, ent.Comp.MaxCharge);
- if (MathHelper.CloseTo(ent.Comp.CurrentCharge, oldCharge) &&
- !(oldCharge != ent.Comp.CurrentCharge && ent.Comp.CurrentCharge == ent.Comp.MaxCharge))
- {
- return;
- }
-
- var ev = new ChargeChangedEvent(ent.Comp.CurrentCharge, ent.Comp.CurrentCharge - oldCharge, ent.Comp.MaxCharge);
- RaiseLocalEvent(ent, ref ev);
- }
-
- public override void SetMaxCharge(Entity ent, float value)
- {
- if (!Resolve(ent, ref ent.Comp))
- return;
-
- var old = ent.Comp.MaxCharge;
- var oldCharge = ent.Comp.CurrentCharge;
- ent.Comp.MaxCharge = Math.Max(value, 0);
- ent.Comp.CurrentCharge = Math.Min(ent.Comp.CurrentCharge, ent.Comp.MaxCharge);
- if (MathHelper.CloseTo(ent.Comp.MaxCharge, old))
- return;
-
- var ev = new ChargeChangedEvent(ent.Comp.CurrentCharge, ent.Comp.CurrentCharge - oldCharge, ent.Comp.MaxCharge);
- RaiseLocalEvent(ent, ref ev);
- }
-
- ///
- /// Gets the battery's current charge.
- ///
- public float GetCharge(Entity ent)
- {
- if (!Resolve(ent, ref ent.Comp, false))
- return 0f;
-
- return ent.Comp.CurrentCharge;
- }
-
- ///
- /// Gets number of remaining uses for the given charge cost.
- ///
- public int GetRemainingUses(Entity ent, float cost)
- {
- if (cost <= 0)
- return 0;
-
- if (!Resolve(ent, ref ent.Comp))
- return 0;
-
- return (int)(ent.Comp.CurrentCharge / cost);
- }
-
- ///
- /// Gets number of maximum uses at full charge for the given charge cost.
- ///
- public int GetMaxUses(Entity ent, float cost)
- {
- if (cost <= 0)
- return 0;
-
- if (!Resolve(ent, ref ent.Comp))
- return 0;
-
- return (int)(ent.Comp.MaxCharge / cost);
- }
-
- public override void TrySetChargeCooldown(Entity ent)
- {
- if (!Resolve(ent, ref ent.Comp, false))
- return;
-
- if (ent.Comp.AutoRechargePauseTime == TimeSpan.Zero)
- return; // no recharge pause
-
- if (_timing.CurTime + ent.Comp.AutoRechargePauseTime <= ent.Comp.NextAutoRecharge)
- return; // the current pause is already longer
-
- SetChargeCooldown(ent, ent.Comp.AutoRechargePauseTime);
- }
-
- public override void SetChargeCooldown(Entity ent, TimeSpan cooldown)
- {
- if (!Resolve(ent, ref ent.Comp))
- return;
-
- ent.Comp.NextAutoRecharge = _timing.CurTime + cooldown;
- }
-
- ///
- /// Returns whether the battery is full.
- ///
- public bool IsFull(Entity ent)
- {
- if (!Resolve(ent, ref ent.Comp))
- return false;
-
- return ent.Comp.CurrentCharge >= ent.Comp.MaxCharge;
- }
-}
diff --git a/Content.Server/Power/EntitySystems/BatterySystem.cs b/Content.Server/Power/EntitySystems/BatterySystem.cs
index 1a88672bc9..a9b750b6d4 100644
--- a/Content.Server/Power/EntitySystems/BatterySystem.cs
+++ b/Content.Server/Power/EntitySystems/BatterySystem.cs
@@ -1,85 +1,58 @@
using Content.Server.Power.Components;
-using Content.Shared.Cargo;
-using Content.Shared.Examine;
-using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Rejuvenate;
-using JetBrains.Annotations;
using Robust.Shared.Utility;
-using Robust.Shared.Timing;
namespace Content.Server.Power.EntitySystems;
-///
-/// Responsible for .
-/// Unpredicted equivalent of .
-/// If you make changes to this make sure to keep the two consistent.
-///
-[UsedImplicitly]
-public sealed partial class BatterySystem : SharedBatterySystem
+public sealed class BatterySystem : SharedBatterySystem
{
- [Dependency] private readonly IGameTiming _timing = default!;
-
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent(OnInit);
- SubscribeLocalEvent(OnExamine);
- SubscribeLocalEvent(OnBatteryRejuvenate);
SubscribeLocalEvent(OnNetBatteryRejuvenate);
- SubscribeLocalEvent(CalculateBatteryPrice);
- SubscribeLocalEvent(OnChangeCharge);
- SubscribeLocalEvent(OnGetCharge);
-
SubscribeLocalEvent(PreSync);
SubscribeLocalEvent(PostSync);
}
- private void OnInit(Entity ent, ref ComponentInit args)
+ protected override void OnStartup(Entity ent, ref ComponentStartup args)
{
- DebugTools.Assert(!HasComp(ent), $"{ent} has both BatteryComponent and PredictedBatteryComponent");
+ // Debug assert to prevent anyone from killing their networking performance by dirtying a battery's charge every single tick.
+ // This checks for components that interact with the power network, have a charge rate that ramps up over time and therefore
+ // have to set the charge in an update loop instead of using a subscription.
+ // This is usually the case for APCs, SMES, battery powered turrets or similar.
+ // For those entities you should disable net sync for the battery in your prototype, using
+ ///
+ /// - type: Battery
+ /// netSync: false
+ ///
+ /// This disables networking and prediction for this battery.
+ if (!ent.Comp.NetSyncEnabled)
+ return;
+
+ DebugTools.Assert(!HasComp(ent), $"{ToPrettyString(ent.Owner)} has a predicted battery connected to the power net. Disable net sync!");
+ DebugTools.Assert(!HasComp(ent), $"{ToPrettyString(ent.Owner)} has a predicted battery connected to the power net. Disable net sync!");
+ DebugTools.Assert(!HasComp(ent), $"{ToPrettyString(ent.Owner)} has a predicted battery connected to the power net. Disable net sync!");
}
+
+
private void OnNetBatteryRejuvenate(Entity ent, ref RejuvenateEvent args)
{
ent.Comp.NetworkBattery.CurrentStorage = ent.Comp.NetworkBattery.Capacity;
}
- private void OnBatteryRejuvenate(Entity ent, ref RejuvenateEvent args)
- {
- SetCharge(ent.AsNullable(), ent.Comp.MaxCharge);
- }
-
- private void OnExamine(Entity ent, ref ExaminedEvent args)
- {
- if (!args.IsInDetailsRange)
- return;
-
- if (!HasComp(ent))
- return;
-
- var chargePercentRounded = 0;
- if (ent.Comp.MaxCharge != 0)
- chargePercentRounded = (int)(100 * ent.Comp.CurrentCharge / ent.Comp.MaxCharge);
-
- args.PushMarkup(
- Loc.GetString(
- "examinable-battery-component-examine-detail",
- ("percent", chargePercentRounded),
- ("markupPercentColor", "green")
- )
- );
- }
private void PreSync(NetworkBatteryPreSync ev)
{
// Ignoring entity pausing. If the entity was paused, neither component's data should have been changed.
var enumerator = AllEntityQuery();
- while (enumerator.MoveNext(out var netBat, out var bat))
+ while (enumerator.MoveNext(out var uid, out var netBat, out var bat))
{
- DebugTools.Assert(bat.CurrentCharge <= bat.MaxCharge && bat.CurrentCharge >= 0);
+ var currentCharge = GetCharge((uid, bat));
+ DebugTools.Assert(currentCharge <= bat.MaxCharge && currentCharge >= 0);
netBat.NetworkBattery.Capacity = bat.MaxCharge;
- netBat.NetworkBattery.CurrentStorage = bat.CurrentCharge;
+ netBat.NetworkBattery.CurrentStorage = currentCharge;
}
}
@@ -92,42 +65,4 @@ public sealed partial class BatterySystem : SharedBatterySystem
SetCharge((uid, bat), netBat.NetworkBattery.CurrentStorage);
}
}
-
- ///
- /// Gets the price for the power contained in an entity's battery.
- ///
- private void CalculateBatteryPrice(Entity ent, ref PriceCalculationEvent args)
- {
- args.Price += ent.Comp.CurrentCharge * ent.Comp.PricePerJoule;
- }
-
- private void OnChangeCharge(Entity ent, ref ChangeChargeEvent args)
- {
- if (args.ResidualValue == 0)
- return;
-
- args.ResidualValue -= ChangeCharge(ent.AsNullable(), args.ResidualValue);
- }
-
- private void OnGetCharge(Entity entity, ref GetChargeEvent args)
- {
- args.CurrentCharge += entity.Comp.CurrentCharge;
- args.MaxCharge += entity.Comp.MaxCharge;
- }
-
- public override void Update(float frameTime)
- {
- var query = EntityQueryEnumerator();
- var curTime = _timing.CurTime;
- while (query.MoveNext(out var uid, out var comp, out var bat))
- {
- if (!comp.AutoRecharge || IsFull((uid, bat)))
- continue;
-
- if (comp.NextAutoRecharge > curTime)
- continue;
-
- SetCharge((uid, bat), bat.CurrentCharge + comp.AutoRechargeRate * frameTime);
- }
- }
}
diff --git a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs
index 25757360b3..e0344f4d6c 100644
--- a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs
+++ b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs
@@ -8,6 +8,7 @@ using Content.Shared.Pinpointer;
using Content.Shared.Station.Components;
using Content.Shared.Power;
using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;
@@ -22,6 +23,7 @@ internal sealed partial class PowerMonitoringConsoleSystem : SharedPowerMonitori
{
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly SharedMapSystem _sharedMapSystem = default!;
+ [Dependency] private readonly SharedBatterySystem _battery = default!;
// Note: this data does not need to be saved
private Dictionary> _gridPowerCableChunks = new();
@@ -510,7 +512,7 @@ internal sealed partial class PowerMonitoringConsoleSystem : SharedPowerMonitori
if (effectiveMax == 0)
effectiveMax = 1;
- return battery.CurrentCharge / effectiveMax;
+ return _battery.GetCharge((uid, battery)) / effectiveMax;
}
private void GetSourcesForNode(EntityUid uid, Node node, out List sources)
diff --git a/Content.Server/Power/EntitySystems/PowerNetSystem.cs b/Content.Server/Power/EntitySystems/PowerNetSystem.cs
index 5b6b922ddf..1c3f76fdee 100644
--- a/Content.Server/Power/EntitySystems/PowerNetSystem.cs
+++ b/Content.Server/Power/EntitySystems/PowerNetSystem.cs
@@ -358,17 +358,18 @@ namespace Content.Server.Power.EntitySystems
if (requireBattery)
{
- _battery.SetCharge((uid, battery), battery.CurrentCharge - apcBattery.IdleLoad * frameTime);
+ _battery.ChangeCharge((uid, battery), -apcBattery.IdleLoad * frameTime);
}
// Otherwise try to charge the battery
else if (powered && !_battery.IsFull((uid, battery)))
{
apcReceiver.Load += apcBattery.BatteryRechargeRate * apcBattery.BatteryRechargeEfficiency;
- _battery.SetCharge((uid, battery), battery.CurrentCharge + apcBattery.BatteryRechargeRate * frameTime);
+ _battery.ChangeCharge((uid, battery), apcBattery.BatteryRechargeRate * frameTime);
}
// Enable / disable the battery if the state changed
- var enableBattery = requireBattery && battery.CurrentCharge > 0;
+ var currentCharge = _battery.GetCharge((uid, battery));
+ var enableBattery = requireBattery && currentCharge > 0;
if (apcBattery.Enabled != enableBattery)
{
diff --git a/Content.Server/Power/EntitySystems/RiggableSystem.cs b/Content.Server/Power/EntitySystems/RiggableSystem.cs
index 390e75eeb8..0bff0f4ba8 100644
--- a/Content.Server/Power/EntitySystems/RiggableSystem.cs
+++ b/Content.Server/Power/EntitySystems/RiggableSystem.cs
@@ -18,7 +18,7 @@ public sealed class RiggableSystem : EntitySystem
{
[Dependency] private readonly ExplosionSystem _explosionSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
- [Dependency] private readonly PredictedBatterySystem _predictedBattery = default!;
+ [Dependency] private readonly SharedBatterySystem _battery = default!;
public override void Initialize()
{
@@ -27,7 +27,6 @@ public sealed class RiggableSystem : EntitySystem
SubscribeLocalEvent(OnMicrowaved);
SubscribeLocalEvent(OnSolutionChanged);
SubscribeLocalEvent(OnChargeChanged);
- SubscribeLocalEvent(OnChargeChanged);
}
private void OnRejuvenate(Entity entity, ref RejuvenateEvent args)
@@ -39,16 +38,7 @@ public sealed class RiggableSystem : EntitySystem
{
if (TryComp(entity, out var batteryComponent))
{
- if (batteryComponent.CurrentCharge == 0f)
- return;
-
- Explode(entity, batteryComponent.CurrentCharge);
- args.Handled = true;
- }
-
- if (TryComp(entity, out var predictedBatteryComponent))
- {
- var charge = _predictedBattery.GetCharge((entity, predictedBatteryComponent));
+ var charge = _battery.GetCharge((entity, batteryComponent));
if (charge == 0f)
return;
@@ -80,20 +70,7 @@ public sealed class RiggableSystem : EntitySystem
QueueDel(uid);
}
- // non-predicted batteries
private void OnChargeChanged(Entity ent, ref ChargeChangedEvent args)
- {
- if (!ent.Comp.IsRigged)
- return;
-
- if (args.Charge == 0f)
- return; // No charge to cause an explosion.
-
- Explode(ent, args.Charge);
- }
-
- // predicted batteries
- private void OnChargeChanged(Entity ent, ref PredictedBatteryChargeChangedEvent args)
{
if (!ent.Comp.IsRigged)
return;
diff --git a/Content.Server/Power/SMES/SmesSystem.cs b/Content.Server/Power/SMES/SmesSystem.cs
index 15c40b3c92..5a8169e1c9 100644
--- a/Content.Server/Power/SMES/SmesSystem.cs
+++ b/Content.Server/Power/SMES/SmesSystem.cs
@@ -2,6 +2,7 @@ using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Power;
using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
using Content.Shared.Rounding;
using Content.Shared.SMES;
using JetBrains.Annotations;
@@ -14,6 +15,7 @@ internal sealed class SmesSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly SharedBatterySystem _battery = default!;
public override void Initialize()
{
@@ -61,7 +63,8 @@ internal sealed class SmesSystem : EntitySystem
if (!Resolve(uid, ref battery, false))
return 0;
- return ContentHelpers.RoundToLevels(battery.CurrentCharge, battery.MaxCharge, 6);
+ var currentCharge = _battery.GetCharge((uid, battery));
+ return ContentHelpers.RoundToLevels(currentCharge, battery.MaxCharge, 6);
}
private ChargeState CalcChargeState(EntityUid uid, PowerNetworkBatteryComponent? netBattery = null)
diff --git a/Content.Server/Power/SetBatteryPercentCommand.cs b/Content.Server/Power/SetBatteryPercentCommand.cs
index bd48e6cd97..f13a1b820e 100644
--- a/Content.Server/Power/SetBatteryPercentCommand.cs
+++ b/Content.Server/Power/SetBatteryPercentCommand.cs
@@ -1,5 +1,4 @@
using Content.Server.Administration;
-using Content.Server.Power.EntitySystems;
using Content.Shared.Administration;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
@@ -10,8 +9,7 @@ namespace Content.Server.Power
[AdminCommand(AdminFlags.Debug)]
public sealed class SetBatteryPercentCommand : LocalizedEntityCommands
{
- [Dependency] private readonly BatterySystem _batterySystem = default!;
- [Dependency] private readonly PredictedBatterySystem _predictedBatterySystem = default!;
+ [Dependency] private readonly SharedBatterySystem _batterySystem = default!;
public override string Command => "setbatterypercent";
@@ -39,8 +37,6 @@ namespace Content.Server.Power
if (EntityManager.TryGetComponent(id, out var battery))
_batterySystem.SetCharge((id.Value, battery), battery.MaxCharge * percent / 100);
- else if (EntityManager.TryGetComponent(id, out var pBattery))
- _predictedBatterySystem.SetCharge((id.Value, pBattery), pBattery.MaxCharge * percent / 100);
else
{
shell.WriteLine(Loc.GetString($"cmd-setbatterypercent-battery-not-found", ("id", id)));
diff --git a/Content.Server/PowerSink/PowerSinkSystem.cs b/Content.Server/PowerSink/PowerSinkSystem.cs
index 328bff89f4..c8ea58a395 100644
--- a/Content.Server/PowerSink/PowerSinkSystem.cs
+++ b/Content.Server/PowerSink/PowerSinkSystem.cs
@@ -68,7 +68,7 @@ namespace Content.Server.PowerSink
_battery.ChangeCharge((entity, battery), networkLoad.NetworkLoad.ReceivingPower * frameTime);
- var currentBatteryThreshold = battery.CurrentCharge / battery.MaxCharge;
+ var currentBatteryThreshold = _battery.GetChargeLevel((entity, battery));
// Check for warning message threshold
if (!component.SentImminentExplosionWarningMessage &&
@@ -90,7 +90,7 @@ namespace Content.Server.PowerSink
}
// Check for explosion
- if (battery.CurrentCharge < battery.MaxCharge)
+ if (!_battery.IsFull((entity, battery)))
continue;
if (component.ExplosionTime == null)
diff --git a/Content.Server/Projectiles/ProjectileSystem.cs b/Content.Server/Projectiles/ProjectileSystem.cs
index 28df1eb42d..01fe80e094 100644
--- a/Content.Server/Projectiles/ProjectileSystem.cs
+++ b/Content.Server/Projectiles/ProjectileSystem.cs
@@ -3,6 +3,7 @@ using Content.Server.Destructible;
using Content.Server.Effects;
using Content.Server.Weapons.Ranged.Systems;
using Content.Shared.Camera;
+using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
@@ -68,45 +69,11 @@ public sealed class ProjectileSystem : SharedProjectileSystem
LogImpact.Medium,
$"Projectile {ToPrettyString(uid):projectile} shot by {ToPrettyString(component.Shooter!.Value):user} hit {otherName:target} and dealt {damage:damage} damage");
- // If penetration is to be considered, we need to do some checks to see if the projectile should stop.
- if (component.PenetrationThreshold != 0)
- {
- // If a damage type is required, stop the bullet if the hit entity doesn't have that type.
- if (component.PenetrationDamageTypeRequirement != null)
- {
- var stopPenetration = false;
- foreach (var requiredDamageType in component.PenetrationDamageTypeRequirement)
- {
- if (!damage.DamageDict.Keys.Contains(requiredDamageType))
- {
- stopPenetration = true;
- break;
- }
- }
- if (stopPenetration)
- component.ProjectileSpent = true;
- }
-
- // If the object won't be destroyed, it "tanks" the penetration hit.
- if (damage.GetTotal() < damageRequired)
- {
- component.ProjectileSpent = true;
- }
-
- if (!component.ProjectileSpent)
- {
- component.PenetrationAmount += damageRequired;
- // The projectile has dealt enough damage to be spent.
- if (component.PenetrationAmount >= component.PenetrationThreshold)
- {
- component.ProjectileSpent = true;
- }
- }
- }
- else
- {
- component.ProjectileSpent = true;
- }
+ component.ProjectileSpent = !TryPenetrate((uid, component), damage, damageRequired);
+ }
+ else
+ {
+ component.ProjectileSpent = true;
}
if (!deleted)
@@ -125,4 +92,41 @@ public sealed class ProjectileSystem : SharedProjectileSystem
RaiseNetworkEvent(new ImpactEffectEvent(component.ImpactEffect, GetNetCoordinates(xform.Coordinates)), Filter.Pvs(xform.Coordinates, entityMan: EntityManager));
}
}
+
+ private bool TryPenetrate(Entity projectile, DamageSpecifier damage, FixedPoint2 damageRequired)
+ {
+ // If penetration is to be considered, we need to do some checks to see if the projectile should stop.
+ if (projectile.Comp.PenetrationThreshold == 0)
+ return false;
+
+ // If a damage type is required, stop the bullet if the hit entity doesn't have that type.
+ if (projectile.Comp.PenetrationDamageTypeRequirement != null)
+ {
+ foreach (var requiredDamageType in projectile.Comp.PenetrationDamageTypeRequirement)
+ {
+ if (damage.DamageDict.Keys.Contains(requiredDamageType))
+ continue;
+
+ return false;
+ }
+ }
+
+ // If the object won't be destroyed, it "tanks" the penetration hit.
+ if (damage.GetTotal() < damageRequired)
+ {
+ return false;
+ }
+
+ if (!projectile.Comp.ProjectileSpent)
+ {
+ projectile.Comp.PenetrationAmount += damageRequired;
+ // The projectile has dealt enough damage to be spent.
+ if (projectile.Comp.PenetrationAmount >= projectile.Comp.PenetrationThreshold)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
}
diff --git a/Content.Server/Radio/EntitySystems/JammerSystem.cs b/Content.Server/Radio/EntitySystems/JammerSystem.cs
index eaa42d744c..dead85f51f 100644
--- a/Content.Server/Radio/EntitySystems/JammerSystem.cs
+++ b/Content.Server/Radio/EntitySystems/JammerSystem.cs
@@ -11,7 +11,7 @@ namespace Content.Server.Radio.EntitySystems;
public sealed class JammerSystem : SharedJammerSystem
{
[Dependency] private readonly PowerCellSystem _powerCell = default!;
- [Dependency] private readonly PredictedBatterySystem _battery = default!;
+ [Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedDeviceNetworkJammerSystem _jammer = default!;
@@ -25,7 +25,7 @@ public sealed class JammerSystem : SharedJammerSystem
}
// TODO: Very important: Make this charge rate based instead of updating every single tick
- // See PredictedBatteryComponent
+ // See BatteryComponent
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator();
diff --git a/Content.Server/SensorMonitoring/BatterySensorSystem.cs b/Content.Server/SensorMonitoring/BatterySensorSystem.cs
index bd94868c5f..2c7666ba96 100644
--- a/Content.Server/SensorMonitoring/BatterySensorSystem.cs
+++ b/Content.Server/SensorMonitoring/BatterySensorSystem.cs
@@ -3,6 +3,7 @@ using Content.Server.Power.Components;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
namespace Content.Server.SensorMonitoring;
@@ -11,6 +12,7 @@ public sealed class BatterySensorSystem : EntitySystem
public const string DeviceNetworkCommandSyncData = "bat_sync_data";
[Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
+ [Dependency] private readonly SharedBatterySystem _battery = default!;
public override void Initialize()
{
@@ -26,13 +28,14 @@ public sealed class BatterySensorSystem : EntitySystem
{
case DeviceNetworkCommandSyncData:
var battery = Comp(uid);
+ var currentCharge = _battery.GetCharge((uid, battery));
var netBattery = Comp(uid);
var payload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = DeviceNetworkCommandSyncData,
[DeviceNetworkCommandSyncData] = new BatterySensorData(
- battery.CurrentCharge,
+ currentCharge,
battery.MaxCharge,
netBattery.CurrentReceiving,
netBattery.MaxChargeRate,
diff --git a/Content.Server/Shuttles/Components/IFFConsoleComponent.cs b/Content.Server/Shuttles/Components/IFFConsoleComponent.cs
index 562bec51c2..c2cf913c2c 100644
--- a/Content.Server/Shuttles/Components/IFFConsoleComponent.cs
+++ b/Content.Server/Shuttles/Components/IFFConsoleComponent.cs
@@ -11,4 +11,10 @@ public sealed partial class IFFConsoleComponent : Component
///
[ViewVariables(VVAccess.ReadWrite), DataField("allowedFlags")]
public IFFFlags AllowedFlags = IFFFlags.HideLabel;
+
+ ///
+ /// If true, automatically applies all supported IFF flags to the console's grid on MapInitEvent.
+ ///
+ [DataField]
+ public bool HideOnInit = false;
}
diff --git a/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs b/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs
index 5e3029333e..57ebf13e73 100644
--- a/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs
+++ b/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs
@@ -203,7 +203,8 @@ public sealed partial class DockingSystem
var spawnPosition = new EntityCoordinates(targetGridXform.MapUid!.Value, _transform.ToMapCoordinates(gridPosition).Position);
// TODO: use tight bounds
- var dockedBounds = new Box2Rotated(shuttleAABB.Translated(spawnPosition.Position), targetAngle, spawnPosition.Position);
+ var targetWorldAngle = (targetGridAngle + targetAngle).Reduced();
+ var dockedBounds = new Box2Rotated(shuttleAABB.Translated(spawnPosition.Position), targetWorldAngle, spawnPosition.Position);
// Check if there's no intersecting grids (AKA oh god it's docking at cargo).
grids.Clear();
diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.IFF.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.IFF.cs
index 5e746fd495..738fb765e1 100644
--- a/Content.Server/Shuttles/Systems/ShuttleSystem.IFF.cs
+++ b/Content.Server/Shuttles/Systems/ShuttleSystem.IFF.cs
@@ -12,7 +12,7 @@ public sealed partial class ShuttleSystem
{
SubscribeLocalEvent(OnIFFConsoleAnchor);
SubscribeLocalEvent(OnIFFShow);
- SubscribeLocalEvent(OnIFFShowVessel);
+ SubscribeLocalEvent(OnInitIFFConsole);
SubscribeLocalEvent(OnGridSplit);
}
@@ -37,37 +37,32 @@ public sealed partial class ShuttleSystem
private void OnIFFShow(EntityUid uid, IFFConsoleComponent component, IFFShowIFFMessage args)
{
- if (!TryComp(uid, out TransformComponent? xform) || xform.GridUid == null ||
- (component.AllowedFlags & IFFFlags.HideLabel) == 0x0)
+ if (!TryComp(uid, out TransformComponent? xform) || xform.GridUid == null)
{
return;
}
if (!args.Show)
{
- AddIFFFlag(xform.GridUid.Value, IFFFlags.HideLabel);
+ AddAllSupportedIFFFlags(xform, component);
}
else
{
RemoveIFFFlag(xform.GridUid.Value, IFFFlags.HideLabel);
+ RemoveIFFFlag(xform.GridUid.Value, IFFFlags.Hide);
}
}
- private void OnIFFShowVessel(EntityUid uid, IFFConsoleComponent component, IFFShowVesselMessage args)
+ private void OnInitIFFConsole(EntityUid uid, IFFConsoleComponent component, MapInitEvent args)
{
- if (!TryComp(uid, out TransformComponent? xform) || xform.GridUid == null ||
- (component.AllowedFlags & IFFFlags.Hide) == 0x0)
+ if (!TryComp(uid, out TransformComponent? xform) || xform.GridUid == null)
{
return;
}
- if (!args.Show)
+ if (component.HideOnInit)
{
- AddIFFFlag(xform.GridUid.Value, IFFFlags.Hide);
- }
- else
- {
- RemoveIFFFlag(xform.GridUid.Value, IFFFlags.Hide);
+ AddAllSupportedIFFFlags(xform, component);
}
}
@@ -111,4 +106,25 @@ public sealed partial class ShuttleSystem
});
}
}
+
+ // Made this method to avoid copy and pasting.
+ ///
+ /// Adds all IFF flags that are allowed by AllowedFlags to the grid.
+ ///
+ private void AddAllSupportedIFFFlags(TransformComponent xform, IFFConsoleComponent component)
+ {
+ if (xform.GridUid == null)
+ {
+ return;
+ }
+
+ if ((component.AllowedFlags & IFFFlags.HideLabel) != 0x0)
+ {
+ AddIFFFlag(xform.GridUid.Value, IFFFlags.HideLabel);
+ }
+ if ((component.AllowedFlags & IFFFlags.Hide) != 0x0)
+ {
+ AddIFFFlag(xform.GridUid.Value, IFFFlags.Hide);
+ }
+ }
}
diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs
index 6126234451..d50eea53ee 100644
--- a/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs
+++ b/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs
@@ -231,35 +231,58 @@ public sealed partial class ShuttleSystem
var knockdownTime = TimeSpan.FromSeconds(5);
var minsq = _minThrowVelocity * _minThrowVelocity;
- // iterate all entities on the grid
- // TODO: only iterate non-static entities
- var childEnumerator = xform.ChildEnumerator;
- while (childEnumerator.MoveNext(out var uid))
- {
- // don't throw static bodies
- if (!_physicsQuery.TryGetComponent(uid, out var physics) || (physics.BodyType & BodyType.Static) != 0)
- continue;
+ // iterate all dynamic entities on the grid
+ if (!TryComp(gridUid, out var lookup) || !_gridQuery.TryComp(gridUid, out var gridComp))
+ return;
+
+ var gridBox = gridComp.LocalAABB;
+ List> list = new();
+ HashSet processed = new();
+ var state = (list, processed, _physicsQuery);
+ lookup.DynamicTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
+ lookup.SundriesTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
+
+ foreach (var ent in list)
+ {
// don't throw if buckled
- if (_buckle.IsBuckled(uid, _buckleQuery.CompOrNull(uid)))
+ if (_buckle.IsBuckled(ent, _buckleQuery.CompOrNull(ent)))
continue;
// don't throw them if they have magboots
- if (movedByPressureQuery.TryComp(uid, out var moved) && !moved.Enabled)
+ if (movedByPressureQuery.TryComp(ent, out var moved) && !moved.Enabled)
continue;
if (direction.LengthSquared() > minsq)
{
- _stuns.TryCrawling(uid, knockdownTime);
- _throwing.TryThrow(uid, direction, physics, Transform(uid), _projQuery, direction.Length(), playSound: false);
+ _stuns.TryCrawling(ent.Owner, knockdownTime);
+ _throwing.TryThrow(ent, direction, ent.Comp, Transform(ent), _projQuery, direction.Length(), playSound: false);
}
else
{
- _physics.ApplyLinearImpulse(uid, direction * physics.Mass, body: physics);
+ _physics.ApplyLinearImpulse(ent, direction * ent.Comp.Mass, body: ent.Comp);
}
}
}
+ private static bool GridQueryCallback(
+ ref (List> List, HashSet Processed, EntityQuery PhysicsQuery) state,
+ in EntityUid uid)
+ {
+ if (state.Processed.Add(uid) && state.PhysicsQuery.TryComp(uid, out var body))
+ state.List.Add((uid, body));
+
+ return true;
+ }
+
+ private static bool GridQueryCallback(
+ ref (List> List, HashSet Processed, EntityQuery PhysicsQuery) state,
+ in FixtureProxy proxy)
+ {
+ var owner = proxy.Entity;
+ return GridQueryCallback(ref state, in owner);
+ }
+
///
/// Structure to hold impact tile processing data for batch processing
///
diff --git a/Content.Server/Silicons/Borgs/BorgSystem.cs b/Content.Server/Silicons/Borgs/BorgSystem.cs
index 6f37d55013..925050f28e 100644
--- a/Content.Server/Silicons/Borgs/BorgSystem.cs
+++ b/Content.Server/Silicons/Borgs/BorgSystem.cs
@@ -25,7 +25,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
[Dependency] private readonly TriggerSystem _trigger = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
- [Dependency] private readonly PredictedBatterySystem _battery = default!;
+ [Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!;
diff --git a/Content.Server/Silicons/StationAi/StationAiSystem.cs b/Content.Server/Silicons/StationAi/StationAiSystem.cs
index e52400d3de..36aa9acd6b 100644
--- a/Content.Server/Silicons/StationAi/StationAiSystem.cs
+++ b/Content.Server/Silicons/StationAi/StationAiSystem.cs
@@ -2,9 +2,10 @@ using Content.Server.Chat.Systems;
using Content.Server.Construction;
using Content.Server.Destructible;
using Content.Server.Ghost;
+using Content.Server.Ghost.Roles;
+using Content.Server.Ghost.Roles.Components;
using Content.Server.Mind;
using Content.Server.Power.Components;
-using Content.Server.Power.EntitySystems;
using Content.Server.Roles;
using Content.Server.Spawners.Components;
using Content.Server.Spawners.EntitySystems;
@@ -21,6 +22,7 @@ using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Power;
+using Content.Shared.Power.EntitySystems;
using Content.Shared.Power.Components;
using Content.Shared.Rejuvenate;
using Content.Shared.Roles;
@@ -47,9 +49,10 @@ public sealed class StationAiSystem : SharedStationAiSystem
[Dependency] private readonly RoleSystem _roles = default!;
[Dependency] private readonly ItemSlotsSystem _slots = default!;
[Dependency] private readonly GhostSystem _ghost = default!;
+ [Dependency] private readonly ToggleableGhostRoleSystem _ghostrole = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly DestructibleSystem _destructible = default!;
- [Dependency] private readonly BatterySystem _battery = default!;
+ [Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly SharedPopupSystem _popups = default!;
[Dependency] private readonly StationSystem _station = default!;
@@ -97,14 +100,30 @@ public sealed class StationAiSystem : SharedStationAiSystem
}
var brain = container.ContainedEntities[0];
+ var hasMind = _mind.TryGetMind(brain, out var mindId, out var mind);
- if (_mind.TryGetMind(brain, out var mindId, out var mind))
+ if (hasMind || HasComp(brain))
{
- // Found an existing mind to transfer into the AI core
var aiBrain = Spawn(_stationAiBrain, Transform(ent.Owner).Coordinates);
- _roles.MindAddJobRole(mindId, mind, false, _stationAiJob);
- _mind.TransferTo(mindId, aiBrain);
+ if (hasMind)
+ {
+ // Found an existing mind to transfer into the AI core
+ _roles.MindAddJobRole(mindId, mind, false, _stationAiJob);
+ _mind.TransferTo(mindId, aiBrain);
+ }
+ else
+ {
+ // If the brain had a ghost role attached, activate the station AI ghost role
+ _ghostrole.ActivateGhostRole(aiBrain);
+
+ // Set the new AI brain to the 'rebooting' state
+ if (TryComp(aiBrain, out var customization))
+ SetStationAiState((aiBrain, customization), StationAiState.Rebooting);
+
+ }
+
+ // Delete the new AI brain if it cannot be inserted into the core
if (!TryComp(ent, out var targetHolder) ||
!_slots.TryInsert(ent, targetHolder.Slot, aiBrain, null))
{
@@ -220,6 +239,7 @@ public sealed class StationAiSystem : SharedStationAiSystem
UpdateDamagedAccent(entity);
}
+ // TODO: This should just read the current damage and charge when speaking instead of updating the component all the time even if we don't even use it.
private void UpdateDamagedAccent(Entity ent)
{
if (!TryGetHeld((ent.Owner, ent.Comp), out var held))
@@ -229,7 +249,7 @@ public sealed class StationAiSystem : SharedStationAiSystem
return;
if (TryComp(ent, out var battery))
- accent.OverrideChargeLevel = battery.CurrentCharge / battery.MaxCharge;
+ accent.OverrideChargeLevel = _battery.GetChargeLevel((ent.Owner, battery));
if (TryComp(ent, out var damageable))
accent.OverrideTotalDamage = damageable.TotalDamage;
@@ -251,7 +271,7 @@ public sealed class StationAiSystem : SharedStationAiSystem
if (!_proto.TryIndex(_batteryAlert, out var proto))
return;
- var chargePercent = battery.CurrentCharge / battery.MaxCharge;
+ var chargePercent = _battery.GetChargeLevel((ent.Owner, battery));
var chargeLevel = Math.Round(chargePercent * proto.MaxSeverity);
_alerts.ShowAlert(held.Value, _batteryAlert, (short)Math.Clamp(chargeLevel, 0, proto.MaxSeverity));
diff --git a/Content.Server/Speech/AccentSystem.cs b/Content.Server/Speech/AccentSystem.cs
index 275a4175c8..6f89a7d14d 100644
--- a/Content.Server/Speech/AccentSystem.cs
+++ b/Content.Server/Speech/AccentSystem.cs
@@ -15,6 +15,9 @@ public sealed class AccentSystem : EntitySystem
private void AccentHandler(TransformSpeechEvent args)
{
+ if (args.Cancelled)
+ return;
+
var accentEvent = new AccentGetEvent(args.Sender, args.Message);
RaiseLocalEvent(args.Sender, accentEvent, true);
diff --git a/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs b/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs
index e5db13ca84..e9bbfdf528 100644
--- a/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs
+++ b/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs
@@ -13,7 +13,7 @@ namespace Content.Server.Speech.EntitySystems;
public sealed class DamagedSiliconAccentSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly PredictedBatterySystem _battery = default!;
+ [Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly DestructibleSystem _destructibleSystem = default!;
diff --git a/Content.Server/Spreader/SpreaderSystem.cs b/Content.Server/Spreader/SpreaderSystem.cs
index 2bc067542d..7ea41431da 100644
--- a/Content.Server/Spreader/SpreaderSystem.cs
+++ b/Content.Server/Spreader/SpreaderSystem.cs
@@ -295,35 +295,38 @@ public sealed class SpreaderSystem : EntitySystem
/// This function activates all spreaders that are adjacent to a given entity. This also activates other spreaders
/// on the same tile as the current entity (for thin airtight entities like windoors).
///
- public void ActivateSpreadableNeighbors(EntityUid uid, (EntityUid Grid, Vector2i Tile)? position = null)
+ public void ActivateSpreadableNeighbors(EntityUid origin, (EntityUid Grid, Vector2i Tile)? position = null)
{
Vector2i tile;
- EntityUid ent;
- MapGridComponent? grid;
+ EntityUid gridUid;
+ MapGridComponent? gridComp;
if (position == null)
{
- var transform = Transform(uid);
- if (!TryComp(transform.GridUid, out grid) || TerminatingOrDeleted(transform.GridUid.Value))
+ var transform = Transform(origin);
+ if (!TryComp(transform.GridUid, out gridComp) || TerminatingOrDeleted(transform.GridUid.Value))
return;
- tile = _map.TileIndicesFor(transform.GridUid.Value, grid, transform.Coordinates);
- ent = transform.GridUid.Value;
+ tile = _map.TileIndicesFor(transform.GridUid.Value, gridComp, transform.Coordinates);
+ gridUid = transform.GridUid.Value;
}
else
{
- if (!TryComp(position.Value.Grid, out grid))
+ if (!TryComp(position.Value.Grid, out gridComp))
return;
- (ent, tile) = position.Value;
+ (gridUid, tile) = position.Value;
}
- var anchored = _map.GetAnchoredEntitiesEnumerator(ent, grid, tile);
+ var anchored = _map.GetAnchoredEntitiesEnumerator(gridUid, gridComp, tile);
while (anchored.MoveNext(out var entity))
{
- if (entity == ent)
+ // Don't re-activate the terminating entity
+ if (entity == origin)
continue;
DebugTools.Assert(Transform(entity.Value).Anchored);
- if (_query.HasComponent(ent) && !TerminatingOrDeleted(entity.Value))
+
+ // Activate any edge spreaders that are non-terminating
+ if (_query.HasComponent(entity) && !TerminatingOrDeleted(entity))
EnsureComp(entity.Value);
}
@@ -331,12 +334,14 @@ public sealed class SpreaderSystem : EntitySystem
{
var direction = (AtmosDirection) (1 << i);
var adjacentTile = SharedMapSystem.GetDirection(tile, direction.ToDirection());
- anchored = _map.GetAnchoredEntitiesEnumerator(ent, grid, adjacentTile);
+ anchored = _map.GetAnchoredEntitiesEnumerator(gridUid, gridComp, adjacentTile);
while (anchored.MoveNext(out var entity))
{
DebugTools.Assert(Transform(entity.Value).Anchored);
- if (_query.HasComponent(ent) && !TerminatingOrDeleted(entity.Value))
+
+ // Activate any edge spreaders that are non-terminating
+ if (_query.HasComponent(entity) && !TerminatingOrDeleted(entity))
EnsureComp(entity.Value);
}
}
diff --git a/Content.Server/Stunnable/Systems/StunbatonSystem.cs b/Content.Server/Stunnable/Systems/StunbatonSystem.cs
index b1bae0127c..90d2dc7588 100644
--- a/Content.Server/Stunnable/Systems/StunbatonSystem.cs
+++ b/Content.Server/Stunnable/Systems/StunbatonSystem.cs
@@ -18,7 +18,7 @@ namespace Content.Server.Stunnable.Systems
{
[Dependency] private readonly RiggableSystem _riggableSystem = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
- [Dependency] private readonly PredictedBatterySystem _battery = default!;
+ [Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly ItemToggleSystem _itemToggle = default!;
public override void Initialize()
@@ -28,13 +28,13 @@ namespace Content.Server.Stunnable.Systems
SubscribeLocalEvent(OnExamined);
SubscribeLocalEvent(OnSolutionChange);
SubscribeLocalEvent(OnStaminaHitAttempt);
- SubscribeLocalEvent(OnChargeChanged);
+ SubscribeLocalEvent(OnChargeChanged);
}
private void OnStaminaHitAttempt(Entity entity, ref StaminaDamageOnHitAttemptEvent args)
{
if (!_itemToggle.IsActivated(entity.Owner) ||
- !TryComp(entity.Owner, out var battery) || !_battery.TryUseCharge((entity.Owner, battery), entity.Comp.EnergyPerUse))
+ !TryComp(entity.Owner, out var battery) || !_battery.TryUseCharge((entity.Owner, battery), entity.Comp.EnergyPerUse))
{
args.Cancelled = true;
}
@@ -47,7 +47,7 @@ namespace Content.Server.Stunnable.Systems
: Loc.GetString("comp-stunbaton-examined-off");
args.PushMarkup(onMsg);
- if (TryComp(entity.Owner, out var battery))
+ if (TryComp(entity.Owner, out var battery))
{
var count = _battery.GetRemainingUses((entity.Owner, battery), entity.Comp.EnergyPerUse);
args.PushMarkup(Loc.GetString("melee-battery-examine", ("color", "yellow"), ("count", count)));
@@ -58,7 +58,7 @@ namespace Content.Server.Stunnable.Systems
{
base.TryTurnOn(entity, ref args);
- if (!TryComp(entity, out var battery) || _battery.GetCharge((entity, battery)) < entity.Comp.EnergyPerUse)
+ if (!TryComp(entity, out var battery) || _battery.GetCharge((entity, battery)) < entity.Comp.EnergyPerUse)
{
args.Cancelled = true;
if (args.User != null)
@@ -79,7 +79,7 @@ namespace Content.Server.Stunnable.Systems
{
// Explode if baton is activated and rigged.
if (!TryComp(entity, out var riggable) ||
- !TryComp(entity, out var battery))
+ !TryComp(entity, out var battery))
return;
if (_itemToggle.IsActivated(entity.Owner) && riggable.IsRigged)
@@ -96,9 +96,9 @@ namespace Content.Server.Stunnable.Systems
});
}
- private void OnChargeChanged(Entity entity, ref PredictedBatteryChargeChangedEvent args)
+ private void OnChargeChanged(Entity entity, ref ChargeChangedEvent args)
{
- if (TryComp(entity.Owner, out var battery) &&
+ if (TryComp(entity.Owner, out var battery) &&
_battery.GetCharge((entity.Owner, battery)) < entity.Comp.EnergyPerUse)
{
_itemToggle.TryDeactivate(entity.Owner, predicted: false);
diff --git a/Content.Server/Temperature/Components/ContainerTemperatureComponent.cs b/Content.Server/Temperature/Components/ContainerTemperatureComponent.cs
new file mode 100644
index 0000000000..7c7b1e81e8
--- /dev/null
+++ b/Content.Server/Temperature/Components/ContainerTemperatureComponent.cs
@@ -0,0 +1,11 @@
+namespace Content.Server.Temperature.Components;
+
+[RegisterComponent]
+public sealed partial class ContainerTemperatureComponent : Component
+{
+ [DataField]
+ public float? HeatDamageThreshold;
+
+ [DataField]
+ public float? ColdDamageThreshold;
+}
diff --git a/Content.Server/Temperature/Components/ContainerTemperatureDamageThresholdsComponent.cs b/Content.Server/Temperature/Components/ContainerTemperatureDamageThresholdsComponent.cs
deleted file mode 100644
index 024b8a013b..0000000000
--- a/Content.Server/Temperature/Components/ContainerTemperatureDamageThresholdsComponent.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace Content.Server.Temperature.Components;
-
-[RegisterComponent]
-public sealed partial class ContainerTemperatureDamageThresholdsComponent: Component
-{
- [DataField, ViewVariables(VVAccess.ReadWrite)]
- public float? HeatDamageThreshold;
-
- [DataField, ViewVariables(VVAccess.ReadWrite)]
- public float? ColdDamageThreshold;
-}
diff --git a/Content.Server/Temperature/Systems/TemperatureSystem.Damage.cs b/Content.Server/Temperature/Systems/TemperatureSystem.Damage.cs
new file mode 100644
index 0000000000..d436977daf
--- /dev/null
+++ b/Content.Server/Temperature/Systems/TemperatureSystem.Damage.cs
@@ -0,0 +1,279 @@
+using Content.Server.Administration.Logs;
+using Content.Server.Body.Components;
+using Content.Server.Temperature.Components;
+using Content.Shared.Alert;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Systems;
+using Content.Shared.Database;
+using Content.Shared.Rounding;
+using Content.Shared.Temperature;
+using Content.Shared.Temperature.Components;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Temperature.Systems;
+
+///
+/// Handles entities taking damage from being too hot or too cold.
+/// Also handles alerts relevant to the same.
+///
+public sealed partial class TemperatureSystem
+{
+ [Dependency] private readonly AlertsSystem _alerts = default!;
+ [Dependency] private readonly DamageableSystem _damageable = default!;
+ [Dependency] private readonly IAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+
+ private EntityQuery _tempDamageQuery;
+ private EntityQuery _containerTemperatureQuery;
+ private EntityQuery _thermalRegulatorQuery;
+
+ ///
+ /// All the components that will have their damage updated at the end of the tick.
+ /// This is done because both AtmosExposed and Flammable call ChangeHeat in the same tick, meaning
+ /// that we need some mechanism to ensure it doesn't double-dip on damage for both calls.
+ ///
+ public HashSet> ShouldUpdateDamage = new();
+
+ ///
+ /// Alert prototype for Temperature.
+ ///
+ public static readonly ProtoId TemperatureAlertCategory = "Temperature";
+
+ ///
+ /// The maximum severity applicable to temperature alerts.
+ ///
+ public static readonly short MaxTemperatureAlertSeverity = 3;
+
+ ///
+ /// On a scale of 0. to 1. where 0. is the ideal temperature and 1. is a temperature damage threshold this is the point where the component starts raising temperature alerts.
+ ///
+ public static readonly float MinAlertTemperatureScale = 0.33f;
+
+ private void InitializeDamage()
+ {
+ SubscribeLocalEvent(ServerAlert);
+
+ SubscribeLocalEvent(EnqueueDamage);
+ SubscribeLocalEvent(OnUnpaused);
+
+ // Allows overriding thresholds based on the parent's thresholds.
+ SubscribeLocalEvent(OnParentChange);
+ SubscribeLocalEvent(OnParentThresholdStartup);
+ SubscribeLocalEvent(OnParentThresholdShutdown);
+
+ _tempDamageQuery = GetEntityQuery();
+ _containerTemperatureQuery = GetEntityQuery();
+ _thermalRegulatorQuery = GetEntityQuery();
+ }
+
+ private void UpdateDamage()
+ {
+ foreach (var entity in ShouldUpdateDamage)
+ {
+ if (Deleted(entity) || Paused(entity))
+ continue;
+
+ var deltaTime = _gameTiming.CurTime - entity.Comp.LastUpdate;
+ if (entity.Comp.TakingDamage && deltaTime < entity.Comp.UpdateInterval)
+ continue;
+
+ ChangeDamage(entity, deltaTime);
+ }
+
+ ShouldUpdateDamage.Clear();
+ }
+
+ private void ChangeDamage(Entity entity, TimeSpan deltaTime)
+ {
+ entity.Comp.LastUpdate = _gameTiming.CurTime;
+
+ if (!HasComp(entity) || !TemperatureQuery.TryComp(entity, out var temperature))
+ return;
+
+ // See this link for where the scaling func comes from:
+ // https://www.desmos.com/calculator/0vknqtdvq9
+ // Based on a logistic curve, which caps out at MaxDamage
+ var heatK = 0.005;
+ var a = 1;
+ var y = entity.Comp.DamageCap;
+ var c = y * 2;
+
+ var heatDamageThreshold = entity.Comp.ParentHeatDamageThreshold ?? entity.Comp.HeatDamageThreshold;
+ var coldDamageThreshold = entity.Comp.ParentColdDamageThreshold ?? entity.Comp.ColdDamageThreshold;
+
+ if (temperature.CurrentTemperature >= heatDamageThreshold)
+ {
+ if (!entity.Comp.TakingDamage)
+ {
+ _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(entity):entity} started taking high temperature damage");
+ entity.Comp.TakingDamage = true;
+ }
+
+ var diff = Math.Abs(temperature.CurrentTemperature - heatDamageThreshold);
+ var tempDamage = c / (1 + a * Math.Pow(Math.E, -heatK * diff)) - y;
+ _damageable.TryChangeDamage(entity.Owner, entity.Comp.HeatDamage * tempDamage * deltaTime.TotalSeconds, ignoreResistances: true, interruptsDoAfters: false);
+ }
+ else if (temperature.CurrentTemperature <= coldDamageThreshold)
+ {
+ if (!entity.Comp.TakingDamage)
+ {
+ _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(entity):entity} started taking low temperature damage");
+ entity.Comp.TakingDamage = true;
+ }
+
+ var diff = Math.Abs(temperature.CurrentTemperature - coldDamageThreshold);
+ var tempDamage =
+ Math.Sqrt(diff * (Math.Pow(entity.Comp.DamageCap.Double(), 2) / coldDamageThreshold));
+ _damageable.TryChangeDamage(entity.Owner, entity.Comp.ColdDamage * tempDamage * deltaTime.TotalSeconds, ignoreResistances: true, interruptsDoAfters: false);
+ }
+ else if (entity.Comp.TakingDamage)
+ {
+ _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(entity):entity} stopped taking temperature damage");
+ entity.Comp.TakingDamage = false;
+ }
+ }
+
+ private void ServerAlert(Entity entity, ref OnTemperatureChangeEvent args)
+ {
+ ProtoId type;
+ float threshold;
+ float idealTemp;
+
+ if (!_tempDamageQuery.TryComp(entity, out var thresholds))
+ {
+ _alerts.ClearAlertCategory(entity.Owner, TemperatureAlertCategory);
+ return;
+ }
+
+ if (_thermalRegulatorQuery.TryComp(entity, out var regulator) &&
+ regulator.NormalBodyTemperature > thresholds.ColdDamageThreshold &&
+ regulator.NormalBodyTemperature < thresholds.HeatDamageThreshold)
+ {
+ idealTemp = regulator.NormalBodyTemperature;
+ }
+ else
+ {
+ idealTemp = (thresholds.ColdDamageThreshold + thresholds.HeatDamageThreshold) / 2;
+ }
+
+ if (args.CurrentTemperature <= idealTemp)
+ {
+ type = thresholds.ColdAlert;
+ threshold = thresholds.ColdDamageThreshold;
+ }
+ else
+ {
+ type = thresholds.HotAlert;
+ threshold = thresholds.HeatDamageThreshold;
+ }
+
+ // Calculates a scale where 0.0 is the ideal temperature and 1.0 is where temperature damage begins
+ // The cold and hot scales will differ in their range if the ideal temperature is not exactly halfway between the thresholds
+ var tempScale = (args.CurrentTemperature - idealTemp) / (threshold - idealTemp);
+ var alertLevel = (short)ContentHelpers.RoundToLevels(tempScale - MinAlertTemperatureScale, 1.00f - MinAlertTemperatureScale, MaxTemperatureAlertSeverity + 1);
+
+ if (alertLevel > 0)
+ _alerts.ShowAlert(entity.AsNullable(), type, alertLevel);
+ else
+ _alerts.ClearAlertCategory(entity.AsNullable(), TemperatureAlertCategory);
+ }
+
+ private void EnqueueDamage(Entity ent, ref OnTemperatureChangeEvent args)
+ {
+ if (ShouldUpdateDamage.Add(ent) && !ent.Comp.TakingDamage)
+ ent.Comp.LastUpdate = _gameTiming.CurTime;
+ }
+
+ private void OnUnpaused(Entity ent, ref EntityUnpausedEvent args)
+ {
+ ent.Comp.LastUpdate += args.PausedTime;
+ }
+
+ private void OnParentChange(Entity entity, ref EntParentChangedMessage args)
+ {
+ // We only need to update thresholds if the thresholds changed for the entity's ancestors.
+ var oldThresholds = args.OldParent != null
+ ? RecalculateParentThresholds(args.OldParent.Value)
+ : (null, null);
+ var xform = Transform(entity.Owner);
+ var newThresholds = RecalculateParentThresholds(xform.ParentUid);
+
+ if (oldThresholds != newThresholds)
+ RecursiveThresholdUpdate((entity, entity.Comp, xform));
+ }
+
+ private void OnParentThresholdStartup(Entity entity, ref ComponentStartup args)
+ {
+ RecursiveThresholdUpdate(entity.Owner);
+ }
+
+ private void OnParentThresholdShutdown(Entity entity, ref ComponentShutdown args)
+ {
+ RecursiveThresholdUpdate(entity.Owner);
+ }
+
+ ///
+ /// Recalculate and apply parent thresholds for the root entity and all its children.
+ ///
+ /// The root entity we're currently updating
+ private void RecursiveThresholdUpdate(Entity root)
+ {
+ RecalculateAndApplyParentThresholds(root);
+
+ var xform = root.Comp2 ?? Transform(root);
+ var enumerator = xform.ChildEnumerator;
+ while (enumerator.MoveNext(out var child))
+ {
+ RecursiveThresholdUpdate(child);
+ }
+ }
+
+ ///
+ /// Recalculate parent thresholds and apply them on the uid temperature component.
+ ///
+ /// The entity whose temperature damage thresholds we're updating
+ private void RecalculateAndApplyParentThresholds(Entity entity)
+ {
+ if (!_tempDamageQuery.Resolve(entity, ref entity.Comp, logMissing: false))
+ return;
+
+ var newThresholds = RecalculateParentThresholds(Transform(entity).ParentUid);
+ entity.Comp.ParentHeatDamageThreshold = newThresholds.Item1;
+ entity.Comp.ParentColdDamageThreshold = newThresholds.Item2;
+ }
+
+ ///
+ /// Recalculate Parent Heat/Cold DamageThreshold by recursively checking each ancestor and fetching the
+ /// maximum HeatDamageThreshold and the minimum ColdDamageThreshold if any exists (aka the best value for each).
+ ///
+ /// parent we start with
+ private (float?, float?) RecalculateParentThresholds(EntityUid initialParentUid)
+ {
+ // Recursively check parents for the best threshold available
+ var parentUid = initialParentUid;
+ float? newHeatThreshold = null;
+ float? newColdThreshold = null;
+ while (parentUid.IsValid())
+ {
+ if (_containerTemperatureQuery.TryComp(parentUid, out var newThresholds))
+ {
+ if (newThresholds.HeatDamageThreshold != null)
+ {
+ newHeatThreshold = Math.Max(newThresholds.HeatDamageThreshold.Value,
+ newHeatThreshold ?? 0);
+ }
+
+ if (newThresholds.ColdDamageThreshold != null)
+ {
+ newColdThreshold = Math.Min(newThresholds.ColdDamageThreshold.Value,
+ newColdThreshold ?? float.MaxValue);
+ }
+ }
+
+ parentUid = Transform(parentUid).ParentUid;
+ }
+
+ return (newHeatThreshold, newColdThreshold);
+ }
+}
diff --git a/Content.Server/Temperature/Systems/TemperatureSystem.cs b/Content.Server/Temperature/Systems/TemperatureSystem.cs
index 928b6ae9b5..484912c780 100644
--- a/Content.Server/Temperature/Systems/TemperatureSystem.cs
+++ b/Content.Server/Temperature/Systems/TemperatureSystem.cs
@@ -1,64 +1,32 @@
-using System.Linq;
-using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems;
-using Content.Server.Body.Components;
using Content.Server.Temperature.Components;
-using Content.Shared.Alert;
using Content.Shared.Atmos;
-using Content.Shared.Damage.Components;
-using Content.Shared.Damage.Systems;
-using Content.Shared.Database;
using Content.Shared.Inventory;
using Content.Shared.Rejuvenate;
using Content.Shared.Temperature;
-using Robust.Shared.Prototypes;
using Content.Shared.Projectiles;
using Content.Shared.Temperature.Components;
using Content.Shared.Temperature.Systems;
namespace Content.Server.Temperature.Systems;
-public sealed class TemperatureSystem : SharedTemperatureSystem
+public sealed partial class TemperatureSystem : SharedTemperatureSystem
{
- [Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
- [Dependency] private readonly DamageableSystem _damageable = default!;
- [Dependency] private readonly IAdminLogManager _adminLogger = default!;
- [Dependency] private readonly TemperatureSystem _temperature = default!;
-
- ///
- /// All the components that will have their damage updated at the end of the tick.
- /// This is done because both AtmosExposed and Flammable call ChangeHeat in the same tick, meaning
- /// that we need some mechanism to ensure it doesn't double dip on damage for both calls.
- ///
- public HashSet> ShouldUpdateDamage = new();
-
- public float UpdateInterval = 1.0f;
-
- private float _accumulatedFrametime;
-
- public static readonly ProtoId TemperatureAlertCategory = "Temperature";
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent(EnqueueDamage);
SubscribeLocalEvent(OnAtmosExposedUpdate);
SubscribeLocalEvent(OnRejuvenate);
- SubscribeLocalEvent(ServerAlert);
Subs.SubscribeWithRelay(OnTemperatureChangeAttempt, held: false);
SubscribeLocalEvent(OnInit);
SubscribeLocalEvent(ChangeTemperatureOnCollide);
- // Allows overriding thresholds based on the parent's thresholds.
- SubscribeLocalEvent(OnParentChange);
- SubscribeLocalEvent(
- OnParentThresholdStartup);
- SubscribeLocalEvent(
- OnParentThresholdShutdown);
+ InitializeDamage();
}
public override void Update(float frameTime)
@@ -88,49 +56,23 @@ public sealed class TemperatureSystem : SharedTemperatureSystem
ForceChangeTemperature(uid, temp.CurrentTemperature - degrees, temp);
}
- UpdateDamage(frameTime);
- }
-
- private void UpdateDamage(float frameTime)
- {
- _accumulatedFrametime += frameTime;
-
- if (_accumulatedFrametime < UpdateInterval)
- return;
- _accumulatedFrametime -= UpdateInterval;
-
- if (!ShouldUpdateDamage.Any())
- return;
-
- foreach (var comp in ShouldUpdateDamage)
- {
- MetaDataComponent? metaData = null;
-
- var uid = comp.Owner;
- if (Deleted(uid, metaData) || Paused(uid, metaData))
- continue;
-
- ChangeDamage(uid, comp);
- }
-
- ShouldUpdateDamage.Clear();
+ UpdateDamage();
}
public void ForceChangeTemperature(EntityUid uid, float temp, TemperatureComponent? temperature = null)
{
- if (!Resolve(uid, ref temperature))
+ if (!TemperatureQuery.Resolve(uid, ref temperature))
return;
- float lastTemp = temperature.CurrentTemperature;
- float delta = temperature.CurrentTemperature - temp;
+ var lastTemp = temperature.CurrentTemperature;
+ var delta = temperature.CurrentTemperature - temp;
temperature.CurrentTemperature = temp;
- RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta),
- true);
+ RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), broadcast: true);
}
public override void ChangeHeat(EntityUid uid, float heatAmount, bool ignoreHeatResistance = false, TemperatureComponent? temperature = null)
{
- if (!Resolve(uid, ref temperature, false))
+ if (!TemperatureQuery.Resolve(uid, ref temperature, false))
return;
if (!ignoreHeatResistance)
@@ -144,11 +86,10 @@ public sealed class TemperatureSystem : SharedTemperatureSystem
temperature.CurrentTemperature += heatAmount / GetHeatCapacity(uid, temperature);
float delta = temperature.CurrentTemperature - lastTemp;
- RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), true);
+ RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), broadcast: true);
}
- private void OnAtmosExposedUpdate(EntityUid uid, TemperatureComponent temperature,
- ref AtmosExposedUpdateEvent args)
+ private void OnAtmosExposedUpdate(EntityUid uid, TemperatureComponent temperature, ref AtmosExposedUpdateEvent args)
{
var transform = args.Transform;
@@ -158,17 +99,18 @@ public sealed class TemperatureSystem : SharedTemperatureSystem
var temperatureDelta = args.GasMixture.Temperature - temperature.CurrentTemperature;
var airHeatCapacity = _atmosphere.GetHeatCapacity(args.GasMixture, false);
var heatCapacity = GetHeatCapacity(uid, temperature);
+ // TODO ATMOS: This heat transfer formula is really really wrong, it needs to be pulled out. Pending on HeatContainers.
var heat = temperatureDelta * (airHeatCapacity * heatCapacity /
(airHeatCapacity + heatCapacity));
ChangeHeat(uid, heat * temperature.AtmosTemperatureTransferEfficiency, temperature: temperature);
}
- private void OnInit(EntityUid uid, InternalTemperatureComponent comp, MapInitEvent args)
+ private void OnInit(Entity entity, ref MapInitEvent args)
{
- if (!TryComp(uid, out var temp))
+ if (!TemperatureQuery.TryComp(entity, out var temp))
return;
- comp.Temperature = temp.CurrentTemperature;
+ entity.Comp.Temperature = temp.CurrentTemperature;
}
private void OnRejuvenate(EntityUid uid, TemperatureComponent comp, RejuvenateEvent args)
@@ -176,116 +118,6 @@ public sealed class TemperatureSystem : SharedTemperatureSystem
ForceChangeTemperature(uid, Atmospherics.T20C, comp);
}
- private void ServerAlert(EntityUid uid, AlertsComponent status, OnTemperatureChangeEvent args)
- {
- ProtoId type;
- float threshold;
- float idealTemp;
-
- if (!TryComp(uid, out var temperature))
- {
- _alerts.ClearAlertCategory(uid, TemperatureAlertCategory);
- return;
- }
-
- if (TryComp(uid, out var regulator) &&
- regulator.NormalBodyTemperature > temperature.ColdDamageThreshold &&
- regulator.NormalBodyTemperature < temperature.HeatDamageThreshold)
- {
- idealTemp = regulator.NormalBodyTemperature;
- }
- else
- {
- idealTemp = (temperature.ColdDamageThreshold + temperature.HeatDamageThreshold) / 2;
- }
-
- if (args.CurrentTemperature <= idealTemp)
- {
- type = temperature.ColdAlert;
- threshold = temperature.ColdDamageThreshold;
- }
- else
- {
- type = temperature.HotAlert;
- threshold = temperature.HeatDamageThreshold;
- }
-
- // Calculates a scale where 1.0 is the ideal temperature and 0.0 is where temperature damage begins
- // The cold and hot scales will differ in their range if the ideal temperature is not exactly halfway between the thresholds
- var tempScale = (args.CurrentTemperature - threshold) / (idealTemp - threshold);
- switch (tempScale)
- {
- case <= 0f:
- _alerts.ShowAlert(uid, type, 3);
- break;
-
- case <= 0.4f:
- _alerts.ShowAlert(uid, type, 2);
- break;
-
- case <= 0.66f:
- _alerts.ShowAlert(uid, type, 1);
- break;
-
- case > 0.66f:
- _alerts.ClearAlertCategory(uid, TemperatureAlertCategory);
- break;
- }
- }
-
- private void EnqueueDamage(Entity temperature, ref OnTemperatureChangeEvent args)
- {
- ShouldUpdateDamage.Add(temperature);
- }
-
- private void ChangeDamage(EntityUid uid, TemperatureComponent temperature)
- {
- if (!HasComp(uid))
- return;
-
- // See this link for where the scaling func comes from:
- // https://www.desmos.com/calculator/0vknqtdvq9
- // Based on a logistic curve, which caps out at MaxDamage
- var heatK = 0.005;
- var a = 1;
- var y = temperature.DamageCap;
- var c = y * 2;
-
- var heatDamageThreshold = temperature.ParentHeatDamageThreshold ?? temperature.HeatDamageThreshold;
- var coldDamageThreshold = temperature.ParentColdDamageThreshold ?? temperature.ColdDamageThreshold;
-
- if (temperature.CurrentTemperature >= heatDamageThreshold)
- {
- if (!temperature.TakingDamage)
- {
- _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(uid):entity} started taking high temperature damage");
- temperature.TakingDamage = true;
- }
-
- var diff = Math.Abs(temperature.CurrentTemperature - heatDamageThreshold);
- var tempDamage = c / (1 + a * Math.Pow(Math.E, -heatK * diff)) - y;
- _damageable.TryChangeDamage(uid, temperature.HeatDamage * tempDamage, ignoreResistances: true, interruptsDoAfters: false);
- }
- else if (temperature.CurrentTemperature <= coldDamageThreshold)
- {
- if (!temperature.TakingDamage)
- {
- _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(uid):entity} started taking low temperature damage");
- temperature.TakingDamage = true;
- }
-
- var diff = Math.Abs(temperature.CurrentTemperature - coldDamageThreshold);
- var tempDamage =
- Math.Sqrt(diff * (Math.Pow(temperature.DamageCap.Double(), 2) / coldDamageThreshold));
- _damageable.TryChangeDamage(uid, temperature.ColdDamage * tempDamage, ignoreResistances: true, interruptsDoAfters: false);
- }
- else if (temperature.TakingDamage)
- {
- _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(uid):entity} stopped taking temperature damage");
- temperature.TakingDamage = false;
- }
- }
-
private void OnTemperatureChangeAttempt(EntityUid uid, TemperatureProtectionComponent component, ModifyChangedTemperatureEvent args)
{
var coefficient = args.TemperatureDelta < 0
@@ -300,118 +132,6 @@ public sealed class TemperatureSystem : SharedTemperatureSystem
private void ChangeTemperatureOnCollide(Entity ent, ref ProjectileHitEvent args)
{
- _temperature.ChangeHeat(args.Target, ent.Comp.Heat, ent.Comp.IgnoreHeatResistance);// adjust the temperature
- }
-
- private void OnParentChange(EntityUid uid, TemperatureComponent component,
- ref EntParentChangedMessage args)
- {
- var temperatureQuery = GetEntityQuery();
- var transformQuery = GetEntityQuery();
- var thresholdsQuery = GetEntityQuery();
- // We only need to update thresholds if the thresholds changed for the entity's ancestors.
- var oldThresholds = args.OldParent != null
- ? RecalculateParentThresholds(args.OldParent.Value, transformQuery, thresholdsQuery)
- : (null, null);
- var newThresholds = RecalculateParentThresholds(transformQuery.GetComponent(uid).ParentUid, transformQuery, thresholdsQuery);
-
- if (oldThresholds != newThresholds)
- {
- RecursiveThresholdUpdate(uid, temperatureQuery, transformQuery, thresholdsQuery);
- }
- }
-
- private void OnParentThresholdStartup(EntityUid uid, ContainerTemperatureDamageThresholdsComponent component,
- ComponentStartup args)
- {
- RecursiveThresholdUpdate(uid, GetEntityQuery(), GetEntityQuery(),
- GetEntityQuery());
- }
-
- private void OnParentThresholdShutdown(EntityUid uid, ContainerTemperatureDamageThresholdsComponent component,
- ComponentShutdown args)
- {
- RecursiveThresholdUpdate(uid, GetEntityQuery(), GetEntityQuery(),
- GetEntityQuery());
- }
-
- ///
- /// Recalculate and apply parent thresholds for the root entity and all its descendant.
- ///
- ///
- ///
- ///
- ///
- private void RecursiveThresholdUpdate(EntityUid root, EntityQuery temperatureQuery,
- EntityQuery transformQuery,
- EntityQuery tempThresholdsQuery)
- {
- RecalculateAndApplyParentThresholds(root, temperatureQuery, transformQuery, tempThresholdsQuery);
-
- var enumerator = Transform(root).ChildEnumerator;
- while (enumerator.MoveNext(out var child))
- {
- RecursiveThresholdUpdate(child, temperatureQuery, transformQuery, tempThresholdsQuery);
- }
- }
-
- ///
- /// Recalculate parent thresholds and apply them on the uid temperature component.
- ///
- ///
- ///
- ///
- ///
- private void RecalculateAndApplyParentThresholds(EntityUid uid,
- EntityQuery temperatureQuery, EntityQuery transformQuery,
- EntityQuery tempThresholdsQuery)
- {
- if (!temperatureQuery.TryGetComponent(uid, out var temperature))
- {
- return;
- }
-
- var newThresholds = RecalculateParentThresholds(transformQuery.GetComponent(uid).ParentUid, transformQuery, tempThresholdsQuery);
- temperature.ParentHeatDamageThreshold = newThresholds.Item1;
- temperature.ParentColdDamageThreshold = newThresholds.Item2;
- }
-
- ///
- /// Recalculate Parent Heat/Cold DamageThreshold by recursively checking each ancestor and fetching the
- /// maximum HeatDamageThreshold and the minimum ColdDamageThreshold if any exists (aka the best value for each).
- ///
- ///
- ///
- ///
- private (float?, float?) RecalculateParentThresholds(
- EntityUid initialParentUid,
- EntityQuery transformQuery,
- EntityQuery tempThresholdsQuery)
- {
- // Recursively check parents for the best threshold available
- var parentUid = initialParentUid;
- float? newHeatThreshold = null;
- float? newColdThreshold = null;
- while (parentUid.IsValid())
- {
- if (tempThresholdsQuery.TryGetComponent(parentUid, out var newThresholds))
- {
- if (newThresholds.HeatDamageThreshold != null)
- {
- newHeatThreshold = Math.Max(newThresholds.HeatDamageThreshold.Value,
- newHeatThreshold ?? 0);
- }
-
- if (newThresholds.ColdDamageThreshold != null)
- {
- newColdThreshold = Math.Min(newThresholds.ColdDamageThreshold.Value,
- newColdThreshold ?? float.MaxValue);
- }
- }
-
- parentUid = transformQuery.GetComponent(parentUid).ParentUid;
- }
-
- return (newHeatThreshold, newColdThreshold);
+ ChangeHeat(args.Target, ent.Comp.Heat, ent.Comp.IgnoreHeatResistance);// adjust the temperature
}
}
diff --git a/Content.Server/Tesla/EntitySystem/TeslaCoilSystem.cs b/Content.Server/Tesla/EntitySystem/TeslaCoilSystem.cs
index 988ade86ce..d418ea5a18 100644
--- a/Content.Server/Tesla/EntitySystem/TeslaCoilSystem.cs
+++ b/Content.Server/Tesla/EntitySystem/TeslaCoilSystem.cs
@@ -24,7 +24,7 @@ public sealed class TeslaCoilSystem : EntitySystem
{
if (TryComp(coil, out var batteryComponent))
{
- _battery.SetCharge((coil, batteryComponent), batteryComponent.CurrentCharge + coil.Comp.ChargeFromLightning);
+ _battery.ChangeCharge((coil, batteryComponent), coil.Comp.ChargeFromLightning);
}
}
}
diff --git a/Content.Server/Toolshed/Commands/StatusEffects/StatusEffectCommand.cs b/Content.Server/Toolshed/Commands/StatusEffects/StatusEffectCommand.cs
new file mode 100644
index 0000000000..4182bac081
--- /dev/null
+++ b/Content.Server/Toolshed/Commands/StatusEffects/StatusEffectCommand.cs
@@ -0,0 +1,141 @@
+using Content.Server.Administration;
+using Content.Server.Toolshed.TypeParsers.StatusEffects;
+using Content.Shared.Administration;
+using Content.Shared.StatusEffectNew;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Toolshed;
+
+namespace Content.Server.Toolshed.Commands.StatusEffects;
+
+[ToolshedCommand, AdminCommand(AdminFlags.VarEdit)]
+public sealed class StatusEffectCommand : ToolshedCommand
+{
+ private StatusEffectsSystem? _statusEffectsSystem;
+
+ [CommandImplementation("add")]
+ public EntityUid? Add([PipedArgument] EntityUid input, [CommandArgument(typeof(StatusEffectCompletionParser))] EntProtoId status, float time, float delay = 0)
+ {
+ _statusEffectsSystem ??= GetSys();
+
+ _statusEffectsSystem.TryAddStatusEffectDuration(
+ input,
+ status,
+ TimeSpan.FromSeconds(time),
+ ZeroAsNull(delay));
+
+ return input;
+ }
+
+ [CommandImplementation("add")]
+ public IEnumerable Add([PipedArgument] IEnumerable input, [CommandArgument(typeof(StatusEffectCompletionParser))] EntProtoId status, float time, float delay = 0)
+ {
+ _statusEffectsSystem ??= GetSys();
+
+ foreach (var ent in input)
+ {
+ _statusEffectsSystem.TryAddStatusEffectDuration(
+ ent,
+ status,
+ TimeSpan.FromSeconds(time),
+ ZeroAsNull(delay));
+
+ yield return ent;
+ }
+ }
+
+ [CommandImplementation("update")]
+ public EntityUid? Update([PipedArgument] EntityUid input, [CommandArgument(typeof(StatusEffectCompletionParser))] EntProtoId status, float time, float delay = 0)
+ {
+ _statusEffectsSystem ??= GetSys();
+
+ _statusEffectsSystem.TryUpdateStatusEffectDuration(
+ input,
+ status,
+ ZeroAsNull(time),
+ ZeroAsNull(delay));
+
+ return input;
+ }
+
+ [CommandImplementation("update")]
+ public IEnumerable Update([PipedArgument] IEnumerable input, [CommandArgument(typeof(StatusEffectCompletionParser))] EntProtoId status, float time, float delay = 0)
+ {
+ _statusEffectsSystem ??= GetSys();
+
+ foreach (var ent in input)
+ {
+ _statusEffectsSystem.TryUpdateStatusEffectDuration(
+ ent,
+ status,
+ ZeroAsNull(time),
+ ZeroAsNull(delay));
+
+ yield return ent;
+ }
+ }
+
+ [CommandImplementation("set")]
+ public EntityUid? Set([PipedArgument] EntityUid input, [CommandArgument(typeof(StatusEffectCompletionParser))] EntProtoId status, float time = 0, float delay = 0)
+ {
+ _statusEffectsSystem ??= GetSys();
+
+ _statusEffectsSystem.TrySetStatusEffectDuration(
+ input,
+ status,
+ ZeroAsNull(time),
+ ZeroAsNull(delay));
+
+ return input;
+ }
+
+ [CommandImplementation("set")]
+ public IEnumerable Set([PipedArgument] IEnumerable input, [CommandArgument(typeof(StatusEffectCompletionParser))] EntProtoId status, float time = 0, float delay = 0)
+ {
+ _statusEffectsSystem ??= GetSys();
+
+ foreach (var ent in input)
+ {
+ _statusEffectsSystem.TrySetStatusEffectDuration(
+ ent,
+ status,
+ ZeroAsNull(time),
+ ZeroAsNull(delay));
+
+ yield return ent;
+ }
+ }
+
+ [CommandImplementation("remove")]
+ public EntityUid? Remove([PipedArgument] EntityUid input, [CommandArgument(typeof(StatusEffectCompletionParser))] EntProtoId status, float time = 0)
+ {
+ _statusEffectsSystem ??= GetSys();
+
+ _statusEffectsSystem.TryRemoveTime(
+ input,
+ status,
+ ZeroAsNull(time));
+
+ return input;
+ }
+
+ [CommandImplementation("remove")]
+ public IEnumerable Remove([PipedArgument] IEnumerable