diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index b2aeb6197a..f8f22fb3f8 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -8,7 +8,7 @@ This isn’t an exhaustive list of things that you can’t do. Rather, take it i
This code of conduct applies specifically to the Github repositories and its spaces managed by the Space Station 14 project or Space Wizards Federation. Some spaces, such as the Space Station 14 Discord or the official Wizard's Den game servers, have their own rules but are in spirit equal to what may be found in here.
-If you believe someone is violating the code of conduct, we ask that you report it by contacting a Maintainer, Project Manager or Wizard staff member through [Discord](https://discord.ss14.io/), [the forums](https://forum.spacestation14.com/), or emailing [telecommunications@spacestation14.com](mailto:telecommunications@spacestation14.com).
+If you believe someone is violating the code of conduct, we ask that you report it by contacting a Maintainer, Project Manager or Wizard staff member through [Discord](https://discord.ss14.io/), [the forums](https://forum.spacestation14.com/), or emailing [support@spacestation14.com](mailto:support@spacestation14.com).
- **Be friendly and patient.**
- **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.
diff --git a/Content.Benchmarks/DestructibleBenchmark.cs b/Content.Benchmarks/DestructibleBenchmark.cs
new file mode 100644
index 0000000000..1b54bacca0
--- /dev/null
+++ b/Content.Benchmarks/DestructibleBenchmark.cs
@@ -0,0 +1,160 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using Content.IntegrationTests;
+using Content.IntegrationTests.Pair;
+using Content.Server.Destructible;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.Damage.Systems;
+using Content.Shared.FixedPoint;
+using Content.Shared.Maps;
+using Robust.Shared;
+using Robust.Shared.Analyzers;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Benchmarks;
+
+[Virtual]
+[GcServer(true)]
+[MemoryDiagnoser]
+public class DestructibleBenchmark
+{
+ ///
+ /// Number of destructible entities per prototype to spawn with a .
+ ///
+ [Params(1, 10, 100, 1000, 5000)]
+ public int EntityCount;
+
+ ///
+ /// Amount of blunt damage we do to each entity.
+ ///
+ [Params(10000)]
+ public FixedPoint2 DamageAmount;
+
+ [Params("Blunt")]
+ public ProtoId DamageType;
+
+ private static readonly EntProtoId WindowProtoId = "Window";
+ private static readonly EntProtoId WallProtoId = "WallReinforced";
+ private static readonly EntProtoId HumanProtoId = "MobHuman";
+
+ private static readonly ProtoId TileRef = "Plating";
+
+ private readonly EntProtoId[] _prototypes = [WindowProtoId, WallProtoId, HumanProtoId];
+
+ private readonly List> _damageables = new();
+ private readonly List> _destructbiles = new();
+
+ private DamageSpecifier _damage;
+
+ private TestPair _pair = default!;
+ private IEntityManager _entMan = default!;
+ private IPrototypeManager _protoMan = default!;
+ private IRobustRandom _random = default!;
+ private ITileDefinitionManager _tileDefMan = default!;
+ private DamageableSystem _damageable = default!;
+ private DestructibleSystem _destructible = default!;
+ private SharedMapSystem _map = default!;
+
+ [GlobalSetup]
+ public async Task SetupAsync()
+ {
+ ProgramShared.PathOffset = "../../../../";
+ PoolManager.Startup();
+ _pair = await PoolManager.GetServerClient();
+ var server = _pair.Server;
+
+ var mapdata = await _pair.CreateTestMap();
+
+ _entMan = server.ResolveDependency();
+ _protoMan = server.ResolveDependency();
+ _random = server.ResolveDependency();
+ _tileDefMan = server.ResolveDependency();
+ _damageable = _entMan.System();
+ _destructible = _entMan.System();
+ _map = _entMan.System();
+
+ if (!_protoMan.Resolve(DamageType, out var type))
+ return;
+
+ _damage = new DamageSpecifier(type, DamageAmount);
+
+ _random.SetSeed(69420); // Randomness needs to be deterministic for benchmarking.
+
+ var plating = _tileDefMan[TileRef].TileId;
+
+ // We make a rectangular grid of destructible entities, and then damage them all simultaneously to stress test the system.
+ // Needed for managing the performance of destructive effects and damage application.
+ await server.WaitPost(() =>
+ {
+ // Set up a thin line of tiles to place our objects on. They should be anchored for a "realistic" scenario...
+ for (var x = 0; x < EntityCount; x++)
+ {
+ for (var y = 0; y < _prototypes.Length; y++)
+ {
+ _map.SetTile(mapdata.Grid, mapdata.Grid, new Vector2i(x, y), new Tile(plating));
+ }
+ }
+
+ for (var x = 0; x < EntityCount; x++)
+ {
+ var y = 0;
+ foreach (var protoId in _prototypes)
+ {
+ var coords = new EntityCoordinates(mapdata.Grid, x + 0.5f, y + 0.5f);
+ _entMan.SpawnEntity(protoId, coords);
+ y++;
+ }
+ }
+
+ var query = _entMan.EntityQueryEnumerator();
+
+ while (query.MoveNext(out var uid, out var damageable, out var destructible))
+ {
+ _damageables.Add((uid, damageable));
+ _destructbiles.Add((uid, damageable, destructible));
+ }
+ });
+ }
+
+ [Benchmark]
+ public async Task PerformDealDamage()
+ {
+ await _pair.Server.WaitPost(() =>
+ {
+ _damageable.ApplyDamageToAllEntities(_damageables, _damage);
+ });
+ }
+
+ [Benchmark]
+ public async Task PerformTestTriggers()
+ {
+ await _pair.Server.WaitPost(() =>
+ {
+ _destructible.TestAllTriggers(_destructbiles);
+ });
+ }
+
+ [Benchmark]
+ public async Task PerformTestBehaviors()
+ {
+ await _pair.Server.WaitPost(() =>
+ {
+ _destructible.TestAllBehaviors(_destructbiles);
+ });
+ }
+
+
+ [GlobalCleanup]
+ public async Task CleanupAsync()
+ {
+ await _pair.DisposeAsync();
+ PoolManager.Shutdown();
+ }
+}
diff --git a/Content.Benchmarks/GasReactionBenchmark.cs b/Content.Benchmarks/GasReactionBenchmark.cs
new file mode 100644
index 0000000000..9ed30373d1
--- /dev/null
+++ b/Content.Benchmarks/GasReactionBenchmark.cs
@@ -0,0 +1,253 @@
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using Content.IntegrationTests;
+using Content.IntegrationTests.Pair;
+using Content.Server.Atmos;
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Atmos.Reactions;
+using Content.Shared.Atmos;
+using Robust.Shared;
+using Robust.Shared.Analyzers;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Maths;
+
+namespace Content.Benchmarks;
+
+///
+/// Benchmarks the performance of different gas reactions.
+/// Tests each reaction type with realistic gas mixtures to measure computational cost.
+///
+[Virtual]
+[GcServer(true)]
+[MemoryDiagnoser]
+public class GasReactionBenchmark
+{
+ private const int Iterations = 1000;
+ private TestPair _pair = default!;
+ private AtmosphereSystem _atmosphereSystem = default!;
+
+ // Grid and tile for reactions that need a holder
+ private EntityUid _testGrid = default!;
+ private TileAtmosphere _testTile = default!;
+ // Reaction instances
+ private PlasmaFireReaction _plasmaFireReaction = default!;
+ private TritiumFireReaction _tritiumFireReaction = default!;
+ private FrezonProductionReaction _frezonProductionReaction = default!;
+ private FrezonCoolantReaction _frezonCoolantReaction = default!;
+ private AmmoniaOxygenReaction _ammoniaOxygenReaction = default!;
+ private N2ODecompositionReaction _n2oDecompositionReaction = default!;
+ private WaterVaporReaction _waterVaporReaction = default!;
+ // Gas mixtures for each reaction type
+ private GasMixture _plasmaFireMixture = default!;
+ private GasMixture _tritiumFireMixture = default!;
+ private GasMixture _frezonProductionMixture = default!;
+ private GasMixture _frezonCoolantMixture = default!;
+ private GasMixture _ammoniaOxygenMixture = default!;
+ private GasMixture _n2oDecompositionMixture = default!;
+ private GasMixture _waterVaporMixture = default!;
+
+ [GlobalSetup]
+ public async Task SetupAsync()
+ {
+ ProgramShared.PathOffset = "../../../../";
+ PoolManager.Startup();
+ _pair = await PoolManager.GetServerClient();
+ var server = _pair.Server;
+
+ // Create test map and grid
+ var mapData = await _pair.CreateTestMap();
+ _testGrid = mapData.Grid;
+
+ await server.WaitPost(() =>
+ {
+ var entMan = server.ResolveDependency();
+ _atmosphereSystem = entMan.System();
+
+ _plasmaFireReaction = new PlasmaFireReaction();
+ _tritiumFireReaction = new TritiumFireReaction();
+ _frezonProductionReaction = new FrezonProductionReaction();
+ _frezonCoolantReaction = new FrezonCoolantReaction();
+ _ammoniaOxygenReaction = new AmmoniaOxygenReaction();
+ _n2oDecompositionReaction = new N2ODecompositionReaction();
+ _waterVaporReaction = new WaterVaporReaction();
+
+ SetupGasMixtures();
+ SetupTile();
+ });
+ }
+
+ private void SetupGasMixtures()
+ {
+ // Plasma Fire: Plasma + Oxygen at high temperature
+ // Temperature must be > PlasmaMinimumBurnTemperature for reaction to occur
+ _plasmaFireMixture = new GasMixture(Atmospherics.CellVolume)
+ {
+ Temperature = Atmospherics.PlasmaMinimumBurnTemperature + 100f // ~673K
+ };
+ _plasmaFireMixture.AdjustMoles(Gas.Plasma, 20f);
+ _plasmaFireMixture.AdjustMoles(Gas.Oxygen, 100f);
+
+ // Tritium Fire: Tritium + Oxygen at high temperature
+ // Temperature must be > FireMinimumTemperatureToExist for reaction to occur
+ _tritiumFireMixture = new GasMixture(Atmospherics.CellVolume)
+ {
+ Temperature = Atmospherics.FireMinimumTemperatureToExist + 100f // ~473K
+ };
+ _tritiumFireMixture.AdjustMoles(Gas.Tritium, 20f);
+ _tritiumFireMixture.AdjustMoles(Gas.Oxygen, 100f);
+
+ // Frezon Production: Oxygen + Tritium + Nitrogen catalyst
+ // Optimal temperature for efficiency (80% of max efficiency temp)
+ _frezonProductionMixture = new GasMixture(Atmospherics.CellVolume)
+ {
+ Temperature = Atmospherics.FrezonProductionMaxEfficiencyTemperature * 0.8f // ~48K
+ };
+ _frezonProductionMixture.AdjustMoles(Gas.Oxygen, 50f);
+ _frezonProductionMixture.AdjustMoles(Gas.Tritium, 50f);
+ _frezonProductionMixture.AdjustMoles(Gas.Nitrogen, 10f);
+
+ // Frezon Coolant: Frezon + Nitrogen
+ // Temperature must be > FrezonCoolLowerTemperature (23.15K) for reaction to occur
+ _frezonCoolantMixture = new GasMixture(Atmospherics.CellVolume)
+ {
+ Temperature = Atmospherics.T20C + 50f // ~343K
+ };
+ _frezonCoolantMixture.AdjustMoles(Gas.Frezon, 30f);
+ _frezonCoolantMixture.AdjustMoles(Gas.Nitrogen, 100f);
+
+ // Ammonia + Oxygen reaction (concentration-dependent, no temp requirement)
+ _ammoniaOxygenMixture = new GasMixture(Atmospherics.CellVolume)
+ {
+ Temperature = Atmospherics.T20C + 100f // ~393K
+ };
+ _ammoniaOxygenMixture.AdjustMoles(Gas.Ammonia, 40f);
+ _ammoniaOxygenMixture.AdjustMoles(Gas.Oxygen, 40f);
+
+ // N2O Decomposition (no temperature requirement, just needs N2O moles)
+ _n2oDecompositionMixture = new GasMixture(Atmospherics.CellVolume)
+ {
+ Temperature = Atmospherics.T20C + 100f // ~393K
+ };
+ _n2oDecompositionMixture.AdjustMoles(Gas.NitrousOxide, 100f);
+
+ // Water Vapor - needs water vapor to condense
+ _waterVaporMixture = new GasMixture(Atmospherics.CellVolume)
+ {
+ Temperature = Atmospherics.T20C
+ };
+ _waterVaporMixture.AdjustMoles(Gas.WaterVapor, 50f);
+ }
+
+ private void SetupTile()
+ {
+ // Create a tile atmosphere to use as holder for all reactions
+ var testIndices = new Vector2i(0, 0);
+ _testTile = new TileAtmosphere(_testGrid, testIndices, new GasMixture(Atmospherics.CellVolume)
+ {
+ Temperature = Atmospherics.T20C
+ });
+ }
+
+ private static GasMixture CloneMixture(GasMixture original)
+ {
+ return new GasMixture(original);
+ }
+
+ [Benchmark]
+ public async Task PlasmaFireReaction()
+ {
+ await _pair.Server.WaitPost(() =>
+ {
+ for (var i = 0; i < Iterations; i++)
+ {
+ var mixture = CloneMixture(_plasmaFireMixture);
+ _plasmaFireReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
+ }
+ });
+ }
+
+ [Benchmark]
+ public async Task TritiumFireReaction()
+ {
+ await _pair.Server.WaitPost(() =>
+ {
+ for (var i = 0; i < Iterations; i++)
+ {
+ var mixture = CloneMixture(_tritiumFireMixture);
+ _tritiumFireReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
+ }
+ });
+ }
+
+ [Benchmark]
+ public async Task FrezonProductionReaction()
+ {
+ await _pair.Server.WaitPost(() =>
+ {
+ for (var i = 0; i < Iterations; i++)
+ {
+ var mixture = CloneMixture(_frezonProductionMixture);
+ _frezonProductionReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
+ }
+ });
+ }
+
+ [Benchmark]
+ public async Task FrezonCoolantReaction()
+ {
+ await _pair.Server.WaitPost(() =>
+ {
+ for (var i = 0; i < Iterations; i++)
+ {
+ var mixture = CloneMixture(_frezonCoolantMixture);
+ _frezonCoolantReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
+ }
+ });
+ }
+
+ [Benchmark]
+ public async Task AmmoniaOxygenReaction()
+ {
+ await _pair.Server.WaitPost(() =>
+ {
+ for (var i = 0; i < Iterations; i++)
+ {
+ var mixture = CloneMixture(_ammoniaOxygenMixture);
+ _ammoniaOxygenReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
+ }
+ });
+ }
+
+ [Benchmark]
+ public async Task N2ODecompositionReaction()
+ {
+ await _pair.Server.WaitPost(() =>
+ {
+ for (var i = 0; i < Iterations; i++)
+ {
+ var mixture = CloneMixture(_n2oDecompositionMixture);
+ _n2oDecompositionReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
+ }
+ });
+ }
+
+ [Benchmark]
+ public async Task WaterVaporReaction()
+ {
+ await _pair.Server.WaitPost(() =>
+ {
+ for (var i = 0; i < Iterations; i++)
+ {
+ var mixture = CloneMixture(_waterVaporMixture);
+ _waterVaporReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
+ }
+ });
+ }
+
+ [GlobalCleanup]
+ public async Task CleanupAsync()
+ {
+ await _pair.DisposeAsync();
+ PoolManager.Shutdown();
+ }
+}
diff --git a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
index ea96b13376..2b8ebf53b7 100644
--- a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
+++ b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
@@ -9,7 +9,6 @@ using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using System.Numerics;
using System.Linq;
-using Content.Client.Stylesheets;
namespace Content.Client.Access.UI
{
diff --git a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
index 7566942506..cb2839f5d0 100644
--- a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
+++ b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
@@ -294,19 +294,19 @@ public sealed partial class BanPanel : DefaultWindow
}
///
- /// Adds a check button specifically for one "role" in a "group"
+ /// Adds a toggle button specifically for one "role" in a "group"
/// E.g. it would add the Chief Medical Officer "role" into the "Medical" group.
///
private void AddRoleCheckbox(string group, string role, GridContainer roleGroupInnerContainer, Button roleGroupCheckbox)
{
var roleCheckboxContainer = new BoxContainer();
- var roleCheckButton = new Button
+ var roleToggleButton = new Button
{
Name = role,
Text = role,
ToggleMode = true,
};
- roleCheckButton.OnToggled += args =>
+ roleToggleButton.OnToggled += args =>
{
// Checks the role group checkbox if all the children are pressed
if (args.Pressed && _roleCheckboxes[group].All(e => e.Item1.Pressed))
@@ -343,12 +343,12 @@ public sealed partial class BanPanel : DefaultWindow
roleCheckboxContainer.AddChild(jobIconTexture);
}
- roleCheckboxContainer.AddChild(roleCheckButton);
+ roleCheckboxContainer.AddChild(roleToggleButton);
roleGroupInnerContainer.AddChild(roleCheckboxContainer);
_roleCheckboxes.TryAdd(group, []);
- _roleCheckboxes[group].Add((roleCheckButton, rolePrototype));
+ _roleCheckboxes[group].Add((roleToggleButton, rolePrototype));
}
public void UpdateBanFlag(bool newFlag)
diff --git a/Content.Client/Atmos/EntitySystems/DeltaPressureSystem.cs b/Content.Client/Atmos/EntitySystems/DeltaPressureSystem.cs
new file mode 100644
index 0000000000..3d9893ac62
--- /dev/null
+++ b/Content.Client/Atmos/EntitySystems/DeltaPressureSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Atmos.EntitySystems;
+
+namespace Content.Client.Atmos.EntitySystems;
+
+public sealed class DeltaPressureSystem : SharedDeltaPressureSystem;
diff --git a/Content.Client/Changelog/ChangelogManager.cs b/Content.Client/Changelog/ChangelogManager.cs
index 657d0cb3ac..545d001aab 100644
--- a/Content.Client/Changelog/ChangelogManager.cs
+++ b/Content.Client/Changelog/ChangelogManager.cs
@@ -52,6 +52,7 @@ namespace Content.Client.Changelog
// Open changelog purely to compare to the last viewed date.
var changelogs = await LoadChangelog();
UpdateChangelogs(changelogs);
+ _configManager.OnValueChanged(CCVars.ServerId, OnServerIdCVarChanged);
}
private void UpdateChangelogs(List changelogs)
@@ -81,6 +82,11 @@ namespace Content.Client.Changelog
MaxId = changelog.Entries.Max(c => c.Id);
+ CheckLastSeenEntry();
+ }
+
+ private void CheckLastSeenEntry()
+ {
var path = new ResPath($"/changelog_last_seen_{_configManager.GetCVar(CCVars.ServerId)}");
if (_resource.UserData.TryReadAllText(path, out var lastReadIdText))
{
@@ -92,6 +98,11 @@ namespace Content.Client.Changelog
NewChangelogEntriesChanged?.Invoke();
}
+ private void OnServerIdCVarChanged(string newValue)
+ {
+ CheckLastSeenEntry();
+ }
+
public Task> LoadChangelog()
{
return Task.Run(() =>
diff --git a/Content.Client/Chat/Managers/ChatManager.cs b/Content.Client/Chat/Managers/ChatManager.cs
index 68707e021c..1b66bf8732 100644
--- a/Content.Client/Chat/Managers/ChatManager.cs
+++ b/Content.Client/Chat/Managers/ChatManager.cs
@@ -31,6 +31,11 @@ internal sealed class ChatManager : IChatManager
// See server-side manager. This just exists for shared code.
}
+ public void SendAdminAlertNoFormatOrEscape(string message)
+ {
+ // See server-side manager. This just exists for shared code.
+ }
+
public void SendMessage(string text, ChatSelectChannel channel)
{
var str = text.ToString();
diff --git a/Content.Client/Damage/DamageVisualsSystem.cs b/Content.Client/Damage/DamageVisualsSystem.cs
index 065bf628bc..ac3ff819aa 100644
--- a/Content.Client/Damage/DamageVisualsSystem.cs
+++ b/Content.Client/Damage/DamageVisualsSystem.cs
@@ -1,5 +1,6 @@
using System.Linq;
using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Client.GameObjects;
diff --git a/Content.Client/Disposal/Mailing/MailingUnitWindow.xaml b/Content.Client/Disposal/Mailing/MailingUnitWindow.xaml
index 0acd300895..ab910d2db4 100644
--- a/Content.Client/Disposal/Mailing/MailingUnitWindow.xaml
+++ b/Content.Client/Disposal/Mailing/MailingUnitWindow.xaml
@@ -47,7 +47,7 @@
Access="Public"
Text="{Loc 'ui-disposal-unit-button-eject'}"
StyleClasses="OpenBoth" />
-
diff --git a/Content.Client/Disposal/Unit/DisposalUnitWindow.xaml b/Content.Client/Disposal/Unit/DisposalUnitWindow.xaml
index 60ca7ba0db..312f80c176 100644
--- a/Content.Client/Disposal/Unit/DisposalUnitWindow.xaml
+++ b/Content.Client/Disposal/Unit/DisposalUnitWindow.xaml
@@ -34,7 +34,7 @@
Access="Public"
Text="{Loc 'ui-disposal-unit-button-eject'}"
StyleClasses="OpenBoth" />
-
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
index 225619b031..533a8b9f2c 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
@@ -1,28 +1,22 @@
using System.Linq;
using System.Numerics;
-using Content.Client.Message;
using Content.Shared.Atmos;
using Content.Client.UserInterface.Controls;
-using Content.Shared.Alert;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.IdentityManagement;
-using Content.Shared.Inventory;
using Content.Shared.MedicalScanner;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
-using Content.Shared.Mobs.Systems;
-using Content.Shared.Nutrition.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.ResourceManagement;
-using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
diff --git a/Content.Client/Instruments/UI/ChannelsMenu.xaml b/Content.Client/Instruments/UI/ChannelsMenu.xaml
index 20e4a3e923..e98b03e6b3 100644
--- a/Content.Client/Instruments/UI/ChannelsMenu.xaml
+++ b/Content.Client/Instruments/UI/ChannelsMenu.xaml
@@ -7,7 +7,7 @@
-
diff --git a/Content.Client/Inventory/StrippableBoundUserInterface.cs b/Content.Client/Inventory/StrippableBoundUserInterface.cs
index 6d18bd6bda..f9a6cd46d6 100644
--- a/Content.Client/Inventory/StrippableBoundUserInterface.cs
+++ b/Content.Client/Inventory/StrippableBoundUserInterface.cs
@@ -190,7 +190,7 @@ namespace Content.Client.Inventory
if (EntMan.TryGetComponent(heldEntity, out var virt))
{
button.Blocked = true;
- if (EntMan.TryGetComponent(Owner, out var cuff) && _cuffable.GetAllCuffs(cuff).Contains(virt.BlockingEntity))
+ if (_cuffable.TryGetAllCuffs(Owner, out var cuffs) && cuffs.Contains(virt.BlockingEntity))
button.BlockedRect.MouseFilter = MouseFilterMode.Ignore;
}
diff --git a/Content.Client/Overlays/EntityHealthBarOverlay.cs b/Content.Client/Overlays/EntityHealthBarOverlay.cs
index cf9d879844..55fc1e0974 100644
--- a/Content.Client/Overlays/EntityHealthBarOverlay.cs
+++ b/Content.Client/Overlays/EntityHealthBarOverlay.cs
@@ -1,7 +1,7 @@
using System.Numerics;
using Content.Client.StatusIcon;
using Content.Client.UserInterface.Systems;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
diff --git a/Content.Client/Overlays/ShowHealthIconsSystem.cs b/Content.Client/Overlays/ShowHealthIconsSystem.cs
index 0a875e79c5..7fbc0cd944 100644
--- a/Content.Client/Overlays/ShowHealthIconsSystem.cs
+++ b/Content.Client/Overlays/ShowHealthIconsSystem.cs
@@ -1,5 +1,4 @@
using Content.Shared.Atmos.Rotting;
-using Content.Shared.Damage;
using Content.Shared.Inventory.Events;
using Content.Shared.Mobs.Components;
using Content.Shared.Overlays;
@@ -7,6 +6,7 @@ using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Prototypes;
using System.Linq;
+using Content.Shared.Damage.Components;
namespace Content.Client.Overlays;
diff --git a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
index 25d3d58226..f1fdb51aef 100644
--- a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
+++ b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
@@ -689,7 +689,7 @@ public sealed partial class ChatUIController : UIController
radioChannel = null;
return _player.LocalEntity is EntityUid { Valid: true } uid
&& _chatSys != null
- && _chatSys.TryProccessRadioMessage(uid, text, out _, out radioChannel, quiet: true);
+ && _chatSys.TryProcessRadioMessage(uid, text, out _, out radioChannel, quiet: true);
}
public void UpdateSelectedChannel(ChatBox box)
diff --git a/Content.Client/UserInterface/Systems/DamageOverlays/DamageOverlayUiController.cs b/Content.Client/UserInterface/Systems/DamageOverlays/DamageOverlayUiController.cs
index 55d00fec18..20db76554d 100644
--- a/Content.Client/UserInterface/Systems/DamageOverlays/DamageOverlayUiController.cs
+++ b/Content.Client/UserInterface/Systems/DamageOverlays/DamageOverlayUiController.cs
@@ -1,4 +1,4 @@
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs
index 49b5b4a25b..3fbd4dce69 100644
--- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs
+++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs
@@ -216,7 +216,7 @@ public sealed partial class MeleeWeaponSystem
var query = EntityQueryEnumerator();
while (query.MoveNext(out var uid, out var arcComponent, out var xform))
{
- if (arcComponent.User == null)
+ if (arcComponent.User == null || EntityManager.Deleted(arcComponent.User))
continue;
Vector2 targetPos = TransformSystem.GetWorldPosition(arcComponent.User.Value);
diff --git a/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs b/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs
index e6cb596b94..e11d5e7158 100644
--- a/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs
+++ b/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs
@@ -149,6 +149,15 @@ public sealed class BulletRender : BaseBulletRenderer
public const int BulletHeight = 12;
public const int VerticalSeparation = 2;
+ private static readonly LayoutParameters LayoutLarge = new LayoutParameters
+ {
+ ItemHeight = BulletHeight,
+ ItemSeparation = 6,
+ ItemWidth = 5,
+ VerticalSeparation = VerticalSeparation,
+ MinCountPerRow = MinCountPerRow
+ };
+
private static readonly LayoutParameters LayoutNormal = new LayoutParameters
{
ItemHeight = BulletHeight,
@@ -185,8 +194,9 @@ public sealed class BulletRender : BaseBulletRenderer
if (_type == value)
return;
- Parameters = _type switch
+ Parameters = value switch
{
+ BulletType.Large => LayoutLarge,
BulletType.Normal => LayoutNormal,
BulletType.Tiny => LayoutTiny,
_ => throw new ArgumentOutOfRangeException()
@@ -218,6 +228,7 @@ public sealed class BulletRender : BaseBulletRenderer
public enum BulletType
{
+ Large,
Normal,
Tiny
}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
index b9a5d1df9a..dc27a5db87 100644
--- a/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
@@ -110,7 +110,12 @@ public sealed partial class GunSystem
_bulletRender.Count = count;
_bulletRender.Capacity = capacity;
- _bulletRender.Type = capacity > 50 ? BulletRender.BulletType.Tiny : BulletRender.BulletType.Normal;
+ _bulletRender.Type = capacity switch
+ {
+ > 50 => BulletRender.BulletType.Tiny,
+ > 15 => BulletRender.BulletType.Normal,
+ _ => BulletRender.BulletType.Large
+ };
}
}
@@ -236,7 +241,12 @@ public sealed partial class GunSystem
_bulletRender.Count = count;
_bulletRender.Capacity = capacity;
- _bulletRender.Type = capacity > 50 ? BulletRender.BulletType.Tiny : BulletRender.BulletType.Normal;
+ _bulletRender.Type = capacity switch
+ {
+ > 50 => BulletRender.BulletType.Tiny,
+ > 15 => BulletRender.BulletType.Normal,
+ _ => BulletRender.BulletType.Large
+ };
_ammoCount.Text = $"x{count:00}";
}
diff --git a/Content.IntegrationTests/Pair/TestPair.Helpers.cs b/Content.IntegrationTests/Pair/TestPair.Helpers.cs
index 1a3b38e829..4d02b560c9 100644
--- a/Content.IntegrationTests/Pair/TestPair.Helpers.cs
+++ b/Content.IntegrationTests/Pair/TestPair.Helpers.cs
@@ -4,8 +4,13 @@ using System.Linq;
using Content.Server.Preferences.Managers;
using Content.Shared.Preferences;
using Content.Shared.Roles;
+using Robust.Shared.EntitySerialization;
+using Robust.Shared.EntitySerialization.Systems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
namespace Content.IntegrationTests.Pair;
@@ -15,13 +20,49 @@ public sealed partial class TestPair
public Task CreateTestMap(bool initialized = true)
=> CreateTestMap(initialized, "Plating");
+ ///
+ /// Loads a test map and returns a representing it.
+ ///
+ /// The to the test map to load.
+ /// Whether to initialize the map on load.
+ /// A representing the loaded map.
+ public async Task LoadTestMap(ResPath testMapPath, bool initialized = true)
+ {
+ TestMapData mapData = new();
+ var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = initialized };
+ var mapLoaderSys = Server.EntMan.System();
+ var mapSys = Server.System();
+
+ // Load our test map in and assert that it exists.
+ await Server.WaitAssertion(() =>
+ {
+ Assert.That(mapLoaderSys.TryLoadMap(testMapPath, out var map, out var gridSet, deserializationOptions),
+ $"Failed to load map {testMapPath}.");
+ Assert.That(gridSet, Is.Not.Empty, "There were no grids loaded from the map!");
+
+ mapData.MapUid = map!.Value.Owner;
+ mapData.MapId = map!.Value.Comp.MapId;
+ mapData.Grid = gridSet!.First();
+ mapData.GridCoords = new EntityCoordinates(mapData.Grid, 0, 0);
+ mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);
+ mapData.Tile = mapSys.GetAllTiles(mapData.Grid.Owner, mapData.Grid.Comp).First();
+ });
+
+ await RunTicksSync(10);
+ mapData.CMapUid = ToClientUid(mapData.MapUid);
+ mapData.CGridUid = ToClientUid(mapData.Grid);
+ mapData.CGridCoords = new EntityCoordinates(mapData.CGridUid, 0, 0);
+
+ return mapData;
+ }
+
///
/// Set a user's antag preferences. Modified preferences are automatically reset at the end of the test.
///
public async Task SetAntagPreference(ProtoId id, bool value, NetUserId? user = null)
{
user ??= Client.User!.Value;
- if (user is not {} userId)
+ if (user is not { } userId)
return;
var prefMan = Server.ResolveDependency();
@@ -30,7 +71,7 @@ public sealed partial class TestPair
// Automatic preference resetting only resets slot 0.
Assert.That(prefs.SelectedCharacterIndex, Is.EqualTo(0));
- var profile = (HumanoidCharacterProfile) prefs.Characters[0];
+ var profile = (HumanoidCharacterProfile)prefs.Characters[0];
var newProfile = profile.WithAntagPreference(id, value);
_modifiedProfiles.Add(userId);
await Server.WaitPost(() => prefMan.SetProfile(userId, 0, newProfile).Wait());
@@ -58,7 +99,7 @@ public sealed partial class TestPair
var prefMan = Server.ResolveDependency();
var prefs = prefMan.GetPreferences(user);
- var profile = (HumanoidCharacterProfile) prefs.Characters[0];
+ var profile = (HumanoidCharacterProfile)prefs.Characters[0];
var dictionary = new Dictionary, JobPriority>(profile.JobPriorities);
// Automatic preference resetting only resets slot 0.
diff --git a/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs b/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
index 839e0ea897..c3b3877c98 100644
--- a/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
+++ b/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
@@ -4,6 +4,7 @@ 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;
diff --git a/Content.IntegrationTests/Tests/Chemistry/SolutionRoundingTest.cs b/Content.IntegrationTests/Tests/Chemistry/SolutionRoundingTest.cs
index 89d33186a2..5b5829d386 100644
--- a/Content.IntegrationTests/Tests/Chemistry/SolutionRoundingTest.cs
+++ b/Content.IntegrationTests/Tests/Chemistry/SolutionRoundingTest.cs
@@ -64,6 +64,11 @@ public sealed class SolutionRoundingTest
SolutionRoundingTestReagentD: 1
";
+ private const string SolutionRoundingTestReagentA = "SolutionRoundingTestReagentA";
+ private const string SolutionRoundingTestReagentB = "SolutionRoundingTestReagentB";
+ private const string SolutionRoundingTestReagentC = "SolutionRoundingTestReagentC";
+ private const string SolutionRoundingTestReagentD = "SolutionRoundingTestReagentD";
+
[Test]
public async Task Test()
{
@@ -84,12 +89,12 @@ public sealed class SolutionRoundingTest
solutionEnt = newSolutionEnt!.Value;
solution = newSolution!;
- system.TryAddSolution(solutionEnt, new Solution("SolutionRoundingTestReagentC", 50));
- system.TryAddSolution(solutionEnt, new Solution("SolutionRoundingTestReagentB", 30));
+ system.TryAddSolution(solutionEnt, new Solution(SolutionRoundingTestReagentC, 50));
+ system.TryAddSolution(solutionEnt, new Solution(SolutionRoundingTestReagentB, 30));
for (var i = 0; i < 9; i++)
{
- system.TryAddSolution(solutionEnt, new Solution("SolutionRoundingTestReagentA", 10));
+ system.TryAddSolution(solutionEnt, new Solution(SolutionRoundingTestReagentA, 10));
}
});
@@ -98,21 +103,21 @@ public sealed class SolutionRoundingTest
Assert.Multiple(() =>
{
Assert.That(
- solution.ContainsReagent("SolutionRoundingTestReagentA", null),
+ solution.ContainsReagent(SolutionRoundingTestReagentA, null),
Is.False,
"Solution should not contain reagent A");
Assert.That(
- solution.ContainsReagent("SolutionRoundingTestReagentB", null),
+ solution.ContainsReagent(SolutionRoundingTestReagentB, null),
Is.False,
"Solution should not contain reagent B");
Assert.That(
- solution![new ReagentId("SolutionRoundingTestReagentC", null)].Quantity,
+ solution![new ReagentId(SolutionRoundingTestReagentC, null)].Quantity,
Is.EqualTo((FixedPoint2) 20));
Assert.That(
- solution![new ReagentId("SolutionRoundingTestReagentD", null)].Quantity,
+ solution![new ReagentId(SolutionRoundingTestReagentD, null)].Quantity,
Is.EqualTo((FixedPoint2) 30));
});
});
diff --git a/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs b/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs
index 6b71dd08be..6f50f54103 100644
--- a/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs
+++ b/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs
@@ -43,6 +43,13 @@ public sealed class SolutionSystemTests
desc: reagent-desc-nothing
physicalDesc: reagent-physical-desc-nothing
";
+
+ private const string TestReagentA = "TestReagentA";
+ private const string TestReagentB = "TestReagentB";
+ private const string TestReagentC = "TestReagentC";
+ private const string Water = "Water";
+ private const string Oil = "Oil";
+
[Test]
public async Task TryAddTwoNonReactiveReagent()
{
@@ -62,8 +69,8 @@ public sealed class SolutionSystemTests
var oilQuantity = FixedPoint2.New(15);
var waterQuantity = FixedPoint2.New(10);
- var oilAdded = new Solution("Oil", oilQuantity);
- var originalWater = new Solution("Water", waterQuantity);
+ var oilAdded = new Solution(Oil, oilQuantity);
+ var originalWater = new Solution(Water, waterQuantity);
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
Assert.That(containerSystem
@@ -73,8 +80,8 @@ public sealed class SolutionSystemTests
Assert.That(containerSystem
.TryAddSolution(solutionEnt.Value, oilAdded));
- var water = solution.GetTotalPrototypeQuantity("Water");
- var oil = solution.GetTotalPrototypeQuantity("Oil");
+ var water = solution.GetTotalPrototypeQuantity(Water);
+ var oil = solution.GetTotalPrototypeQuantity(Oil);
Assert.Multiple(() =>
{
Assert.That(water, Is.EqualTo(waterQuantity));
@@ -107,8 +114,8 @@ public sealed class SolutionSystemTests
var oilQuantity = FixedPoint2.New(1500);
var waterQuantity = FixedPoint2.New(10);
- var oilAdded = new Solution("Oil", oilQuantity);
- var originalWater = new Solution("Water", waterQuantity);
+ var oilAdded = new Solution(Oil, oilQuantity);
+ var originalWater = new Solution(Water, waterQuantity);
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
Assert.That(containerSystem
@@ -118,8 +125,8 @@ public sealed class SolutionSystemTests
Assert.That(containerSystem
.TryAddSolution(solutionEnt.Value, oilAdded), Is.False);
- var water = solution.GetTotalPrototypeQuantity("Water");
- var oil = solution.GetTotalPrototypeQuantity("Oil");
+ var water = solution.GetTotalPrototypeQuantity(Water);
+ var oil = solution.GetTotalPrototypeQuantity(Oil);
Assert.Multiple(() =>
{
Assert.That(water, Is.EqualTo(waterQuantity));
@@ -153,8 +160,8 @@ public sealed class SolutionSystemTests
var waterQuantity = FixedPoint2.New(10);
var oilQuantity = FixedPoint2.New(ratio * waterQuantity.Int());
- var oilAdded = new Solution("Oil", oilQuantity);
- var originalWater = new Solution("Water", waterQuantity);
+ var oilAdded = new Solution(Oil, oilQuantity);
+ var originalWater = new Solution(Water, waterQuantity);
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
Assert.That(containerSystem
@@ -168,15 +175,15 @@ public sealed class SolutionSystemTests
{
Assert.That(solution.Volume, Is.EqualTo(FixedPoint2.New(threshold)));
- var waterMix = solution.GetTotalPrototypeQuantity("Water");
- var oilMix = solution.GetTotalPrototypeQuantity("Oil");
+ var waterMix = solution.GetTotalPrototypeQuantity(Water);
+ var oilMix = solution.GetTotalPrototypeQuantity(Oil);
Assert.That(waterMix, Is.EqualTo(FixedPoint2.New(threshold / (ratio + 1))));
Assert.That(oilMix, Is.EqualTo(FixedPoint2.New(threshold / (ratio + 1) * ratio)));
Assert.That(overflowingSolution.Volume, Is.EqualTo(FixedPoint2.New(80)));
- var waterOverflow = overflowingSolution.GetTotalPrototypeQuantity("Water");
- var oilOverFlow = overflowingSolution.GetTotalPrototypeQuantity("Oil");
+ var waterOverflow = overflowingSolution.GetTotalPrototypeQuantity(Water);
+ var oilOverFlow = overflowingSolution.GetTotalPrototypeQuantity(Oil);
Assert.That(waterOverflow, Is.EqualTo(waterQuantity - waterMix));
Assert.That(oilOverFlow, Is.EqualTo(oilQuantity - oilMix));
});
@@ -207,8 +214,8 @@ public sealed class SolutionSystemTests
var waterQuantity = FixedPoint2.New(10);
var oilQuantity = FixedPoint2.New(ratio * waterQuantity.Int());
- var oilAdded = new Solution("Oil", oilQuantity);
- var originalWater = new Solution("Water", waterQuantity);
+ var oilAdded = new Solution(Oil, oilQuantity);
+ var originalWater = new Solution(Water, waterQuantity);
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
Assert.That(containerSystem
@@ -234,24 +241,23 @@ public sealed class SolutionSystemTests
// Adding reagent with adjusts temperature
await server.WaitAssertion(() =>
{
-
- var solution = new Solution("TestReagentA", FixedPoint2.New(100)) { Temperature = temp };
+ var solution = new Solution(TestReagentA, FixedPoint2.New(100)) { Temperature = temp };
Assert.That(solution.Temperature, Is.EqualTo(temp * 1));
- solution.AddSolution(new Solution("TestReagentA", FixedPoint2.New(100)) { Temperature = temp * 3 }, protoMan);
+ solution.AddSolution(new Solution(TestReagentA, FixedPoint2.New(100)) { Temperature = temp * 3 }, protoMan);
Assert.That(solution.Temperature, Is.EqualTo(temp * 2));
- solution.AddSolution(new Solution("TestReagentB", FixedPoint2.New(100)) { Temperature = temp * 5 }, protoMan);
+ solution.AddSolution(new Solution(TestReagentB, FixedPoint2.New(100)) { Temperature = temp * 5 }, protoMan);
Assert.That(solution.Temperature, Is.EqualTo(temp * 3));
});
// adding solutions combines thermal energy
await server.WaitAssertion(() =>
{
- var solutionOne = new Solution("TestReagentA", FixedPoint2.New(100)) { Temperature = temp };
+ var solutionOne = new Solution(TestReagentA, FixedPoint2.New(100)) { Temperature = temp };
- var solutionTwo = new Solution("TestReagentB", FixedPoint2.New(100)) { Temperature = temp };
- solutionTwo.AddReagent("TestReagentC", FixedPoint2.New(100));
+ var solutionTwo = new Solution(TestReagentB, FixedPoint2.New(100)) { Temperature = temp };
+ solutionTwo.AddReagent(TestReagentC, FixedPoint2.New(100));
var thermalEnergyOne = solutionOne.GetHeatCapacity(protoMan) * solutionOne.Temperature;
var thermalEnergyTwo = solutionTwo.GetHeatCapacity(protoMan) * solutionTwo.Temperature;
diff --git a/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs b/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs
index 53604b2c1e..87ff00691a 100644
--- a/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs
+++ b/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs
@@ -1,6 +1,8 @@
using Content.Shared.Administration.Systems;
using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
+using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
diff --git a/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs b/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs
index c232ccf415..57e8bfd449 100644
--- a/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs
+++ b/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs
@@ -1,6 +1,8 @@
using System.Linq;
using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
+using Content.Shared.Damage.Systems;
using Content.Shared.Execution;
using Content.Shared.FixedPoint;
using Content.Shared.Ghost;
@@ -280,7 +282,7 @@ public sealed class SuicideCommandTests
await server.WaitAssertion(() =>
{
// Heal all damage first (possible low pressure damage taken)
- damageableSystem.SetAllDamage(player, damageableComp, 0);
+ damageableSystem.ClearAllDamage((player, damageableComp));
consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last();
@@ -355,7 +357,7 @@ public sealed class SuicideCommandTests
await server.WaitAssertion(() =>
{
// Heal all damage first (possible low pressure damage taken)
- damageableSystem.SetAllDamage(player, damageableComp, 0);
+ damageableSystem.ClearAllDamage((player, damageableComp));
consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last();
diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/WindowRepair.cs b/Content.IntegrationTests/Tests/Construction/Interaction/WindowRepair.cs
index 192604edfd..5a4d284c0f 100644
--- a/Content.IntegrationTests/Tests/Construction/Interaction/WindowRepair.cs
+++ b/Content.IntegrationTests/Tests/Construction/Interaction/WindowRepair.cs
@@ -1,6 +1,8 @@
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
+using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
@@ -21,7 +23,7 @@ public sealed class WindowRepair : InteractionTest
var damageType = Server.ProtoMan.Index(BluntDamageType);
var damage = new DamageSpecifier(damageType, FixedPoint2.New(10));
Assert.That(comp.Damage.GetTotal(), Is.EqualTo(FixedPoint2.Zero));
- await Server.WaitPost(() => sys.TryChangeDamage(SEntMan.GetEntity(Target), damage, ignoreResistances: true));
+ await Server.WaitPost(() => sys.TryChangeDamage(SEntMan.GetEntity(Target).Value, damage, ignoreResistances: true));
await RunTicks(5);
Assert.That(comp.Damage.GetTotal(), Is.GreaterThan(FixedPoint2.Zero));
diff --git a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
index f610ab732e..72e8901631 100644
--- a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
+++ b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
@@ -1,5 +1,7 @@
using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
+using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
@@ -232,10 +234,14 @@ namespace Content.IntegrationTests.Tests.Damageable
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
});
- // Test SetAll function
- sDamageableSystem.SetAllDamage(sDamageableEntity, sDamageableComponent, 10);
+ // Test SetAll and ClearAll function
+ sDamageableSystem.SetAllDamage((sDamageableEntity, sDamageableComponent), 10);
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.New(10 * sDamageableComponent.Damage.DamageDict.Count)));
- sDamageableSystem.SetAllDamage(sDamageableEntity, sDamageableComponent, 0);
+ sDamageableSystem.SetAllDamage((sDamageableEntity, sDamageableComponent), 0);
+ Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
+ sDamageableSystem.SetAllDamage((sDamageableEntity, sDamageableComponent), 10);
+ Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.New(10 * sDamageableComponent.Damage.DamageDict.Count)));
+ sDamageableSystem.ClearAllDamage((sDamageableEntity, sDamageableComponent));
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
// Test 'wasted' healing
diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs
index 3650fd69d7..99f68b3fa3 100644
--- a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs
+++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs
@@ -1,5 +1,7 @@
using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
+using Content.Shared.Damage.Systems;
using Content.Shared.Destructible.Thresholds.Triggers;
using Content.Shared.FixedPoint;
using Robust.Shared.GameObjects;
@@ -130,7 +132,7 @@ namespace Content.IntegrationTests.Tests.Destructible
sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Heal both classes of damage to 0
- sDamageableSystem.SetAllDamage(sDestructibleEntity, sDamageableComponent, 0);
+ sDamageableSystem.ClearAllDamage((sDestructibleEntity, sDamageableComponent));
// No new thresholds reached, healing should not trigger it
Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty);
@@ -174,7 +176,7 @@ namespace Content.IntegrationTests.Tests.Destructible
threshold.TriggersOnce = true;
// Heal brute and burn back to 0
- sDamageableSystem.SetAllDamage(sDestructibleEntity, sDamageableComponent, 0);
+ sDamageableSystem.ClearAllDamage((sDestructibleEntity, sDamageableComponent));
// No new thresholds reached from healing
Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty);
diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs
index 4b1dc1ab3b..70baaea95a 100644
--- a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs
+++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs
@@ -1,5 +1,7 @@
using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
+using Content.Shared.Damage.Systems;
using Content.Shared.Destructible.Thresholds.Triggers;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs
index b1342eeafe..df98294ee9 100644
--- a/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs
+++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs
@@ -2,6 +2,7 @@ using System.Linq;
using Content.Server.Destructible.Thresholds.Behaviors;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
+using Content.Shared.Damage.Systems;
using Content.Shared.Destructible.Thresholds;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs
index fb3c40ec3c..1736d3d7e3 100644
--- a/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs
+++ b/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs
@@ -3,7 +3,9 @@ using Content.Server.Destructible;
using Content.Server.Destructible.Thresholds;
using Content.Server.Destructible.Thresholds.Behaviors;
using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
+using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Robust.Shared.Audio.Systems;
using Content.Shared.Destructible;
@@ -124,7 +126,7 @@ namespace Content.IntegrationTests.Tests.Destructible
Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty);
// Set damage to 0
- sDamageableSystem.SetAllDamage(sDestructibleEntity, sDamageableComponent, 0);
+ sDamageableSystem.ClearAllDamage((sDestructibleEntity, sDamageableComponent));
// Damage for 100, up to 100
sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * 10, true);
@@ -185,7 +187,7 @@ namespace Content.IntegrationTests.Tests.Destructible
sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Heal all damage
- sDamageableSystem.SetAllDamage(sDestructibleEntity, sDamageableComponent, 0);
+ sDamageableSystem.ClearAllDamage((sDestructibleEntity, sDamageableComponent));
// Damage up to 50
sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * 5, true);
@@ -247,7 +249,7 @@ namespace Content.IntegrationTests.Tests.Destructible
sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Heal the entity completely
- sDamageableSystem.SetAllDamage(sDestructibleEntity, sDamageableComponent, 0);
+ sDamageableSystem.ClearAllDamage((sDestructibleEntity, sDamageableComponent));
// Check that the entity has 0 damage
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
diff --git a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
index 4f92fd4e55..246a770190 100644
--- a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
+++ b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
@@ -10,7 +10,7 @@ using Content.Server.Roles;
using Content.Server.RoundEnd;
using Content.Server.Shuttles.Components;
using Content.Shared.CCVar;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.FixedPoint;
using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
index 4a33fdc03b..245aeab9ee 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
@@ -24,6 +24,7 @@ using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
+using Robust.Shared.Utility;
using Robust.UnitTesting;
namespace Content.IntegrationTests.Tests.Interaction;
@@ -39,10 +40,20 @@ namespace Content.IntegrationTests.Tests.Interaction;
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
public abstract partial class InteractionTest
{
+ ///
+ /// The prototype that will be spawned for the player entity at .
+ /// This is not a full humanoid and only has one hand by default.
+ ///
protected virtual string PlayerPrototype => "InteractionTestMob";
+ ///
+ /// The map path to load for the integration test.
+ /// If null an empty map with a single 1x1 plating grid will be generated.
+ ///
+ protected virtual ResPath? TestMapPath => null;
+
protected TestPair Pair = default!;
- protected TestMapData MapData => Pair.TestMap!;
+ protected TestMapData MapData = default!;
protected RobustIntegrationTest.ServerIntegrationInstance Server => Pair.Server;
protected RobustIntegrationTest.ClientIntegrationInstance Client => Pair.Client;
@@ -199,7 +210,10 @@ public abstract partial class InteractionTest
CUiSys = CEntMan.System();
// Setup map.
- await Pair.CreateTestMap();
+ if (TestMapPath == null)
+ MapData = await Pair.CreateTestMap();
+ else
+ MapData = await Pair.LoadTestMap(TestMapPath.Value);
PlayerCoords = SEntMan.GetNetCoordinates(Transform.WithEntityId(MapData.GridCoords.Offset(new Vector2(0.5f, 0.5f)), MapData.MapUid));
TargetCoords = SEntMan.GetNetCoordinates(Transform.WithEntityId(MapData.GridCoords.Offset(new Vector2(1.5f, 0.5f)), MapData.MapUid));
@@ -214,14 +228,14 @@ public abstract partial class InteractionTest
ServerSession = sPlayerMan.GetSessionById(ClientSession.UserId);
// Spawn player entity & attach
- EntityUid? old = default;
+ NetEntity? old = default;
await Server.WaitPost(() =>
{
// Fuck you mind system I want an hour of my life back
// Mind system is a time vampire
SEntMan.System().WipeMind(ServerSession.ContentData()?.Mind);
- old = cPlayerMan.LocalEntity;
+ CEntMan.TryGetNetEntity(cPlayerMan.LocalEntity, out old);
SPlayer = SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords));
Player = SEntMan.GetNetEntity(SPlayer);
Server.PlayerMan.SetAttachedEntity(ServerSession, SPlayer);
@@ -237,8 +251,8 @@ public abstract partial class InteractionTest
// Delete old player entity.
await Server.WaitPost(() =>
{
- if (old != null)
- SEntMan.DeleteEntity(old.Value);
+ if (SEntMan.TryGetEntity(old, out var uid))
+ SEntMan.DeleteEntity(uid);
});
// Change UI state to in-game.
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTestTests.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTestTests.cs
new file mode 100644
index 0000000000..54417b6c0b
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTestTests.cs
@@ -0,0 +1,31 @@
+using System.Linq;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Utility;
+
+namespace Content.IntegrationTests.Tests.Interaction;
+
+///
+/// Makes sure that interaction test helper methods are working as intended.
+///
+public sealed class InteractionTestTests : InteractionTest
+{
+ protected override ResPath? TestMapPath => new("Maps/Test/empty.yml");
+
+ ///
+ /// Tests that map loading is working correctly.
+ ///
+ [Test]
+ public void MapLoadingTest()
+ {
+ // Make sure that there is only one grid.
+ var grids = SEntMan.AllEntities().ToList();
+ Assert.That(grids, Has.Count.EqualTo(1), "Test map did not have exactly one grid.");
+ Assert.That(grids, Does.Contain(MapData.Grid), "MapData did not contain the loaded grid.");
+
+ // Make sure we loaded the right map.
+ // This name is defined in empty.yml
+ Assert.That(SEntMan.GetComponent(MapData.MapUid).EntityName, Is.EqualTo("Empty Debug Map"));
+ }
+}
+
diff --git a/Content.IntegrationTests/Tests/Lobby/CharacterCreationTest.cs b/Content.IntegrationTests/Tests/Lobby/CharacterCreationTest.cs
index 60501a781f..d5791861cf 100644
--- a/Content.IntegrationTests/Tests/Lobby/CharacterCreationTest.cs
+++ b/Content.IntegrationTests/Tests/Lobby/CharacterCreationTest.cs
@@ -1,114 +1,122 @@
using Content.Client.Lobby;
using Content.Server.Preferences.Managers;
+using Content.Shared.Humanoid;
using Content.Shared.Preferences;
using Robust.Client.State;
-using Robust.Shared.Network;
-namespace Content.IntegrationTests.Tests.Lobby
+namespace Content.IntegrationTests.Tests.Lobby;
+
+[TestFixture]
+[TestOf(typeof(ClientPreferencesManager))]
+[TestOf(typeof(ServerPreferencesManager))]
+public sealed class CharacterCreationTest
{
- [TestFixture]
- [TestOf(typeof(ClientPreferencesManager))]
- [TestOf(typeof(ServerPreferencesManager))]
- public sealed class CharacterCreationTest
+ [Test]
+ public async Task CreateDeleteCreateTest()
{
- [Test]
- public async Task CreateDeleteCreateTest()
+ await using var pair = await PoolManager.GetServerClient(new PoolSettings { InLobby = true });
+ var server = pair.Server;
+ var client = pair.Client;
+ var user = pair.Client.User!.Value;
+ var clientPrefManager = client.Resolve();
+ var serverPrefManager = server.Resolve();
+
+ Assert.That(client.Resolve().CurrentState, Is.TypeOf());
+ await client.WaitPost(() => clientPrefManager.SelectCharacter(0));
+ await pair.RunTicksSync(5);
+
+ var clientCharacters = clientPrefManager.Preferences?.Characters;
+ Assert.That(clientCharacters, Is.Not.Null);
+ Assert.That(clientCharacters, Has.Count.EqualTo(1));
+
+ HumanoidCharacterProfile profile = null;
+ await client.WaitPost(() =>
{
- await using var pair = await PoolManager.GetServerClient(new PoolSettings { InLobby = true });
- var server = pair.Server;
- var client = pair.Client;
+ profile = HumanoidCharacterProfile.Random();
+ clientPrefManager.CreateCharacter(profile);
+ });
+ await pair.RunTicksSync(5);
- var clientNetManager = client.ResolveDependency();
- var clientStateManager = client.ResolveDependency();
- var clientPrefManager = client.ResolveDependency();
+ clientCharacters = clientPrefManager.Preferences?.Characters;
+ Assert.That(clientCharacters, Is.Not.Null);
+ Assert.That(clientCharacters, Has.Count.EqualTo(2));
+ AssertEqual(clientCharacters[1], profile);
- var serverPrefManager = server.ResolveDependency();
+ await PoolManager.WaitUntil(server, () => serverPrefManager.GetPreferences(user).Characters.Count == 2, maxTicks: 60);
+ var serverCharacters = serverPrefManager.GetPreferences(user).Characters;
+ Assert.That(serverCharacters, Has.Count.EqualTo(2));
+ AssertEqual(serverCharacters[1], profile);
- // Need to run them in sync to receive the messages.
- await pair.RunTicksSync(1);
+ await client.WaitAssertion(() => clientPrefManager.DeleteCharacter(1));
+ await pair.RunTicksSync(5);
+ Assert.That(clientPrefManager.Preferences?.Characters.Count, Is.EqualTo(1));
+ await PoolManager.WaitUntil(server, () => serverPrefManager.GetPreferences(user).Characters.Count == 1, maxTicks: 60);
+ Assert.That(serverPrefManager.GetPreferences(user).Characters.Count, Is.EqualTo(1));
- await PoolManager.WaitUntil(client, () => clientStateManager.CurrentState is LobbyState, 600);
+ await client.WaitIdleAsync();
- Assert.That(clientNetManager.ServerChannel, Is.Not.Null);
+ await client.WaitAssertion(() =>
+ {
+ profile = HumanoidCharacterProfile.Random();
+ clientPrefManager.CreateCharacter(profile);
+ });
+ await pair.RunTicksSync(5);
- var clientNetId = clientNetManager.ServerChannel.UserId;
- HumanoidCharacterProfile profile = null;
+ clientCharacters = clientPrefManager.Preferences?.Characters;
+ Assert.That(clientCharacters, Is.Not.Null);
+ Assert.That(clientCharacters, Has.Count.EqualTo(2));
+ AssertEqual(clientCharacters[1], profile);
- await client.WaitAssertion(() =>
- {
- clientPrefManager.SelectCharacter(0);
+ await PoolManager.WaitUntil(server, () => serverPrefManager.GetPreferences(user).Characters.Count == 2, maxTicks: 60);
+ serverCharacters = serverPrefManager.GetPreferences(user).Characters;
+ Assert.That(serverCharacters, Has.Count.EqualTo(2));
+ AssertEqual(serverCharacters[1], profile);
+ await pair.CleanReturnAsync();
+ }
- var clientCharacters = clientPrefManager.Preferences?.Characters;
- Assert.That(clientCharacters, Is.Not.Null);
- Assert.Multiple(() =>
- {
- Assert.That(clientCharacters, Has.Count.EqualTo(1));
+ private void AssertEqual(ICharacterProfile clientCharacter, HumanoidCharacterProfile b)
+ {
+ if (clientCharacter.MemberwiseEquals(b))
+ return;
- Assert.That(clientStateManager.CurrentState, Is.TypeOf());
- });
-
- profile = HumanoidCharacterProfile.Random();
- clientPrefManager.CreateCharacter(profile);
-
- clientCharacters = clientPrefManager.Preferences?.Characters;
-
- Assert.That(clientCharacters, Is.Not.Null);
- Assert.That(clientCharacters, Has.Count.EqualTo(2));
- Assert.That(clientCharacters[1].MemberwiseEquals(profile));
- });
-
- await PoolManager.WaitUntil(server, () => serverPrefManager.GetPreferences(clientNetId).Characters.Count == 2, maxTicks: 60);
-
- await server.WaitAssertion(() =>
- {
- var serverCharacters = serverPrefManager.GetPreferences(clientNetId).Characters;
-
- Assert.That(serverCharacters, Has.Count.EqualTo(2));
- Assert.That(serverCharacters[1].MemberwiseEquals(profile));
- });
-
- await client.WaitAssertion(() =>
- {
- clientPrefManager.DeleteCharacter(1);
-
- var clientCharacters = clientPrefManager.Preferences?.Characters.Count;
- Assert.That(clientCharacters, Is.EqualTo(1));
- });
-
- await PoolManager.WaitUntil(server, () => serverPrefManager.GetPreferences(clientNetId).Characters.Count == 1, maxTicks: 60);
-
- await server.WaitAssertion(() =>
- {
- var serverCharacters = serverPrefManager.GetPreferences(clientNetId).Characters.Count;
- Assert.That(serverCharacters, Is.EqualTo(1));
- });
-
- await client.WaitIdleAsync();
-
- await client.WaitAssertion(() =>
- {
- profile = HumanoidCharacterProfile.Random();
-
- clientPrefManager.CreateCharacter(profile);
-
- var clientCharacters = clientPrefManager.Preferences?.Characters;
-
- Assert.That(clientCharacters, Is.Not.Null);
- Assert.That(clientCharacters, Has.Count.EqualTo(2));
- Assert.That(clientCharacters[1].MemberwiseEquals(profile));
- });
-
- await PoolManager.WaitUntil(server, () => serverPrefManager.GetPreferences(clientNetId).Characters.Count == 2, maxTicks: 60);
-
- await server.WaitAssertion(() =>
- {
- var serverCharacters = serverPrefManager.GetPreferences(clientNetId).Characters;
-
- Assert.That(serverCharacters, Has.Count.EqualTo(2));
- Assert.That(serverCharacters[1].MemberwiseEquals(profile));
- });
- await pair.CleanReturnAsync();
+ if (clientCharacter is not HumanoidCharacterProfile a)
+ {
+ Assert.Fail($"Not a {nameof(HumanoidCharacterProfile)}");
+ return;
}
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(a.Name, Is.EqualTo(b.Name));
+ Assert.That(a.Age, Is.EqualTo(b.Age));
+ Assert.That(a.Sex, Is.EqualTo(b.Sex));
+ Assert.That(a.Gender, Is.EqualTo(b.Gender));
+ Assert.That(a.Species, Is.EqualTo(b.Species));
+ Assert.That(a.PreferenceUnavailable, Is.EqualTo(b.PreferenceUnavailable));
+ Assert.That(a.SpawnPriority, Is.EqualTo(b.SpawnPriority));
+ Assert.That(a.FlavorText, Is.EqualTo(b.FlavorText));
+ Assert.That(a.JobPriorities, Is.EquivalentTo(b.JobPriorities));
+ Assert.That(a.AntagPreferences, Is.EquivalentTo(b.AntagPreferences));
+ Assert.That(a.TraitPreferences, Is.EquivalentTo(b.TraitPreferences));
+ Assert.That(a.Loadouts, Is.EquivalentTo(b.Loadouts));
+ AssertEqual(a.Appearance, b.Appearance);
+ Assert.Fail("Profile not equal");
+ });
+ }
+
+ private void AssertEqual(HumanoidCharacterAppearance a, HumanoidCharacterAppearance b)
+ {
+ if (a.MemberwiseEquals(b))
+ return;
+
+ Assert.That(a.HairStyleId, Is.EqualTo(b.HairStyleId));
+ Assert.That(a.HairColor, Is.EqualTo(b.HairColor));
+ Assert.That(a.FacialHairStyleId, Is.EqualTo(b.FacialHairStyleId));
+ Assert.That(a.FacialHairColor, Is.EqualTo(b.FacialHairColor));
+ Assert.That(a.EyeColor, Is.EqualTo(b.EyeColor));
+ Assert.That(a.SkinColor, Is.EqualTo(b.SkinColor));
+ Assert.That(a.Markings, Is.EquivalentTo(b.Markings));
+ Assert.Fail("Appearance not equal");
}
}
diff --git a/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs b/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs
index dcb47fb81c..3c6c372b75 100644
--- a/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs
+++ b/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs
@@ -28,20 +28,10 @@ namespace Content.IntegrationTests.Tests;
[TestFixture]
public sealed class MaterialArbitrageTest
{
- // These recipes are currently broken and need fixing. You should not be adding to these sets.
- private readonly HashSet _destructionArbitrageIgnore =
- [
- "BaseChemistryEmptyVial", "DrinkShotGlass", "SodiumLightTube", "DrinkGlassCoupeShaped",
- "LedLightBulb", "ExteriorLightTube", "LightTube", "DrinkGlass", "DimLightBulb", "LightBulb", "LedLightTube",
- "ChemistryEmptyBottle01", "WarmLightBulb",
- ];
-
- private readonly HashSet _compositionArbitrageIgnore =
- [
- "FoodPlateSmall", "AirTank", "FoodPlateTin", "FoodPlateMuffinTin", "WeaponCapacitorRechargerCircuitboard",
- "WeaponCapacitorRechargerCircuitboard", "BorgChargerCircuitboard", "BorgChargerCircuitboard", "FoodPlate",
- "CellRechargerCircuitboard", "CellRechargerCircuitboard",
- ];
+ // These sets are for selectively excluding recipes from arbitrage.
+ // You should NOT be adding to these. They exist here for downstreams and potential future issues.
+ private readonly HashSet _destructionArbitrageIgnore = [];
+ private readonly HashSet _compositionArbitrageIgnore = [];
[Test]
public async Task NoMaterialArbitrage()
diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.cs b/Content.IntegrationTests/Tests/Minds/MindTests.cs
index 1bda6fd4db..35069339ba 100644
--- a/Content.IntegrationTests/Tests/Minds/MindTests.cs
+++ b/Content.IntegrationTests/Tests/Minds/MindTests.cs
@@ -4,7 +4,9 @@ using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Mind;
using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
+using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
@@ -147,7 +149,7 @@ public sealed partial class MindTests
var damageable = entMan.GetComponent(entity);
var prototype = protoMan.Index(BluntDamageType);
- damageableSystem.SetDamage(entity, damageable, new DamageSpecifier(prototype, FixedPoint2.New(401)));
+ damageableSystem.SetDamage((entity, damageable), new DamageSpecifier(prototype, FixedPoint2.New(401)));
Assert.That(mindSystem.GetMind(entity, mindContainerComp), Is.EqualTo(mindId));
});
diff --git a/Content.IntegrationTests/Tests/Mousetrap/MousetrapTest.cs b/Content.IntegrationTests/Tests/Mousetrap/MousetrapTest.cs
index 422d58cdcf..5f1b9172dd 100644
--- a/Content.IntegrationTests/Tests/Mousetrap/MousetrapTest.cs
+++ b/Content.IntegrationTests/Tests/Mousetrap/MousetrapTest.cs
@@ -1,7 +1,6 @@
using Content.IntegrationTests.Tests.Movement;
using Content.Server.NPC.HTN;
-using Content.Shared.Damage;
-using Content.Shared.FixedPoint;
+using Content.Shared.Damage.Components;
using Content.Shared.Item.ItemToggle;
using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.Mobs;
diff --git a/Content.IntegrationTests/Tests/Storage/EntityStorageTests.cs b/Content.IntegrationTests/Tests/Storage/EntityStorageTests.cs
index 45ee69a9ef..f80cc089de 100644
--- a/Content.IntegrationTests/Tests/Storage/EntityStorageTests.cs
+++ b/Content.IntegrationTests/Tests/Storage/EntityStorageTests.cs
@@ -1,5 +1,6 @@
using Content.Server.Storage.EntitySystems;
using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
diff --git a/Content.IntegrationTests/Tests/Vending/VendingInteractionTest.cs b/Content.IntegrationTests/Tests/Vending/VendingInteractionTest.cs
index 3645667737..af16f1278a 100644
--- a/Content.IntegrationTests/Tests/Vending/VendingInteractionTest.cs
+++ b/Content.IntegrationTests/Tests/Vending/VendingInteractionTest.cs
@@ -2,7 +2,9 @@ using System.Linq;
using Content.IntegrationTests.Tests.Interaction;
using Content.Server.VendingMachines;
using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
+using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.VendingMachines;
using Robust.Shared.Prototypes;
@@ -200,7 +202,7 @@ public sealed class VendingInteractionTest : InteractionTest
// Damage the vending machine to the point that it breaks
var damageType = ProtoMan.Index(TestDamageType);
var damage = new DamageSpecifier(damageType, FixedPoint2.New(100));
- await Server.WaitPost(() => damageableSys.TryChangeDamage(SEntMan.GetEntity(Target), damage, ignoreResistances: true));
+ await Server.WaitPost(() => damageableSys.TryChangeDamage(SEntMan.GetEntity(Target).Value, damage, ignoreResistances: true));
await RunTicks(5);
Assert.That(damageableComp.Damage.GetTotal(), Is.GreaterThan(FixedPoint2.Zero), $"{VendingMachineProtoId} did not take damage.");
}
diff --git a/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs b/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs
index f30eed0651..01770fe107 100644
--- a/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs
+++ b/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs
@@ -5,6 +5,7 @@ using Content.Server.Wires;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
+using Content.Shared.Damage.Systems;
using Content.Shared.Prototypes;
using Content.Shared.Storage.Components;
using Content.Shared.VendingMachines;
@@ -296,14 +297,12 @@ namespace Content.IntegrationTests.Tests
restock = entityManager.SpawnEntity("TestRestockExplode", coordinates);
var damageSpec = new DamageSpecifier(prototypeManager.Index(TestDamageType), 100);
- var damageResult = damageableSystem.TryChangeDamage(restock, damageSpec);
+ var damageResult = damageableSystem.ChangeDamage(restock, damageSpec);
#pragma warning disable NUnit2045
- Assert.That(damageResult, Is.Not.Null,
- "Received null damageResult when attempting to damage restock box.");
+ Assert.That(!damageResult.Empty, "Received empty damageResult when attempting to damage restock box.");
- Assert.That((int) damageResult!.GetTotal(), Is.GreaterThan(0),
- "Box damage result was not greater than 0.");
+ Assert.That((int) damageResult.GetTotal(), Is.GreaterThan(0), "Box damage result was not greater than 0.");
#pragma warning restore NUnit2045
});
await server.WaitRunTicks(15);
diff --git a/Content.IntegrationTests/Tests/Weapons/WeaponTests.cs b/Content.IntegrationTests/Tests/Weapons/WeaponTests.cs
index bf240ba9e2..135e75f8be 100644
--- a/Content.IntegrationTests/Tests/Weapons/WeaponTests.cs
+++ b/Content.IntegrationTests/Tests/Weapons/WeaponTests.cs
@@ -1,5 +1,5 @@
using Content.IntegrationTests.Tests.Interaction;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Systems;
using Content.Shared.Wieldable.Components;
diff --git a/Content.Server/Access/Systems/IdCardConsoleSystem.cs b/Content.Server/Access/Systems/IdCardConsoleSystem.cs
index 7c772830ba..9f21fd68d8 100644
--- a/Content.Server/Access/Systems/IdCardConsoleSystem.cs
+++ b/Content.Server/Access/Systems/IdCardConsoleSystem.cs
@@ -12,6 +12,7 @@ using Content.Shared.Chat;
using Content.Shared.Construction;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Roles;
using Content.Shared.StationRecords;
diff --git a/Content.Server/Administration/Logs/AdminLogManager.cs b/Content.Server/Administration/Logs/AdminLogManager.cs
index e7682cf559..2587d4b8f9 100644
--- a/Content.Server/Administration/Logs/AdminLogManager.cs
+++ b/Content.Server/Administration/Logs/AdminLogManager.cs
@@ -12,13 +12,16 @@ using Content.Shared.Database;
using Content.Shared.Mind;
using Content.Shared.Players.PlayTimeTracking;
using Prometheus;
+using Robust.Server.GameObjects;
using Robust.Shared;
using Robust.Shared.Configuration;
+using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Timing;
+using Robust.Shared.Utility;
namespace Content.Server.Administration.Logs;
@@ -338,7 +341,7 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
Players = players,
};
- DoAdminAlerts(players, message, impact);
+ DoAdminAlerts(players, message, impact, handler);
if (preRound)
{
@@ -380,6 +383,34 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
return players;
}
+ ///
+ /// Get a list of coordinates from the s values. Will transform all coordinate types
+ /// to map coordinates!
+ ///
+ /// A list of map coordinates that were found in the value input, can return an empty list.
+ private List GetCoordinates(Dictionary values)
+ {
+ List coordList = new();
+ EntityManager.TrySystem(out TransformSystem? transform);
+
+ foreach (var value in values.Values)
+ {
+ switch (value)
+ {
+ case EntityCoordinates entCords:
+ if (transform != null)
+ coordList.Add(transform.ToMapCoordinates(entCords));
+ continue;
+
+ case MapCoordinates mapCord:
+ coordList.Add(mapCord);
+ continue;
+ }
+ }
+
+ return coordList;
+ }
+
private void AddPlayer(List players, Guid user, int logId)
{
// The majority of logs have a single player, or maybe two. Instead of allocating a List and
@@ -397,10 +428,11 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
});
}
- private void DoAdminAlerts(List players, string message, LogImpact impact)
+ private void DoAdminAlerts(List players, string message, LogImpact impact, LogStringHandler handler)
{
var adminLog = false;
var logMessage = message;
+ var playerNetEnts = new List<(NetEntity, string)>();
foreach (var player in players)
{
@@ -419,6 +451,8 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
("name", cachedInfo.CharacterName),
("subtype", subtype));
}
+ if (cachedInfo != null && cachedInfo.NetEntity != null)
+ playerNetEnts.Add((cachedInfo.NetEntity.Value, cachedInfo.CharacterName));
}
if (adminLog)
@@ -442,7 +476,73 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
}
if (adminLog)
+ {
_chat.SendAdminAlert(logMessage);
+
+ if (CreateTpLinks(playerNetEnts, out var tpLinks))
+ _chat.SendAdminAlertNoFormatOrEscape(tpLinks);
+
+ var coords = GetCoordinates(handler.Values);
+
+ if (CreateCordLinks(coords, out var cordLinks))
+ _chat.SendAdminAlertNoFormatOrEscape(cordLinks);
+ }
+ }
+
+ ///
+ /// Creates a list of tpto command links of the given players
+ ///
+ private bool CreateTpLinks(List<(NetEntity NetEnt, string CharacterName)> players, out string outString)
+ {
+ outString = string.Empty;
+
+ if (players.Count == 0)
+ return false;
+
+ outString = Loc.GetString("admin-alert-tp-to-players-header");
+
+ for (var i = 0; i < players.Count; i++)
+ {
+ var player = players[i];
+ outString += $"[cmdlink=\"{EscapeText(player.CharacterName)}\" command=\"tpto {player.NetEnt}\"/]";
+
+ if (i < players.Count - 1)
+ outString += ", ";
+ }
+
+ return true;
+ }
+
+ ///
+ /// Creates a list of toto command links for the given map coordinates.
+ ///
+ private bool CreateCordLinks(List cords, out string outString)
+ {
+ outString = string.Empty;
+
+ if (cords.Count == 0)
+ return false;
+
+ outString = Loc.GetString("admin-alert-tp-to-coords-header");
+
+ for (var i = 0; i < cords.Count; i++)
+ {
+ var cord = cords[i];
+ outString += $"[cmdlink=\"{cord.ToString()}\" command=\"tp {cord.X} {cord.Y} {cord.MapId}\"/]";
+
+ if (i < cords.Count - 1)
+ outString += ", ";
+ }
+
+ return true;
+ }
+
+ ///
+ /// Escape the given text to not allow breakouts of the cmdlink tags.
+ ///
+ private string EscapeText(string text)
+ {
+ return FormattedMessage.EscapeText(text).Replace("\"", "\\\"").Replace("'", "\\'");
}
public async Task> All(LogFilter? filter = null, Func>? listProvider = null)
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
index d03b799ff2..617451f955 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
@@ -1,6 +1,5 @@
using Content.Server.Administration.Components;
using Content.Server.Atmos.EntitySystems;
-using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Electrocution;
using Content.Server.Explosion.EntitySystems;
@@ -24,7 +23,6 @@ using Content.Shared.Body.Part;
using Content.Shared.Clothing.Components;
using Content.Shared.Clumsy;
using Content.Shared.Cluwne;
-using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Electrocution;
@@ -58,6 +56,7 @@ using Robust.Shared.Spawners;
using Robust.Shared.Utility;
using System.Numerics;
using System.Threading;
+using Content.Shared.Damage.Components;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.Administration.Systems;
diff --git a/Content.Server/Atmos/Commands/PauseAtmosCommand.cs b/Content.Server/Atmos/Commands/PauseAtmosCommand.cs
new file mode 100644
index 0000000000..984f2a0869
--- /dev/null
+++ b/Content.Server/Atmos/Commands/PauseAtmosCommand.cs
@@ -0,0 +1,69 @@
+using Content.Server.Administration;
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Administration;
+using Robust.Shared.Console;
+
+namespace Content.Server.Atmos.Commands;
+
+[AdminCommand(AdminFlags.Debug)]
+public sealed class PauseAtmosCommand : LocalizedEntityCommands
+{
+ [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
+
+ public override string Command => "pauseatmos";
+
+ public override void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ var grid = default(EntityUid);
+
+ switch (args.Length)
+ {
+ case 0:
+ if (!EntityManager.TryGetComponent(shell.Player?.AttachedEntity,
+ out var playerxform) ||
+ playerxform.GridUid == null)
+ {
+ shell.WriteError(Loc.GetString("cmd-error-no-grid-provided-or-invalid-grid"));
+ return;
+ }
+
+ grid = playerxform.GridUid.Value;
+ break;
+ case 1:
+ if (!EntityUid.TryParse(args[0], out var parsedGrid) || !EntityManager.EntityExists(parsedGrid))
+ {
+ shell.WriteError(Loc.GetString("cmd-error-couldnt-parse-entity"));
+ return;
+ }
+
+ grid = parsedGrid;
+ break;
+ }
+
+ if (!EntityManager.TryGetComponent(grid, out var gridAtmos))
+ {
+ shell.WriteError(Loc.GetString("cmd-error-no-gridatmosphere"));
+ return;
+ }
+
+ var newEnt = new Entity(grid, gridAtmos);
+
+ _atmosphereSystem.SetAtmosphereSimulation(newEnt, !newEnt.Comp.Simulated);
+ shell.WriteLine(Loc.GetString("cmd-pauseatmos-set-atmos-simulation",
+ ("grid", EntityManager.ToPrettyString(grid)),
+ ("state", newEnt.Comp.Simulated)));
+ }
+
+ public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
+ {
+ if (args.Length == 1)
+ {
+ return CompletionResult.FromHintOptions(
+ CompletionHelper.Components(args[0], EntityManager),
+ Loc.GetString("cmd-pauseatmos-completion-grid-pause"));
+ }
+
+ return CompletionResult.Empty;
+ }
+}
diff --git a/Content.Server/Atmos/Commands/SubstepAtmosCommand.cs b/Content.Server/Atmos/Commands/SubstepAtmosCommand.cs
new file mode 100644
index 0000000000..554abff4a8
--- /dev/null
+++ b/Content.Server/Atmos/Commands/SubstepAtmosCommand.cs
@@ -0,0 +1,104 @@
+using Content.Server.Administration;
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Administration;
+using Content.Shared.Atmos.Components;
+using Robust.Shared.Console;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+
+namespace Content.Server.Atmos.Commands;
+
+[AdminCommand(AdminFlags.Debug)]
+public sealed class SubstepAtmosCommand : LocalizedEntityCommands
+{
+ [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
+
+ public override string Command => "substepatmos";
+
+ public override void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ var grid = default(EntityUid);
+
+ switch (args.Length)
+ {
+ case 0:
+ if (!EntityManager.TryGetComponent(shell.Player?.AttachedEntity,
+ out var playerxform) ||
+ playerxform.GridUid == null)
+ {
+ shell.WriteError(Loc.GetString("cmd-error-no-grid-provided-or-invalid-grid"));
+ return;
+ }
+
+ grid = playerxform.GridUid.Value;
+ break;
+ case 1:
+ if (!EntityUid.TryParse(args[0], out var parsedGrid) || !EntityManager.EntityExists(parsedGrid))
+ {
+ shell.WriteError(Loc.GetString("cmd-error-couldnt-parse-entity"));
+ return;
+ }
+
+ grid = parsedGrid;
+ break;
+ }
+
+ // i'm straight piratesoftwaremaxxing
+ if (!EntityManager.TryGetComponent(grid, out var gridAtmos))
+ {
+ shell.WriteError(Loc.GetString("cmd-error-no-gridatmosphere"));
+ return;
+ }
+
+ if (!EntityManager.TryGetComponent(grid, out var gasTile))
+ {
+ shell.WriteError(Loc.GetString("cmd-error-no-gastileoverlay"));
+ return;
+ }
+
+ if (!EntityManager.TryGetComponent(grid, out var mapGrid))
+ {
+ shell.WriteError(Loc.GetString("cmd-error-no-mapgrid"));
+ return;
+ }
+
+ var xform = EntityManager.GetComponent(grid);
+
+ if (xform.MapUid == null || xform.MapID == MapId.Nullspace)
+ {
+ shell.WriteError(Loc.GetString("cmd-error-no-valid-map"));
+ return;
+ }
+
+ var newEnt =
+ new Entity(grid,
+ gridAtmos,
+ gasTile,
+ mapGrid,
+ xform);
+
+ if (gridAtmos.Simulated)
+ {
+ shell.WriteLine(Loc.GetString("cmd-substepatmos-info-implicitly-paused-simulation",
+ ("grid", EntityManager.ToPrettyString(grid))));
+ }
+
+ _atmosphereSystem.SetAtmosphereSimulation(newEnt, false);
+ _atmosphereSystem.RunProcessingFull(newEnt, xform.MapUid.Value, _atmosphereSystem.AtmosTickRate);
+
+ shell.WriteLine(Loc.GetString("cmd-substepatmos-info-substepped-grid", ("grid", EntityManager.ToPrettyString(grid))));
+ }
+
+ public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
+ {
+ if (args.Length == 1)
+ {
+ return CompletionResult.FromHintOptions(
+ CompletionHelper.Components(args[0], EntityManager),
+ Loc.GetString("cmd-substepatmos-completion-grid-substep"));
+ }
+
+ return CompletionResult.Empty;
+ }
+}
diff --git a/Content.Server/Atmos/Components/GridAtmosphereComponent.cs b/Content.Server/Atmos/Components/GridAtmosphereComponent.cs
index 2d36d2bd14..2a0d87515c 100644
--- a/Content.Server/Atmos/Components/GridAtmosphereComponent.cs
+++ b/Content.Server/Atmos/Components/GridAtmosphereComponent.cs
@@ -3,6 +3,7 @@ using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Serialization;
using Content.Server.NodeContainer.NodeGroups;
+using Content.Shared.Atmos.Components;
namespace Content.Server.Atmos.Components
{
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BenchmarkHelpers.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BenchmarkHelpers.cs
index f86ebcee73..62cbbae68a 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BenchmarkHelpers.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BenchmarkHelpers.cs
@@ -46,4 +46,27 @@ public sealed partial class AtmosphereSystem
return processingPaused;
}
+
+ ///
+ /// Fully runs one entity through the entire Atmos processing loop.
+ ///
+ /// The entity to simulate.
+ /// The that belongs to the grid's map.
+ /// Elapsed time to simulate. Recommended value is .
+ public void RunProcessingFull(Entity ent,
+ Entity mapAtmosphere,
+ float frameTime)
+ {
+ while (ProcessAtmosphere(ent, mapAtmosphere, frameTime) != AtmosphereProcessingCompletionState.Finished) { }
+ }
+
+ ///
+ /// Allows or disallows atmosphere simulation on a .
+ ///
+ /// The atmosphere to pause or unpause processing.
+ /// The state to set. True means that the atmosphere is allowed to simulate, false otherwise.
+ public void SetAtmosphereSimulation(Entity ent, bool simulate)
+ {
+ ent.Comp.Simulated = simulate;
+ }
}
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs
index efddcaf1a7..d6ab2a0087 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs
@@ -1,5 +1,6 @@
using Content.Server.Atmos.Components;
using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
using Content.Shared.Damage;
using Robust.Shared.Random;
using Robust.Shared.Threading;
@@ -175,7 +176,7 @@ public sealed partial class AtmosphereSystem
/// containing the queue.
/// The current absolute pressure being experienced by the entity.
/// The current delta pressure being experienced by the entity.
- private static void EnqueueDeltaPressureDamage(Entity ent,
+ private void EnqueueDeltaPressureDamage(Entity ent,
GridAtmosphereComponent gridAtmosComp,
float pressure,
float delta)
@@ -184,7 +185,7 @@ public sealed partial class AtmosphereSystem
var aboveMinDeltaPressure = delta > ent.Comp.MinPressureDelta;
if (!aboveMinPressure && !aboveMinDeltaPressure)
{
- ent.Comp.IsTakingDamage = false;
+ SetIsTakingDamageState(ent, false);
return;
}
@@ -250,8 +251,21 @@ public sealed partial class AtmosphereSystem
var maxPressureCapped = Math.Min(maxPressure, ent.Comp.MaxEffectivePressure);
var appliedDamage = ScaleDamage(ent, ent.Comp.BaseDamage, maxPressureCapped);
- _damage.TryChangeDamage(ent, appliedDamage, ignoreResistances: true, interruptsDoAfters: false);
- ent.Comp.IsTakingDamage = true;
+ _damage.ChangeDamage(ent.Owner, appliedDamage, ignoreResistances: true, interruptsDoAfters: false);
+ SetIsTakingDamageState(ent, true);
+ }
+
+ ///
+ /// Helper function to prevent spamming clients with dirty events when the damage state hasn't changed.
+ ///
+ /// The entity to check.
+ /// The value to set.
+ private void SetIsTakingDamageState(Entity ent, bool toSet)
+ {
+ if (ent.Comp.IsTakingDamage == toSet)
+ return;
+ ent.Comp.IsTakingDamage = toSet;
+ Dirty(ent);
}
///
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.ExcitedGroup.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.ExcitedGroup.cs
index 0d622f3067..6ae251dd29 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.ExcitedGroup.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.ExcitedGroup.cs
@@ -4,148 +4,199 @@ using Content.Shared.Atmos.Components;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
-namespace Content.Server.Atmos.EntitySystems
+namespace Content.Server.Atmos.EntitySystems;
+
+public sealed partial class AtmosphereSystem
{
- public sealed partial class AtmosphereSystem
+ /*
+ Handles Excited Groups, an optimization routine executed during LINDA
+ that groups active tiles together.
+
+ Groups of active tiles that have very low mole deltas between them
+ are dissolved after a cooldown period, performing a final equalization
+ on all tiles in the group before deactivating them.
+
+ If tiles are so close together in pressure that the final equalization
+ would result in negligible gas transfer, the group is dissolved without
+ performing an equalization.
+
+ This prevents LINDA from constantly transferring tiny amounts of gas
+ between tiles that are already nearly equalized.
+ */
+
+ ///
+ /// Adds a tile to an , resetting the group's cooldowns in the process.
+ ///
+ /// The to add the tile to.
+ /// The to add.
+ private void ExcitedGroupAddTile(ExcitedGroup excitedGroup, TileAtmosphere tile)
{
- private void ExcitedGroupAddTile(ExcitedGroup excitedGroup, TileAtmosphere tile)
+ DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
+ DebugTools.Assert(tile.ExcitedGroup == null, "Tried to add a tile to an excited group when it's already in another one!");
+ excitedGroup.Tiles.Add(tile);
+ tile.ExcitedGroup = excitedGroup;
+ ExcitedGroupResetCooldowns(excitedGroup);
+ }
+
+ ///
+ /// Removes a tile from an .
+ ///
+ /// The to remove the tile from.
+ /// The to remove.
+ private void ExcitedGroupRemoveTile(ExcitedGroup excitedGroup, TileAtmosphere tile)
+ {
+ DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
+ DebugTools.Assert(tile.ExcitedGroup == excitedGroup, "Tried to remove a tile from an excited group it's not present in!");
+ tile.ExcitedGroup = null;
+ excitedGroup.Tiles.Remove(tile);
+ }
+
+ ///
+ /// Merges two , transferring all tiles from one to the other.
+ /// The larger group receives the tiles of the smaller group.
+ /// The smaller group is then disposed of without deactivating its tiles.
+ ///
+ /// The of the grid.
+ /// The first to merge.
+ /// The second to merge.
+ private void ExcitedGroupMerge(GridAtmosphereComponent gridAtmosphere, ExcitedGroup ourGroup, ExcitedGroup otherGroup)
+ {
+ DebugTools.Assert(!ourGroup.Disposed, "Excited group is disposed!");
+ DebugTools.Assert(!otherGroup.Disposed, "Excited group is disposed!");
+ DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(ourGroup), "Grid Atmosphere does not contain Excited Group!");
+ DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(otherGroup), "Grid Atmosphere does not contain Excited Group!");
+ var ourSize = ourGroup.Tiles.Count;
+ var otherSize = otherGroup.Tiles.Count;
+
+ ExcitedGroup winner;
+ ExcitedGroup loser;
+
+ if (ourSize > otherSize)
{
- DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
- DebugTools.Assert(tile.ExcitedGroup == null, "Tried to add a tile to an excited group when it's already in another one!");
- excitedGroup.Tiles.Add(tile);
- tile.ExcitedGroup = excitedGroup;
- ExcitedGroupResetCooldowns(excitedGroup);
+ winner = ourGroup;
+ loser = otherGroup;
+ }
+ else
+ {
+ winner = otherGroup;
+ loser = ourGroup;
}
- private void ExcitedGroupRemoveTile(ExcitedGroup excitedGroup, TileAtmosphere tile)
+ foreach (var tile in loser.Tiles)
+ {
+ tile.ExcitedGroup = winner;
+ winner.Tiles.Add(tile);
+ }
+
+ loser.Tiles.Clear();
+ ExcitedGroupDispose(gridAtmosphere, loser);
+ ExcitedGroupResetCooldowns(winner);
+ }
+
+ ///
+ /// Resets the cooldowns of an excited group.
+ ///
+ /// The to reset cooldowns for.
+ private void ExcitedGroupResetCooldowns(ExcitedGroup excitedGroup)
+ {
+ DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
+ excitedGroup.BreakdownCooldown = 0;
+ excitedGroup.DismantleCooldown = 0;
+ }
+
+ ///
+ /// Performs a final equalization on all tiles in an excited group before deactivating it.
+ ///
+ /// The grid.
+ /// The to equalize and dissolve.
+ private void ExcitedGroupSelfBreakdown(
+ Entity ent,
+ ExcitedGroup excitedGroup)
+ {
+ DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
+ DebugTools.Assert(ent.Comp1.ExcitedGroups.Contains(excitedGroup), "Grid Atmosphere does not contain Excited Group!");
+ var combined = new GasMixture(Atmospherics.CellVolume);
+
+ var tileSize = excitedGroup.Tiles.Count;
+
+ if (excitedGroup.Disposed)
+ return;
+
+ if (tileSize == 0)
+ {
+ ExcitedGroupDispose(ent.Comp1, excitedGroup);
+ return;
+ }
+
+ // Combine all gasses in the group into a single mixture
+ // for distribution into each individual tile.
+ foreach (var tile in excitedGroup.Tiles)
+ {
+ if (tile?.Air == null)
+ continue;
+
+ Merge(combined, tile.Air);
+
+ // If this tile is space and space is all-consuming, the final equalization
+ // will result in a vacuum, so we can skip the rest of the equalization.
+ if (!ExcitedGroupsSpaceIsAllConsuming || !tile.Space)
+ continue;
+
+ combined.Clear();
+ break;
+ }
+
+ combined.Multiply(1 / (float)tileSize);
+
+ // Distribute the combined mixture evenly to all tiles in the group.
+ foreach (var tile in excitedGroup.Tiles)
+ {
+ if (tile?.Air == null)
+ continue;
+
+ tile.Air.CopyFrom(combined);
+ InvalidateVisuals(ent, tile);
+ }
+
+ excitedGroup.BreakdownCooldown = 0;
+ }
+
+ ///
+ /// Deactivates and removes all tiles from an excited group without performing a final equalization.
+ /// Used when an excited group is expected to be nearly equalized already to avoid unnecessary processing.
+ ///
+ /// The of the grid.
+ /// The to dissolve.
+ private void DeactivateGroupTiles(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup)
+ {
+ foreach (var tile in excitedGroup.Tiles)
{
- DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
- DebugTools.Assert(tile.ExcitedGroup == excitedGroup, "Tried to remove a tile from an excited group it's not present in!");
tile.ExcitedGroup = null;
- excitedGroup.Tiles.Remove(tile);
+ RemoveActiveTile(gridAtmosphere, tile);
}
- private void ExcitedGroupMerge(GridAtmosphereComponent gridAtmosphere, ExcitedGroup ourGroup, ExcitedGroup otherGroup)
+ excitedGroup.Tiles.Clear();
+ }
+
+ ///
+ /// Removes and disposes of an excited group without performing any final equalization
+ /// or deactivation of its tiles.
+ ///
+ private void ExcitedGroupDispose(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup)
+ {
+ if (excitedGroup.Disposed)
+ return;
+
+ DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(excitedGroup), "Grid Atmosphere does not contain Excited Group!");
+
+ excitedGroup.Disposed = true;
+ gridAtmosphere.ExcitedGroups.Remove(excitedGroup);
+
+ foreach (var tile in excitedGroup.Tiles)
{
- DebugTools.Assert(!ourGroup.Disposed, "Excited group is disposed!");
- DebugTools.Assert(!otherGroup.Disposed, "Excited group is disposed!");
- DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(ourGroup), "Grid Atmosphere does not contain Excited Group!");
- DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(otherGroup), "Grid Atmosphere does not contain Excited Group!");
- var ourSize = ourGroup.Tiles.Count;
- var otherSize = otherGroup.Tiles.Count;
-
- ExcitedGroup winner;
- ExcitedGroup loser;
-
- if (ourSize > otherSize)
- {
- winner = ourGroup;
- loser = otherGroup;
- }
- else
- {
- winner = otherGroup;
- loser = ourGroup;
- }
-
- foreach (var tile in loser.Tiles)
- {
- tile.ExcitedGroup = winner;
- winner.Tiles.Add(tile);
- }
-
- loser.Tiles.Clear();
- ExcitedGroupDispose(gridAtmosphere, loser);
- ExcitedGroupResetCooldowns(winner);
+ tile.ExcitedGroup = null;
}
- private void ExcitedGroupResetCooldowns(ExcitedGroup excitedGroup)
- {
- DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
- excitedGroup.BreakdownCooldown = 0;
- excitedGroup.DismantleCooldown = 0;
- }
-
- private void ExcitedGroupSelfBreakdown(
- Entity ent,
- ExcitedGroup excitedGroup)
- {
- DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
- DebugTools.Assert(ent.Comp1.ExcitedGroups.Contains(excitedGroup), "Grid Atmosphere does not contain Excited Group!");
- var combined = new GasMixture(Atmospherics.CellVolume);
-
- var tileSize = excitedGroup.Tiles.Count;
-
- if (excitedGroup.Disposed)
- return;
-
- if (tileSize == 0)
- {
- ExcitedGroupDispose(ent.Comp1, excitedGroup);
- return;
- }
-
- foreach (var tile in excitedGroup.Tiles)
- {
- if (tile?.Air == null)
- continue;
-
- Merge(combined, tile.Air);
-
- if (!ExcitedGroupsSpaceIsAllConsuming || !tile.Space)
- continue;
-
- combined.Clear();
- break;
- }
-
- combined.Multiply(1 / (float)tileSize);
-
- foreach (var tile in excitedGroup.Tiles)
- {
- if (tile?.Air == null)
- continue;
-
- tile.Air.CopyFrom(combined);
- InvalidateVisuals(ent, tile);
- }
-
- excitedGroup.BreakdownCooldown = 0;
- }
-
- ///
- /// This de-activates and removes all tiles in an excited group.
- ///
- private void DeactivateGroupTiles(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup)
- {
- foreach (var tile in excitedGroup.Tiles)
- {
- tile.ExcitedGroup = null;
- RemoveActiveTile(gridAtmosphere, tile);
- }
-
- excitedGroup.Tiles.Clear();
- }
-
- ///
- /// This removes an excited group without de-activating its tiles.
- ///
- private void ExcitedGroupDispose(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup)
- {
- if (excitedGroup.Disposed)
- return;
-
- DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(excitedGroup), "Grid Atmosphere does not contain Excited Group!");
-
- excitedGroup.Disposed = true;
- gridAtmosphere.ExcitedGroups.Remove(excitedGroup);
-
- foreach (var tile in excitedGroup.Tiles)
- {
- tile.ExcitedGroup = null;
- }
-
- excitedGroup.Tiles.Clear();
- }
+ excitedGroup.Tiles.Clear();
}
}
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs
index 613fcd14a6..c2702679d9 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs
@@ -10,198 +10,280 @@ using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
-namespace Content.Server.Atmos.EntitySystems
+namespace Content.Server.Atmos.EntitySystems;
+
+public sealed partial class AtmosphereSystem
{
- public sealed partial class AtmosphereSystem
+ /*
+ Handles Hotspots, which are gas-based tile fires that slowly grow and spread
+ to adjacent tiles if conditions are met.
+
+ You can think of a hotspot as a small flame on a tile that
+ grows by consuming a fuel and oxidizer from the tile's air,
+ with a certain volume and temperature.
+
+ This volume grows bigger and bigger as the fire continues,
+ until it effectively engulfs the entire tile, at which point
+ it starts spreading to adjacent tiles by radiating heat.
+ */
+
+ ///
+ /// Collection of hotspot sounds to play.
+ ///
+ private static readonly ProtoId DefaultHotspotSounds = "AtmosHotspot";
+
+ [Dependency] private readonly DecalSystem _decalSystem = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+
+ ///
+ /// Number of cycles the hotspot system must process before it can play another sound
+ /// on a hotspot.
+ ///
+ private const int HotspotSoundCooldownCycles = 200;
+
+ ///
+ /// Cooldown counter for hotspot sounds.
+ ///
+ private int _hotspotSoundCooldown = 0;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public SoundSpecifier? HotspotSound = new SoundCollectionSpecifier(DefaultHotspotSounds);
+
+ ///
+ /// Processes a hotspot on a .
+ ///
+ /// The grid entity that belongs to the tile to process.
+ /// The to process.
+ private void ProcessHotspot(
+ Entity ent,
+ TileAtmosphere tile)
{
- private static readonly ProtoId DefaultHotspotSounds = "AtmosHotspot";
+ var gridAtmosphere = ent.Comp1;
- [Dependency] private readonly DecalSystem _decalSystem = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
-
- private const int HotspotSoundCooldownCycles = 200;
-
- private int _hotspotSoundCooldown = 0;
-
- [ViewVariables(VVAccess.ReadWrite)]
- public SoundSpecifier? HotspotSound { get; private set; } = new SoundCollectionSpecifier(DefaultHotspotSounds);
-
- private void ProcessHotspot(
- Entity ent,
- TileAtmosphere tile)
+ // Hotspots that have fizzled out are assigned a new Hotspot struct
+ // with Valid set to false, so we can just check that here in
+ // one central place instead of manually removing it everywhere.
+ if (!tile.Hotspot.Valid)
{
- var gridAtmosphere = ent.Comp1;
- if (!tile.Hotspot.Valid)
+ gridAtmosphere.HotspotTiles.Remove(tile);
+ return;
+ }
+
+ AddActiveTile(gridAtmosphere, tile);
+
+ // Prevent the hotspot from processing on the same cycle it was created (???)
+ // TODO ATMOS: Is this even necessary anymore? The queue is kept per processing stage
+ // and is not updated until tne next cycle, so the condition of a hotspot being created
+ // and processed in the same cycle is impossible.
+ if (!tile.Hotspot.SkippedFirstProcess)
+ {
+ tile.Hotspot.SkippedFirstProcess = true;
+ return;
+ }
+
+ if (tile.ExcitedGroup != null)
+ ExcitedGroupResetCooldowns(tile.ExcitedGroup);
+
+ if (tile.Hotspot.Temperature < Atmospherics.FireMinimumTemperatureToExist ||
+ tile.Hotspot.Volume <= 1f ||
+ tile.Air == null ||
+ tile.Air.GetMoles(Gas.Oxygen) < 0.5f ||
+ tile.Air.GetMoles(Gas.Plasma) < 0.5f && tile.Air.GetMoles(Gas.Tritium) < 0.5f)
+ {
+ tile.Hotspot = new Hotspot();
+ InvalidateVisuals(ent, tile);
+ return;
+ }
+
+ PerformHotspotExposure(tile);
+
+ // This tile has now turned into a full-blown tile-fire.
+ // Start applying fire effects and spreading to adjacent tiles.
+ if (tile.Hotspot.Bypassing)
+ {
+ tile.Hotspot.State = 3;
+
+ var gridUid = ent.Owner;
+ var tilePos = tile.GridIndices;
+
+ // Get the existing decals on the tile
+ var tileDecals = _decalSystem.GetDecalsInRange(gridUid, tilePos);
+
+ // Count the burnt decals on the tile
+ var tileBurntDecals = 0;
+
+ foreach (var set in tileDecals)
{
- gridAtmosphere.HotspotTiles.Remove(tile);
- return;
+ if (Array.IndexOf(_burntDecals, set.Decal.Id) == -1)
+ continue;
+
+ tileBurntDecals++;
+
+ if (tileBurntDecals > 4)
+ break;
}
- AddActiveTile(gridAtmosphere, tile);
-
- if (!tile.Hotspot.SkippedFirstProcess)
+ // Add a random burned decal to the tile only if there are less than 4 of them
+ if (tileBurntDecals < 4)
{
- tile.Hotspot.SkippedFirstProcess = true;
- return;
+ _decalSystem.TryAddDecal(_burntDecals[_random.Next(_burntDecals.Length)],
+ new EntityCoordinates(gridUid, tilePos),
+ out _,
+ cleanable: true);
}
- if(tile.ExcitedGroup != null)
- ExcitedGroupResetCooldowns(tile.ExcitedGroup);
-
- if ((tile.Hotspot.Temperature < Atmospherics.FireMinimumTemperatureToExist) || (tile.Hotspot.Volume <= 1f)
- || tile.Air == null || tile.Air.GetMoles(Gas.Oxygen) < 0.5f || (tile.Air.GetMoles(Gas.Plasma) < 0.5f && tile.Air.GetMoles(Gas.Tritium) < 0.5f))
+ if (tile.Air.Temperature > Atmospherics.FireMinimumTemperatureToSpread)
{
- tile.Hotspot = new Hotspot();
- InvalidateVisuals(ent, tile);
- return;
- }
-
- PerformHotspotExposure(tile);
-
- if (tile.Hotspot.Bypassing)
- {
- tile.Hotspot.State = 3;
-
- var gridUid = ent.Owner;
- var tilePos = tile.GridIndices;
-
- // Get the existing decals on the tile
- var tileDecals = _decalSystem.GetDecalsInRange(gridUid, tilePos);
-
- // Count the burnt decals on the tile
- var tileBurntDecals = 0;
-
- foreach (var set in tileDecals)
+ var radiatedTemperature = tile.Air.Temperature * Atmospherics.FireSpreadRadiosityScale;
+ foreach (var otherTile in tile.AdjacentTiles)
{
- if (Array.IndexOf(_burntDecals, set.Decal.Id) == -1)
+ // TODO ATMOS: This is sus. Suss this out.
+ // Spread this fire to other tiles by exposing them to a hotspot if air can flow there.
+ // Unsure as to why this is sus.
+ if (otherTile == null)
continue;
- tileBurntDecals++;
-
- if (tileBurntDecals > 4)
- break;
+ if (!otherTile.Hotspot.Valid)
+ HotspotExpose(gridAtmosphere, otherTile, radiatedTemperature, Atmospherics.CellVolume / 4);
}
-
- // Add a random burned decal to the tile only if there are less than 4 of them
- if (tileBurntDecals < 4)
- _decalSystem.TryAddDecal(_burntDecals[_random.Next(_burntDecals.Length)], new EntityCoordinates(gridUid, tilePos), out _, cleanable: true);
-
- if (tile.Air.Temperature > Atmospherics.FireMinimumTemperatureToSpread)
- {
- var radiatedTemperature = tile.Air.Temperature * Atmospherics.FireSpreadRadiosityScale;
- foreach (var otherTile in tile.AdjacentTiles)
- {
- // TODO ATMOS: This is sus. Suss this out.
- if (otherTile == null)
- continue;
-
- if(!otherTile.Hotspot.Valid)
- HotspotExpose(gridAtmosphere, otherTile, radiatedTemperature, Atmospherics.CellVolume/4);
- }
- }
- }
- else
- {
- tile.Hotspot.State = (byte) (tile.Hotspot.Volume > Atmospherics.CellVolume * 0.4f ? 2 : 1);
- }
-
- if (tile.Hotspot.Temperature > tile.MaxFireTemperatureSustained)
- tile.MaxFireTemperatureSustained = tile.Hotspot.Temperature;
-
- if (_hotspotSoundCooldown++ == 0 && HotspotSound != null)
- {
- var coordinates = _mapSystem.ToCenterCoordinates(tile.GridIndex, tile.GridIndices);
-
- // A few details on the audio parameters for fire.
- // The greater the fire state, the lesser the pitch variation.
- // The greater the fire state, the greater the volume.
- _audio.PlayPvs(HotspotSound, coordinates, HotspotSound.Params.WithVariation(0.15f / tile.Hotspot.State).WithVolume(-5f + 5f * tile.Hotspot.State));
- }
-
- if (_hotspotSoundCooldown > HotspotSoundCooldownCycles)
- _hotspotSoundCooldown = 0;
-
- // TODO ATMOS Maybe destroy location here?
- }
-
- private void HotspotExpose(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile,
- float exposedTemperature, float exposedVolume, bool soh = false, EntityUid? sparkSourceUid = null)
- {
- if (tile.Air == null)
- return;
-
- var oxygen = tile.Air.GetMoles(Gas.Oxygen);
-
- if (oxygen < 0.5f)
- return;
-
- var plasma = tile.Air.GetMoles(Gas.Plasma);
- var tritium = tile.Air.GetMoles(Gas.Tritium);
-
- if (tile.Hotspot.Valid)
- {
- if (soh)
- {
- if (plasma > 0.5f || tritium > 0.5f)
- {
- if (tile.Hotspot.Temperature < exposedTemperature)
- tile.Hotspot.Temperature = exposedTemperature;
- if (tile.Hotspot.Volume < exposedVolume)
- tile.Hotspot.Volume = exposedVolume;
- }
- }
-
- return;
- }
-
- if ((exposedTemperature > Atmospherics.PlasmaMinimumBurnTemperature) && (plasma > 0.5f || tritium > 0.5f))
- {
- if (sparkSourceUid.HasValue)
- _adminLog.Add(LogType.Flammable, LogImpact.High, $"Heat/spark of {ToPrettyString(sparkSourceUid.Value)} caused atmos ignition of gas: {tile.Air.Temperature.ToString():temperature}K - {oxygen}mol Oxygen, {plasma}mol Plasma, {tritium}mol Tritium");
-
- tile.Hotspot = new Hotspot
- {
- Volume = exposedVolume * 25f,
- Temperature = exposedTemperature,
- SkippedFirstProcess = tile.CurrentCycle > gridAtmosphere.UpdateCounter,
- Valid = true,
- State = 1
- };
-
- AddActiveTile(gridAtmosphere, tile);
- gridAtmosphere.HotspotTiles.Add(tile);
}
}
-
- private void PerformHotspotExposure(TileAtmosphere tile)
+ else
{
- if (tile.Air == null || !tile.Hotspot.Valid) return;
+ // Little baby fire. Set the sprite state based on the current size of the fire.
+ tile.Hotspot.State = (byte)(tile.Hotspot.Volume > Atmospherics.CellVolume * 0.4f ? 2 : 1);
+ }
- tile.Hotspot.Bypassing = tile.Hotspot.SkippedFirstProcess && tile.Hotspot.Volume > tile.Air.Volume*0.95f;
+ if (tile.Hotspot.Temperature > tile.MaxFireTemperatureSustained)
+ tile.MaxFireTemperatureSustained = tile.Hotspot.Temperature;
- if (tile.Hotspot.Bypassing)
+ if (_hotspotSoundCooldown++ == 0 && HotspotSound != null)
+ {
+ var coordinates = _mapSystem.ToCenterCoordinates(tile.GridIndex, tile.GridIndices);
+
+ // A few details on the audio parameters for fire.
+ // The greater the fire state, the lesser the pitch variation.
+ // The greater the fire state, the greater the volume.
+ _audio.PlayPvs(HotspotSound,
+ coordinates,
+ HotspotSound.Params.WithVariation(0.15f / tile.Hotspot.State)
+ .WithVolume(-5f + 5f * tile.Hotspot.State));
+ }
+
+ if (_hotspotSoundCooldown > HotspotSoundCooldownCycles)
+ _hotspotSoundCooldown = 0;
+
+ // TODO ATMOS Maybe destroy location here?
+ }
+
+ ///
+ /// Exposes a tile to a hotspot of given temperature and volume, igniting it if conditions are met.
+ ///
+ /// The of the grid the tile is on.
+ /// The to expose.
+ /// The temperature of the hotspot to expose.
+ /// You can think of this as exposing a temperature of a flame.
+ /// The volume of the hotspot to expose.
+ /// You can think of this as how big the flame is initially.
+ /// Bigger flames will ramp a fire faster.
+ /// Whether to "boost" a fire that's currently on the tile already.
+ /// Does nothing if the tile isn't already a hotspot.
+ /// This clamps the temperature and volume of the hotspot to the maximum
+ /// of the provided parameters and whatever's on the tile.
+ /// Entity that started the exposure for admin logging.
+ private void HotspotExpose(GridAtmosphereComponent gridAtmosphere,
+ TileAtmosphere tile,
+ float exposedTemperature,
+ float exposedVolume,
+ bool soh = false,
+ EntityUid? sparkSourceUid = null)
+ {
+ if (tile.Air == null)
+ return;
+
+ var oxygen = tile.Air.GetMoles(Gas.Oxygen);
+
+ if (oxygen < 0.5f)
+ return;
+
+ var plasma = tile.Air.GetMoles(Gas.Plasma);
+ var tritium = tile.Air.GetMoles(Gas.Tritium);
+
+ if (tile.Hotspot.Valid)
+ {
+ if (soh)
{
- tile.Hotspot.Volume = tile.Air.ReactionResults[(byte)GasReaction.Fire] * Atmospherics.FireGrowthRate;
- tile.Hotspot.Temperature = tile.Air.Temperature;
- }
- else
- {
- var affected = tile.Air.RemoveVolume(tile.Hotspot.Volume);
- affected.Temperature = tile.Hotspot.Temperature;
- React(affected, tile);
- tile.Hotspot.Temperature = affected.Temperature;
- tile.Hotspot.Volume = affected.ReactionResults[(byte)GasReaction.Fire] * Atmospherics.FireGrowthRate;
- Merge(tile.Air, affected);
+ if (plasma > 0.5f || tritium > 0.5f)
+ {
+ tile.Hotspot.Temperature = MathF.Max(tile.Hotspot.Temperature, exposedTemperature);
+ tile.Hotspot.Volume = MathF.Max(tile.Hotspot.Volume, exposedVolume);
+ }
}
- var fireEvent = new TileFireEvent(tile.Hotspot.Temperature, tile.Hotspot.Volume);
- _entSet.Clear();
- _lookup.GetLocalEntitiesIntersecting(tile.GridIndex, tile.GridIndices, _entSet, 0f);
+ return;
+ }
- foreach (var entity in _entSet)
+ if (exposedTemperature > Atmospherics.PlasmaMinimumBurnTemperature && (plasma > 0.5f || tritium > 0.5f))
+ {
+ if (sparkSourceUid.HasValue)
{
- RaiseLocalEvent(entity, ref fireEvent);
+ _adminLog.Add(LogType.Flammable,
+ LogImpact.High,
+ $"Heat/spark of {ToPrettyString(sparkSourceUid.Value)} caused atmos ignition of gas: {tile.Air.Temperature.ToString():temperature}K - {oxygen}mol Oxygen, {plasma}mol Plasma, {tritium}mol Tritium");
}
+
+ tile.Hotspot = new Hotspot
+ {
+ Volume = exposedVolume * 25f,
+ Temperature = exposedTemperature,
+ SkippedFirstProcess = tile.CurrentCycle > gridAtmosphere.UpdateCounter,
+ Valid = true,
+ State = 1
+ };
+
+ AddActiveTile(gridAtmosphere, tile);
+ gridAtmosphere.HotspotTiles.Add(tile);
+ }
+ }
+
+ ///
+ /// Performs hotspot exposure processing on a .
+ ///
+ /// The to process.
+ private void PerformHotspotExposure(TileAtmosphere tile)
+ {
+ if (tile.Air == null || !tile.Hotspot.Valid)
+ return;
+
+ // Determine if the tile has become a full-blown fire if the volume of the fire has effectively reached
+ // the volume of the tile's air.
+ tile.Hotspot.Bypassing = tile.Hotspot.SkippedFirstProcess && tile.Hotspot.Volume > tile.Air.Volume * 0.95f;
+
+ // If the tile is effectively a full fire, use the tile's air for reactions, don't bother partitioning.
+ if (tile.Hotspot.Bypassing)
+ {
+ tile.Hotspot.Volume = tile.Air.ReactionResults[(byte)GasReaction.Fire] * Atmospherics.FireGrowthRate;
+ tile.Hotspot.Temperature = tile.Air.Temperature;
+ }
+ // Otherwise, pull out a fraction of the tile's air (the current hotspot volume) to perform reactions on.
+ else
+ {
+ var affected = tile.Air.RemoveVolume(tile.Hotspot.Volume);
+ affected.Temperature = tile.Hotspot.Temperature;
+ React(affected, tile);
+ tile.Hotspot.Temperature = affected.Temperature;
+ // Scale the fire based on the type of reaction that occured.
+ tile.Hotspot.Volume = affected.ReactionResults[(byte)GasReaction.Fire] * Atmospherics.FireGrowthRate;
+ Merge(tile.Air, affected);
+ }
+
+ var fireEvent = new TileFireEvent(tile.Hotspot.Temperature, tile.Hotspot.Volume);
+ _entSet.Clear();
+ _lookup.GetLocalEntitiesIntersecting(tile.GridIndex, tile.GridIndices, _entSet, 0f);
+
+ foreach (var entity in _entSet)
+ {
+ RaiseLocalEvent(entity, ref fireEvent);
}
}
}
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs
index 55b38924c0..ad19770bfe 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs
@@ -129,9 +129,16 @@ namespace Content.Server.Atmos.EntitySystems
switch (tile.LastShare)
{
+ // Refresh this tile's suspension cooldown if it had significant sharing.
case > Atmospherics.MinimumAirToSuspend:
ExcitedGroupResetCooldowns(tile.ExcitedGroup);
break;
+
+ // If this tile moved a very small amount of air, but not enough to matter,
+ // we set the dismantle cooldown to 0.
+ // This dissolves the group without performing an equalization as we expect
+ // the group to be mostly equalized already if we're moving around miniscule
+ // amounts of air.
case > Atmospherics.MinimumMolesDeltaToMove:
tile.ExcitedGroup.DismantleCooldown = 0;
break;
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs
index 9b8654af6d..c0f081f9ba 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs
@@ -365,7 +365,6 @@ namespace Content.Server.Atmos.EntitySystems
ExcitedGroupSelfBreakdown(ent, excitedGroup);
else if (excitedGroup.DismantleCooldown > Atmospherics.ExcitedGroupsDismantleCycles)
DeactivateGroupTiles(gridAtmosphere, excitedGroup);
- // TODO ATMOS. What is the point of this? why is this only de-exciting the group? Shouldn't it also dismantle it?
if (number++ < LagCheckIterations)
continue;
@@ -649,143 +648,196 @@ namespace Content.Server.Atmos.EntitySystems
if (atmosphere.LifeStage >= ComponentLifeStage.Stopping || Paused(owner) || !atmosphere.Simulated)
continue;
- atmosphere.Timer += frameTime;
-
- if (atmosphere.Timer < AtmosTime)
- continue;
-
- // We subtract it so it takes lost time into account.
- atmosphere.Timer -= AtmosTime;
-
var map = new Entity(xform.MapUid.Value, _mapAtmosQuery.CompOrNull(xform.MapUid.Value));
- switch (atmosphere.State)
+ var completionState = ProcessAtmosphere(ent, map, frameTime);
+
+ switch (completionState)
{
- case AtmosphereProcessingState.Revalidate:
- if (!ProcessRevalidate(ent))
- {
- atmosphere.ProcessingPaused = true;
- return;
- }
-
- atmosphere.ProcessingPaused = false;
-
- // Next state depends on whether monstermos equalization is enabled or not.
- // Note: We do this here instead of on the tile equalization step to prevent ending it early.
- // Therefore, a change to this CVar might only be applied after that step is over.
- atmosphere.State = MonstermosEqualization
- ? AtmosphereProcessingState.TileEqualize
- : AtmosphereProcessingState.ActiveTiles;
+ case AtmosphereProcessingCompletionState.Return:
+ return;
+ case AtmosphereProcessingCompletionState.Continue:
continue;
- case AtmosphereProcessingState.TileEqualize:
- if (!ProcessTileEqualize(ent))
- {
- atmosphere.ProcessingPaused = true;
- return;
- }
-
- atmosphere.ProcessingPaused = false;
- atmosphere.State = AtmosphereProcessingState.ActiveTiles;
- continue;
- case AtmosphereProcessingState.ActiveTiles:
- if (!ProcessActiveTiles(ent))
- {
- atmosphere.ProcessingPaused = true;
- return;
- }
-
- atmosphere.ProcessingPaused = false;
- // Next state depends on whether excited groups are enabled or not.
- atmosphere.State = ExcitedGroups ? AtmosphereProcessingState.ExcitedGroups : AtmosphereProcessingState.HighPressureDelta;
- continue;
- case AtmosphereProcessingState.ExcitedGroups:
- if (!ProcessExcitedGroups(ent))
- {
- atmosphere.ProcessingPaused = true;
- return;
- }
-
- atmosphere.ProcessingPaused = false;
- atmosphere.State = AtmosphereProcessingState.HighPressureDelta;
- continue;
- case AtmosphereProcessingState.HighPressureDelta:
- if (!ProcessHighPressureDelta((ent, ent)))
- {
- atmosphere.ProcessingPaused = true;
- return;
- }
-
- atmosphere.ProcessingPaused = false;
- atmosphere.State = DeltaPressureDamage
- ? AtmosphereProcessingState.DeltaPressure
- : AtmosphereProcessingState.Hotspots;
- continue;
- case AtmosphereProcessingState.DeltaPressure:
- if (!ProcessDeltaPressure(ent))
- {
- atmosphere.ProcessingPaused = true;
- return;
- }
-
- atmosphere.ProcessingPaused = false;
- atmosphere.State = AtmosphereProcessingState.Hotspots;
- continue;
- case AtmosphereProcessingState.Hotspots:
- if (!ProcessHotspots(ent))
- {
- atmosphere.ProcessingPaused = true;
- return;
- }
-
- atmosphere.ProcessingPaused = false;
- // Next state depends on whether superconduction is enabled or not.
- // Note: We do this here instead of on the tile equalization step to prevent ending it early.
- // Therefore, a change to this CVar might only be applied after that step is over.
- atmosphere.State = Superconduction
- ? AtmosphereProcessingState.Superconductivity
- : AtmosphereProcessingState.PipeNet;
- continue;
- case AtmosphereProcessingState.Superconductivity:
- if (!ProcessSuperconductivity(atmosphere))
- {
- atmosphere.ProcessingPaused = true;
- return;
- }
-
- atmosphere.ProcessingPaused = false;
- atmosphere.State = AtmosphereProcessingState.PipeNet;
- continue;
- case AtmosphereProcessingState.PipeNet:
- if (!ProcessPipeNets(atmosphere))
- {
- atmosphere.ProcessingPaused = true;
- return;
- }
-
- atmosphere.ProcessingPaused = false;
- atmosphere.State = AtmosphereProcessingState.AtmosDevices;
- continue;
- case AtmosphereProcessingState.AtmosDevices:
- if (!ProcessAtmosDevices(ent, map))
- {
- atmosphere.ProcessingPaused = true;
- return;
- }
-
- atmosphere.ProcessingPaused = false;
- atmosphere.State = AtmosphereProcessingState.Revalidate;
-
- // We reached the end of this atmosphere's update tick. Break out of the switch.
+ case AtmosphereProcessingCompletionState.Finished:
break;
}
-
- // And increase the update counter.
- atmosphere.UpdateCounter++;
}
// We finished processing all atmospheres successfully, therefore we won't be paused next tick.
_simulationPaused = false;
}
+
+ ///
+ /// Processes a through its processing stages.
+ ///
+ /// The entity to process.
+ /// The belonging to the
+ /// 's map.
+ /// The elapsed time since the last frame.
+ /// An that represents the completion state.
+ private AtmosphereProcessingCompletionState ProcessAtmosphere(Entity ent,
+ Entity mapAtmosphere,
+ float frameTime)
+ {
+ // They call me the deconstructor the way i be deconstructing it
+ // and by it, i mean... my entity
+ var (owner, atmosphere, visuals, grid, xform) = ent;
+
+ atmosphere.Timer += frameTime;
+
+ if (atmosphere.Timer < AtmosTime)
+ return AtmosphereProcessingCompletionState.Continue;
+
+ // We subtract it so it takes lost time into account.
+ atmosphere.Timer -= AtmosTime;
+
+ switch (atmosphere.State)
+ {
+ case AtmosphereProcessingState.Revalidate:
+ if (!ProcessRevalidate(ent))
+ {
+ atmosphere.ProcessingPaused = true;
+ return AtmosphereProcessingCompletionState.Return;
+ }
+
+ atmosphere.ProcessingPaused = false;
+
+ // Next state depends on whether monstermos equalization is enabled or not.
+ // Note: We do this here instead of on the tile equalization step to prevent ending it early.
+ // Therefore, a change to this CVar might only be applied after that step is over.
+ atmosphere.State = MonstermosEqualization
+ ? AtmosphereProcessingState.TileEqualize
+ : AtmosphereProcessingState.ActiveTiles;
+ return AtmosphereProcessingCompletionState.Continue;
+ case AtmosphereProcessingState.TileEqualize:
+ if (!ProcessTileEqualize(ent))
+ {
+ atmosphere.ProcessingPaused = true;
+ return AtmosphereProcessingCompletionState.Return;
+ }
+
+ atmosphere.ProcessingPaused = false;
+ atmosphere.State = AtmosphereProcessingState.ActiveTiles;
+ return AtmosphereProcessingCompletionState.Continue;
+ case AtmosphereProcessingState.ActiveTiles:
+ if (!ProcessActiveTiles(ent))
+ {
+ atmosphere.ProcessingPaused = true;
+ return AtmosphereProcessingCompletionState.Return;
+ }
+
+ atmosphere.ProcessingPaused = false;
+ // Next state depends on whether excited groups are enabled or not.
+ atmosphere.State = ExcitedGroups ? AtmosphereProcessingState.ExcitedGroups : AtmosphereProcessingState.HighPressureDelta;
+ return AtmosphereProcessingCompletionState.Continue;
+ case AtmosphereProcessingState.ExcitedGroups:
+ if (!ProcessExcitedGroups(ent))
+ {
+ atmosphere.ProcessingPaused = true;
+ return AtmosphereProcessingCompletionState.Return;
+ }
+
+ atmosphere.ProcessingPaused = false;
+ atmosphere.State = AtmosphereProcessingState.HighPressureDelta;
+ return AtmosphereProcessingCompletionState.Continue;
+ case AtmosphereProcessingState.HighPressureDelta:
+ if (!ProcessHighPressureDelta((ent, ent)))
+ {
+ atmosphere.ProcessingPaused = true;
+ return AtmosphereProcessingCompletionState.Return;
+ }
+
+ atmosphere.ProcessingPaused = false;
+ atmosphere.State = DeltaPressureDamage
+ ? AtmosphereProcessingState.DeltaPressure
+ : AtmosphereProcessingState.Hotspots;
+ return AtmosphereProcessingCompletionState.Continue;
+ case AtmosphereProcessingState.DeltaPressure:
+ if (!ProcessDeltaPressure(ent))
+ {
+ atmosphere.ProcessingPaused = true;
+ return AtmosphereProcessingCompletionState.Return;
+ }
+
+ atmosphere.ProcessingPaused = false;
+ atmosphere.State = AtmosphereProcessingState.Hotspots;
+ return AtmosphereProcessingCompletionState.Continue;
+ case AtmosphereProcessingState.Hotspots:
+ if (!ProcessHotspots(ent))
+ {
+ atmosphere.ProcessingPaused = true;
+ return AtmosphereProcessingCompletionState.Return;
+ }
+
+ atmosphere.ProcessingPaused = false;
+ // Next state depends on whether superconduction is enabled or not.
+ // Note: We do this here instead of on the tile equalization step to prevent ending it early.
+ // Therefore, a change to this CVar might only be applied after that step is over.
+ atmosphere.State = Superconduction
+ ? AtmosphereProcessingState.Superconductivity
+ : AtmosphereProcessingState.PipeNet;
+ return AtmosphereProcessingCompletionState.Continue;
+ case AtmosphereProcessingState.Superconductivity:
+ if (!ProcessSuperconductivity(atmosphere))
+ {
+ atmosphere.ProcessingPaused = true;
+ return AtmosphereProcessingCompletionState.Return;
+ }
+
+ atmosphere.ProcessingPaused = false;
+ atmosphere.State = AtmosphereProcessingState.PipeNet;
+ return AtmosphereProcessingCompletionState.Continue;
+ case AtmosphereProcessingState.PipeNet:
+ if (!ProcessPipeNets(atmosphere))
+ {
+ atmosphere.ProcessingPaused = true;
+ return AtmosphereProcessingCompletionState.Return;
+ }
+
+ atmosphere.ProcessingPaused = false;
+ atmosphere.State = AtmosphereProcessingState.AtmosDevices;
+ return AtmosphereProcessingCompletionState.Continue;
+ case AtmosphereProcessingState.AtmosDevices:
+ if (!ProcessAtmosDevices(ent, mapAtmosphere))
+ {
+ atmosphere.ProcessingPaused = true;
+ return AtmosphereProcessingCompletionState.Return;
+ }
+
+ atmosphere.ProcessingPaused = false;
+ atmosphere.State = AtmosphereProcessingState.Revalidate;
+
+ // We reached the end of this atmosphere's update tick. Break out of the switch.
+ break;
+ }
+
+ atmosphere.UpdateCounter++;
+
+ return AtmosphereProcessingCompletionState.Finished;
+ }
+ }
+
+ ///
+ /// An enum representing the completion state of a 's processing steps.
+ /// The processing of a spans over multiple stages and sticks,
+ /// with the method handling the processing having multiple return types.
+ ///
+ public enum AtmosphereProcessingCompletionState : byte
+ {
+ ///
+ /// Method is returning, ex. due to delegating processing to the next tick.
+ ///
+ Return,
+
+ ///
+ /// Method is continuing, ex. due to finishing a single processing stage.
+ ///
+ Continue,
+
+ ///
+ /// Method is finished with the GridAtmosphere.
+ ///
+ Finished,
}
public enum AtmosphereProcessingState : byte
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Utils.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Utils.cs
index 36355d7ba0..596368f000 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Utils.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Utils.cs
@@ -61,7 +61,22 @@ public partial class AtmosphereSystem
return Atmospherics.CellVolume * mapGrid.TileSize * tiles;
}
- public readonly record struct AirtightData(AtmosDirection BlockedDirections, bool NoAirWhenBlocked,
+ ///
+ /// Data on the airtightness of a .
+ /// Cached on the and updated during
+ /// if it was invalidated.
+ ///
+ /// The current directions blocked on this tile.
+ /// This is where air cannot flow to.
+ /// Whether the tile can have air when blocking directions.
+ /// Common for entities like thin windows which only block one face but can still have air in the residing tile.
+ /// If true, Atmospherics will generate air (yes, creating matter from nothing)
+ /// using the adjacent tiles as a seed if the airtightness is removed and the tile has no air.
+ /// This allows stuff like airlocks that void air when becoming airtight to keep opening/closing without
+ /// draining a room by continuously voiding air.
+ public readonly record struct AirtightData(
+ AtmosDirection BlockedDirections,
+ bool NoAirWhenBlocked,
bool FixVacuum);
private void UpdateAirtightData(EntityUid uid, GridAtmosphereComponent atmos, MapGridComponent grid, TileAtmosphere tile)
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
index 00b7e16913..8120caca4e 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
@@ -14,7 +14,7 @@ using Robust.Shared.Map;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using System.Linq;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Robust.Shared.Threading;
namespace Content.Server.Atmos.EntitySystems;
diff --git a/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs b/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs
index ec508790ba..c23f58637d 100644
--- a/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs
@@ -3,7 +3,8 @@ using Content.Server.Administration.Logs;
using Content.Server.Atmos.Components;
using Content.Shared.Alert;
using Content.Shared.Atmos;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Inventory;
diff --git a/Content.Server/Atmos/EntitySystems/DeltaPressureSystem.cs b/Content.Server/Atmos/EntitySystems/DeltaPressureSystem.cs
index dd7091c270..669735b90d 100644
--- a/Content.Server/Atmos/EntitySystems/DeltaPressureSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/DeltaPressureSystem.cs
@@ -1,6 +1,6 @@
using Content.Server.Atmos.Components;
-using Content.Shared.Examine;
-using Robust.Shared.Map.Components;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.EntitySystems;
namespace Content.Server.Atmos.EntitySystems;
@@ -14,7 +14,7 @@ namespace Content.Server.Atmos.EntitySystems;
/// This system handles the adding and removing of entities to a processing list,
/// as well as any field changes via the API.
///
-public sealed class DeltaPressureSystem : EntitySystem
+public sealed partial class DeltaPressureSystem : SharedDeltaPressureSystem
{
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
@@ -24,8 +24,6 @@ public sealed class DeltaPressureSystem : EntitySystem
SubscribeLocalEvent(OnComponentInit);
SubscribeLocalEvent(OnComponentShutdown);
- SubscribeLocalEvent(OnExamined);
-
SubscribeLocalEvent(OnGridChanged);
}
@@ -48,12 +46,6 @@ public sealed class DeltaPressureSystem : EntitySystem
_atmosphereSystem.TryRemoveDeltaPressureEntity(ent.Comp.GridUid.Value, ent);
}
- private void OnExamined(Entity ent, ref ExaminedEvent args)
- {
- if (ent.Comp.IsTakingDamage)
- args.PushMarkup(Loc.GetString("window-taking-damage"));
- }
-
private void OnGridChanged(Entity ent, ref GridUidChangedEvent args)
{
if (args.OldGrid != null)
diff --git a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs
index 81bd4e5c6c..071c63c500 100644
--- a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs
@@ -7,7 +7,7 @@ using Content.Shared.ActionBlocker;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.IgnitionSource;
using Content.Shared.Interaction;
diff --git a/Content.Server/Atmos/ExcitedGroup.cs b/Content.Server/Atmos/ExcitedGroup.cs
index 73dc6c2329..1554318f65 100644
--- a/Content.Server/Atmos/ExcitedGroup.cs
+++ b/Content.Server/Atmos/ExcitedGroup.cs
@@ -1,13 +1,44 @@
-namespace Content.Server.Atmos
+namespace Content.Server.Atmos;
+
+///
+/// Internal Atmospherics class that stores data about a group of s
+/// that are excited and need to be processed.
+///
+/// Excited Groups is an optimization routine executed during LINDA
+/// that bunches small groups of active s
+/// together and performs equalization processing on the entire group when the group dissolves.
+/// Dissolution happens when LINDA operations between the tiles decrease to very low mole deltas.
+///
+public sealed class ExcitedGroup
{
- public sealed class ExcitedGroup
- {
- [ViewVariables] public bool Disposed = false;
+ ///
+ /// Whether this Active Group has been disposed of.
+ /// Used to make sure we don't perform operations on active groups that
+ /// we've already dissolved.
+ ///
+ [ViewVariables]
+ public bool Disposed = false;
- [ViewVariables] public readonly List Tiles = new(100);
+ ///
+ /// List of tiles that belong to this excited group.
+ ///
+ [ViewVariables]
+ public readonly List Tiles = new(100);
- [ViewVariables] public int DismantleCooldown { get; set; } = 0;
+ ///
+ /// Cycles before this excited group will be queued for dismantling.
+ /// Dismantling is the process of equalizing the atmosphere
+ /// across all tiles in the excited group and removing the group.
+ ///
+ [ViewVariables]
+ public int DismantleCooldown = 0;
- [ViewVariables] public int BreakdownCooldown { get; set; } = 0;
- }
+ ///
+ /// Cycles before this excited group will be allowed to break down and deactivate.
+ /// Breakdown occurs when the excited group is small enough and inactive enough
+ /// to be safely removed without equalization. Used where the mole deltas across
+ /// the group are very low but not high enough for an equalization to occur.
+ ///
+ [ViewVariables]
+ public int BreakdownCooldown = 0;
}
diff --git a/Content.Server/Atmos/Hotspot.cs b/Content.Server/Atmos/Hotspot.cs
index 987acf73c2..2783362c31 100644
--- a/Content.Server/Atmos/Hotspot.cs
+++ b/Content.Server/Atmos/Hotspot.cs
@@ -1,26 +1,57 @@
-namespace Content.Server.Atmos
+namespace Content.Server.Atmos;
+
+///
+/// Internal Atmospherics struct that stores data about a hotspot in a tile.
+/// Hotspots are used to model (slow-spreading) fires and firestarters.
+///
+public struct Hotspot
{
- public struct Hotspot
- {
- [ViewVariables]
- public bool Valid;
+ ///
+ /// Whether this hotspot is currently representing fire and needs to be processed.
+ /// Set when the hotspot "becomes alight". This is never set to false
+ /// because Atmospherics will just assign
+ /// a new struct when the fire goes out.
+ ///
+ [ViewVariables]
+ public bool Valid;
- [ViewVariables]
- public bool SkippedFirstProcess;
+ ///
+ /// Whether this hotspot has skipped its first process cycle.
+ /// AtmosphereSystem.Hotspot skips processing a hotspot beyond
+ /// setting it to active (for LINDA processing) the first
+ /// time it is processed.
+ ///
+ [ViewVariables]
+ public bool SkippedFirstProcess;
- [ViewVariables]
- public bool Bypassing;
+ ///
+ /// Whether this hotspot is currently using the tile for reacting and fire processing
+ /// instead of a fraction of the tile's air.
+ ///
+ /// When a tile is considered a hotspot, Hotspot will pull a fraction of that tile's
+ /// air out of the tile and perform a reaction on that air, merging it back afterward.
+ /// Bypassing triggers when the hotspot volume nears the tile's volume, making the system
+ /// use the tile's GasMixture instead of pulling a fraction out.
+ ///
+ [ViewVariables]
+ public bool Bypassing;
- [ViewVariables]
- public float Temperature;
+ ///
+ /// Current temperature of the hotspot's volume, in Kelvin.
+ ///
+ [ViewVariables]
+ public float Temperature;
- [ViewVariables]
- public float Volume;
+ ///
+ /// Current volume of the hotspot, in liters.
+ /// You can think of this as the volume of the current fire in the tile.
+ ///
+ [ViewVariables]
+ public float Volume;
- ///
- /// State for the fire sprite.
- ///
- [ViewVariables]
- public byte State;
- }
+ ///
+ /// State for the fire sprite.
+ ///
+ [ViewVariables]
+ public byte State;
}
diff --git a/Content.Server/Atmos/MonstermosInfo.cs b/Content.Server/Atmos/MonstermosInfo.cs
index 810ce71d2c..67995b5fb5 100644
--- a/Content.Server/Atmos/MonstermosInfo.cs
+++ b/Content.Server/Atmos/MonstermosInfo.cs
@@ -1,80 +1,148 @@
using Content.Shared.Atmos;
-namespace Content.Server.Atmos
+namespace Content.Server.Atmos;
+
+///
+/// Atmospherics class that stores data on tiles for Monstermos calculations and operations.
+///
+public struct MonstermosInfo
{
- public struct MonstermosInfo
+ ///
+ /// The last cycle this tile was processed for monstermos calculations.
+ /// Used to determine if Monstermos has already processed this tile in the
+ /// current tick's processing run.
+ ///
+ [ViewVariables]
+ public int LastCycle;
+
+ ///
+ /// The last global cycle (on the GridAtmosphereComponent) this tile was processed for
+ /// monstermos calculations.
+ /// Monstermos can process multiple groups, and these groups may intersect with each other.
+ /// This allows Monstermos to check if a tile belongs to another group that has already been processed,
+ /// and skip processing it again.
+ ///
+ /// Used for exploring the current area for determining tiles that should be equalized
+ /// using a BFS fill (see https://en.wikipedia.org/wiki/Breadth-first_search)
+ ///
+ [ViewVariables]
+ public long LastQueueCycle;
+
+ ///
+ /// Similar to . Monstermos performs a second slow pass after the main
+ /// BFS fill in order to build a gradient map to determine transfer directions and amounts.
+ /// This field also tracks if we've already processed this tile in that slow pass so we don't re-queue it.
+ ///
+ [ViewVariables]
+ public long LastSlowQueueCycle;
+
+ ///
+ /// Difference in the amount of moles in this tile compared to the tile's neighbors.
+ /// Used to determine "how strongly" air wants to flow in/out of this tile from/to its neighbors.
+ ///
+ [ViewVariables]
+ public float MoleDelta;
+
+ ///
+ /// Number of moles that are going to be transferred in this direction during final equalization.
+ ///
+ [ViewVariables]
+ public float TransferDirectionEast;
+
+ ///
+ /// Number of moles that are going to be transferred in this direction during final equalization.
+ ///
+ [ViewVariables]
+ public float TransferDirectionWest;
+
+ ///
+ /// Number of moles that are going to be transferred in this direction during final equalization.
+ ///
+ [ViewVariables]
+ public float TransferDirectionNorth;
+
+ ///
+ /// Number of moles that are going to be transferred in this direction during final equalization.
+ ///
+ [ViewVariables]
+ public float TransferDirectionSouth;
+
+ ///
+ /// Number of moles that are going to be transferred to this tile during final equalization.
+ /// You can think of this as molar flow rate, or the amount of air currently flowing through this tile.
+ /// Used for space wind and airflow sounds during explosive decompression or big movements.
+ ///
+ /// During equalization calculations, Monstermos determines how much air is going to be transferred
+ /// between tiles, and sums that up into this field. It then either
+ ///
+ /// determines how many moles to transfer in the direction of , or
+ ///
+ /// determines how many moles to move in each direction using ,
+ /// setting the TransferDirection fields accordingly based on the ratio obtained
+ /// from .
+ ///
+ [ViewVariables]
+ public float CurrentTransferAmount;
+
+ ///
+ /// A pointer from the current tile to the direction in which air is being transferred the most.
+ ///
+ [ViewVariables]
+ public AtmosDirection CurrentTransferDirection;
+
+ ///
+ /// Marks this tile as being equalized using the O(n log n) algorithm.
+ ///
+ [ViewVariables]
+ public bool FastDone;
+
+ ///
+ /// Gets or sets the TransferDirection in the given direction.
+ ///
+ ///
+ /// Thrown when an invalid direction is given
+ /// (a non-cardinal direction)
+ public float this[AtmosDirection direction]
{
- [ViewVariables]
- public int LastCycle;
-
- [ViewVariables]
- public long LastQueueCycle;
-
- [ViewVariables]
- public long LastSlowQueueCycle;
-
- [ViewVariables]
- public float MoleDelta;
-
- [ViewVariables]
- public float TransferDirectionEast;
-
- [ViewVariables]
- public float TransferDirectionWest;
-
- [ViewVariables]
- public float TransferDirectionNorth;
-
- [ViewVariables]
- public float TransferDirectionSouth;
-
- [ViewVariables]
- public float CurrentTransferAmount;
-
- [ViewVariables]
- public AtmosDirection CurrentTransferDirection;
-
- [ViewVariables]
- public bool FastDone;
-
- public float this[AtmosDirection direction]
- {
- get =>
- direction switch
- {
- AtmosDirection.East => TransferDirectionEast,
- AtmosDirection.West => TransferDirectionWest,
- AtmosDirection.North => TransferDirectionNorth,
- AtmosDirection.South => TransferDirectionSouth,
- _ => throw new ArgumentOutOfRangeException(nameof(direction))
- };
-
- set
+ get =>
+ direction switch
{
- switch (direction)
- {
- case AtmosDirection.East:
- TransferDirectionEast = value;
- break;
- case AtmosDirection.West:
- TransferDirectionWest = value;
- break;
- case AtmosDirection.North:
- TransferDirectionNorth = value;
- break;
- case AtmosDirection.South:
- TransferDirectionSouth = value;
- break;
- default:
- throw new ArgumentOutOfRangeException(nameof(direction));
- }
+ AtmosDirection.East => TransferDirectionEast,
+ AtmosDirection.West => TransferDirectionWest,
+ AtmosDirection.North => TransferDirectionNorth,
+ AtmosDirection.South => TransferDirectionSouth,
+ _ => throw new ArgumentOutOfRangeException(nameof(direction))
+ };
+
+ set
+ {
+ switch (direction)
+ {
+ case AtmosDirection.East:
+ TransferDirectionEast = value;
+ break;
+ case AtmosDirection.West:
+ TransferDirectionWest = value;
+ break;
+ case AtmosDirection.North:
+ TransferDirectionNorth = value;
+ break;
+ case AtmosDirection.South:
+ TransferDirectionSouth = value;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(direction));
}
}
+ }
- public float this[int index]
- {
- get => this[(AtmosDirection) (1 << index)];
- set => this[(AtmosDirection) (1 << index)] = value;
- }
+ ///
+ /// Gets or sets the TransferDirection by index.
+ ///
+ /// The index of the direction
+ public float this[int index]
+ {
+ get => this[(AtmosDirection) (1 << index)];
+ set => this[(AtmosDirection) (1 << index)] = value;
}
}
diff --git a/Content.Server/Atmos/Piping/EntitySystems/GasPipeManifoldSystem.cs b/Content.Server/Atmos/Piping/EntitySystems/GasPipeManifoldSystem.cs
index bb7678d28d..6dfb1afa86 100644
--- a/Content.Server/Atmos/Piping/EntitySystems/GasPipeManifoldSystem.cs
+++ b/Content.Server/Atmos/Piping/EntitySystems/GasPipeManifoldSystem.cs
@@ -51,6 +51,7 @@ public sealed partial class GasPipeManifoldSystem : EntitySystem
return;
var pipeNames = ent.Comp.InletNames.Union(ent.Comp.OutletNames);
+ var pipeCount = pipeNames.Count();
foreach (var pipeName in pipeNames)
{
@@ -58,8 +59,8 @@ public sealed partial class GasPipeManifoldSystem : EntitySystem
continue;
var pipeLocal = pipe.Air.Clone();
- pipeLocal.Multiply(pipe.Volume / pipe.Air.Volume);
- pipeLocal.Volume = pipe.Volume;
+ pipeLocal.Multiply(pipe.Volume * pipeCount / pipe.Air.Volume);
+ pipeLocal.Volume = pipe.Volume * pipeCount;
args.GasMixtures.Add((Name(ent), pipeLocal));
break;
diff --git a/Content.Server/Atmos/Rotting/RottingSystem.cs b/Content.Server/Atmos/Rotting/RottingSystem.cs
index 57c1504b16..5feb95e3c4 100644
--- a/Content.Server/Atmos/Rotting/RottingSystem.cs
+++ b/Content.Server/Atmos/Rotting/RottingSystem.cs
@@ -2,7 +2,7 @@ using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Rotting;
using Content.Shared.Body.Events;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Temperature.Components;
using Robust.Server.Containers;
using Robust.Shared.Physics.Components;
diff --git a/Content.Server/Atmos/TileAtmosphere.cs b/Content.Server/Atmos/TileAtmosphere.cs
index 46a85990fa..eba0df192a 100644
--- a/Content.Server/Atmos/TileAtmosphere.cs
+++ b/Content.Server/Atmos/TileAtmosphere.cs
@@ -1,165 +1,248 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
-using Content.Shared.Maps;
-using Robust.Shared.Map;
-namespace Content.Server.Atmos
+namespace Content.Server.Atmos;
+
+///
+/// Internal Atmospherics class that stores data on an atmosphere in a single tile.
+/// You should not be using these directly outside of .
+/// Use the public APIs in instead.
+///
+[Access(typeof(AtmosphereSystem), typeof(GasTileOverlaySystem), typeof(AtmosDebugOverlaySystem))]
+public sealed class TileAtmosphere : IGasMixtureHolder
{
///
- /// Internal Atmos class that stores data about the atmosphere in a grid.
- /// You shouldn't use this directly, use instead.
+ /// The last cycle this tile's air was archived into .
+ /// See for more info on archival.
///
- [Access(typeof(AtmosphereSystem), typeof(GasTileOverlaySystem), typeof(AtmosDebugOverlaySystem))]
- public sealed class TileAtmosphere : IGasMixtureHolder
+ [ViewVariables]
+ public int ArchivedCycle;
+
+ ///
+ /// Current cycle this tile was processed.
+ /// Used to prevent double-processing in a single cycle in many processing stages.
+ ///
+ [ViewVariables]
+ public int CurrentCycle;
+
+ ///
+ /// Current temperature of this tile, in Kelvin.
+ /// Used for Superconduction.
+ /// This is not the temperature of the attached !
+ ///
+ [ViewVariables]
+ public float Temperature = Atmospherics.T20C;
+
+ ///
+ /// The current target tile for pressure movement for the current cycle.
+ /// Gas will be moved towards this tile during pressure equalization.
+ /// Also see .
+ ///
+ [ViewVariables]
+ public TileAtmosphere? PressureSpecificTarget;
+
+ ///
+ /// The current pressure difference (delta) between this tile and its pressure target.
+ /// If Monstermos is enabled, this value represents the quantity of moles transferred.
+ ///
+ [ViewVariables]
+ public float PressureDifference;
+
+ ///
+ /// The current heat capacity of this tile.
+ /// Used for Superconduction.
+ /// This is not the heat capacity of the attached !
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float HeatCapacity = Atmospherics.MinimumHeatCapacity;
+
+ ///
+ /// The current thermal conductivity of this tile.
+ /// Describes how well heat moves between this tile and adjacent tiles during superconduction.
+ ///
+ [ViewVariables]
+ public float ThermalConductivity = 0.05f;
+
+ ///
+ /// Designates whether this tile is currently excited for processing in an excited group or LINDA.
+ ///
+ [ViewVariables]
+ public bool Excited;
+
+ ///
+ /// Whether this tile should be considered space.
+ ///
+ [ViewVariables]
+ public bool Space;
+
+ ///
+ /// Cached adjacent tiles for this tile.
+ /// Ordered in the same order as
+ /// (should be North, South, East, West).
+ /// Adjacent tiles can be null if air cannot flow to them.
+ ///
+ [ViewVariables]
+ public readonly TileAtmosphere?[] AdjacentTiles = new TileAtmosphere[Atmospherics.Directions];
+
+ ///
+ /// Neighbouring tiles to which air can flow. This is a combination of this tile's unblocked direction, and the
+ /// unblocked directions on adjacent tiles.
+ ///
+ [ViewVariables]
+ public AtmosDirection AdjacentBits = AtmosDirection.Invalid;
+
+ ///
+ /// Current information for this tile.
+ ///
+ [ViewVariables]
+ [Access(typeof(AtmosphereSystem), Other = AccessPermissions.ReadExecute)]
+ public MonstermosInfo MonstermosInfo;
+
+ ///
+ /// Current information for this tile.
+ ///
+ [ViewVariables]
+ public Hotspot Hotspot;
+
+ ///
+ /// Points to the direction of the recipient tile for pressure equalization logic
+ /// (Monstermos or HighPressureDelta otherwise).
+ ///
+ [ViewVariables]
+ public AtmosDirection PressureDirection;
+
+ ///
+ /// Last cycle's for debugging purposes.
+ ///
+ [ViewVariables]
+ public AtmosDirection LastPressureDirection;
+
+ ///
+ /// Grid entity this tile belongs to.
+ ///
+ [ViewVariables]
+ [Access(typeof(AtmosphereSystem))]
+ public EntityUid GridIndex;
+
+ ///
+ /// The grid indices of this tile.
+ ///
+ [ViewVariables]
+ public Vector2i GridIndices;
+
+ ///
+ /// The excited group this tile belongs to, if any.
+ ///
+ [ViewVariables]
+ public ExcitedGroup? ExcitedGroup;
+
+ ///
+ /// The air in this tile. If null, this tile is completely air-blocked.
+ /// This can be immutable if the tile is spaced.
+ ///
+ [ViewVariables]
+ [Access(typeof(AtmosphereSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
+ public GasMixture? Air;
+
+ ///
+ /// A copy of the air in this tile from the last time it was archived at .
+ /// LINDA archives the air before doing any necessary processing and uses this to perform its calculations,
+ /// making the results of LINDA independent of the order in which tiles are processed.
+ ///
+ [ViewVariables]
+ public GasMixture? AirArchived;
+
+ ///
+ /// The amount of gas last shared to adjacent tiles during LINDA processing.
+ /// Used to determine when LINDA should dismantle an excited group
+ /// or extend its time alive.
+ ///
+ [DataField("lastShare")]
+ public float LastShare;
+
+ ///
+ /// Implementation of .
+ ///
+ GasMixture IGasMixtureHolder.Air
{
- [ViewVariables]
- public int ArchivedCycle;
+ get => Air ?? new GasMixture(Atmospherics.CellVolume){ Temperature = Temperature };
+ set => Air = value;
+ }
- [ViewVariables]
- public int CurrentCycle;
+ ///
+ /// The maximum temperature this tile has sustained during hotspot fire processing.
+ /// Used for debugging.
+ ///
+ [ViewVariables]
+ public float MaxFireTemperatureSustained;
- [ViewVariables]
- public float Temperature { get; set; } = Atmospherics.T20C;
+ ///
+ /// If true, then this tile is directly exposed to the map's atmosphere, either because the grid has no tile at
+ /// this position, or because the tile type is not airtight.
+ ///
+ [ViewVariables]
+ public bool MapAtmosphere;
- [ViewVariables]
- public TileAtmosphere? PressureSpecificTarget { get; set; }
+ ///
+ /// If true, this tile does not actually exist on the grid, it only exists to represent the map's atmosphere for
+ /// adjacent grid tiles.
+ /// This tile often has immutable air and is sitting off the edge of the grid, where there is no grid.
+ ///
+ [ViewVariables]
+ public bool NoGridTile;
- ///
- /// This is either the pressure difference, or the quantity of moles transferred if monstermos is enabled.
- ///
- [ViewVariables]
- public float PressureDifference { get; set; }
+ ///
+ /// If true, this tile is queued for processing in
+ ///
+ [ViewVariables]
+ public bool TrimQueued;
- [ViewVariables(VVAccess.ReadWrite)]
- public float HeatCapacity { get; set; } = Atmospherics.MinimumHeatCapacity;
+ ///
+ /// Cached information about airtight entities on this tile. This gets updated anytime a tile gets invalidated
+ /// (i.e., gets added to ).
+ ///
+ public AtmosphereSystem.AirtightData AirtightData;
- [ViewVariables]
- public float ThermalConductivity { get; set; } = 0.05f;
+ ///
+ /// Creates a new TileAtmosphere.
+ ///
+ /// The grid entity this tile belongs to.
+ /// >The grid indices of this tile.
+ /// The gas mixture of this tile.
+ /// If true, the gas mixture will be marked immutable.
+ /// If true, this tile is considered space.
+ public TileAtmosphere(EntityUid gridIndex, Vector2i gridIndices, GasMixture? mixture = null, bool immutable = false, bool space = false)
+ {
+ GridIndex = gridIndex;
+ GridIndices = gridIndices;
+ Air = mixture;
+ AirArchived = Air?.Clone();
+ Space = space;
- [ViewVariables]
- public bool Excited { get; set; }
+ if(immutable)
+ Air?.MarkImmutable();
+ }
- ///
- /// Whether this tile should be considered space.
- ///
- [ViewVariables]
- public bool Space { get; set; }
+ ///
+ /// Creates a copy of another TileAtmosphere.
+ ///
+ /// The TileAtmosphere to copy.
+ public TileAtmosphere(TileAtmosphere other)
+ {
+ GridIndex = other.GridIndex;
+ GridIndices = other.GridIndices;
+ Space = other.Space;
+ NoGridTile = other.NoGridTile;
+ MapAtmosphere = other.MapAtmosphere;
+ Air = other.Air?.Clone();
+ AirArchived = Air != null ? Air.Clone() : null;
+ }
- ///
- /// Adjacent tiles in the same order as . (NSEW)
- ///
- [ViewVariables]
- public readonly TileAtmosphere?[] AdjacentTiles = new TileAtmosphere[Atmospherics.Directions];
-
- ///
- /// Neighbouring tiles to which air can flow. This is a combination of this tile's unblocked direction, and the
- /// unblocked directions on adjacent tiles.
- ///
- [ViewVariables]
- public AtmosDirection AdjacentBits = AtmosDirection.Invalid;
-
- [ViewVariables, Access(typeof(AtmosphereSystem), Other = AccessPermissions.ReadExecute)]
- public MonstermosInfo MonstermosInfo;
-
- [ViewVariables]
- public Hotspot Hotspot;
-
- [ViewVariables]
- public AtmosDirection PressureDirection;
-
- // For debug purposes.
- [ViewVariables]
- public AtmosDirection LastPressureDirection;
-
- [ViewVariables]
- [Access(typeof(AtmosphereSystem))]
- public EntityUid GridIndex { get; set; }
-
- [ViewVariables]
- public Vector2i GridIndices;
-
- [ViewVariables]
- public ExcitedGroup? ExcitedGroup { get; set; }
-
- ///
- /// The air in this tile. If null, this tile is completely air-blocked.
- /// This can be immutable if the tile is spaced.
- ///
- [ViewVariables]
- [Access(typeof(AtmosphereSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
- public GasMixture? Air { get; set; }
-
- ///
- /// Like Air, but a copy stored each atmos tick before tile processing takes place. This lets us update Air
- /// in-place without affecting the results based on update order.
- ///
- [ViewVariables]
- public GasMixture? AirArchived;
-
- [DataField("lastShare")]
- public float LastShare;
-
- GasMixture IGasMixtureHolder.Air
- {
- get => Air ?? new GasMixture(Atmospherics.CellVolume){ Temperature = Temperature };
- set => Air = value;
- }
-
- [ViewVariables]
- public float MaxFireTemperatureSustained { get; set; }
-
- ///
- /// If true, then this tile is directly exposed to the map's atmosphere, either because the grid has no tile at
- /// this position, or because the tile type is not airtight.
- ///
- [ViewVariables]
- public bool MapAtmosphere;
-
- ///
- /// If true, this tile does not actually exist on the grid, it only exists to represent the map's atmosphere for
- /// adjacent grid tiles.
- ///
- [ViewVariables]
- public bool NoGridTile;
-
- ///
- /// If true, this tile is queued for processing in
- ///
- [ViewVariables]
- public bool TrimQueued;
-
- ///
- /// Cached information about airtight entities on this tile. This gets updated anytime a tile gets invalidated
- /// (i.e., gets added to ).
- ///
- public AtmosphereSystem.AirtightData AirtightData;
-
- public TileAtmosphere(EntityUid gridIndex, Vector2i gridIndices, GasMixture? mixture = null, bool immutable = false, bool space = false)
- {
- GridIndex = gridIndex;
- GridIndices = gridIndices;
- Air = mixture;
- AirArchived = Air != null ? Air.Clone() : null;
- Space = space;
-
- if(immutable)
- Air?.MarkImmutable();
- }
-
- public TileAtmosphere(TileAtmosphere other)
- {
- GridIndex = other.GridIndex;
- GridIndices = other.GridIndices;
- Space = other.Space;
- NoGridTile = other.NoGridTile;
- MapAtmosphere = other.MapAtmosphere;
- Air = other.Air?.Clone();
- AirArchived = Air != null ? Air.Clone() : null;
- }
-
- public TileAtmosphere()
- {
- }
+ ///
+ /// Creates a new empty TileAtmosphere.
+ ///
+ public TileAtmosphere()
+ {
}
}
diff --git a/Content.Server/Atmos/TileFireEvent.cs b/Content.Server/Atmos/TileFireEvent.cs
deleted file mode 100644
index 5dad4e8fc0..0000000000
--- a/Content.Server/Atmos/TileFireEvent.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace Content.Server.Atmos
-{
- ///
- /// Event raised directed to an entity when it is standing on a tile that's on fire.
- ///
- [ByRefEvent]
- public readonly struct TileFireEvent
- {
- public readonly float Temperature;
- public readonly float Volume;
-
- public TileFireEvent(float temperature, float volume)
- {
- Temperature = temperature;
- Volume = volume;
- }
- }
-}
diff --git a/Content.Server/Bed/BedSystem.cs b/Content.Server/Bed/BedSystem.cs
index 8cfb28acb6..f6c2862a84 100644
--- a/Content.Server/Bed/BedSystem.cs
+++ b/Content.Server/Bed/BedSystem.cs
@@ -2,7 +2,7 @@ using Content.Shared.Bed;
using Content.Shared.Bed.Components;
using Content.Shared.Bed.Sleep;
using Content.Shared.Buckle.Components;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Mobs.Systems;
namespace Content.Server.Bed
diff --git a/Content.Server/Bible/BibleSystem.cs b/Content.Server/Bible/BibleSystem.cs
index 2aabb5eb27..eb11a465bd 100644
--- a/Content.Server/Bible/BibleSystem.cs
+++ b/Content.Server/Bible/BibleSystem.cs
@@ -4,7 +4,7 @@ using Content.Server.Popups;
using Content.Shared.ActionBlocker;
using Content.Shared.Actions;
using Content.Shared.Bible;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Ghost.Roles.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
@@ -14,7 +14,6 @@ using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Timing;
using Content.Shared.Verbs;
-using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;
@@ -133,9 +132,7 @@ namespace Content.Server.Bible
}
}
- var damage = _damageableSystem.TryChangeDamage(args.Target.Value, component.Damage, true, origin: uid);
-
- if (damage == null || damage.Empty)
+ if (_damageableSystem.TryChangeDamage(args.Target.Value, component.Damage, true, origin: uid))
{
var othersMessage = Loc.GetString(component.LocPrefix + "-heal-success-none-others", ("user", Identity.Entity(args.User, EntityManager)), ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid));
_popupSystem.PopupEntity(othersMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.Medium);
diff --git a/Content.Server/Body/Systems/RespiratorSystem.cs b/Content.Server/Body/Systems/RespiratorSystem.cs
index ca3ee2c9e1..79457cb036 100644
--- a/Content.Server/Body/Systems/RespiratorSystem.cs
+++ b/Content.Server/Body/Systems/RespiratorSystem.cs
@@ -12,7 +12,7 @@ using Content.Shared.Chat;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.EntityConditions;
using Content.Shared.EntityConditions.Conditions.Body;
@@ -367,7 +367,7 @@ public sealed class RespiratorSystem : EntitySystem
if (ent.Comp.SuffocationCycles == 2)
_adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} started suffocating");
- _damageableSys.TryChangeDamage(ent, ent.Comp.Damage, interruptsDoAfters: false);
+ _damageableSys.ChangeDamage(ent.Owner, ent.Comp.Damage, interruptsDoAfters: false);
if (ent.Comp.SuffocationCycles < ent.Comp.SuffocationCycleThreshold)
return;
@@ -381,7 +381,7 @@ public sealed class RespiratorSystem : EntitySystem
if (ent.Comp.SuffocationCycles >= 2)
_adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} stopped suffocating");
- _damageableSys.TryChangeDamage(ent, ent.Comp.DamageRecovery);
+ _damageableSys.ChangeDamage(ent.Owner, ent.Comp.DamageRecovery);
var ev = new StopSuffocatingEvent();
RaiseLocalEvent(ent, ref ev);
diff --git a/Content.Server/Botany/Systems/PlantHolderSystem.cs b/Content.Server/Botany/Systems/PlantHolderSystem.cs
index 2554f95455..d5f331c157 100644
--- a/Content.Server/Botany/Systems/PlantHolderSystem.cs
+++ b/Content.Server/Botany/Systems/PlantHolderSystem.cs
@@ -23,7 +23,6 @@ using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
-using Content.Shared.Administration.Logs;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
@@ -886,12 +885,13 @@ public sealed class PlantHolderSystem : EntitySystem
if (solution.Volume > 0 && component.MutationLevel < 25)
{
- var amt = FixedPoint2.New(1);
- foreach (var entry in _solutionContainerSystem.RemoveEachReagent(component.SoilSolution.Value, amt))
+ foreach (var entry in component.SoilSolution.Value.Comp.Solution.Contents)
{
var reagentProto = _prototype.Index(entry.Reagent.Prototype);
- _entityEffects.ApplyEffects(uid, reagentProto.PlantMetabolisms.ToArray());
+ _entityEffects.ApplyEffects(uid, reagentProto.PlantMetabolisms.ToArray(), entry.Quantity.Float());
}
+
+ _solutionContainerSystem.RemoveEachReagent(component.SoilSolution.Value, FixedPoint2.New(1));
}
CheckLevelSanity(uid, component);
diff --git a/Content.Server/CardboardBox/CardboardBoxSystem.cs b/Content.Server/CardboardBox/CardboardBoxSystem.cs
index 836dc485d9..9fdd23e780 100644
--- a/Content.Server/CardboardBox/CardboardBoxSystem.cs
+++ b/Content.Server/CardboardBox/CardboardBoxSystem.cs
@@ -1,20 +1,16 @@
-using Content.Server.Storage.Components;
using Content.Server.Storage.EntitySystems;
using Content.Shared.Access.Components;
using Content.Shared.CardboardBox;
using Content.Shared.CardboardBox.Components;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Interaction;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.Stealth;
using Content.Shared.Stealth.Components;
using Content.Shared.Storage.Components;
-using Robust.Server.GameObjects;
-using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
-using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server.CardboardBox;
@@ -109,10 +105,10 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
//Relay damage to the mover
private void OnDamage(EntityUid uid, CardboardBoxComponent component, DamageChangedEvent args)
{
- if (args.DamageDelta != null && args.DamageIncreased)
- {
- _damageable.TryChangeDamage(component.Mover, args.DamageDelta, origin: args.Origin);
- }
+ if (args.DamageDelta == null || !args.DamageIncreased || component.Mover is not { } mover)
+ return;
+
+ _damageable.ChangeDamage(mover, args.DamageDelta, origin: args.Origin);
}
private void OnEntInserted(EntityUid uid, CardboardBoxComponent component, EntInsertedIntoContainerMessage args)
diff --git a/Content.Server/Chat/Managers/ChatManager.cs b/Content.Server/Chat/Managers/ChatManager.cs
index 2e3000d3f0..ed50841743 100644
--- a/Content.Server/Chat/Managers/ChatManager.cs
+++ b/Content.Server/Chat/Managers/ChatManager.cs
@@ -163,14 +163,20 @@ internal sealed partial class ChatManager : IChatManager
public void SendAdminAlert(string message)
{
- var clients = _adminManager.ActiveAdmins.Select(p => p.Channel);
-
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(message)));
- ChatMessageToMany(ChatChannel.AdminAlert, message, wrappedMessage, default, false, true, clients);
+ SendAdminAlertNoFormatOrEscape(wrappedMessage);
}
+ public void SendAdminAlertNoFormatOrEscape(string message)
+ {
+ var clients = _adminManager.ActiveAdmins.Select(p => p.Channel);
+
+ ChatMessageToMany(ChatChannel.AdminAlert, message, message, default, false, true, clients);
+ }
+
+
public void SendAdminAlert(EntityUid player, string message)
{
var mindSystem = _entityManager.System();
diff --git a/Content.Server/Chat/SuicideSystem.cs b/Content.Server/Chat/SuicideSystem.cs
index dca2959f98..9f901b2ad8 100644
--- a/Content.Server/Chat/SuicideSystem.cs
+++ b/Content.Server/Chat/SuicideSystem.cs
@@ -2,7 +2,7 @@ using Content.Server.Ghost;
using Content.Server.Hands.Systems;
using Content.Shared.Administration.Logs;
using Content.Shared.Chat;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Database;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction.Events;
diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs
index 438129c24d..7833450a34 100644
--- a/Content.Server/Chat/Systems/ChatSystem.cs
+++ b/Content.Server/Chat/Systems/ChatSystem.cs
@@ -57,7 +57,6 @@ public sealed partial class ChatSystem : SharedChatSystem
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly ReplacementAccentSystem _wordreplacement = default!;
- [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
// Corvax-TTS-Start: Moved from Server to Shared
@@ -220,7 +219,7 @@ public sealed partial class ChatSystem : SharedChatSystem
// This message may have a radio prefix, and should then be whispered to the resolved radio channel
if (checkRadioPrefix)
{
- if (TryProccessRadioMessage(source, message, out var modMessage, out var channel))
+ if (TryProcessRadioMessage(source, message, out var modMessage, out var channel))
{
SendEntityWhisper(source, modMessage, range, channel, nameOverride, hideLog, ignoreActionBlocker);
return;
diff --git a/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs b/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs
index 4afb885c4c..2b2be1e869 100644
--- a/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs
+++ b/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs
@@ -1,3 +1,5 @@
+using Content.Shared.Damage.Systems;
+
namespace Content.Server.Chat.Systems;
using Content.Shared.Chat;
diff --git a/Content.Server/Cloning/CloningPodSystem.cs b/Content.Server/Cloning/CloningPodSystem.cs
index f413a1863f..67d6dc39a6 100644
--- a/Content.Server/Cloning/CloningPodSystem.cs
+++ b/Content.Server/Cloning/CloningPodSystem.cs
@@ -12,7 +12,7 @@ using Content.Shared.CCVar;
using Content.Shared.Chemistry.Components;
using Content.Shared.Cloning;
using Content.Shared.Chat;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
@@ -29,6 +29,7 @@ using Robust.Shared.Containers;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
+using Content.Shared.Chemistry.Reagent;
namespace Content.Server.Cloning;
@@ -58,6 +59,7 @@ public sealed class CloningPodSystem : EntitySystem
public readonly Dictionary ClonesWaitingForMind = new();
public readonly ProtoId SettingsId = "CloningPod";
public const float EasyModeCloningCost = 0.7f;
+ private static readonly ProtoId BloodId = "Blood";
public override void Initialize()
{
@@ -302,7 +304,7 @@ public sealed class CloningPodSystem : EntitySystem
while (i < 1)
{
tileMix?.AdjustMoles(Gas.Ammonia, 6f);
- bloodSolution.AddReagent("Blood", 50);
+ bloodSolution.AddReagent(BloodId, 50);
if (_robustRandom.Prob(0.2f))
i++;
}
diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs
index 6e0f38ad51..40f8a36dfa 100644
--- a/Content.Server/Cloning/CloningSystem.cs
+++ b/Content.Server/Cloning/CloningSystem.cs
@@ -84,6 +84,14 @@ public sealed partial class CloningSystem : SharedCloningSystem
return true;
}
+ public override void CloneComponents(EntityUid original, EntityUid clone, ProtoId settings)
+ {
+ if (!_prototype.Resolve(settings, out var proto))
+ return;
+
+ CloneComponents(original, clone, proto);
+ }
+
public override void CloneComponents(EntityUid original, EntityUid clone, CloningSettingsPrototype settings)
{
var componentsToCopy = settings.Components;
diff --git a/Content.Server/Cluwne/CluwneSystem.cs b/Content.Server/Cluwne/CluwneSystem.cs
index ab13548e04..97d7005279 100644
--- a/Content.Server/Cluwne/CluwneSystem.cs
+++ b/Content.Server/Cluwne/CluwneSystem.cs
@@ -8,7 +8,7 @@ using Content.Shared.Chat;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Clumsy;
using Content.Shared.Cluwne;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Mobs;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Popups;
diff --git a/Content.Server/Construction/Conditions/MinHealth.cs b/Content.Server/Construction/Conditions/MinHealth.cs
index 980f6a49ca..cef30db71f 100644
--- a/Content.Server/Construction/Conditions/MinHealth.cs
+++ b/Content.Server/Construction/Conditions/MinHealth.cs
@@ -1,13 +1,8 @@
using Content.Server.Destructible;
using Content.Shared.Construction;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace Content.Server.Construction.Conditions;
diff --git a/Content.Server/Cuffs/CuffableSystem.cs b/Content.Server/Cuffs/CuffableSystem.cs
index 2c28603c3f..622eabd953 100644
--- a/Content.Server/Cuffs/CuffableSystem.cs
+++ b/Content.Server/Cuffs/CuffableSystem.cs
@@ -15,7 +15,7 @@ namespace Content.Server.Cuffs
SubscribeLocalEvent(OnCuffableGetState);
}
- private void OnCuffableGetState(EntityUid uid, CuffableComponent component, ref ComponentGetState args)
+ private void OnCuffableGetState(Entity entity, ref ComponentGetState args)
{
// there are 2 approaches i can think of to handle the handcuff overlay on players
// 1 - make the current RSI the handcuff type that's currently active. all handcuffs on the player will appear the same.
@@ -23,12 +23,12 @@ namespace Content.Server.Cuffs
// approach #2 would be more difficult/time consuming to do and the payoff doesn't make it worth it.
// right now we're doing approach #1.
HandcuffComponent? cuffs = null;
- if (component.CuffedHandCount > 0)
- TryComp(component.LastAddedCuffs, out cuffs);
- args.State = new CuffableComponentState(component.CuffedHandCount,
- component.CanStillInteract,
+ if (TryGetLastCuff((entity, entity.Comp), out var cuff))
+ TryComp(cuff, out cuffs);
+ args.State = new CuffableComponentState(entity.Comp.CuffedHandCount,
+ entity.Comp.CanStillInteract,
cuffs?.CuffedRSI,
- $"{cuffs?.BodyIconState}-{component.CuffedHandCount}",
+ $"{cuffs?.BodyIconState}-{entity.Comp.CuffedHandCount}",
cuffs?.Color);
// the iconstate is formatted as blah-2, blah-4, blah-6, etc.
// the number corresponds to how many hands are cuffed.
diff --git a/Content.Server/Damage/Commands/HurtCommand.cs b/Content.Server/Damage/Commands/HurtCommand.cs
index af9c1ee8fc..849f6e543e 100644
--- a/Content.Server/Damage/Commands/HurtCommand.cs
+++ b/Content.Server/Damage/Commands/HurtCommand.cs
@@ -4,7 +4,7 @@ using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
-using Content.Shared.FixedPoint;
+using Content.Shared.Damage.Systems;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
diff --git a/Content.Server/Damage/ForceSay/DamageForceSaySystem.cs b/Content.Server/Damage/ForceSay/DamageForceSaySystem.cs
index a46d42f3e3..5597c47fdb 100644
--- a/Content.Server/Damage/ForceSay/DamageForceSaySystem.cs
+++ b/Content.Server/Damage/ForceSay/DamageForceSaySystem.cs
@@ -2,6 +2,7 @@ using Content.Shared.Bed.Sleep;
using Content.Shared.Damage;
using Content.Shared.Damage.Events;
using Content.Shared.Damage.ForceSay;
+using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;
diff --git a/Content.Server/Damage/Systems/DamageOnHitSystem.cs b/Content.Server/Damage/Systems/DamageOnHitSystem.cs
index f129a14f79..4b13106cc8 100644
--- a/Content.Server/Damage/Systems/DamageOnHitSystem.cs
+++ b/Content.Server/Damage/Systems/DamageOnHitSystem.cs
@@ -1,8 +1,7 @@
using Content.Server.Damage.Components;
-using Content.Shared.Damage;
-using Robust.Shared.Player;
using Content.Shared.Weapons.Melee.Events;
using System.Linq;
+using Content.Shared.Damage.Systems;
namespace Content.Server.Damage.Systems;
diff --git a/Content.Server/Damage/Systems/DamageOnLandSystem.cs b/Content.Server/Damage/Systems/DamageOnLandSystem.cs
index 3cf103e6ee..8dede48dee 100644
--- a/Content.Server/Damage/Systems/DamageOnLandSystem.cs
+++ b/Content.Server/Damage/Systems/DamageOnLandSystem.cs
@@ -9,7 +9,7 @@ namespace Content.Server.Damage.Systems
///
public sealed class DamageOnLandSystem : EntitySystem
{
- [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ [Dependency] private readonly Shared.Damage.Systems.DamageableSystem _damageableSystem = default!;
public override void Initialize()
{
diff --git a/Content.Server/Damage/Systems/DamageOnToolInteractSystem.cs b/Content.Server/Damage/Systems/DamageOnToolInteractSystem.cs
index 8c0e0a1382..88fe02510f 100644
--- a/Content.Server/Damage/Systems/DamageOnToolInteractSystem.cs
+++ b/Content.Server/Damage/Systems/DamageOnToolInteractSystem.cs
@@ -11,7 +11,7 @@ namespace Content.Server.Damage.Systems
{
public sealed class DamageOnToolInteractSystem : EntitySystem
{
- [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ [Dependency] private readonly Shared.Damage.Systems.DamageableSystem _damageableSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedToolSystem _toolSystem = default!;
@@ -35,22 +35,22 @@ namespace Content.Server.Damage.Systems
&& itemToggle.Activated
&& !welder.TankSafe)
{
- var dmg = _damageableSystem.TryChangeDamage(args.Target, weldingDamage, origin: args.User);
-
- if (dmg != null)
+ if (_damageableSystem.TryChangeDamage(args.Target, weldingDamage, out var dmg, origin: args.User))
+ {
_adminLogger.Add(LogType.Damaged,
$"{ToPrettyString(args.User):user} used {ToPrettyString(args.Used):used} as a welder to deal {dmg.GetTotal():damage} damage to {ToPrettyString(args.Target):target}");
+ }
args.Handled = true;
}
else if (component.DefaultDamage is {} damage
&& _toolSystem.HasQuality(args.Used, component.Tools))
{
- var dmg = _damageableSystem.TryChangeDamage(args.Target, damage, origin: args.User);
-
- if (dmg != null)
+ if (_damageableSystem.TryChangeDamage(args.Target, damage, out var dmg, origin: args.User))
+ {
_adminLogger.Add(LogType.Damaged,
$"{ToPrettyString(args.User):user} used {ToPrettyString(args.Used):used} as a tool to deal {dmg.GetTotal():damage} damage to {ToPrettyString(args.Target):target}");
+ }
args.Handled = true;
}
diff --git a/Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs b/Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs
index 10930ca5c9..02bc7334a6 100644
--- a/Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs
+++ b/Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs
@@ -17,7 +17,7 @@ public sealed class DamageOtherOnHitSystem : SharedDamageOtherOnHitSystem
{
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly GunSystem _guns = default!;
- [Dependency] private readonly DamageableSystem _damageable = default!;
+ [Dependency] private readonly Shared.Damage.Systems.DamageableSystem _damageable = default!;
[Dependency] private readonly SharedCameraRecoilSystem _sharedCameraRecoil = default!;
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
@@ -33,13 +33,13 @@ public sealed class DamageOtherOnHitSystem : SharedDamageOtherOnHitSystem
if (TerminatingOrDeleted(args.Target))
return;
- var dmg = _damageable.TryChangeDamage(args.Target, component.Damage * _damageable.UniversalThrownDamageModifier, component.IgnoreResistances, origin: args.Component.Thrower);
+ var dmg = _damageable.ChangeDamage(args.Target, component.Damage * _damageable.UniversalThrownDamageModifier, component.IgnoreResistances, origin: args.Component.Thrower);
// Log damage only for mobs. Useful for when people throw spears at each other, but also avoids log-spam when explosions send glass shards flying.
- if (dmg != null && HasComp(args.Target))
+ if (HasComp(args.Target))
_adminLogger.Add(LogType.ThrowHit, $"{ToPrettyString(args.Target):target} received {dmg.GetTotal():damage} damage from collision");
- if (dmg is { Empty: false })
+ if (!dmg.Empty)
{
_color.RaiseEffect(Color.Red, [args.Target], Filter.Pvs(args.Target, entityManager: EntityManager));
}
diff --git a/Content.Server/Damage/Systems/DamageRandomPopupSystem.cs b/Content.Server/Damage/Systems/DamageRandomPopupSystem.cs
index 8bdbf84147..25e7bc1644 100644
--- a/Content.Server/Damage/Systems/DamageRandomPopupSystem.cs
+++ b/Content.Server/Damage/Systems/DamageRandomPopupSystem.cs
@@ -1,6 +1,7 @@
using Content.Server.Damage.Components;
using Content.Server.Popups;
using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;
diff --git a/Content.Server/Damage/Systems/ExaminableDamageSystem.cs b/Content.Server/Damage/Systems/ExaminableDamageSystem.cs
index b0dfae71b7..a2a9f234f5 100644
--- a/Content.Server/Damage/Systems/ExaminableDamageSystem.cs
+++ b/Content.Server/Damage/Systems/ExaminableDamageSystem.cs
@@ -1,6 +1,6 @@
using Content.Server.Damage.Components;
using Content.Server.Destructible;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Examine;
using Content.Shared.Rounding;
using Robust.Shared.Prototypes;
diff --git a/Content.Server/Destructible/DestructibleSystem.BenchmarkHelpers.cs b/Content.Server/Destructible/DestructibleSystem.BenchmarkHelpers.cs
new file mode 100644
index 0000000000..a00194c911
--- /dev/null
+++ b/Content.Server/Destructible/DestructibleSystem.BenchmarkHelpers.cs
@@ -0,0 +1,35 @@
+using Content.Shared.Damage;
+
+namespace Content.Server.Destructible;
+
+public sealed partial class DestructibleSystem
+{
+ ///
+ /// Tests all triggers in a DestructibleComponent to see how expensive it is to query them.
+ ///
+ public void TestAllTriggers(List> destructibles)
+ {
+ foreach (var (uid, damageable, destructible) in destructibles)
+ {
+ foreach (var threshold in destructible.Thresholds)
+ {
+ // Chances are, none of these triggers will pass!
+ Triggered(threshold, (uid, damageable));
+ }
+ }
+ }
+
+ ///
+ /// Tests all behaviours in a DestructibleComponent to see how expensive it is to query them.
+ ///
+ public void TestAllBehaviors(List> destructibles)
+ {
+ foreach (var (uid, damageable, destructible) in destructibles)
+ {
+ foreach (var threshold in destructible.Thresholds)
+ {
+ Execute(threshold, uid);
+ }
+ }
+ }
+}
diff --git a/Content.Server/Destructible/DestructibleSystem.cs b/Content.Server/Destructible/DestructibleSystem.cs
index 682baa04ca..7ed736fffd 100644
--- a/Content.Server/Destructible/DestructibleSystem.cs
+++ b/Content.Server/Destructible/DestructibleSystem.cs
@@ -11,6 +11,7 @@ using Content.Server.Fluids.EntitySystems;
using Content.Server.Stack;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Destructible;
using Content.Shared.Destructible.Thresholds.Triggers;
@@ -26,7 +27,7 @@ using Robust.Shared.Random;
namespace Content.Server.Destructible
{
[UsedImplicitly]
- public sealed class DestructibleSystem : SharedDestructibleSystem
+ public sealed partial class DestructibleSystem : SharedDestructibleSystem
{
[Dependency] public readonly IRobustRandom Random = default!;
public new IEntityManager EntityManager => base.EntityManager;
@@ -111,7 +112,7 @@ namespace Content.Server.Destructible
///
/// Check if the given threshold should trigger.
///
- public bool Triggered(DamageThreshold threshold, Entity owner)
+ public bool Triggered(DamageThreshold threshold, Entity owner)
{
if (threshold.Trigger == null)
return false;
@@ -135,7 +136,7 @@ namespace Content.Server.Destructible
///
/// Check if the conditions for the given threshold are currently true.
///
- public bool Reached(DamageThreshold threshold, Entity owner)
+ public bool Reached(DamageThreshold threshold, Entity owner)
{
if (threshold.Trigger == null)
return false;
diff --git a/Content.Server/Discord/DiscordLink/DiscordChatLink.cs b/Content.Server/Discord/DiscordLink/DiscordChatLink.cs
index 358bc4ab3e..34ddc39f71 100644
--- a/Content.Server/Discord/DiscordLink/DiscordChatLink.cs
+++ b/Content.Server/Discord/DiscordLink/DiscordChatLink.cs
@@ -1,6 +1,7 @@
using Content.Server.Chat.Managers;
using Content.Shared.CCVar;
using Content.Shared.Chat;
+using NetCord;
using NetCord.Gateway;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
@@ -24,6 +25,10 @@ public sealed class DiscordChatLink : IPostInjectInit
{
_discordLink.OnMessageReceived += OnMessageReceived;
+ #if DEBUG
+ _discordLink.RegisterCommandCallback(OnDebugCommandRun, "debug");
+ #endif
+
_configurationManager.OnValueChanged(CCVars.OocDiscordChannelId, OnOocChannelIdChanged, true);
_configurationManager.OnValueChanged(CCVars.AdminChatDiscordChannelId, OnAdminChannelIdChanged, true);
}
@@ -36,6 +41,14 @@ public sealed class DiscordChatLink : IPostInjectInit
_configurationManager.UnsubValueChanged(CCVars.AdminChatDiscordChannelId, OnAdminChannelIdChanged);
}
+ #if DEBUG
+ private void OnDebugCommandRun(CommandReceivedEventArgs ev)
+ {
+ var args = string.Join('\n', ev.Arguments);
+ _sawmill.Info($"Provided arguments: \n{args}");
+ }
+ #endif
+
private void OnOocChannelIdChanged(string channelId)
{
if (string.IsNullOrEmpty(channelId))
diff --git a/Content.Server/Discord/DiscordLink/DiscordLink.cs b/Content.Server/Discord/DiscordLink/DiscordLink.cs
index cbfe12f180..5bfb61d4d1 100644
--- a/Content.Server/Discord/DiscordLink/DiscordLink.cs
+++ b/Content.Server/Discord/DiscordLink/DiscordLink.cs
@@ -1,9 +1,11 @@
-using System.Threading.Tasks;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
using Content.Shared.CCVar;
using NetCord;
using NetCord.Gateway;
using NetCord.Rest;
using Robust.Shared.Configuration;
+using Robust.Shared.Utility;
namespace Content.Server.Discord.DiscordLink;
@@ -18,9 +20,16 @@ public sealed class CommandReceivedEventArgs
public string Command { get; init; } = string.Empty;
///
- /// The arguments to the command. This is everything after the command
+ /// The raw arguments to the command. This is everything after the command
///
- public string Arguments { get; init; } = string.Empty;
+ public string RawArguments { get; init; } = string.Empty;
+
+ ///
+ /// A list of arguments to the command.
+ /// This uses mostly for maintainability.
+ ///
+ public List Arguments { get; init; } = [];
+
///
/// Information about the message that the command was received from. This includes the message content, author, etc.
/// Use this to reply to the message, delete it, etc.
@@ -66,6 +75,7 @@ public sealed class DiscordLink : IPostInjectInit
///
public event Action? OnMessageReceived;
+ // TODO: consider implementing this in a way where we can unregister it in a similar way
public void RegisterCommandCallback(Action callback, string command)
{
OnCommandReceived += args =>
@@ -180,24 +190,28 @@ public sealed class DiscordLink : IPostInjectInit
var trimmedInput = content[BotPrefix.Length..].Trim();
var firstSpaceIndex = trimmedInput.IndexOf(' ');
- string command, arguments;
+ string command, rawArguments;
if (firstSpaceIndex == -1)
{
command = trimmedInput;
- arguments = string.Empty;
+ rawArguments = string.Empty;
}
else
{
command = trimmedInput[..firstSpaceIndex];
- arguments = trimmedInput[(firstSpaceIndex + 1)..].Trim();
+ rawArguments = trimmedInput[(firstSpaceIndex + 1)..].Trim();
}
+ var argumentList = new List();
+ CommandParsing.ParseArguments(rawArguments, argumentList);
+
// Raise the event!
OnCommandReceived?.Invoke(new CommandReceivedEventArgs
{
Command = command,
- Arguments = arguments,
+ Arguments = argumentList,
+ RawArguments = rawArguments,
Message = message,
});
return ValueTask.CompletedTask;
diff --git a/Content.Server/Disposal/Unit/DisposableSystem.cs b/Content.Server/Disposal/Unit/DisposableSystem.cs
index d307488110..73365c4f62 100644
--- a/Content.Server/Disposal/Unit/DisposableSystem.cs
+++ b/Content.Server/Disposal/Unit/DisposableSystem.cs
@@ -1,7 +1,7 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Disposal.Tube;
using Content.Shared.Body.Components;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Disposal.Components;
using Content.Shared.Item;
using Content.Shared.Throwing;
diff --git a/Content.Server/Dragon/DragonRiftSystem.cs b/Content.Server/Dragon/DragonRiftSystem.cs
index 9cab018fd7..842b27ec86 100644
--- a/Content.Server/Dragon/DragonRiftSystem.cs
+++ b/Content.Server/Dragon/DragonRiftSystem.cs
@@ -2,16 +2,14 @@ using Content.Server.Chat.Systems;
using Content.Server.NPC;
using Content.Server.NPC.Systems;
using Content.Server.Pinpointer;
-using Content.Shared.Damage;
using Content.Shared.Dragon;
using Content.Shared.Examine;
using Content.Shared.Sprite;
-using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Serialization.Manager;
using System.Numerics;
-using Robust.Shared.Audio;
+using Content.Shared.Damage.Components;
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Utility;
diff --git a/Content.Server/Electrocution/ElectrocutionSystem.cs b/Content.Server/Electrocution/ElectrocutionSystem.cs
index a162b29e19..fe9fe44571 100644
--- a/Content.Server/Electrocution/ElectrocutionSystem.cs
+++ b/Content.Server/Electrocution/ElectrocutionSystem.cs
@@ -1,14 +1,12 @@
using Content.Server.Administration.Logs;
-using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
-using Content.Server.NodeContainer.NodeGroups;
-using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Power.NodeGroups;
using Content.Server.Weapons.Melee;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
+using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Electrocution;
using Content.Shared.IdentityManagement;
@@ -27,7 +25,6 @@ using Content.Shared.Tag;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
-using Robust.Shared.Map;
using Robust.Shared.Physics.Events;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
@@ -402,19 +399,16 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
? _stun.TryUpdateParalyzeDuration(uid, time * ParalyzeTimeMultiplier)
: _stun.TryAddParalyzeDuration(uid, time * ParalyzeTimeMultiplier);
}
-
+
// TODO: Sparks here.
if (shockDamage is { } dmg)
{
- var actual = _damageable.TryChangeDamage(uid,
- new DamageSpecifier(_prototypeManager.Index(DamageType), dmg), origin: sourceUid);
-
- if (actual != null)
+ if (_damageable.TryChangeDamage(uid, new DamageSpecifier(_prototypeManager.Index(DamageType), dmg), out var damage, origin: sourceUid))
{
_adminLogger.Add(LogType.Electrocution,
- $"{ToPrettyString(uid):entity} received {actual.GetTotal():damage} powered electrocution damage{(sourceUid != null ? " from " + ToPrettyString(sourceUid.Value) : ""):source}");
+ $"{ToPrettyString(uid):entity} received {damage:damage} powered electrocution damage{(sourceUid != null ? " from " + ToPrettyString(sourceUid.Value) : ""):source}");
}
}
diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs
index 3d55a7e823..303c4e8cab 100644
--- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs
+++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs
@@ -1,9 +1,7 @@
using Content.Server.Atmos.Components;
-using Content.Server.Destructible;
using Content.Shared.Atmos;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Explosion;
-using Content.Shared.Explosion.EntitySystems;
using Content.Shared.FixedPoint;
using Robust.Shared.Map.Components;
diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs
index 263fdabf98..c6528b0142 100644
--- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs
+++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs
@@ -1,13 +1,10 @@
-using System.Linq;
using System.Numerics;
-using Content.Server.Atmos.EntitySystems;
-using Content.Server.Explosion.Components;
using Content.Shared.CCVar;
using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Database;
using Content.Shared.Explosion;
using Content.Shared.Explosion.Components;
-using Content.Shared.Explosion.EntitySystems;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Projectiles;
@@ -17,7 +14,6 @@ using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
-using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -439,28 +435,25 @@ public sealed partial class ExplosionSystem
float? fireStacksOnIgnite,
EntityUid? cause)
{
- if (originalDamage != null)
+ if (originalDamage is not null)
{
GetEntitiesToDamage(uid, originalDamage, id);
foreach (var (entity, damage) in _toDamage)
{
- if (_actorQuery.HasComp(entity))
- {
- // Log damage to player entities only, cause this will create a massive amount of log spam otherwise.
- if (cause != null)
- {
- _adminLogger.Add(LogType.ExplosionHit, LogImpact.Medium, $"Explosion of {ToPrettyString(cause):actor} dealt {damage.GetTotal()} damage to {ToPrettyString(entity):subject}");
- }
- else
- {
- _adminLogger.Add(LogType.ExplosionHit, LogImpact.Medium, $"Explosion at {epicenter:epicenter} dealt {damage.GetTotal()} damage to {ToPrettyString(entity):subject}");
- }
-
- }
+ if (!_damageableQuery.TryComp(entity, out var damageable))
+ continue;
// TODO EXPLOSIONS turn explosions into entities, and pass the the entity in as the damage origin.
- _damageableSystem.TryChangeDamage(entity, damage, ignoreResistances: true, ignoreGlobalModifiers: true);
+ _damageableSystem.TryChangeDamage((entity, damageable), damage, ignoreResistances: true, ignoreGlobalModifiers: true);
+ if (_actorQuery.HasComp(entity))
+ {
+ // Log damage to player entities only; this will create a massive amount of log spam otherwise.
+ if (cause is not null)
+ _adminLogger.Add(LogType.ExplosionHit, LogImpact.Medium, $"Explosion of {ToPrettyString(cause):actor} dealt {damage.GetTotal()} damage to {ToPrettyString(entity):subject}");
+ else
+ _adminLogger.Add(LogType.ExplosionHit, LogImpact.Medium, $"Explosion at {epicenter:epicenter} dealt {damage.GetTotal()} damage to {ToPrettyString(entity):subject}");
+ }
}
}
@@ -666,7 +659,7 @@ sealed class Explosion
private readonly IEntityManager _entMan;
private readonly ExplosionSystem _system;
private readonly SharedMapSystem _mapSystem;
- private readonly DamageableSystem _damageable;
+ private readonly Shared.Damage.Systems.DamageableSystem _damageable;
public readonly EntityUid VisualEnt;
@@ -690,7 +683,7 @@ sealed class Explosion
EntityUid visualEnt,
EntityUid? cause,
SharedMapSystem mapSystem,
- DamageableSystem damageable)
+ Shared.Damage.Systems.DamageableSystem damageable)
{
VisualEnt = visualEnt;
Cause = cause;
diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs
index 67dbe97b29..70863d6f54 100644
--- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs
+++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs
@@ -9,7 +9,8 @@ using Content.Server.NPC.Pathfinding;
using Content.Shared.Atmos.Components;
using Content.Shared.Camera;
using Content.Shared.CCVar;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Explosion;
using Content.Shared.Explosion.Components;
@@ -256,11 +257,14 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
var logImpact = (alertMinExplosionIntensity > -1 && totalIntensity >= alertMinExplosionIntensity)
? LogImpact.Extreme
: LogImpact.High;
- _adminLogger.Add(LogType.Explosion, logImpact,
- $"{ToPrettyString(user.Value):user} caused {ToPrettyString(uid):entity} to explode ({typeId}) at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not found]")} with intensity {totalIntensity} slope {slope}");
+ if (posFound)
+ _adminLogger.Add(LogType.Explosion, logImpact, $"{ToPrettyString(user.Value):user} caused {ToPrettyString(uid):entity} to explode ({typeId}) at Pos:{gridPos:coordinates} with intensity {totalIntensity} slope {slope}");
+ else
+ _adminLogger.Add(LogType.Explosion, logImpact, $"{ToPrettyString(user.Value):user} caused {ToPrettyString(uid):entity} to explode ({typeId}) at Pos:[Grid or Map not found] with intensity {totalIntensity} slope {slope}");
}
}
+
///
/// Queue an explosion, with a specified epicenter and set of starting tiles.
///
diff --git a/Content.Server/GameTicking/GameTicker.GamePreset.cs b/Content.Server/GameTicking/GameTicker.GamePreset.cs
index 40608e45cb..69cc1c4165 100644
--- a/Content.Server/GameTicking/GameTicker.GamePreset.cs
+++ b/Content.Server/GameTicking/GameTicker.GamePreset.cs
@@ -59,7 +59,7 @@ public sealed partial class GameTicker
foreach (var preset in fallbackPresets)
{
ClearGameRules();
- SetGamePreset(preset);
+ SetGamePreset(preset, resetDelay: 1);
AddGamePresetRules();
StartGamePresetRules();
@@ -129,11 +129,11 @@ public sealed partial class GameTicker
}
}
- public void SetGamePreset(string preset, bool force = false)
+ public void SetGamePreset(string preset, bool force = false, int? resetDelay = null)
{
var proto = FindGamePreset(preset);
if (proto != null)
- SetGamePreset(proto, force);
+ SetGamePreset(proto, force, null, resetDelay);
}
public GamePresetPrototype? FindGamePreset(string preset)
diff --git a/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs
index f09fff5eaf..f1bd1e5eef 100644
--- a/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs
+++ b/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs
@@ -34,7 +34,11 @@ public sealed partial class LoadMapRuleComponent : Component
///
/// A to move to a new map.
/// If there are no instances left nothing is done.
+ ///
+ /// This is deprecated. Do not create new content that uses this field,
+ /// and migrate existing content to be loaded dynamically during the round.
+ ///
///
- [DataField]
+ [DataField, Obsolete("Do not pre-load grids. This causes the server to have to keep that grid loaded in memory during the entire round, even if that grid is never summoned to the playspace.")]
public ProtoId? PreloadedGrid;
}
diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs
index 1a3c9031fe..af1a433d1a 100644
--- a/Content.Server/Ghost/GhostSystem.cs
+++ b/Content.Server/Ghost/GhostSystem.cs
@@ -3,13 +3,14 @@ using System.Numerics;
using Content.Server.Administration.Logs;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
-using Content.Server.Ghost.Components;
using Content.Server.Mind;
using Content.Server.Roles.Jobs;
using Content.Shared.Actions;
using Content.Shared.CCVar;
using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
+using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.Eye;
@@ -586,7 +587,7 @@ namespace Content.Server.Ghost
DamageSpecifier damage = new(_prototypeManager.Index(AsphyxiationDamageType), dealtDamage);
- _damageable.TryChangeDamage(playerEntity, damage, true);
+ _damageable.ChangeDamage(playerEntity.Value, damage, true);
}
}
diff --git a/Content.Server/Guardian/GuardianSystem.cs b/Content.Server/Guardian/GuardianSystem.cs
index ea1a6f4f4f..5f2597afef 100644
--- a/Content.Server/Guardian/GuardianSystem.cs
+++ b/Content.Server/Guardian/GuardianSystem.cs
@@ -1,7 +1,7 @@
using Content.Server.Body.Systems;
using Content.Server.Popups;
using Content.Shared.Actions;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Guardian;
@@ -285,8 +285,8 @@ namespace Content.Server.Guardian
if (args.DamageDelta == null || component.Host == null || component.DamageShare == 0)
return;
- _damageSystem.TryChangeDamage(
- component.Host,
+ _damageSystem.ChangeDamage(
+ component.Host.Value,
args.DamageDelta * component.DamageShare,
origin: args.Origin,
ignoreResistances: true,
diff --git a/Content.Server/ImmovableRod/ImmovableRodSystem.cs b/Content.Server/ImmovableRod/ImmovableRodSystem.cs
index bcbcfda9af..646b5c97bb 100644
--- a/Content.Server/ImmovableRod/ImmovableRodSystem.cs
+++ b/Content.Server/ImmovableRod/ImmovableRodSystem.cs
@@ -1,10 +1,9 @@
using Content.Server.Body.Systems;
using Content.Server.Destructible;
-using Content.Server.Examine;
using Content.Server.Polymorph.Components;
using Content.Server.Popups;
using Content.Shared.Body.Components;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Examine;
using Content.Shared.Popups;
using Robust.Shared.Audio.Systems;
diff --git a/Content.Server/KillTracking/KillTrackingSystem.cs b/Content.Server/KillTracking/KillTrackingSystem.cs
index ba27ea5d9e..d40e0e13dd 100644
--- a/Content.Server/KillTracking/KillTrackingSystem.cs
+++ b/Content.Server/KillTracking/KillTrackingSystem.cs
@@ -1,5 +1,6 @@
using Content.Server.NPC.HTN;
using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;
diff --git a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs
index e23bea7bb4..1c6ed26d48 100644
--- a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs
+++ b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs
@@ -7,7 +7,6 @@ using Content.Server.Hands.Systems;
using Content.Server.Kitchen.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
-using Content.Server.Temperature.Components;
using Content.Server.Temperature.Systems;
using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
@@ -40,9 +39,8 @@ using Robust.Shared.Timing;
using Content.Shared.Stacks;
using Content.Server.Construction.Components;
using Content.Shared.Chat;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Temperature.Components;
-using Robust.Shared.Utility;
namespace Content.Server.Kitchen.EntitySystems
{
diff --git a/Content.Server/Lightning/LightningTargetSystem.cs b/Content.Server/Lightning/LightningTargetSystem.cs
index 4a0ee23c5b..eac23c3016 100644
--- a/Content.Server/Lightning/LightningTargetSystem.cs
+++ b/Content.Server/Lightning/LightningTargetSystem.cs
@@ -2,6 +2,7 @@ using Content.Server.Explosion.EntitySystems;
using Content.Server.Lightning;
using Content.Server.Lightning.Components;
using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Robust.Server.GameObjects;
namespace Content.Server.Tesla.EntitySystems;
@@ -26,7 +27,7 @@ public sealed class LightningTargetSystem : EntitySystem
{
DamageSpecifier damage = new();
damage.DamageDict.Add("Structural", uid.Comp.DamageFromLightning);
- _damageable.TryChangeDamage(uid, damage, true);
+ _damageable.ChangeDamage(uid.Owner, damage, true);
if (uid.Comp.LightningExplode)
{
diff --git a/Content.Server/Mech/Systems/MechSystem.cs b/Content.Server/Mech/Systems/MechSystem.cs
index 917f4f5035..923c701868 100644
--- a/Content.Server/Mech/Systems/MechSystem.cs
+++ b/Content.Server/Mech/Systems/MechSystem.cs
@@ -4,7 +4,7 @@ using Content.Server.Body.Systems;
using Content.Server.Mech.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.ActionBlocker;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction;
@@ -265,7 +265,7 @@ public sealed partial class MechSystem : SharedMechSystem
component.PilotSlot.ContainedEntity != null)
{
var damage = args.DamageDelta * component.MechToPilotDamageMultiplier;
- _damageable.TryChangeDamage(component.PilotSlot.ContainedEntity, damage);
+ _damageable.ChangeDamage(component.PilotSlot.ContainedEntity.Value, damage);
}
}
diff --git a/Content.Server/Medical/DefibrillatorSystem.cs b/Content.Server/Medical/DefibrillatorSystem.cs
index 1578f11629..f0dfceb14e 100644
--- a/Content.Server/Medical/DefibrillatorSystem.cs
+++ b/Content.Server/Medical/DefibrillatorSystem.cs
@@ -8,7 +8,8 @@ using Content.Server.Popups;
using Content.Server.PowerCell;
using Content.Shared.Traits.Assorted;
using Content.Shared.Chat;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Systems;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.Item.ItemToggle;
diff --git a/Content.Server/Medical/HealthAnalyzerSystem.cs b/Content.Server/Medical/HealthAnalyzerSystem.cs
index 657ac3e636..f022dff5e3 100644
--- a/Content.Server/Medical/HealthAnalyzerSystem.cs
+++ b/Content.Server/Medical/HealthAnalyzerSystem.cs
@@ -2,7 +2,7 @@ using Content.Server.Medical.Components;
using Content.Server.PowerCell;
using Content.Shared.Body.Components;
using Content.Shared.Chemistry.EntitySystems;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.DoAfter;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
diff --git a/Content.Server/Mindshield/MindShieldSystem.cs b/Content.Server/Mindshield/MindShieldSystem.cs
index bc5b65159b..f69e7b45fb 100644
--- a/Content.Server/Mindshield/MindShieldSystem.cs
+++ b/Content.Server/Mindshield/MindShieldSystem.cs
@@ -32,9 +32,6 @@ public sealed class MindShieldSystem : EntitySystem
private void OnImplantImplanted(Entity ent, ref ImplantImplantedEvent ev)
{
- if (ev.Implanted == null)
- return;
-
EnsureComp(ev.Implanted);
MindShieldRemovalCheck(ev.Implanted, ev.Implant);
}
diff --git a/Content.Server/Mining/MeteorSystem.cs b/Content.Server/Mining/MeteorSystem.cs
index 3b0c6920b3..361844ba25 100644
--- a/Content.Server/Mining/MeteorSystem.cs
+++ b/Content.Server/Mining/MeteorSystem.cs
@@ -1,6 +1,7 @@
using Content.Server.Administration.Logs;
using Content.Server.Destructible;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs.Systems;
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickNearbyInjectableOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickNearbyInjectableOperator.cs
index f351d582c6..67a8198c38 100644
--- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickNearbyInjectableOperator.cs
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickNearbyInjectableOperator.cs
@@ -3,7 +3,7 @@ using System.Threading.Tasks;
using Content.Shared.NPC.Components;
using Content.Server.NPC.Pathfinding;
using Content.Shared.Chemistry.Components.SolutionManager;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Components;
using Content.Shared.Silicons.Bots;
diff --git a/Content.Server/NPC/Systems/NPCRetaliationSystem.cs b/Content.Server/NPC/Systems/NPCRetaliationSystem.cs
index d6b2000f32..a970d34bda 100644
--- a/Content.Server/NPC/Systems/NPCRetaliationSystem.cs
+++ b/Content.Server/NPC/Systems/NPCRetaliationSystem.cs
@@ -1,6 +1,7 @@
using Content.Server.NPC.Components;
using Content.Shared.CombatMode;
using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Mobs.Components;
using Content.Shared.NPC.Components;
using Content.Shared.NPC.Systems;
diff --git a/Content.Server/NPC/Systems/NPCUtilitySystem.cs b/Content.Server/NPC/Systems/NPCUtilitySystem.cs
index bc82e72692..9605b62847 100644
--- a/Content.Server/NPC/Systems/NPCUtilitySystem.cs
+++ b/Content.Server/NPC/Systems/NPCUtilitySystem.cs
@@ -6,7 +6,6 @@ using Content.Server.NPC.Queries.Curves;
using Content.Server.NPC.Queries.Queries;
using Content.Server.Nutrition.Components;
using Content.Shared.Chemistry.EntitySystems;
-using Content.Shared.Damage;
using Content.Shared.Examine;
using Content.Shared.Fluids.Components;
using Content.Shared.Inventory;
@@ -29,6 +28,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Content.Shared.Atmos.Components;
using System.Linq;
+using Content.Shared.Damage.Components;
using Content.Shared.Temperature.Components;
namespace Content.Server.NPC.Systems;
diff --git a/Content.Server/Ninja/Systems/StunProviderSystem.cs b/Content.Server/Ninja/Systems/StunProviderSystem.cs
index 8697692e5e..98df8a039a 100644
--- a/Content.Server/Ninja/Systems/StunProviderSystem.cs
+++ b/Content.Server/Ninja/Systems/StunProviderSystem.cs
@@ -1,6 +1,6 @@
using Content.Server.Ninja.Events;
using Content.Server.Power.EntitySystems;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Interaction;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
@@ -9,8 +9,6 @@ using Content.Shared.Stunnable;
using Content.Shared.Timing;
using Content.Shared.Whitelist;
using Robust.Shared.Audio.Systems;
-using Robust.Shared.Timing;
-using Robust.Shared.Prototypes;
namespace Content.Server.Ninja.Systems;
@@ -62,7 +60,7 @@ public sealed class StunProviderSystem : SharedStunProviderSystem
_audio.PlayPvs(comp.Sound, target);
- _damageable.TryChangeDamage(target, comp.StunDamage, false, true, null, origin: uid);
+ _damageable.ChangeDamage(target, comp.StunDamage, origin: uid);
_stun.TryAddParalyzeDuration(target, comp.StunTime);
// short cooldown to prevent instant stunlocking
diff --git a/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs b/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs
index 6905979a5f..871a15ee73 100644
--- a/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs
+++ b/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs
@@ -4,7 +4,7 @@ using Content.Server.Nutrition.Components;
using Content.Server.Popups;
using Content.Shared.Body.Components;
using Content.Shared.Atmos;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.DoAfter;
using Content.Shared.Emag.Systems;
using Content.Shared.IdentityManagement;
diff --git a/Content.Server/Objectives/Components/NotJobRequirementComponent.cs b/Content.Server/Objectives/Components/NotJobRequirementComponent.cs
index 6f6619da2b..cfc007e1e1 100644
--- a/Content.Server/Objectives/Components/NotJobRequirementComponent.cs
+++ b/Content.Server/Objectives/Components/NotJobRequirementComponent.cs
@@ -2,6 +2,7 @@ using Content.Server.Objectives.Systems;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Prototypes;
///
/// Requires that the player not have a certain job to have this objective.
@@ -9,9 +10,10 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy
[RegisterComponent, Access(typeof(NotJobRequirementSystem))]
public sealed partial class NotJobRequirementComponent : Component
{
+
///
- /// ID of the job to ban from having this objective.
+ /// List of job prototype IDs to ban from having this objective.
///
- [DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
- public string Job = string.Empty;
+ [DataField]
+ public List> Jobs = new List>();
}
diff --git a/Content.Server/Objectives/Systems/NotJobRequirementSystem.cs b/Content.Server/Objectives/Systems/NotJobRequirementSystem.cs
index ac7e579c38..4c88bf03f3 100644
--- a/Content.Server/Objectives/Systems/NotJobRequirementSystem.cs
+++ b/Content.Server/Objectives/Systems/NotJobRequirementSystem.cs
@@ -25,7 +25,7 @@ public sealed class NotJobRequirementSystem : EntitySystem
_jobs.MindTryGetJob(args.MindId, out var proto);
// if player has no job then don't care
- if (proto is not null && proto.ID == comp.Job)
+ if (proto is not null && comp.Jobs.Contains(proto.ID))
args.Cancelled = true;
}
}
diff --git a/Content.Server/Polymorph/Systems/PolymorphSystem.cs b/Content.Server/Polymorph/Systems/PolymorphSystem.cs
index db7fb6a92f..897ad72047 100644
--- a/Content.Server/Polymorph/Systems/PolymorphSystem.cs
+++ b/Content.Server/Polymorph/Systems/PolymorphSystem.cs
@@ -4,7 +4,8 @@ using Content.Server.Inventory;
using Content.Server.Polymorph.Components;
using Content.Shared.Buckle;
using Content.Shared.Coordinates;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Systems;
using Content.Shared.Destructible;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.IdentityManagement;
@@ -228,7 +229,7 @@ public sealed partial class PolymorphSystem : EntitySystem
_mobThreshold.GetScaledDamage(uid, child, out var damage) &&
damage != null)
{
- _damageable.SetDamage(child, damageParent, damage);
+ _damageable.SetDamage((child, damageParent), damage);
}
if (configuration.Inventory == PolymorphInventoryChange.Transfer)
@@ -323,7 +324,7 @@ public sealed partial class PolymorphSystem : EntitySystem
_mobThreshold.GetScaledDamage(uid, parent, out var damage) &&
damage != null)
{
- _damageable.SetDamage(parent, damageParent, damage);
+ _damageable.SetDamage((parent, damageParent), damage);
}
if (component.Configuration.Inventory == PolymorphInventoryChange.Transfer)
diff --git a/Content.Server/Projectiles/ProjectileSystem.cs b/Content.Server/Projectiles/ProjectileSystem.cs
index 4c054a4561..28df1eb42d 100644
--- a/Content.Server/Projectiles/ProjectileSystem.cs
+++ b/Content.Server/Projectiles/ProjectileSystem.cs
@@ -3,7 +3,8 @@ 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;
using Content.Shared.FixedPoint;
using Content.Shared.Projectiles;
@@ -54,64 +55,63 @@ public sealed class ProjectileSystem : SharedProjectileSystem
damageRequired -= damageableComponent.TotalDamage;
damageRequired = FixedPoint2.Max(damageRequired, FixedPoint2.Zero);
}
- var modifiedDamage = _damageableSystem.TryChangeDamage(target, ev.Damage, component.IgnoreResistances, damageable: damageableComponent, origin: component.Shooter);
var deleted = Deleted(target);
- if (modifiedDamage is not null && Exists(component.Shooter))
+ if (_damageableSystem.TryChangeDamage((target, damageableComponent), ev.Damage, out var damage, component.IgnoreResistances, origin: component.Shooter) && Exists(component.Shooter))
{
- if (modifiedDamage.AnyPositive() && !deleted)
+ if (!deleted)
{
_color.RaiseEffect(Color.Red, new List { target }, Filter.Pvs(target, entityManager: EntityManager));
}
_adminLogger.Add(LogType.BulletHit,
LogImpact.Medium,
- $"Projectile {ToPrettyString(uid):projectile} shot by {ToPrettyString(component.Shooter!.Value):user} hit {otherName:target} and dealt {modifiedDamage.GetTotal():damage} damage");
- }
+ $"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 (modifiedDamage is not null && 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)
+ // If penetration is to be considered, we need to do some checks to see if the projectile should stop.
+ if (component.PenetrationThreshold != 0)
{
- var stopPenetration = false;
- foreach (var requiredDamageType in component.PenetrationDamageTypeRequirement)
+ // If a damage type is required, stop the bullet if the hit entity doesn't have that type.
+ if (component.PenetrationDamageTypeRequirement != null)
{
- if (!modifiedDamage.DamageDict.Keys.Contains(requiredDamageType))
+ var stopPenetration = false;
+ foreach (var requiredDamageType in component.PenetrationDamageTypeRequirement)
{
- stopPenetration = true;
- break;
+ 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;
}
}
- if (stopPenetration)
- component.ProjectileSpent = true;
}
-
- // If the object won't be destroyed, it "tanks" the penetration hit.
- if (modifiedDamage.GetTotal() < damageRequired)
+ else
{
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;
}
if (!deleted)
{
- _guns.PlayImpactSound(target, modifiedDamage, component.SoundHit, component.ForceSound);
+ _guns.PlayImpactSound(target, damage, component.SoundHit, component.ForceSound);
if (!args.OurBody.LinearVelocity.IsLengthZero())
_sharedCameraRecoil.KickCamera(target, args.OurBody.LinearVelocity.Normalized());
diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs
index 38c3480078..8089cba61b 100644
--- a/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs
+++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs
@@ -213,7 +213,7 @@ public sealed partial class RevenantSystem
return;
DamageSpecifier dspec = new();
dspec.DamageDict.Add("Cold", damage.Value);
- _damage.TryChangeDamage(args.Args.Target, dspec, true, origin: uid);
+ _damage.ChangeDamage(args.Args.Target.Value, dspec, true, origin: uid);
args.Handled = true;
}
diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs
index 6c8972be58..c5080c0b06 100644
--- a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs
+++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs
@@ -3,7 +3,7 @@ using Content.Server.Actions;
using Content.Server.GameTicking;
using Content.Server.Store.Systems;
using Content.Shared.Alert;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Eye;
@@ -27,7 +27,6 @@ namespace Content.Server.Revenant.EntitySystems;
public sealed partial class RevenantSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly ActionsSystem _action = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly DamageableSystem _damage = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs
index b5adeb04db..6126234451 100644
--- a/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs
+++ b/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs
@@ -20,6 +20,7 @@ using Robust.Shared.Physics.Events;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Numerics;
+using Content.Shared.Damage.Components;
namespace Content.Server.Shuttles.Systems;
@@ -372,7 +373,7 @@ public sealed partial class ShuttleSystem
damageSpec.DamageDict["Blunt"] = scaledDamage;
damageSpec.DamageDict["Structural"] = scaledDamage * _structuralDamage;
- _damageSys.TryChangeDamage(localEnt, damageSpec, damageable: damageable);
+ _damageSys.ChangeDamage((localEnt, damageable), damageSpec);
}
// might've been destroyed
if (TerminatingOrDeleted(localEnt) || EntityManager.IsQueuedForDeletion(localEnt))
diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.cs
index e96ceffd16..86b97c40a6 100644
--- a/Content.Server/Shuttles/Systems/ShuttleSystem.cs
+++ b/Content.Server/Shuttles/Systems/ShuttleSystem.cs
@@ -8,7 +8,7 @@ using Content.Server.Shuttles.Events;
using Content.Server.Station.Systems;
using Content.Server.Stunnable;
using Content.Shared.Buckle.Components;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Light.Components;
using Content.Shared.Movement.Events;
using Content.Shared.Salvage;
diff --git a/Content.Server/Shuttles/Systems/ThrusterSystem.cs b/Content.Server/Shuttles/Systems/ThrusterSystem.cs
index f7f0a8b251..20040ab40a 100644
--- a/Content.Server/Shuttles/Systems/ThrusterSystem.cs
+++ b/Content.Server/Shuttles/Systems/ThrusterSystem.cs
@@ -1,16 +1,14 @@
using System.Numerics;
using Content.Server.Audio;
-using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Shuttles.Components;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Shuttles.Components;
using Content.Shared.Temperature;
-using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs b/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs
index bbd62d7a01..67408d1d5a 100644
--- a/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs
+++ b/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs
@@ -65,6 +65,16 @@ public sealed partial class BorgSystem
_actions.SetEntityIcon(actEnt, uid);
if (TryComp(uid, out var moduleIconComp))
_actions.SetIcon(actEnt, moduleIconComp.Icon);
+
+ /// Set a custom name and description on the action. The borg module action prototypes are shared across
+ /// all modules. Extract localized names, then populate variables with the info from the module itself.
+ var moduleName = Name(uid);
+ var actionMetaData = MetaData(component.ModuleSwapActionEntity.Value);
+
+ var instanceName = Loc.GetString("borg-module-action-name", ("moduleName", moduleName));
+ _metaData.SetEntityName(component.ModuleSwapActionEntity.Value, instanceName, actionMetaData);
+ var instanceDesc = Loc.GetString("borg-module-action-description", ("moduleName", moduleName));
+ _metaData.SetEntityDescription(component.ModuleSwapActionEntity.Value, instanceDesc, actionMetaData);
}
if (!TryComp(chassis, out BorgChassisComponent? chassisComp))
diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs b/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs
index 082e6776f0..fd3b910753 100644
--- a/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs
+++ b/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs
@@ -1,6 +1,6 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.DeviceNetwork;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;
diff --git a/Content.Server/Silicons/StationAi/StationAiSystem.cs b/Content.Server/Silicons/StationAi/StationAiSystem.cs
index 4ee2a07d72..a9198d5816 100644
--- a/Content.Server/Silicons/StationAi/StationAiSystem.cs
+++ b/Content.Server/Silicons/StationAi/StationAiSystem.cs
@@ -12,7 +12,8 @@ using Content.Server.Station.Systems;
using Content.Shared.Alert;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Containers.ItemSlots;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Systems;
using Content.Shared.Destructible;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.DoAfter;
@@ -127,10 +128,7 @@ public sealed class StationAiSystem : SharedStationAiSystem
_battery.SetCharge(ent, battery.MaxCharge);
}
- if (TryComp(ent, out var damageable))
- {
- _damageable.SetAllDamage(ent, damageable, 0);
- }
+ _damageable.ClearAllDamage(ent.Owner);
}
protected override void OnAiInsert(Entity ent, ref EntInsertedIntoContainerMessage args)
diff --git a/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs b/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs
index 884d625045..e303717e9a 100644
--- a/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs
+++ b/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs
@@ -17,7 +17,6 @@ public sealed class SingularityGeneratorSystem : SharedSingularityGeneratorSyste
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly PhysicsSystem _physics = default!;
[Dependency] private readonly IGameTiming _timing = default!;
- [Dependency] private readonly MetaDataSystem _metadata = default!;
#endregion Dependencies
public override void Initialize()
@@ -53,6 +52,9 @@ public sealed class SingularityGeneratorSystem : SharedSingularityGeneratorSyste
return;
SetPower(uid, 0, comp);
+
+ // Other particle entities from the same wave could trigger additional teslas to spawn, so we must block the generator
+ comp.Inert = true;
Spawn(comp.SpawnPrototype, Transform(uid).Coordinates);
}
@@ -112,7 +114,8 @@ public sealed class SingularityGeneratorSystem : SharedSingularityGeneratorSyste
if (!TryComp(args.OtherEntity, out var generatorComp))
return;
- if (_timing.CurTime < _metadata.GetPauseTime(uid) + generatorComp.NextFailsafe && !generatorComp.FailsafeDisabled)
+ if (generatorComp.Inert ||
+ _timing.CurTime < generatorComp.NextFailsafe && !generatorComp.FailsafeDisabled)
{
QueueDel(uid);
return;
diff --git a/Content.Server/Speech/EntitySystems/BarkAccentSystem.cs b/Content.Server/Speech/EntitySystems/BarkAccentSystem.cs
index 776199e9bd..66cbde5048 100644
--- a/Content.Server/Speech/EntitySystems/BarkAccentSystem.cs
+++ b/Content.Server/Speech/EntitySystems/BarkAccentSystem.cs
@@ -1,3 +1,4 @@
+using Content.Shared.StatusEffectNew;
using Content.Server.Speech.Components;
using Content.Shared.Speech;
using Robust.Shared.Random;
@@ -29,6 +30,7 @@ namespace Content.Server.Speech.EntitySystems
public override void Initialize()
{
SubscribeLocalEvent(OnAccent);
+ SubscribeLocalEvent>(OnAccentRelayed);
}
public string Accentuate(string message)
@@ -45,9 +47,14 @@ namespace Content.Server.Speech.EntitySystems
//Corvax-Localization-End
}
- private void OnAccent(EntityUid uid, BarkAccentComponent component, AccentGetEvent args)
+ private void OnAccent(Entity entity, ref AccentGetEvent args)
{
args.Message = Accentuate(args.Message);
}
+
+ private void OnAccentRelayed(Entity entity, ref StatusEffectRelayedEvent args)
+ {
+ args.Args.Message = Accentuate(args.Args.Message);
+ }
}
}
diff --git a/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs b/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs
index d4a37a07cd..a5e30346bd 100644
--- a/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs
+++ b/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs
@@ -2,7 +2,7 @@
using Content.Server.Destructible;
using Content.Server.PowerCell;
using Content.Shared.Speech.Components;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Speech;
using Robust.Shared.Random;
diff --git a/Content.Server/Speech/EntitySystems/OwOAccentSystem.cs b/Content.Server/Speech/EntitySystems/OwOAccentSystem.cs
index 72ae1a22a9..570d59ed79 100644
--- a/Content.Server/Speech/EntitySystems/OwOAccentSystem.cs
+++ b/Content.Server/Speech/EntitySystems/OwOAccentSystem.cs
@@ -1,5 +1,6 @@
using Content.Server.Speech.Components;
using Content.Shared.Speech;
+using Content.Shared.StatusEffectNew;
using Robust.Shared.Random;
namespace Content.Server.Speech.EntitySystems
@@ -21,6 +22,7 @@ namespace Content.Server.Speech.EntitySystems
public override void Initialize()
{
SubscribeLocalEvent(OnAccent);
+ SubscribeLocalEvent>(OnAccentRelayed);
}
public string Accentuate(string message)
@@ -39,9 +41,15 @@ namespace Content.Server.Speech.EntitySystems
.Replace("l", "w").Replace("L", "W");
}
- private void OnAccent(EntityUid uid, OwOAccentComponent component, AccentGetEvent args)
+ private void OnAccent(Entity entity, ref AccentGetEvent args)
{
args.Message = Accentuate(args.Message);
}
+
+ private void OnAccentRelayed(Entity entity, ref StatusEffectRelayedEvent args)
+ {
+ args.Args.Message = Accentuate(args.Args.Message);
+ }
+
}
}
diff --git a/Content.Server/Spreader/KudzuSystem.cs b/Content.Server/Spreader/KudzuSystem.cs
index fbc809c15b..e8470ebd57 100644
--- a/Content.Server/Spreader/KudzuSystem.cs
+++ b/Content.Server/Spreader/KudzuSystem.cs
@@ -1,4 +1,5 @@
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Systems;
using Content.Shared.Spreader;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
diff --git a/Content.Server/Temperature/Systems/TemperatureSystem.cs b/Content.Server/Temperature/Systems/TemperatureSystem.cs
index f6a7536994..928b6ae9b5 100644
--- a/Content.Server/Temperature/Systems/TemperatureSystem.cs
+++ b/Content.Server/Temperature/Systems/TemperatureSystem.cs
@@ -5,7 +5,8 @@ using Content.Server.Body.Components;
using Content.Server.Temperature.Components;
using Content.Shared.Alert;
using Content.Shared.Atmos;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Inventory;
using Content.Shared.Rejuvenate;
@@ -40,6 +41,8 @@ public sealed class TemperatureSystem : SharedTemperatureSystem
public override void Initialize()
{
+ base.Initialize();
+
SubscribeLocalEvent(EnqueueDamage);
SubscribeLocalEvent(OnAtmosExposedUpdate);
SubscribeLocalEvent(OnRejuvenate);
diff --git a/Content.Server/Toolshed/Commands/Misc/CloneCommand.cs b/Content.Server/Toolshed/Commands/Misc/CloneCommand.cs
new file mode 100644
index 0000000000..d7434c48c9
--- /dev/null
+++ b/Content.Server/Toolshed/Commands/Misc/CloneCommand.cs
@@ -0,0 +1,82 @@
+using Content.Server.Administration;
+using Content.Server.Humanoid;
+using Content.Shared.Administration;
+using Content.Shared.Cloning;
+using Content.Shared.Inventory;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Toolshed;
+
+namespace Content.Server.Cloning.Commands;
+
+[ToolshedCommand, AdminCommand(AdminFlags.Fun)]
+public sealed class CloneCommand : ToolshedCommand
+{
+ private HumanoidAppearanceSystem? _appearance;
+ private CloningSystem? _cloning;
+ private MetaDataSystem? _metadata;
+
+ [CommandImplementation("humanoidappearance")]
+ public IEnumerable HumanoidAppearance([PipedArgument] IEnumerable targets, EntityUid source, bool rename)
+ {
+ _appearance ??= GetSys();
+ _metadata ??= GetSys();
+
+ foreach (var ent in targets)
+ {
+ _appearance.CloneAppearance(source, ent);
+
+ if (rename)
+ _metadata.SetEntityName(ent, MetaData(source).EntityName, raiseEvents: true);
+
+ yield return ent;
+ }
+ }
+
+ [CommandImplementation("comps")]
+ public IEnumerable Comps([PipedArgument] IEnumerable targets, EntityUid source, ProtoId settings)
+ {
+ _cloning ??= GetSys();
+
+ foreach (var ent in targets)
+ {
+ _cloning.CloneComponents(source, ent, settings);
+ yield return ent;
+ }
+ }
+
+ [CommandImplementation("equipment")]
+ public IEnumerable Equipment([PipedArgument] IEnumerable targets, EntityUid source, SlotFlags flags)
+ {
+ _cloning ??= GetSys();
+
+ foreach (var ent in targets)
+ {
+ _cloning.CopyEquipment(source, ent, flags);
+ yield return ent;
+ }
+ }
+
+ [CommandImplementation("implants")]
+ public IEnumerable Implants([PipedArgument] IEnumerable targets, EntityUid source, bool copyStorage)
+ {
+ _cloning ??= GetSys();
+
+ foreach (var ent in targets)
+ {
+ _cloning.CopyImplants(source, ent, copyStorage);
+ yield return ent;
+ }
+ }
+
+ [CommandImplementation("storage")]
+ public IEnumerable InternalStorage([PipedArgument] IEnumerable targets, EntityUid source)
+ {
+ _cloning ??= GetSys();
+
+ foreach (var ent in targets)
+ {
+ _cloning.CopyStorage(source, ent);
+ yield return ent;
+ }
+ }
+}
diff --git a/Content.Server/VendingMachines/VendingMachineSystem.cs b/Content.Server/VendingMachines/VendingMachineSystem.cs
index 86a7b512b6..1fb695ae61 100644
--- a/Content.Server/VendingMachines/VendingMachineSystem.cs
+++ b/Content.Server/VendingMachines/VendingMachineSystem.cs
@@ -5,6 +5,7 @@ using Content.Server.Power.Components;
using Content.Server.Vocalization.Systems;
using Content.Shared.Cargo;
using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Destructible;
using Content.Shared.Emp;
using Content.Shared.Power;
diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.AutoFire.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.AutoFire.cs
index 47b0d5f3c6..995e29727b 100644
--- a/Content.Server/Weapons/Ranged/Systems/GunSystem.AutoFire.cs
+++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.AutoFire.cs
@@ -1,4 +1,4 @@
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.Map;
diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs
index dcfa3e6654..8bbab9503e 100644
--- a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs
+++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs
@@ -22,7 +22,6 @@ namespace Content.Server.Weapons.Ranged.Systems;
public sealed partial class GunSystem : SharedGunSystem
{
- [Dependency] private readonly DamageExamineSystem _damageExamine = default!;
[Dependency] private readonly PricingSystem _pricing = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPortalSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPortalSystem.cs
index 2de5dd0bfe..6bede34b68 100644
--- a/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPortalSystem.cs
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPortalSystem.cs
@@ -20,7 +20,6 @@ public sealed class XAEPortalSystem : BaseXAESystem
[Dependency] private readonly LinkedEntitySystem _link = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
- [Dependency] private readonly IGameTiming _timing = default!;
///
protected override void OnActivated(Entity ent, ref XenoArtifactNodeActivatedEvent args)
diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs
index f5b94deee4..ce97a4b33b 100644
--- a/Content.Server/Zombies/ZombieSystem.Transform.cs
+++ b/Content.Server/Zombies/ZombieSystem.Transform.cs
@@ -14,10 +14,9 @@ using Content.Server.NPC.Systems;
using Content.Server.StationEvents.Components;
using Content.Server.Speech.Components;
using Content.Shared.Body.Components;
-using Content.Shared.Chat;
using Content.Shared.CombatMode;
using Content.Shared.CombatMode.Pacification;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.CombatMode.Pacification;
@@ -249,8 +248,7 @@ public sealed partial class ZombieSystem
tempComp.ColdDamage.ClampMax(0);
//Heals the zombie from all the damage it took while human
- if (TryComp(target, out var damageablecomp))
- _damageable.SetAllDamage(target, damageablecomp, 0);
+ _damageable.ClearAllDamage(target);
_mobState.ChangeMobState(target, MobState.Alive);
_faction.ClearFactions(target, dirty: false);
diff --git a/Content.Server/Zombies/ZombieSystem.cs b/Content.Server/Zombies/ZombieSystem.cs
index d4f86fa96c..72c5236930 100644
--- a/Content.Server/Zombies/ZombieSystem.cs
+++ b/Content.Server/Zombies/ZombieSystem.cs
@@ -1,4 +1,3 @@
-using System.Linq;
using Content.Shared.NPC.Prototypes;
using Content.Server.Actions;
using Content.Server.Body.Systems;
@@ -11,7 +10,7 @@ using Content.Shared.Armor;
using Content.Shared.Bed.Sleep;
using Content.Shared.Cloning.Events;
using Content.Shared.Chat;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Humanoid;
using Content.Shared.Inventory;
using Content.Shared.Mind;
@@ -118,7 +117,7 @@ namespace Content.Server.Zombies
var curTime = _timing.CurTime;
// Hurt the living infected
- var query = EntityQueryEnumerator();
+ var query = EntityQueryEnumerator();
while (query.MoveNext(out var uid, out var comp, out var damage, out var mobState))
{
// Process only once per second
@@ -138,11 +137,11 @@ namespace Content.Server.Zombies
? comp.CritDamageMultiplier
: 1f;
- _damageable.TryChangeDamage(uid, comp.Damage * multiplier, true, false, damage);
+ _damageable.ChangeDamage((uid, damage), comp.Damage * multiplier, true, false);
}
// Heal the zombified
- var zombQuery = EntityQueryEnumerator();
+ var zombQuery = EntityQueryEnumerator();
while (zombQuery.MoveNext(out var uid, out var comp, out var damage, out var mobState))
{
// Process only once per second
@@ -159,7 +158,7 @@ namespace Content.Server.Zombies
: 1f;
// Gradual healing for living zombies.
- _damageable.TryChangeDamage(uid, comp.PassiveHealing * multiplier, true, false, damage);
+ _damageable.ChangeDamage((uid, damage), comp.PassiveHealing * multiplier, true, false);
}
}
diff --git a/Content.Shared/Armor/SharedArmorSystem.cs b/Content.Shared/Armor/SharedArmorSystem.cs
index 972289460f..6a3db3184d 100644
--- a/Content.Shared/Armor/SharedArmorSystem.cs
+++ b/Content.Shared/Armor/SharedArmorSystem.cs
@@ -1,5 +1,6 @@
using Content.Shared.Clothing.Components;
using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Examine;
using Content.Shared.Inventory;
using Content.Shared.Silicons.Borgs;
diff --git a/Content.Server/Atmos/Components/DeltaPressureComponent.cs b/Content.Shared/Atmos/Components/DeltaPressureComponent.cs
similarity index 83%
rename from Content.Server/Atmos/Components/DeltaPressureComponent.cs
rename to Content.Shared/Atmos/Components/DeltaPressureComponent.cs
index 0d2debe08a..064d67f606 100644
--- a/Content.Server/Atmos/Components/DeltaPressureComponent.cs
+++ b/Content.Shared/Atmos/Components/DeltaPressureComponent.cs
@@ -1,37 +1,37 @@
-using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Guidebook;
+using Robust.Shared.GameStates;
-namespace Content.Server.Atmos.Components;
+namespace Content.Shared.Atmos.Components;
///
/// Entities that have this component will have damage done to them depending on the local pressure
/// environment that they reside in.
///
/// Atmospherics.DeltaPressure batch-processes entities with this component in a list on
-/// the grid's .
+/// the grid's GridAtmosphereComponent.
/// The entities are automatically added and removed from this list, and automatically
/// added on initialization.
///
-/// Note that the entity should have an and be a grid structure.
+/// Note that the entity should have an AirtightComponent and be a grid structure.
[RegisterComponent]
+[NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedAtmosphereSystem), typeof(SharedDeltaPressureSystem))]
public sealed partial class DeltaPressureComponent : Component
{
///
- /// Whether the entity is currently in the processing list of the grid's .
+ /// Whether the entity is currently in the processing list of the grid's GridAtmosphereComponent.
///
[DataField(readOnly: true)]
[ViewVariables(VVAccess.ReadOnly)]
- [Access(typeof(DeltaPressureSystem), typeof(AtmosphereSystem))]
public bool InProcessingList;
///
/// Whether this entity is currently taking damage from pressure.
///
- [DataField(readOnly: true)]
- [ViewVariables(VVAccess.ReadOnly)]
- [Access(typeof(DeltaPressureSystem), typeof(AtmosphereSystem))]
+ [DataField, AutoNetworkedField]
public bool IsTakingDamage;
///
@@ -39,9 +39,12 @@ public sealed partial class DeltaPressureComponent : Component
/// Required for proper deletion, as we cannot reference the grid
/// for removal while the entity is being deleted.
///
- /// Note that while already stores the grid,
- /// we cannot trust it to be available on init or when the entity is being deleted. Tragic.
- [DataField]
+ /// Note that while AirtightComponent already stores the grid,
+ /// we cannot trust it to be available on init or when the entity is being deleted. Tragic.
+ /// Double note: this is set during ComponentInit and thus does not need to be a datafield
+ /// or else it will spam serialization.
+ /// TODO ATMOS: Simply use AirtightComponent's GridUID caching and handle entity removal from the processing list on an invalidation system similar to InvalidTiles.
+ [ViewVariables(VVAccess.ReadOnly)]
public EntityUid? GridUid;
///
diff --git a/Content.Shared/Atmos/EntitySystems/SharedDeltaPressureSystem.cs b/Content.Shared/Atmos/EntitySystems/SharedDeltaPressureSystem.cs
new file mode 100644
index 0000000000..4ea9880707
--- /dev/null
+++ b/Content.Shared/Atmos/EntitySystems/SharedDeltaPressureSystem.cs
@@ -0,0 +1,20 @@
+using Content.Shared.Atmos.Components;
+using Content.Shared.Examine;
+
+namespace Content.Shared.Atmos.EntitySystems;
+
+public abstract partial class SharedDeltaPressureSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnExaminedEvent);
+ }
+
+ private void OnExaminedEvent(Entity ent, ref ExaminedEvent args)
+ {
+ if (ent.Comp.IsTakingDamage)
+ args.PushMarkup(Loc.GetString("window-taking-damage"));
+ }
+}
diff --git a/Content.Shared/Atmos/TileFireEvent.cs b/Content.Shared/Atmos/TileFireEvent.cs
new file mode 100644
index 0000000000..f0045f58fd
--- /dev/null
+++ b/Content.Shared/Atmos/TileFireEvent.cs
@@ -0,0 +1,10 @@
+namespace Content.Shared.Atmos;
+
+///
+/// Event raised on an entity when it is standing on a tile that's on fire.
+///
+/// Current temperature of the hotspot this entity is exposed to.
+/// Current volume of the hotspot this entity is exposed to.
+/// This is not the volume of the tile this entity is on.
+[ByRefEvent]
+public readonly record struct TileFireEvent(float Temperature, float Volume);
diff --git a/Content.Shared/Bed/Sleep/SleepingSystem.cs b/Content.Shared/Bed/Sleep/SleepingSystem.cs
index 27e11bc878..661c8399a1 100644
--- a/Content.Shared/Bed/Sleep/SleepingSystem.cs
+++ b/Content.Shared/Bed/Sleep/SleepingSystem.cs
@@ -4,6 +4,7 @@ using Content.Shared.Buckle.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Events;
using Content.Shared.Damage.ForceSay;
+using Content.Shared.Damage.Systems;
using Content.Shared.Emoting;
using Content.Shared.Examine;
using Content.Shared.Eye.Blinding.Systems;
diff --git a/Content.Shared/Blocking/BlockingSystem.User.cs b/Content.Shared/Blocking/BlockingSystem.User.cs
index 2cd1db7f1f..db59a8d5f6 100644
--- a/Content.Shared/Blocking/BlockingSystem.User.cs
+++ b/Content.Shared/Blocking/BlockingSystem.User.cs
@@ -1,6 +1,6 @@
using Content.Shared.Damage;
-using Content.Shared.Damage.Prototypes;
-using Robust.Shared.Audio;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Systems;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
@@ -42,31 +42,31 @@ public sealed partial class BlockingSystem
private void OnUserDamageModified(EntityUid uid, BlockingUserComponent component, DamageModifyEvent args)
{
- if (TryComp(component.BlockingItem, out var blocking))
+ if (component.BlockingItem is not { } item || !TryComp(item, out var blocking))
+ return;
+
+ if (args.Damage.GetTotal() <= 0)
+ return;
+
+ // A shield should only block damage it can itself absorb. To determine that we need the Damageable component on it.
+ if (!TryComp(item, out var dmgComp))
+ return;
+
+ var blockFraction = blocking.IsBlocking ? blocking.ActiveBlockFraction : blocking.PassiveBlockFraction;
+ blockFraction = Math.Clamp(blockFraction, 0, 1);
+ _damageable.TryChangeDamage((item, dmgComp), blockFraction * args.OriginalDamage);
+
+ var modify = new DamageModifierSet();
+ foreach (var key in dmgComp.Damage.DamageDict.Keys)
{
- if (args.Damage.GetTotal() <= 0)
- return;
+ modify.Coefficients.TryAdd(key, 1 - blockFraction);
+ }
- // A shield should only block damage it can itself absorb. To determine that we need the Damageable component on it.
- if (!TryComp(component.BlockingItem, out var dmgComp))
- return;
+ args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, modify);
- var blockFraction = blocking.IsBlocking ? blocking.ActiveBlockFraction : blocking.PassiveBlockFraction;
- blockFraction = Math.Clamp(blockFraction, 0, 1);
- _damageable.TryChangeDamage(component.BlockingItem, blockFraction * args.OriginalDamage);
-
- var modify = new DamageModifierSet();
- foreach (var key in dmgComp.Damage.DamageDict.Keys)
- {
- modify.Coefficients.TryAdd(key, 1 - blockFraction);
- }
-
- args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, modify);
-
- if (blocking.IsBlocking && !args.Damage.Equals(args.OriginalDamage))
- {
- _audio.PlayPvs(blocking.BlockSound, uid);
- }
+ if (blocking.IsBlocking && !args.Damage.Equals(args.OriginalDamage))
+ {
+ _audio.PlayPvs(blocking.BlockSound, uid);
}
}
diff --git a/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs b/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs
index 693eede7d8..688e3ccb92 100644
--- a/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs
+++ b/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs
@@ -6,8 +6,8 @@ using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.EntityEffects.Effects.Solution;
-using Content.Shared.EntityEffects.Effects.Transform;
using Content.Shared.FixedPoint;
using Content.Shared.Fluids;
using Content.Shared.Forensics.Components;
@@ -99,8 +99,7 @@ public abstract class SharedBloodstreamSystem : EntitySystem
// bloodloss damage is based on the base value, and modified by how low your blood level is.
var amt = bloodstream.BloodlossDamage / (0.1f + bloodPercentage);
- _damageableSystem.TryChangeDamage(uid, amt,
- ignoreResistances: false, interruptsDoAfters: false);
+ _damageableSystem.TryChangeDamage(uid, amt, ignoreResistances: false, interruptsDoAfters: false);
// Apply dizziness as a symptom of bloodloss.
// The effect is applied in a way that it will never be cleared without being healthy.
diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs
index 78d270ddc9..0b2efdce59 100644
--- a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs
+++ b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs
@@ -181,7 +181,7 @@ public partial class SharedBodySystem
{
// TODO BODY SYSTEM KILL : remove this when wounding and required parts are implemented properly
var damage = new DamageSpecifier(Prototypes.Index(BloodlossDamageType), 300);
- Damageable.TryChangeDamage(bodyEnt, damage);
+ Damageable.ChangeDamage(bodyEnt.Owner, damage);
}
}
diff --git a/Content.Shared/Body/Systems/SharedBodySystem.cs b/Content.Shared/Body/Systems/SharedBodySystem.cs
index a45966fcc3..f359ebc632 100644
--- a/Content.Shared/Body/Systems/SharedBodySystem.cs
+++ b/Content.Shared/Body/Systems/SharedBodySystem.cs
@@ -1,4 +1,4 @@
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Movement.Systems;
using Content.Shared.Standing;
using Robust.Shared.Containers;
diff --git a/Content.Shared/Body/Systems/SharedInternalsSystem.cs b/Content.Shared/Body/Systems/SharedInternalsSystem.cs
index 7db02a376c..c0dc6c1172 100644
--- a/Content.Shared/Body/Systems/SharedInternalsSystem.cs
+++ b/Content.Shared/Body/Systems/SharedInternalsSystem.cs
@@ -7,6 +7,7 @@ using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Internals;
using Content.Shared.Inventory;
+using Content.Shared.Movement.Components;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
@@ -258,11 +259,15 @@ public abstract class SharedInternalsSystem : EntitySystem
Entity user)
{
// TODO use _respirator.CanMetabolizeGas() to prioritize metabolizable gasses
- // Prioritise
- // 1. back equipped tanks
- // 2. exo-slot tanks
- // 3. in-hand tanks
- // 4. pocket/belt tanks
+ // Lookup order:
+ // 1. Back
+ // 2. Exo-slot
+ // 3. In-hand
+ // 4. Pocket/belt
+ // Jetpacks will only be used as a fallback if no other tank is found
+
+ // Store the first jetpack seen
+ Entity? found = null;
if (!Resolve(user, ref user.Comp2, ref user.Comp3))
return null;
@@ -271,22 +276,36 @@ public abstract class SharedInternalsSystem : EntitySystem
TryComp(backEntity, out var backGasTank) &&
_gasTank.CanConnectToInternals((backEntity.Value, backGasTank)))
{
- return (backEntity.Value, backGasTank);
+ found = (backEntity.Value, backGasTank);
+ if (!HasComp(backEntity.Value))
+ {
+ return found;
+ }
}
if (_inventory.TryGetSlotEntity(user, "suitstorage", out var entity, user.Comp2, user.Comp3) &&
TryComp(entity, out var gasTank) &&
_gasTank.CanConnectToInternals((entity.Value, gasTank)))
{
- return (entity.Value, gasTank);
+ found ??= (entity.Value, gasTank);
+ if (!HasComp(entity.Value))
+ {
+ return (entity.Value, gasTank);
+ }
}
foreach (var item in _inventory.GetHandOrInventoryEntities((user.Owner, user.Comp1, user.Comp2)))
{
if (TryComp(item, out gasTank) && _gasTank.CanConnectToInternals((item, gasTank)))
- return (item, gasTank);
+ {
+ found ??= (item, gasTank);
+ if (!HasComp(item))
+ {
+ return (item, gasTank);
+ }
+ }
}
- return null;
+ return found;
}
}
diff --git a/Content.Shared/Changeling/Components/ChangelingClonerComponent.cs b/Content.Shared/Changeling/Components/ChangelingClonerComponent.cs
new file mode 100644
index 0000000000..20cb690835
--- /dev/null
+++ b/Content.Shared/Changeling/Components/ChangelingClonerComponent.cs
@@ -0,0 +1,100 @@
+using Content.Shared.Charges.Components;
+using Content.Shared.Cloning;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Changeling.Components;
+
+///
+/// Changeling transformation in item form!
+/// An entity with this component works like an implanter:
+/// First you use it on a humanoid to make a copy of their identity, along with all species relevant components,
+/// then use it on someone else to tranform them into a clone of them.
+/// Can be used in combination with
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class ChangelingClonerComponent : Component
+{
+ ///
+ /// A clone of the player you have copied the identity from.
+ /// This is a full humanoid backup, stored on a paused map.
+ ///
+ ///
+ /// Since this entity is stored on a separate map it will be outside PVS range.
+ ///
+ [DataField, AutoNetworkedField]
+ public EntityUid? ClonedBackup;
+
+ ///
+ /// Current state of the item.
+ ///
+ [DataField, AutoNetworkedField]
+ public ChangelingClonerState State = ChangelingClonerState.Empty;
+
+ ///
+ /// The cloning settings to use.
+ ///
+ [DataField, AutoNetworkedField]
+ public ProtoId Settings = "ChangelingCloningSettings";
+
+ ///
+ /// Doafter time for drawing and injecting.
+ ///
+ [DataField, AutoNetworkedField]
+ public TimeSpan DoAfter = TimeSpan.FromSeconds(5);
+
+ ///
+ /// Can this item be used more than once?
+ ///
+ [DataField, AutoNetworkedField]
+ public bool Reusable = true;
+
+ ///
+ /// Whether or not to add a reset verb to purge the stored identity,
+ /// allowing you to draw a new one.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool CanReset = true;
+
+ ///
+ /// Raise events when renaming the target?
+ /// This will change their ID card, crew manifest entry, and so on.
+ /// For admeme purposes.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool RaiseNameChangeEvents;
+
+ ///
+ /// The sound to play when taking someone's identity with the item.
+ ///
+ [DataField, AutoNetworkedField]
+ public SoundSpecifier? DrawSound;
+
+ ///
+ /// The sound to play when someone is transformed.
+ ///
+ [DataField, AutoNetworkedField]
+ public SoundSpecifier? InjectSound;
+}
+
+///
+/// Current state of the item.
+///
+[Serializable, NetSerializable]
+public enum ChangelingClonerState : byte
+{
+ ///
+ /// No sample taken yet.
+ ///
+ Empty,
+ ///
+ /// Filled with a DNA sample.
+ ///
+ Filled,
+ ///
+ /// Has been used (single use only).
+ ///
+ Spent,
+}
diff --git a/Content.Shared/Changeling/Systems/ChangelingClonerSystem.cs b/Content.Shared/Changeling/Systems/ChangelingClonerSystem.cs
new file mode 100644
index 0000000000..d65d39ca40
--- /dev/null
+++ b/Content.Shared/Changeling/Systems/ChangelingClonerSystem.cs
@@ -0,0 +1,308 @@
+using Content.Shared.Administration.Logs;
+using Content.Shared.Changeling.Components;
+using Content.Shared.Cloning;
+using Content.Shared.Database;
+using Content.Shared.DoAfter;
+using Content.Shared.Examine;
+using Content.Shared.Forensics.Systems;
+using Content.Shared.Humanoid;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction;
+using Content.Shared.Popups;
+using Content.Shared.Verbs;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Changeling.Systems;
+
+public sealed class ChangelingClonerSystem : EntitySystem
+{
+ [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+ [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearance = default!;
+ [Dependency] private readonly MetaDataSystem _metaData = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedCloningSystem _cloning = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentity = default!;
+ [Dependency] private readonly SharedForensicsSystem _forensics = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnExamine);
+ SubscribeLocalEvent>(OnGetVerbs);
+ SubscribeLocalEvent(OnAfterInteract);
+ SubscribeLocalEvent(OnDraw);
+ SubscribeLocalEvent(OnInject);
+ SubscribeLocalEvent(OnShutDown);
+ }
+
+ private void OnShutDown(Entity ent, ref ComponentShutdown args)
+ {
+ // Delete the stored clone.
+ PredictedQueueDel(ent.Comp.ClonedBackup);
+ }
+
+ private void OnExamine(Entity ent, ref ExaminedEvent args)
+ {
+ if (!args.IsInDetailsRange)
+ return;
+
+ var msg = ent.Comp.State switch
+ {
+ ChangelingClonerState.Empty => "changeling-cloner-component-empty",
+ ChangelingClonerState.Filled => "changeling-cloner-component-filled",
+ ChangelingClonerState.Spent => "changeling-cloner-component-spent",
+ _ => "error"
+ };
+
+ args.PushMarkup(Loc.GetString(msg));
+
+ }
+
+ private void OnGetVerbs(Entity ent, ref GetVerbsEvent args)
+ {
+ if (!args.CanInteract || !args.CanAccess || args.Hands == null)
+ return;
+
+ if (!ent.Comp.CanReset || ent.Comp.State == ChangelingClonerState.Spent)
+ return;
+
+ var user = args.User;
+ args.Verbs.Add(new Verb
+ {
+ Text = Loc.GetString("changeling-cloner-component-reset-verb"),
+ Disabled = ent.Comp.ClonedBackup == null,
+ Act = () => Reset(ent.AsNullable(), user),
+ DoContactInteraction = true,
+ });
+ }
+
+ private void OnAfterInteract(Entity ent, ref AfterInteractEvent args)
+ {
+ if (args.Handled || !args.CanReach || args.Target == null)
+ return;
+
+ switch (ent.Comp.State)
+ {
+ case ChangelingClonerState.Empty:
+ args.Handled |= TryDraw(ent.AsNullable(), args.Target.Value, args.User);
+ break;
+ case ChangelingClonerState.Filled:
+ args.Handled |= TryInject(ent.AsNullable(), args.Target.Value, args.User);
+ break;
+ case ChangelingClonerState.Spent:
+ default:
+ break;
+ }
+
+ }
+
+ private void OnDraw(Entity ent, ref ClonerDrawDoAfterEvent args)
+ {
+ if (args.Handled || args.Cancelled || args.Target == null)
+ return;
+
+ Draw(ent.AsNullable(), args.Target.Value, args.User);
+ args.Handled = true;
+ }
+
+ private void OnInject(Entity ent, ref ClonerInjectDoAfterEvent args)
+ {
+ if (args.Handled || args.Cancelled || args.Target == null)
+ return;
+
+ Inject(ent.AsNullable(), args.Target.Value, args.User);
+ args.Handled = true;
+ }
+
+ ///
+ /// Start a DoAfter to draw a DNA sample from the target.
+ ///
+ public bool TryDraw(Entity ent, EntityUid target, EntityUid user)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return false;
+
+ if (ent.Comp.State != ChangelingClonerState.Empty)
+ return false;
+
+ if (!HasComp(target))
+ return false; // cloning only works for humanoids at the moment
+
+ var args = new DoAfterArgs(EntityManager, user, ent.Comp.DoAfter, new ClonerDrawDoAfterEvent(), ent, target: target, used: ent)
+ {
+ BreakOnDamage = true,
+ BreakOnMove = true,
+ NeedHand = true,
+ };
+
+ if (!_doAfter.TryStartDoAfter(args))
+ return false;
+
+ var userIdentity = Identity.Entity(user, EntityManager);
+ var targetIdentity = Identity.Entity(target, EntityManager);
+ var userMsg = Loc.GetString("changeling-cloner-component-draw-user", ("user", userIdentity), ("target", targetIdentity));
+ var targetMsg = Loc.GetString("changeling-cloner-component-draw-target", ("user", userIdentity), ("target", targetIdentity));
+ _popup.PopupClient(userMsg, target, user);
+
+ if (user != target) // don't show the warning if using the item on yourself
+ _popup.PopupEntity(targetMsg, user, target, PopupType.LargeCaution);
+
+ return true;
+ }
+
+ ///
+ /// Start a DoAfter to inject a DNA sample into someone, turning them into a clone of the original.
+ ///
+ public bool TryInject(Entity ent, EntityUid target, EntityUid user)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return false;
+
+ if (ent.Comp.State != ChangelingClonerState.Filled)
+ return false;
+
+ if (!HasComp(target))
+ return false; // cloning only works for humanoids at the moment
+
+ var args = new DoAfterArgs(EntityManager, user, ent.Comp.DoAfter, new ClonerInjectDoAfterEvent(), ent, target: target, used: ent)
+ {
+ BreakOnDamage = true,
+ BreakOnMove = true,
+ NeedHand = true,
+ };
+
+ if (!_doAfter.TryStartDoAfter(args))
+ return false;
+
+ var userIdentity = Identity.Entity(user, EntityManager);
+ var targetIdentity = Identity.Entity(target, EntityManager);
+ var userMsg = Loc.GetString("changeling-cloner-component-inject-user", ("user", userIdentity), ("target", targetIdentity));
+ var targetMsg = Loc.GetString("changeling-cloner-component-inject-target", ("user", userIdentity), ("target", targetIdentity));
+ _popup.PopupClient(userMsg, target, user);
+
+ if (user != target) // don't show the warning if using the item on yourself
+ _popup.PopupEntity(targetMsg, user, target, PopupType.LargeCaution);
+
+ return true;
+ }
+
+ ///
+ /// Draw a DNA sample from the target.
+ /// This will create a clone stored on a paused map for data storage.
+ ///
+ public void Draw(Entity ent, EntityUid target, EntityUid user)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return;
+
+ if (ent.Comp.State != ChangelingClonerState.Empty)
+ return;
+
+ if (!HasComp(target))
+ return; // cloning only works for humanoids at the moment
+
+ if (!_prototype.Resolve(ent.Comp.Settings, out var settings))
+ return;
+
+ _adminLogger.Add(LogType.Identity,
+ $"{user} is using {ent.Owner} to draw DNA from {target}.");
+
+ // Make a copy of the target on a paused map, so that we can apply their components later.
+ ent.Comp.ClonedBackup = _changelingIdentity.CloneToPausedMap(settings, target);
+ ent.Comp.State = ChangelingClonerState.Filled;
+ _appearance.SetData(ent.Owner, ChangelingClonerVisuals.State, ChangelingClonerState.Filled);
+ Dirty(ent);
+
+ _audio.PlayPredicted(ent.Comp.DrawSound, target, user);
+ _forensics.TransferDna(ent, target);
+ }
+
+ ///
+ /// Inject a DNA sample into someone, turning them into a clone of the original.
+ ///
+ public void Inject(Entity ent, EntityUid target, EntityUid user)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return;
+
+ if (ent.Comp.State != ChangelingClonerState.Filled)
+ return;
+
+ if (!HasComp(target))
+ return; // cloning only works for humanoids at the moment
+
+ if (!_prototype.Resolve(ent.Comp.Settings, out var settings))
+ return;
+
+ _audio.PlayPredicted(ent.Comp.InjectSound, target, user);
+ _forensics.TransferDna(ent, target); // transfer DNA before overwriting it
+
+ if (!ent.Comp.Reusable)
+ {
+ ent.Comp.State = ChangelingClonerState.Spent;
+ _appearance.SetData(ent.Owner, ChangelingClonerVisuals.State, ChangelingClonerState.Spent);
+ Dirty(ent);
+ }
+
+ if (!Exists(ent.Comp.ClonedBackup))
+ return; // the entity is likely out of PVS range on the client
+
+ _adminLogger.Add(LogType.Identity,
+ $"{user} is using {ent.Owner} to inject DNA into {target} changing their identity to {ent.Comp.ClonedBackup.Value}.");
+
+ // Do the actual transformation.
+ _humanoidAppearance.CloneAppearance(ent.Comp.ClonedBackup.Value, target);
+ _cloning.CloneComponents(ent.Comp.ClonedBackup.Value, target, settings);
+ _metaData.SetEntityName(target, Name(ent.Comp.ClonedBackup.Value), raiseEvents: ent.Comp.RaiseNameChangeEvents);
+
+ }
+
+ ///
+ /// Purge the stored DNA and allow to draw again.
+ ///
+ public void Reset(Entity ent, EntityUid? user)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return;
+
+ // Delete the stored clone.
+ PredictedQueueDel(ent.Comp.ClonedBackup);
+ ent.Comp.ClonedBackup = null;
+ ent.Comp.State = ChangelingClonerState.Empty;
+ _appearance.SetData(ent.Owner, ChangelingClonerVisuals.State, ChangelingClonerState.Empty);
+ Dirty(ent);
+
+ if (user == null)
+ return;
+
+ _popup.PopupClient(Loc.GetString("changeling-cloner-component-reset-popup"), user.Value, user.Value);
+ }
+}
+
+///
+/// Doafter event for drawing a DNA sample.
+///
+[Serializable, NetSerializable]
+public sealed partial class ClonerDrawDoAfterEvent : SimpleDoAfterEvent;
+
+///
+/// DoAfterEvent for injecting a DNA sample, turning a player into someone else.
+///
+[Serializable, NetSerializable]
+public sealed partial class ClonerInjectDoAfterEvent : SimpleDoAfterEvent;
+
+///
+/// Key for the generic visualizer.
+///
+[Serializable, NetSerializable]
+public enum ChangelingClonerVisuals : byte
+{
+ State,
+}
diff --git a/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs b/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs
index 500ee06b22..a30387a807 100644
--- a/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs
+++ b/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs
@@ -4,7 +4,8 @@ using Content.Shared.Armor;
using Content.Shared.Atmos.Rotting;
using Content.Shared.Body.Components;
using Content.Shared.Changeling.Components;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.Humanoid;
@@ -92,7 +93,7 @@ public sealed class ChangelingDevourSystem : EntitySystem
if (damage.Damage.DamageDict.TryGetValue(damagePoints.Key, out var val) && val > comp.DevourConsumeDamageCap)
return;
}
- _damageable.TryChangeDamage(target, comp.DamagePerTick, true, true, damage, user);
+ _damageable.ChangeDamage((target.Value, damage), comp.DamagePerTick, true, true, user);
}
///
diff --git a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs
index e7e46d79a1..830aed6ab6 100644
--- a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs
+++ b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs
@@ -83,20 +83,19 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
}
///
- /// Clone a target humanoid into nullspace and add it to the Changelings list of identities.
- /// It creates a perfect copy of the target and can be used to pull components down for future use
+ /// Clone a target humanoid to a paused map.
+ /// It creates a perfect copy of the target and can be used to pull components down for future use.
///
- /// the Changeling
- /// the targets uid
- public EntityUid? CloneToPausedMap(Entity ent, EntityUid target)
+ /// The settings to use for cloning.
+ /// The target to clone.
+ public EntityUid? CloneToPausedMap(CloningSettingsPrototype settings, EntityUid target)
{
// Don't create client side duplicate clones or a clientside map.
if (_net.IsClient)
return null;
if (!TryComp(target, out var humanoid)
- || !_prototype.Resolve(humanoid.Species, out var speciesPrototype)
- || !_prototype.Resolve(ent.Comp.IdentityCloningSettings, out var settings))
+ || !_prototype.Resolve(humanoid.Species, out var speciesPrototype))
return null;
EnsurePausedMap();
@@ -117,10 +116,30 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
var targetName = _nameMod.GetBaseName(target);
_metaSystem.SetEntityName(clone, targetName);
- ent.Comp.ConsumedIdentities.Add(clone);
+
+ return clone;
+ }
+
+ ///
+ /// Clone a target humanoid to a paused map and add it to the Changelings list of identities.
+ /// It creates a perfect copy of the target and can be used to pull components down for future use.
+ ///
+ /// The Changeling.
+ /// The target to clone.
+ public EntityUid? CloneToPausedMap(Entity ent, EntityUid target)
+ {
+ if (!_prototype.Resolve(ent.Comp.IdentityCloningSettings, out var settings))
+ return null;
+
+ var clone = CloneToPausedMap(settings, target);
+
+ if (clone == null)
+ return null;
+
+ ent.Comp.ConsumedIdentities.Add(clone.Value);
Dirty(ent);
- HandlePvsOverride(ent, clone);
+ HandlePvsOverride(ent, clone.Value);
return clone;
}
diff --git a/Content.Shared/Charges/Systems/SharedChargesSystem.cs b/Content.Shared/Charges/Systems/SharedChargesSystem.cs
index 504648c41d..a48fbaaa8d 100644
--- a/Content.Shared/Charges/Systems/SharedChargesSystem.cs
+++ b/Content.Shared/Charges/Systems/SharedChargesSystem.cs
@@ -1,6 +1,7 @@
using Content.Shared.Actions.Events;
using Content.Shared.Charges.Components;
using Content.Shared.Examine;
+using Content.Shared.Rejuvenate;
using JetBrains.Annotations;
using Robust.Shared.Timing;
@@ -19,7 +20,7 @@ public abstract class SharedChargesSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent(OnExamine);
-
+ SubscribeLocalEvent(OnRejuvenate);
SubscribeLocalEvent(OnChargesAttempt);
SubscribeLocalEvent(OnChargesMapInit);
SubscribeLocalEvent(OnChargesPerformed);
@@ -48,6 +49,11 @@ public abstract class SharedChargesSystem : EntitySystem
args.PushMarkup(Loc.GetString("limited-charges-recharging", ("seconds", timeRemaining.TotalSeconds.ToString("F1"))));
}
+ private void OnRejuvenate(Entity ent, ref RejuvenateEvent args)
+ {
+ ResetCharges(ent.AsNullable());
+ }
+
private void OnChargesAttempt(Entity ent, ref ActionAttemptEvent args)
{
if (args.Cancelled)
diff --git a/Content.Shared/Chat/ISharedChatManager.cs b/Content.Shared/Chat/ISharedChatManager.cs
index 39c1d85dd2..76fb4fbea8 100644
--- a/Content.Shared/Chat/ISharedChatManager.cs
+++ b/Content.Shared/Chat/ISharedChatManager.cs
@@ -3,6 +3,28 @@ namespace Content.Shared.Chat;
public interface ISharedChatManager
{
void Initialize();
+
+ ///
+ /// Send an admin alert to the admin chat channel.
+ ///
+ /// The message to send.
void SendAdminAlert(string message);
+
+ ///
+ /// Send an admin alert to the admin chat channel specifically about the given player.
+ /// Will include info extra like their antag status and name.
+ ///
+ /// The player that the message is about.
+ /// The message to send.
void SendAdminAlert(EntityUid player, string message);
+
+ ///
+ /// This is a dangerous function! Only pass in property escaped text.
+ /// See:
+ ///
+ /// Use this for things that need to be unformatted (like tpto links) but ensure that everything else
+ /// is formated properly. If it's not, players could sneak in ban links or other nasty commands that the admins
+ /// could clink on.
+ ///
+ void SendAdminAlertNoFormatOrEscape(string message);
}
diff --git a/Content.Shared/Chat/SharedChatSystem.cs b/Content.Shared/Chat/SharedChatSystem.cs
index b0fef0ac0d..9ae5f61f5c 100644
--- a/Content.Shared/Chat/SharedChatSystem.cs
+++ b/Content.Shared/Chat/SharedChatSystem.cs
@@ -146,7 +146,7 @@ public abstract partial class SharedChatSystem : EntitySystem
/// The channel that was requested, if any
/// Whether or not to generate an informative pop-up message.
///
- public bool TryProccessRadioMessage(
+ public bool TryProcessRadioMessage(
EntityUid source,
string input,
out string output,
diff --git a/Content.Shared/Chat/SharedSuicideSystem.cs b/Content.Shared/Chat/SharedSuicideSystem.cs
index 4b9eaf24b7..0484e51ab2 100644
--- a/Content.Shared/Chat/SharedSuicideSystem.cs
+++ b/Content.Shared/Chat/SharedSuicideSystem.cs
@@ -1,8 +1,10 @@
+using System.Linq;
using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
+using Content.Shared.Damage.Systems;
using Content.Shared.Mobs.Components;
using Robust.Shared.Prototypes;
-using System.Linq;
namespace Content.Shared.Chat;
@@ -40,7 +42,7 @@ public sealed class SharedSuicideSystem : EntitySystem
appliedDamageSpecifier.DamageDict[key] = Math.Ceiling((double) (value * lethalAmountOfDamage / totalDamage));
}
- _damageableSystem.TryChangeDamage(target, appliedDamageSpecifier, true, origin: target);
+ _damageableSystem.ChangeDamage(target.AsNullable(), appliedDamageSpecifier, true, origin: target);
}
///
@@ -64,6 +66,6 @@ public sealed class SharedSuicideSystem : EntitySystem
}
var damage = new DamageSpecifier(damagePrototype, lethalAmountOfDamage);
- _damageableSystem.TryChangeDamage(target, damage, true, origin: target);
+ _damageableSystem.ChangeDamage(target.AsNullable(), damage, true, origin: target);
}
}
diff --git a/Content.Shared/Climbing/Systems/ClimbSystem.cs b/Content.Shared/Climbing/Systems/ClimbSystem.cs
index 45055ebbcc..9cc0a55ce1 100644
--- a/Content.Shared/Climbing/Systems/ClimbSystem.cs
+++ b/Content.Shared/Climbing/Systems/ClimbSystem.cs
@@ -2,7 +2,7 @@ using Content.Shared.ActionBlocker;
using Content.Shared.Buckle.Components;
using Content.Shared.Climbing.Components;
using Content.Shared.Climbing.Events;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.DoAfter;
using Content.Shared.DragDrop;
using Content.Shared.Hands.Components;
diff --git a/Content.Shared/Cloning/SharedCloningSystem.cs b/Content.Shared/Cloning/SharedCloningSystem.cs
index d8ab8a2aa1..e44264fb41 100644
--- a/Content.Shared/Cloning/SharedCloningSystem.cs
+++ b/Content.Shared/Cloning/SharedCloningSystem.cs
@@ -1,3 +1,5 @@
+using Robust.Shared.Prototypes;
+
namespace Content.Shared.Cloning;
public abstract partial class SharedCloningSystem : EntitySystem
@@ -11,4 +13,14 @@ public abstract partial class SharedCloningSystem : EntitySystem
public virtual void CloneComponents(EntityUid original, EntityUid clone, CloningSettingsPrototype settings)
{
}
+
+ ///
+ /// Copy components from one entity to another based on a CloningSettingsPrototype.
+ ///
+ /// The orignal Entity to clone components from.
+ /// The target Entity to clone components to.
+ /// The clone settings prototype id containing the list of components to clone.
+ public virtual void CloneComponents(EntityUid original, EntityUid clone, ProtoId settings)
+ {
+ }
}
diff --git a/Content.Shared/Clothing/SharedCursedMaskSystem.cs b/Content.Shared/Clothing/SharedCursedMaskSystem.cs
index 8ba83be151..359e8ef769 100644
--- a/Content.Shared/Clothing/SharedCursedMaskSystem.cs
+++ b/Content.Shared/Clothing/SharedCursedMaskSystem.cs
@@ -1,5 +1,6 @@
using Content.Shared.Clothing.Components;
using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.Examine;
using Content.Shared.Inventory;
using Content.Shared.Movement.Systems;
diff --git a/Content.Shared/Clumsy/ClumsySystem.cs b/Content.Shared/Clumsy/ClumsySystem.cs
index d7b4019eb8..35866b155a 100644
--- a/Content.Shared/Clumsy/ClumsySystem.cs
+++ b/Content.Shared/Clumsy/ClumsySystem.cs
@@ -2,7 +2,7 @@ using Content.Shared.CCVar;
using Content.Shared.Chemistry.Hypospray.Events;
using Content.Shared.Climbing.Components;
using Content.Shared.Climbing.Events;
-using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
using Content.Shared.IdentityManagement;
using Content.Shared.Medical;
using Content.Shared.Popups;
@@ -95,7 +95,7 @@ public sealed class ClumsySystem : EntitySystem
args.Cancelled = true; // fail to catch
if (ent.Comp.CatchingFailDamage != null)
- _damageable.TryChangeDamage(ent, ent.Comp.CatchingFailDamage, origin: args.Item);
+ _damageable.ChangeDamage(ent.Owner, ent.Comp.CatchingFailDamage, origin: args.Item);
// Collisions don't work properly with PopupPredicted or PlayPredicted.
// So we make this server only.
@@ -127,7 +127,7 @@ public sealed class ClumsySystem : EntitySystem
return;
if (ent.Comp.GunShootFailDamage != null)
- _damageable.TryChangeDamage(ent, ent.Comp.GunShootFailDamage, origin: ent);
+ _damageable.ChangeDamage(ent.Owner, ent.Comp.GunShootFailDamage, origin: ent);
_stun.TryUpdateParalyzeDuration(ent, ent.Comp.GunShootFailStunTime);
@@ -199,7 +199,7 @@ public sealed class ClumsySystem : EntitySystem
{
stunTime = bonkComp.BonkTime;
if (bonkComp.BonkDamage != null)
- _damageable.TryChangeDamage(target, bonkComp.BonkDamage, true);
+ _damageable.ChangeDamage(target.Owner, bonkComp.BonkDamage, true);
}
_stun.TryUpdateParalyzeDuration(target, stunTime);
diff --git a/Content.Shared/CombatMode/Pacification/PacificationSystem.cs b/Content.Shared/CombatMode/Pacification/PacificationSystem.cs
index 6bc32c5b96..22fbb53a93 100644
--- a/Content.Shared/CombatMode/Pacification/PacificationSystem.cs
+++ b/Content.Shared/CombatMode/Pacification/PacificationSystem.cs
@@ -6,6 +6,7 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Interaction.Events;
using Content.Shared.Popups;
using Content.Shared.Throwing;
+using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Shared.Timing;
@@ -65,6 +66,10 @@ public sealed class PacificationSystem : EntitySystem
if (HasComp(args.Used))
return;
+ if (TryComp(args.Used, out var component))
+ if (component.FireModes[component.CurrentFireMode].PacifismAllowedMode)
+ return;
+
// Disallow firing guns in all cases.
ShowPopup(ent, args.Used, "pacified-cannot-fire-gun");
args.Cancel();
diff --git a/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs b/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs
index d53a100acc..3985bd3051 100644
--- a/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs
+++ b/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using Content.Shared.Administration.Logs;
using Content.Shared.Examine;
using Content.Shared.Construction.Components;
diff --git a/Content.Shared/Cuffs/Components/CuffableComponent.cs b/Content.Shared/Cuffs/Components/CuffableComponent.cs
index a7eba34d8c..046dd504c0 100644
--- a/Content.Shared/Cuffs/Components/CuffableComponent.cs
+++ b/Content.Shared/Cuffs/Components/CuffableComponent.cs
@@ -24,12 +24,6 @@ public sealed partial class CuffableComponent : Component
[ViewVariables]
public int CuffedHandCount => Container.ContainedEntities.Count * 2;
- ///
- /// The last pair of cuffs that was added to this entity.
- ///
- [ViewVariables]
- public EntityUid LastAddedCuffs => Container.ContainedEntities[^1];
-
///
/// Container of various handcuffs currently applied to the entity.
///
diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs
index ff4201acaf..f8efa20afa 100644
--- a/Content.Shared/Cuffs/SharedCuffableSystem.cs
+++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Components;
@@ -260,7 +261,7 @@ namespace Content.Shared.Cuffs
{
if (args.Handled)
return;
- TryUncuff(ent, ent, cuffable: ent.Comp);
+ TryUncuff((ent, ent.Comp), ent);
args.Handled = true;
}
@@ -278,7 +279,7 @@ namespace Content.Shared.Cuffs
Verb verb = new()
{
- Act = () => TryUncuff(uid, args.User, cuffable: component),
+ Act = () => TryUncuff((uid, component), args.User),
DoContactInteraction = true,
Text = Loc.GetString("uncuff-verb-get-data-text")
};
@@ -585,41 +586,31 @@ namespace Content.Shared.Cuffs
return true;
}
+ ///
+ public void TryUncuff(Entity target, EntityUid user)
+ {
+ if (!TryGetLastCuff(target, out var cuff))
+ return;
+
+ TryUncuff(target, user, cuff.Value);
+ }
+
///
/// Attempt to uncuff a cuffed entity. Can be called by the cuffed entity, or another entity trying to help uncuff them.
/// If the uncuffing succeeds, the cuffs will drop on the floor.
///
- ///
- /// The cuffed entity
- /// Optional param for the handcuff entity to remove from the cuffed entity. If null, uses the most recently added handcuff entity.
- ///
- ///
- public void TryUncuff(EntityUid target, EntityUid user, EntityUid? cuffsToRemove = null, CuffableComponent? cuffable = null, HandcuffComponent? cuff = null)
+ /// The entity we're trying to remove cuffs from.
+ /// The entity doing the cuffing.
+ /// The handcuff entity we're attempting to remove.
+ public void TryUncuff(Entity target, EntityUid user, Entity cuff)
{
- if (!Resolve(target, ref cuffable))
+ if (!Resolve(target, ref target.Comp) || !Resolve(cuff, ref cuff.Comp))
return;
- var isOwner = user == target;
+ var isOwner = user == target.Owner;
- if (cuffsToRemove == null)
- {
- if (cuffable.Container.ContainedEntities.Count == 0)
- {
- return;
- }
-
- cuffsToRemove = cuffable.LastAddedCuffs;
- }
- else
- {
- if (!cuffable.Container.ContainedEntities.Contains(cuffsToRemove.Value))
- {
- Log.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!");
- }
- }
-
- if (!Resolve(cuffsToRemove.Value, ref cuff))
- return;
+ if (!target.Comp.Container.ContainedEntities.Contains(cuff))
+ Log.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!");
var attempt = new UncuffAttemptEvent(user, target);
RaiseLocalEvent(user, ref attempt, true);
@@ -629,29 +620,28 @@ namespace Content.Shared.Cuffs
return;
}
- if (!isOwner && !_interaction.InRangeUnobstructed(user, target))
+ if (!isOwner && !_interaction.InRangeUnobstructed(user, target.Owner))
{
_popup.PopupClient(Loc.GetString("cuffable-component-cannot-remove-cuffs-too-far-message"), user, user);
return;
}
-
- var ev = new ModifyUncuffDurationEvent(user, target, isOwner ? cuff.BreakoutTime : cuff.UncuffTime);
+ var ev = new ModifyUncuffDurationEvent(user, target, isOwner ? cuff.Comp.BreakoutTime : cuff.Comp.UncuffTime);
RaiseLocalEvent(user, ref ev);
var uncuffTime = ev.Duration;
if (isOwner)
{
- if (!TryComp(cuffsToRemove.Value, out UseDelayComponent? useDelay))
+ if (!TryComp(cuff, out UseDelayComponent? useDelay))
return;
- if (!_delay.TryResetDelay((cuffsToRemove.Value, useDelay), true))
+ if (!_delay.TryResetDelay((cuff, useDelay), true))
{
return;
}
}
- var doAfterEventArgs = new DoAfterArgs(EntityManager, user, uncuffTime, new UnCuffDoAfterEvent(), target, target, cuffsToRemove)
+ var doAfterEventArgs = new DoAfterArgs(EntityManager, user, uncuffTime, new UnCuffDoAfterEvent(), target, target, cuff)
{
BreakOnMove = true,
BreakOnWeightlessMove = false,
@@ -666,7 +656,7 @@ namespace Content.Shared.Cuffs
_adminLog.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(user):player} is trying to uncuff {ToPrettyString(target):subject}");
- var popupText = user == target
+ var popupText = user == target.Owner
? "cuffable-component-start-uncuffing-self-observer"
: "cuffable-component-start-uncuffing-observer";
_popup.PopupEntity(
@@ -678,7 +668,7 @@ namespace Content.Shared.Cuffs
.RemoveWhere(e => e.AttachedEntity == target || e.AttachedEntity == user),
true);
- if (target == user)
+ if (isOwner)
{
_popup.PopupClient(Loc.GetString("cuffable-component-start-uncuffing-self"), user, user);
}
@@ -694,7 +684,7 @@ namespace Content.Shared.Cuffs
target);
}
- _audio.PlayPredicted(isOwner ? cuff.StartBreakoutSound : cuff.StartUncuffSound, target, user);
+ _audio.PlayPredicted(isOwner ? cuff.Comp.StartBreakoutSound : cuff.Comp.StartUncuffSound, target, user);
}
public void Uncuff(EntityUid target, EntityUid? user, EntityUid cuffsToRemove, CuffableComponent? cuffable = null, HandcuffComponent? cuff = null)
@@ -818,9 +808,56 @@ namespace Content.Shared.Cuffs
#endregion
- public IReadOnlyList GetAllCuffs(CuffableComponent component)
+ ///
+ /// Tries to get a list of all the handcuffs stored in an entity's .
+ ///
+ /// The cuffable entity in question.
+ /// A list of cuffs if it exists.
+ /// True if a list of cuffs with cuffs exists. False if no list exists or if it is empty.
+ public bool TryGetAllCuffs(Entity entity, out IReadOnlyList cuffs)
{
- return component.Container.ContainedEntities;
+ cuffs = GetAllCuffs(entity);
+
+ return cuffs.Count > 0;
+ }
+
+ ///
+ /// Tries to get a list of all the handcuffs stored in a entity's .
+ ///
+ /// The cuffable entity in question.
+ /// A list of cuffs if it exists, or null if there are no cuffs.
+ public IReadOnlyList GetAllCuffs(Entity entity)
+ {
+ if (!Resolve(entity, ref entity.Comp))
+ return [];
+
+ return entity.Comp.Container.ContainedEntities;
+ }
+
+ ///
+ /// Tries to get the most recently added pair of handcuffs added to an entity with .
+ ///
+ /// The cuffable entity in question.
+ /// The most recently added cuff.
+ /// Returns true if a cuff exists and false if one doesn't.
+ public bool TryGetLastCuff(Entity entity, [NotNullWhen(true)] out EntityUid? cuff)
+ {
+ cuff = GetLastCuffOrNull(entity);
+
+ return cuff != null;
+ }
+
+ ///
+ /// Tries to get the most recently added pair of handcuffs added to an entity with .
+ ///
+ /// The cuffable entity in question.
+ /// The most recently added cuff or null if none exists.
+ public EntityUid? GetLastCuffOrNull(Entity entity)
+ {
+ if (!Resolve(entity, ref entity.Comp))
+ return null;
+
+ return entity.Comp.Container.ContainedEntities.Count == 0 ? null : entity.Comp.Container.ContainedEntities.Last();
}
}
diff --git a/Content.Shared/Damage/Components/ClothingSlowOnDamageModifierComponent.cs b/Content.Shared/Damage/Components/ClothingSlowOnDamageModifierComponent.cs
index 3d4bdd597c..faf8a1bb57 100644
--- a/Content.Shared/Damage/Components/ClothingSlowOnDamageModifierComponent.cs
+++ b/Content.Shared/Damage/Components/ClothingSlowOnDamageModifierComponent.cs
@@ -1,3 +1,4 @@
+using Content.Shared.Damage.Systems;
using Robust.Shared.GameStates;
namespace Content.Shared.Damage.Components;
diff --git a/Content.Shared/Damage/Components/DamageableComponent.cs b/Content.Shared/Damage/Components/DamageableComponent.cs
index 1d290181ec..cef27d4d2f 100644
--- a/Content.Shared/Damage/Components/DamageableComponent.cs
+++ b/Content.Shared/Damage/Components/DamageableComponent.cs
@@ -1,4 +1,5 @@
using Content.Shared.Damage.Prototypes;
+using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Content.Shared.StatusIcon;
@@ -6,105 +7,100 @@ using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
-namespace Content.Shared.Damage
+namespace Content.Shared.Damage.Components;
+
+///
+/// Component that allows entities to take damage.
+///
+///
+/// The supported damage types are specified using a s. DamageContainers
+/// may also have resistances to certain damage types, defined via a .
+///
+[RegisterComponent]
+[NetworkedComponent]
+[Access(typeof(DamageableSystem), Other = AccessPermissions.ReadExecute)]
+public sealed partial class DamageableComponent : Component
{
///
- /// Component that allows entities to take damage.
+ /// This specifies what damage types are supported by this component.
+ /// If null, all damage types will be supported.
+ ///
+ [DataField("damageContainer")]
+ // ReSharper disable once InconsistentNaming - This is wrong but fixing it is potentially annoying for downstreams.
+ public ProtoId? DamageContainerID;
+
+ ///
+ /// This will be applied to any damage that is dealt to this container,
+ /// unless the damage explicitly ignores resistances.
///
///
- /// The supported damage types are specified using a s. DamageContainers
- /// may also have resistances to certain damage types, defined via a .
+ /// Though DamageModifierSets can be deserialized directly, we only want to use the prototype version here
+ /// to reduce duplication.
///
- [RegisterComponent]
- [NetworkedComponent]
- [Access(typeof(DamageableSystem), Other = AccessPermissions.ReadExecute)]
- public sealed partial class DamageableComponent : Component
+ [DataField("damageModifierSet")]
+ public ProtoId? DamageModifierSetId;
+
+ ///
+ /// All the damage information is stored in this .
+ ///
+ ///
+ /// If this data-field is specified, this allows damageable components to be initialized with non-zero damage.
+ ///
+ [DataField(readOnly: true)] //TODO FULL GAME SAVE
+ public DamageSpecifier Damage = new();
+
+ ///
+ /// Damage, indexed by ID keys.
+ ///
+ ///
+ /// Groups which have no members that are supported by this component will not be present in this
+ /// dictionary.
+ ///
+ [ViewVariables] public Dictionary DamagePerGroup = new();
+
+ ///
+ /// The sum of all damages in the DamageableComponent.
+ ///
+ [ViewVariables]
+ public FixedPoint2 TotalDamage;
+
+ [DataField("radiationDamageTypes")]
+ // ReSharper disable once UseCollectionExpression - Cannot refactor this as it's a potential sandbox violation.
+ public List> RadiationDamageTypeIDs = new() { "Radiation" };
+
+ ///
+ /// Group types that affect the pain overlay.
+ ///
+ /// TODO: Add support for adding damage types specifically rather than whole damage groups
+ [DataField]
+ // ReSharper disable once UseCollectionExpression - Cannot refactor this as it's a potential sandbox volation.
+ public List> PainDamageGroups = new() { "Brute", "Burn" };
+
+ [DataField]
+ public Dictionary> HealthIcons = new()
{
- ///
- /// This specifies what damage types are supported by this component.
- /// If null, all damage types will be supported.
- ///
- [DataField("damageContainer")]
- public ProtoId? DamageContainerID;
+ { MobState.Alive, "HealthIconFine" },
+ { MobState.Critical, "HealthIconCritical" },
+ { MobState.Dead, "HealthIconDead" },
+ };
- ///
- /// This will be applied to any damage that is dealt to this container,
- /// unless the damage explicitly ignores resistances.
- ///
- ///
- /// Though DamageModifierSets can be deserialized directly, we only want to use the prototype version here
- /// to reduce duplication.
- ///
- [DataField("damageModifierSet")]
- public ProtoId? DamageModifierSetId;
+ [DataField]
+ public ProtoId RottingIcon = "HealthIconRotting";
- ///
- /// All the damage information is stored in this .
- ///
- ///
- /// If this data-field is specified, this allows damageable components to be initialized with non-zero damage.
- ///
- [DataField(readOnly: true)] // TODO FULL GAME SAVE
- public DamageSpecifier Damage = new();
-
- ///
- /// Damage, indexed by ID keys.
- ///
- ///
- /// Groups which have no members that are supported by this component will not be present in this
- /// dictionary.
- ///
- [ViewVariables] public Dictionary DamagePerGroup = new();
-
- ///
- /// The sum of all damages in the DamageableComponent.
- ///
- [ViewVariables]
- public FixedPoint2 TotalDamage;
-
- [DataField("radiationDamageTypes")]
- public List> RadiationDamageTypeIDs = new() { "Radiation" };
-
- ///
- /// Group types that affect the pain overlay.
- ///
- /// TODO: Add support for adding damage types specifically rather than whole damage groups
- [DataField]
- public List> PainDamageGroups = new() { "Brute", "Burn" };
-
- [DataField]
- public Dictionary> HealthIcons = new()
- {
- { MobState.Alive, "HealthIconFine" },
- { MobState.Critical, "HealthIconCritical" },
- { MobState.Dead, "HealthIconDead" },
- };
-
- [DataField]
- public ProtoId RottingIcon = "HealthIconRotting";
-
- [DataField]
- public FixedPoint2? HealthBarThreshold;
- }
-
- [Serializable, NetSerializable]
- public sealed class DamageableComponentState : ComponentState
- {
- public readonly Dictionary DamageDict;
- public readonly string? DamageContainerId;
- public readonly string? ModifierSetId;
- public readonly FixedPoint2? HealthBarThreshold;
-
- public DamageableComponentState(
- Dictionary damageDict,
- string? damageContainerId,
- string? modifierSetId,
- FixedPoint2? healthBarThreshold)
- {
- DamageDict = damageDict;
- DamageContainerId = damageContainerId;
- ModifierSetId = modifierSetId;
- HealthBarThreshold = healthBarThreshold;
- }
- }
+ [DataField]
+ public FixedPoint2? HealthBarThreshold;
+}
+
+[Serializable, NetSerializable]
+public sealed class DamageableComponentState(
+ Dictionary damageDict,
+ ProtoId? damageContainerId,
+ ProtoId? modifierSetId,
+ FixedPoint2? healthBarThreshold)
+ : ComponentState
+{
+ public readonly Dictionary DamageDict = damageDict;
+ public readonly ProtoId? DamageContainerId = damageContainerId;
+ public readonly ProtoId? ModifierSetId = modifierSetId;
+ public readonly FixedPoint2? HealthBarThreshold = healthBarThreshold;
}
diff --git a/Content.Shared/Damage/Components/IgnoreSlowOnDamageComponent.cs b/Content.Shared/Damage/Components/IgnoreSlowOnDamageComponent.cs
index e933eb1a79..101a494bc9 100644
--- a/Content.Shared/Damage/Components/IgnoreSlowOnDamageComponent.cs
+++ b/Content.Shared/Damage/Components/IgnoreSlowOnDamageComponent.cs
@@ -1,3 +1,4 @@
+using Content.Shared.Damage.Systems;
using Robust.Shared.GameStates;
namespace Content.Shared.Damage.Components;
diff --git a/Content.Shared/Damage/Components/RequireProjectileTargetComponent.cs b/Content.Shared/Damage/Components/RequireProjectileTargetComponent.cs
index 5bd8292daa..3c94dcbc38 100644
--- a/Content.Shared/Damage/Components/RequireProjectileTargetComponent.cs
+++ b/Content.Shared/Damage/Components/RequireProjectileTargetComponent.cs
@@ -1,3 +1,4 @@
+using Content.Shared.Damage.Systems;
using Robust.Shared.GameStates;
namespace Content.Shared.Damage.Components;
diff --git a/Content.Shared/Damage/DamageModifierSet.cs b/Content.Shared/Damage/DamageModifierSet.cs
index eaa6e93da4..d8b00ad1f2 100644
--- a/Content.Shared/Damage/DamageModifierSet.cs
+++ b/Content.Shared/Damage/DamageModifierSet.cs
@@ -1,3 +1,4 @@
+using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
diff --git a/Content.Shared/Damage/DamageSpecifier.cs b/Content.Shared/Damage/DamageSpecifier.cs
index 7bf921baa2..6c11f88b1e 100644
--- a/Content.Shared/Damage/DamageSpecifier.cs
+++ b/Content.Shared/Damage/DamageSpecifier.cs
@@ -1,5 +1,6 @@
using System.Linq;
using System.Text.Json.Serialization;
+using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;
diff --git a/Content.Shared/Damage/Prototypes/DamageContainerPrototype.cs b/Content.Shared/Damage/Prototypes/DamageContainerPrototype.cs
index 00322f0884..1643a5469d 100644
--- a/Content.Shared/Damage/Prototypes/DamageContainerPrototype.cs
+++ b/Content.Shared/Damage/Prototypes/DamageContainerPrototype.cs
@@ -1,5 +1,5 @@
+using Content.Shared.Damage.Components;
using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
namespace Content.Shared.Damage.Prototypes
{
diff --git a/Content.Shared/Damage/Prototypes/DamageGroupPrototype.cs b/Content.Shared/Damage/Prototypes/DamageGroupPrototype.cs
index a33064f934..facdcce0e8 100644
--- a/Content.Shared/Damage/Prototypes/DamageGroupPrototype.cs
+++ b/Content.Shared/Damage/Prototypes/DamageGroupPrototype.cs
@@ -1,5 +1,5 @@
+using Content.Shared.Damage.Components;
using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
namespace Content.Shared.Damage.Prototypes
{
diff --git a/Content.Shared/Damage/Systems/DamageOnAttackedSystem.cs b/Content.Shared/Damage/Systems/DamageOnAttackedSystem.cs
index 29de43fba4..d628f40b76 100644
--- a/Content.Shared/Damage/Systems/DamageOnAttackedSystem.cs
+++ b/Content.Shared/Damage/Systems/DamageOnAttackedSystem.cs
@@ -73,9 +73,9 @@ public sealed class DamageOnAttackedSystem : EntitySystem
}
}
- totalDamage = _damageableSystem.TryChangeDamage(args.User, totalDamage, entity.Comp.IgnoreResistances, origin: entity);
+ totalDamage = _damageableSystem.ChangeDamage(args.User, totalDamage, entity.Comp.IgnoreResistances, origin: entity);
- if (totalDamage != null && totalDamage.AnyPositive())
+ if (totalDamage.AnyPositive())
{
_adminLogger.Add(LogType.Damaged, $"{ToPrettyString(args.User):user} injured themselves by attacking {ToPrettyString(entity):target} and received {totalDamage.GetTotal():damage} damage");
_audioSystem.PlayPredicted(entity.Comp.InteractSound, entity, args.User);
diff --git a/Content.Shared/Damage/Systems/DamageOnInteractSystem.cs b/Content.Shared/Damage/Systems/DamageOnInteractSystem.cs
index bd3b6979f7..401c94f33e 100644
--- a/Content.Shared/Damage/Systems/DamageOnInteractSystem.cs
+++ b/Content.Shared/Damage/Systems/DamageOnInteractSystem.cs
@@ -65,7 +65,7 @@ public sealed class DamageOnInteractSystem : EntitySystem
// or checking the entity for the comp itself if the inventory didn't work
if (protectiveEntity.Comp == null && TryComp(args.User, out var protectiveComp))
protectiveEntity = (args.User, protectiveComp);
-
+
// if protectiveComp isn't null after all that, it means the user has protection,
// so let's calculate how much they resist
@@ -75,9 +75,9 @@ public sealed class DamageOnInteractSystem : EntitySystem
}
}
- totalDamage = _damageableSystem.TryChangeDamage(args.User, totalDamage, origin: args.Target);
+ totalDamage = _damageableSystem.ChangeDamage(args.User, totalDamage, origin: args.Target);
- if (totalDamage != null && totalDamage.AnyPositive())
+ if (totalDamage.AnyPositive())
{
// Record this interaction and determine when a user is allowed to interact with this entity again
entity.Comp.LastInteraction = _gameTiming.CurTime;
diff --git a/Content.Shared/Damage/Systems/DamageableSystem.API.cs b/Content.Shared/Damage/Systems/DamageableSystem.API.cs
new file mode 100644
index 0000000000..c2a1374901
--- /dev/null
+++ b/Content.Shared/Damage/Systems/DamageableSystem.API.cs
@@ -0,0 +1,235 @@
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.FixedPoint;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Damage.Systems;
+
+public sealed partial class DamageableSystem
+{
+ ///
+ /// Directly sets the damage specifier of a damageable component.
+ ///
+ ///
+ /// Useful for some unfriendly folk. Also ensures that cached values are updated and that a damage changed
+ /// event is raised.
+ ///
+ public void SetDamage(Entity ent, DamageSpecifier damage)
+ {
+ if (!_damageableQuery.Resolve(ent, ref ent.Comp, false))
+ return;
+
+ ent.Comp.Damage = damage;
+
+ OnEntityDamageChanged((ent, ent.Comp));
+ }
+
+ ///
+ /// Applies damage specified via a .
+ ///
+ ///
+ /// is effectively just a dictionary of damage types and damage values. This
+ /// function just applies the container's resistances (unless otherwise specified) and then changes the
+ /// stored damage data. Division of group damage into types is managed by .
+ ///
+ ///
+ /// If the attempt was successful or not.
+ ///
+ public bool TryChangeDamage(
+ Entity ent,
+ DamageSpecifier damage,
+ bool ignoreResistances = false,
+ bool interruptsDoAfters = true,
+ EntityUid? origin = null,
+ bool ignoreGlobalModifiers = false
+ )
+ {
+ //! Empty just checks if the DamageSpecifier is _literally_ empty, as in, is internal dictionary of damage types is empty.
+ // If you deal 0.0 of some damage type, Empty will be false!
+ return !TryChangeDamage(ent, damage, out _, ignoreResistances, interruptsDoAfters, origin, ignoreGlobalModifiers);
+ }
+
+ ///
+ /// Applies damage specified via a .
+ ///
+ ///
+ /// is effectively just a dictionary of damage types and damage values. This
+ /// function just applies the container's resistances (unless otherwise specified) and then changes the
+ /// stored damage data. Division of group damage into types is managed by .
+ ///
+ ///
+ /// If the attempt was successful or not.
+ ///
+ public bool TryChangeDamage(
+ Entity ent,
+ DamageSpecifier damage,
+ out DamageSpecifier newDamage,
+ bool ignoreResistances = false,
+ bool interruptsDoAfters = true,
+ EntityUid? origin = null,
+ bool ignoreGlobalModifiers = false
+ )
+ {
+ //! Empty just checks if the DamageSpecifier is _literally_ empty, as in, is internal dictionary of damage types is empty.
+ // If you deal 0.0 of some damage type, Empty will be false!
+ newDamage = ChangeDamage(ent, damage, ignoreResistances, interruptsDoAfters, origin, ignoreGlobalModifiers);
+ return !damage.Empty;
+ }
+
+ ///
+ /// Applies damage specified via a .
+ ///
+ ///
+ /// is effectively just a dictionary of damage types and damage values. This
+ /// function just applies the container's resistances (unless otherwise specified) and then changes the
+ /// stored damage data. Division of group damage into types is managed by .
+ ///
+ ///
+ /// The actual amount of damage taken, as a DamageSpecifier.
+ ///
+ public DamageSpecifier ChangeDamage(
+ Entity ent,
+ DamageSpecifier damage,
+ bool ignoreResistances = false,
+ bool interruptsDoAfters = true,
+ EntityUid? origin = null,
+ bool ignoreGlobalModifiers = false
+ )
+ {
+ var damageDone = new DamageSpecifier();
+
+ if (!_damageableQuery.Resolve(ent, ref ent.Comp, false))
+ return damageDone;
+
+ if (damage.Empty)
+ return damageDone;
+
+ var before = new BeforeDamageChangedEvent(damage, origin);
+ RaiseLocalEvent(ent, ref before);
+
+ if (before.Cancelled)
+ return damageDone;
+
+ // Apply resistances
+ if (!ignoreResistances)
+ {
+ if (
+ ent.Comp.DamageModifierSetId != null &&
+ _prototypeManager.Resolve(ent.Comp.DamageModifierSetId, out var modifierSet)
+ )
+ damage = DamageSpecifier.ApplyModifierSet(damage, modifierSet);
+
+ // TODO DAMAGE
+ // byref struct event.
+ var ev = new DamageModifyEvent(damage, origin);
+ RaiseLocalEvent(ent, ev);
+ damage = ev.Damage;
+
+ if (damage.Empty)
+ return damageDone;
+ }
+
+ if (!ignoreGlobalModifiers)
+ damage = ApplyUniversalAllModifiers(damage);
+
+
+ damageDone.DamageDict.EnsureCapacity(damage.DamageDict.Count);
+
+ var dict = ent.Comp.Damage.DamageDict;
+ foreach (var (type, value) in damage.DamageDict)
+ {
+ // CollectionsMarshal my beloved.
+ if (!dict.TryGetValue(type, out var oldValue))
+ continue;
+
+ var newValue = FixedPoint2.Max(FixedPoint2.Zero, oldValue + value);
+ if (newValue == oldValue)
+ continue;
+
+ dict[type] = newValue;
+ damageDone.DamageDict[type] = newValue - oldValue;
+ }
+
+ if (!damageDone.Empty)
+ OnEntityDamageChanged((ent, ent.Comp), damageDone, interruptsDoAfters, origin);
+
+ return damageDone;
+ }
+
+ ///
+ /// Applies the two universal "All" modifiers, if set.
+ /// Individual damage source modifiers are set in their respective code.
+ ///
+ /// The damage to be changed.
+ public DamageSpecifier ApplyUniversalAllModifiers(DamageSpecifier damage)
+ {
+ // Checks for changes first since they're unlikely in normal play.
+ if (
+ MathHelper.CloseToPercent(UniversalAllDamageModifier, 1f) &&
+ MathHelper.CloseToPercent(UniversalAllHealModifier, 1f)
+ )
+ return damage;
+
+ foreach (var (key, value) in damage.DamageDict)
+ {
+ if (value == 0)
+ continue;
+
+ if (value > 0)
+ {
+ damage.DamageDict[key] *= UniversalAllDamageModifier;
+
+ continue;
+ }
+
+ if (value < 0)
+ damage.DamageDict[key] *= UniversalAllHealModifier;
+ }
+
+ return damage;
+ }
+
+ public void ClearAllDamage(Entity ent)
+ {
+ SetAllDamage(ent, FixedPoint2.Zero);
+ }
+
+ ///
+ /// Sets all damage types supported by a to the specified value.
+ ///
+ ///
+ /// Does nothing If the given damage value is negative.
+ ///
+ public void SetAllDamage(Entity ent, FixedPoint2 newValue)
+ {
+ if (!_damageableQuery.Resolve(ent, ref ent.Comp, false))
+ return;
+
+ if (newValue < 0)
+ return;
+
+ foreach (var type in ent.Comp.Damage.DamageDict.Keys)
+ {
+ ent.Comp.Damage.DamageDict[type] = newValue;
+ }
+
+ // Setting damage does not count as 'dealing' damage, even if it is set to a larger value, so we pass an
+ // empty damage delta.
+ OnEntityDamageChanged((ent, ent.Comp), new DamageSpecifier());
+ }
+
+ ///
+ /// Set's the damage modifier set prototype for this entity.
+ ///
+ /// The entity we're setting the modifier set of.
+ /// The prototype we're setting.
+ public void SetDamageModifierSetId(Entity ent, ProtoId? damageModifierSetId)
+ {
+ if (!_damageableQuery.Resolve(ent, ref ent.Comp, false))
+ return;
+
+ ent.Comp.DamageModifierSetId = damageModifierSetId;
+
+ Dirty(ent);
+ }
+}
diff --git a/Content.Shared/Damage/Systems/DamageableSystem.BenchmarkHelpers.cs b/Content.Shared/Damage/Systems/DamageableSystem.BenchmarkHelpers.cs
new file mode 100644
index 0000000000..5ca7fe1992
--- /dev/null
+++ b/Content.Shared/Damage/Systems/DamageableSystem.BenchmarkHelpers.cs
@@ -0,0 +1,17 @@
+using Content.Shared.Damage.Components;
+
+namespace Content.Shared.Damage.Systems;
+
+public sealed partial class DamageableSystem
+{
+ ///
+ /// Applies damage to all entities to see how expensive it is to deal damage.
+ ///
+ public void ApplyDamageToAllEntities(List> damageables, DamageSpecifier damage)
+ {
+ foreach (var (uid, damageable) in damageables)
+ {
+ TryChangeDamage((uid, damageable), damage);
+ }
+ }
+}
diff --git a/Content.Shared/Damage/Systems/DamageableSystem.Events.cs b/Content.Shared/Damage/Systems/DamageableSystem.Events.cs
new file mode 100644
index 0000000000..3e985ba204
--- /dev/null
+++ b/Content.Shared/Damage/Systems/DamageableSystem.Events.cs
@@ -0,0 +1,290 @@
+using Content.Shared.CCVar;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.FixedPoint;
+using Content.Shared.Inventory;
+using Content.Shared.Radiation.Events;
+using Content.Shared.Rejuvenate;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Damage.Systems;
+
+public sealed partial class DamageableSystem
+{
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(DamageableInit);
+ SubscribeLocalEvent(DamageableHandleState);
+ SubscribeLocalEvent(DamageableGetState);
+ SubscribeLocalEvent(OnIrradiated);
+ SubscribeLocalEvent(OnRejuvenate);
+
+ _appearanceQuery = GetEntityQuery();
+ _damageableQuery = GetEntityQuery();
+
+ // Damage modifier CVars are updated and stored here to be queried in other systems.
+ // Note that certain modifiers requires reloading the guidebook.
+ Subs.CVar(
+ _config,
+ CCVars.PlaytestAllDamageModifier,
+ value =>
+ {
+ UniversalAllDamageModifier = value;
+ _chemistryGuideData.ReloadAllReagentPrototypes();
+ _explosion.ReloadMap();
+ },
+ true
+ );
+ Subs.CVar(
+ _config,
+ CCVars.PlaytestAllHealModifier,
+ value =>
+ {
+ UniversalAllHealModifier = value;
+ _chemistryGuideData.ReloadAllReagentPrototypes();
+ },
+ true
+ );
+ Subs.CVar(
+ _config,
+ CCVars.PlaytestProjectileDamageModifier,
+ value => UniversalProjectileDamageModifier = value,
+ true
+ );
+ Subs.CVar(
+ _config,
+ CCVars.PlaytestMeleeDamageModifier,
+ value => UniversalMeleeDamageModifier = value,
+ true
+ );
+ Subs.CVar(
+ _config,
+ CCVars.PlaytestProjectileDamageModifier,
+ value => UniversalProjectileDamageModifier = value,
+ true
+ );
+ Subs.CVar(
+ _config,
+ CCVars.PlaytestHitscanDamageModifier,
+ value => UniversalHitscanDamageModifier = value,
+ true
+ );
+ Subs.CVar(
+ _config,
+ CCVars.PlaytestReagentDamageModifier,
+ value =>
+ {
+ UniversalReagentDamageModifier = value;
+ _chemistryGuideData.ReloadAllReagentPrototypes();
+ },
+ true
+ );
+ Subs.CVar(
+ _config,
+ CCVars.PlaytestReagentHealModifier,
+ value =>
+ {
+ UniversalReagentHealModifier = value;
+ _chemistryGuideData.ReloadAllReagentPrototypes();
+ },
+ true
+ );
+ Subs.CVar(
+ _config,
+ CCVars.PlaytestExplosionDamageModifier,
+ value =>
+ {
+ UniversalExplosionDamageModifier = value;
+ _explosion.ReloadMap();
+ },
+ true
+ );
+ Subs.CVar(
+ _config,
+ CCVars.PlaytestThrownDamageModifier,
+ value => UniversalThrownDamageModifier = value,
+ true
+ );
+ Subs.CVar(
+ _config,
+ CCVars.PlaytestTopicalsHealModifier,
+ value => UniversalTopicalsHealModifier = value,
+ true
+ );
+ Subs.CVar(
+ _config,
+ CCVars.PlaytestMobDamageModifier,
+ value => UniversalMobDamageModifier = value,
+ true
+ );
+ }
+
+ ///
+ /// Initialize a damageable component
+ ///
+ private void DamageableInit(Entity ent, ref ComponentInit _)
+ {
+ if (
+ ent.Comp.DamageContainerID is null ||
+ !_prototypeManager.Resolve(ent.Comp.DamageContainerID, out var damageContainerPrototype)
+ )
+ {
+ // No DamageContainerPrototype was given. So we will allow the container to support all damage types
+ foreach (var type in _prototypeManager.EnumeratePrototypes())
+ {
+ ent.Comp.Damage.DamageDict.TryAdd(type.ID, FixedPoint2.Zero);
+ }
+ }
+ else
+ {
+ // Initialize damage dictionary, using the types and groups from the damage
+ // container prototype
+ foreach (var type in damageContainerPrototype.SupportedTypes)
+ {
+ ent.Comp.Damage.DamageDict.TryAdd(type, FixedPoint2.Zero);
+ }
+
+ foreach (var groupId in damageContainerPrototype.SupportedGroups)
+ {
+ var group = _prototypeManager.Index(groupId);
+ foreach (var type in group.DamageTypes)
+ {
+ ent.Comp.Damage.DamageDict.TryAdd(type, FixedPoint2.Zero);
+ }
+ }
+ }
+
+ ent.Comp.Damage.GetDamagePerGroup(_prototypeManager, ent.Comp.DamagePerGroup);
+ ent.Comp.TotalDamage = ent.Comp.Damage.GetTotal();
+ }
+
+ private void OnIrradiated(Entity ent, ref OnIrradiatedEvent args)
+ {
+ var damageValue = FixedPoint2.New(args.TotalRads);
+
+ // Radiation should really just be a damage group instead of a list of types.
+ DamageSpecifier damage = new();
+ foreach (var typeId in ent.Comp.RadiationDamageTypeIDs)
+ {
+ damage.DamageDict.Add(typeId, damageValue);
+ }
+
+ ChangeDamage(ent.Owner, damage, interruptsDoAfters: false, origin: args.Origin);
+ }
+
+ private void OnRejuvenate(Entity ent, ref RejuvenateEvent args)
+ {
+ // Do this so that the state changes when we set the damage
+ _mobThreshold.SetAllowRevives(ent, true);
+ ClearAllDamage(ent.AsNullable());
+ _mobThreshold.SetAllowRevives(ent, false);
+ }
+
+ private void DamageableHandleState(Entity