diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 14f591ec87..defe08ef23 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -28,6 +28,7 @@ /Content.*/Stunnable/ @Princess-Cheeseballs /Content.*/Nutrition/ @Princess-Cheeseballs +/Content.*/EntityEffects @Princess-Cheeseballs @sowelipililimute # SKREEEE /Content.*.Database/ @PJB3005 @DrSmugleaf diff --git a/BuildChecker/git_helper.py b/BuildChecker/git_helper.py index 66d2463669..bd6603bd34 100644 --- a/BuildChecker/git_helper.py +++ b/BuildChecker/git_helper.py @@ -13,7 +13,7 @@ from typing import List SOLUTION_PATH = Path("..") / "SpaceStation14.sln" # If this doesn't match the saved version we overwrite them all. -CURRENT_HOOKS_VERSION = "3" +CURRENT_HOOKS_VERSION = "4" QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet" diff --git a/BuildChecker/hooks/post-checkout b/BuildChecker/hooks/post-checkout index ee4309de1d..1b91112ff0 100755 --- a/BuildChecker/hooks/post-checkout +++ b/BuildChecker/hooks/post-checkout @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash gitroot=$(git rev-parse --show-toplevel) diff --git a/BuildChecker/hooks/post-merge b/BuildChecker/hooks/post-merge index 5cf3d91120..864a9cff50 100755 --- a/BuildChecker/hooks/post-merge +++ b/BuildChecker/hooks/post-merge @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Just call post-checkout since it does the same thing. gitroot=$(git rev-parse --git-path hooks) 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 209c58c950..2b8ebf53b7 100644 --- a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs +++ b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs @@ -49,12 +49,12 @@ namespace Content.Client.Access.UI icons.Sort((x, y) => string.Compare(x.LocalizedJobName, y.LocalizedJobName, StringComparison.CurrentCulture)); foreach (var jobIcon in icons) { - String styleBase = StyleBase.ButtonOpenBoth; + String styleBase = StyleClass.ButtonOpenBoth; var modulo = i % JobIconColumnCount; if (modulo == 0) - styleBase = StyleBase.ButtonOpenRight; + styleBase = StyleClass.ButtonOpenRight; else if (modulo == JobIconColumnCount - 1) - styleBase = StyleBase.ButtonOpenLeft; + styleBase = StyleClass.ButtonOpenLeft; // Generate buttons var jobIconButton = new Button diff --git a/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs index 7af78d9e5f..2a85530c48 100644 --- a/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs +++ b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs @@ -119,11 +119,11 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer if (_groupedAccessLevels.Count > 1) { if (AccessGroupList.ChildCount == 0) - accessGroupButton.AddStyleClass(StyleBase.ButtonOpenLeft); + accessGroupButton.AddStyleClass(StyleClass.ButtonOpenLeft); else if (_groupedAccessLevels.Count > 1 && AccessGroupList.ChildCount == (_groupedAccessLevels.Count - 1)) - accessGroupButton.AddStyleClass(StyleBase.ButtonOpenRight); + accessGroupButton.AddStyleClass(StyleClass.ButtonOpenRight); else - accessGroupButton.AddStyleClass(StyleBase.ButtonOpenBoth); + accessGroupButton.AddStyleClass(StyleClass.ButtonOpenBoth); } accessGroupButton.Pressed = _accessGroupTabIndex == orderedAccessGroups.IndexOf(accessGroup); diff --git a/Content.Client/Actions/UI/ActionAlertTooltip.cs b/Content.Client/Actions/UI/ActionAlertTooltip.cs index 664a67b406..065c16d61b 100644 --- a/Content.Client/Actions/UI/ActionAlertTooltip.cs +++ b/Content.Client/Actions/UI/ActionAlertTooltip.cs @@ -23,9 +23,10 @@ namespace Content.Client.Actions.UI public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null) { + Stylesheet = IoCManager.Resolve().SheetSystem; _gameTiming = IoCManager.Resolve(); - SetOnlyStyleClass(StyleNano.StyleClassTooltipPanel); + SetOnlyStyleClass(StyleClass.TooltipPanel); BoxContainer vbox; AddChild(vbox = new BoxContainer @@ -36,7 +37,7 @@ namespace Content.Client.Actions.UI var nameLabel = new RichTextLabel { MaxWidth = TooltipTextMaxWidth, - StyleClasses = {StyleNano.StyleClassTooltipActionTitle} + StyleClasses = { StyleClass.TooltipTitle } }; nameLabel.SetMessage(name); vbox.AddChild(nameLabel); @@ -46,7 +47,7 @@ namespace Content.Client.Actions.UI var description = new RichTextLabel { MaxWidth = TooltipTextMaxWidth, - StyleClasses = {StyleNano.StyleClassTooltipActionDescription} + StyleClasses = { StyleClass.TooltipDesc } }; description.SetMessage(desc); vbox.AddChild(description); @@ -55,7 +56,7 @@ namespace Content.Client.Actions.UI vbox.AddChild(_cooldownLabel = new RichTextLabel { MaxWidth = TooltipTextMaxWidth, - StyleClasses = {StyleNano.StyleClassTooltipActionCooldown}, + StyleClasses = { StyleClass.TooltipDesc }, Visible = false }); @@ -64,7 +65,7 @@ namespace Content.Client.Actions.UI var requiresLabel = new RichTextLabel { MaxWidth = TooltipTextMaxWidth, - StyleClasses = {StyleNano.StyleClassTooltipActionRequirements} + StyleClasses = { StyleClass.TooltipDesc } }; if (!FormattedMessage.TryFromMarkup("[color=#635c5c]" + requires + "[/color]", out var markup)) diff --git a/Content.Client/Administration/UI/AdminRemarks/AdminMessagePopupWindow.xaml b/Content.Client/Administration/UI/AdminRemarks/AdminMessagePopupWindow.xaml index cc5207bb3a..bfe6b31c28 100644 --- a/Content.Client/Administration/UI/AdminRemarks/AdminMessagePopupWindow.xaml +++ b/Content.Client/Administration/UI/AdminRemarks/AdminMessagePopupWindow.xaml @@ -7,7 +7,7 @@ - + diff --git a/Content.Client/Administration/UI/AdminRemarks/AdminMessagePopupWindow.xaml.cs b/Content.Client/Administration/UI/AdminRemarks/AdminMessagePopupWindow.xaml.cs index 148cbf4e18..7870dae7b2 100644 --- a/Content.Client/Administration/UI/AdminRemarks/AdminMessagePopupWindow.xaml.cs +++ b/Content.Client/Administration/UI/AdminRemarks/AdminMessagePopupWindow.xaml.cs @@ -12,17 +12,19 @@ namespace Content.Client.Administration.UI.AdminRemarks; [GenerateTypedNameReferences] public sealed partial class AdminMessagePopupWindow : Control { + [Dependency] private readonly IStylesheetManager _styleMan = default!; + private float _timer = float.MaxValue; public event Action? OnDismissPressed; - public event Action? OnAcceptPressed; public AdminMessagePopupWindow() { RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); - Stylesheet = IoCManager.Resolve().SheetSpace; + Stylesheet = _styleMan.SheetSystem; AcceptButton.OnPressed += OnAcceptButtonPressed; DismissButton.OnPressed += OnDismissButtonPressed; @@ -49,7 +51,8 @@ public sealed partial class AdminMessagePopupWindow : Control MessageContainer.AddChild(new AdminMessagePopupMessage(message)); } - Description.SetMessage(FormattedMessage.FromMarkupOrThrow(Loc.GetString("admin-notes-message-desc", ("count", state.Messages.Length)))); + Description.SetMessage( + FormattedMessage.FromMarkup(Loc.GetString("admin-notes-message-desc", ("count", state.Messages.Length)))); } private void OnDismissButtonPressed(BaseButton.ButtonEventArgs obj) 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/Administration/UI/Bwoink/BwoinkControl.xaml b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml index 2c27fdd2ce..fdaa838201 100644 --- a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml +++ b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml @@ -18,7 +18,7 @@ diff --git a/Content.Client/GPS/UI/HandheldGpsStatusControl.cs b/Content.Client/GPS/UI/HandheldGpsStatusControl.cs index 57645e386e..2feca1f176 100644 --- a/Content.Client/GPS/UI/HandheldGpsStatusControl.cs +++ b/Content.Client/GPS/UI/HandheldGpsStatusControl.cs @@ -21,7 +21,7 @@ public sealed class HandheldGpsStatusControl : Control _parent = parent; _entMan = IoCManager.Resolve(); _transform = _entMan.System(); - _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } }; + _label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } }; AddChild(_label); UpdateGpsDetails(); } diff --git a/Content.Client/GameTicking/Managers/TitleWindowManager.cs b/Content.Client/GameTicking/Managers/TitleWindowManager.cs index 18ce16f634..bc33e78411 100644 --- a/Content.Client/GameTicking/Managers/TitleWindowManager.cs +++ b/Content.Client/GameTicking/Managers/TitleWindowManager.cs @@ -15,48 +15,29 @@ public sealed class TitleWindowManager public void Initialize() { - _cfg.OnValueChanged(CVars.GameHostName, OnHostnameChange, true); - _cfg.OnValueChanged(CCVars.GameHostnameInTitlebar, OnHostnameTitleChange, true); + _cfg.OnValueChanged(CVars.GameHostName, _ => OnHostnameChange(), true); + _cfg.OnValueChanged(CCVars.GameHostnameInTitlebar, _ => OnHostnameChange(), true); - _client.RunLevelChanged += OnRunLevelChangedChange; + _client.RunLevelChanged += (_, _) => OnHostnameChange(); } - public void Shutdown() - { - _cfg.UnsubValueChanged(CVars.GameHostName, OnHostnameChange); - _cfg.UnsubValueChanged(CCVars.GameHostnameInTitlebar, OnHostnameTitleChange); - } - - private void OnHostnameChange(string hostname) + private void OnHostnameChange() { var defaultWindowTitle = _gameController.GameTitle(); - // Since the game assumes the server name is MyServer and that GameHostnameInTitlebar CCVar is true by default - // Lets just... not show anything. This also is used to revert back to just the game title on disconnect. - if (_client.RunLevel == ClientRunLevel.Initialize) + // When the client starts connecting, it will be using either the default hostname, or whatever hostname + // is set in its config file (aka the last server they connected to) until it receives the latest cvars. + // If they are not connected then we will not show anything other than the usual window title. + if (_client.RunLevel != ClientRunLevel.InGame) { _clyde.SetWindowTitle(defaultWindowTitle); return; } - if (_cfg.GetCVar(CCVars.GameHostnameInTitlebar)) - // If you really dislike the dash I guess change it here - _clyde.SetWindowTitle(hostname + " - " + defaultWindowTitle); - else - _clyde.SetWindowTitle(defaultWindowTitle); - } - - // Clients by default assume game.hostname_in_titlebar is true - // but we need to clear it as soon as we join and actually receive the servers preference on this. - // This will ensure we rerun OnHostnameChange and set the correct title bar name. - private void OnHostnameTitleChange(bool colonthree) - { - OnHostnameChange(_cfg.GetCVar(CVars.GameHostName)); - } - - // This is just used we can rerun the hostname change function when we disconnect to revert back to just the games title. - private void OnRunLevelChangedChange(object? sender, RunLevelChangedEventArgs runLevelChangedEventArgs) - { - OnHostnameChange(_cfg.GetCVar(CVars.GameHostName)); + _clyde.SetWindowTitle( + _cfg.GetCVar(CCVars.GameHostnameInTitlebar) + ? _cfg.GetCVar(CVars.GameHostName) + " - " + defaultWindowTitle + : defaultWindowTitle); } + // You thought I would remove the :3 from this code? You were wrong. } diff --git a/Content.Client/Gateway/UI/GatewayWindow.xaml.cs b/Content.Client/Gateway/UI/GatewayWindow.xaml.cs index 9fb7c339d3..4e4076d98c 100644 --- a/Content.Client/Gateway/UI/GatewayWindow.xaml.cs +++ b/Content.Client/Gateway/UI/GatewayWindow.xaml.cs @@ -159,7 +159,7 @@ public sealed partial class GatewayWindow : FancyWindow, if (Pressable()) { - openButton.AddStyleClass(StyleBase.ButtonCaution); + openButton.AddStyleClass(StyleClass.Negative); } var buttonContainer = new BoxContainer() diff --git a/Content.Client/Ghost/UI/ReturnToBodyMenu.cs b/Content.Client/Ghost/UI/ReturnToBodyMenu.cs index 69f04993c9..384ffb6be6 100644 --- a/Content.Client/Ghost/UI/ReturnToBodyMenu.cs +++ b/Content.Client/Ghost/UI/ReturnToBodyMenu.cs @@ -15,7 +15,7 @@ public sealed class ReturnToBodyMenu : DefaultWindow { Title = Loc.GetString("ghost-return-to-body-title"); - Contents.AddChild(new BoxContainer + ContentsContainer.AddChild(new BoxContainer { Orientation = LayoutOrientation.Vertical, Children = diff --git a/Content.Client/Guidebook/Controls/GuideLawsetEmbed.xaml b/Content.Client/Guidebook/Controls/GuideLawsetEmbed.xaml new file mode 100644 index 0000000000..0af8ee3ec0 --- /dev/null +++ b/Content.Client/Guidebook/Controls/GuideLawsetEmbed.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/Content.Client/Guidebook/Controls/GuideLawsetEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideLawsetEmbed.xaml.cs new file mode 100644 index 0000000000..86f7dce9e3 --- /dev/null +++ b/Content.Client/Guidebook/Controls/GuideLawsetEmbed.xaml.cs @@ -0,0 +1,96 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Client.Guidebook.Richtext; +using Content.Client.Message; +using Content.Client.UserInterface.ControlExtensions; +using JetBrains.Annotations; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; + +using Content.Shared.Silicons.Laws; +using Robust.Shared.Utility; + +namespace Content.Client.Guidebook.Controls; + +/// +/// Control for embedding an AI Lawset in a guidebook +/// +[UsedImplicitly, GenerateTypedNameReferences] +public sealed partial class GuideLawsetEmbed : Control, IDocumentTag, ISearchableControl, IPrototypeRepresentationControl +{ + [Dependency] private readonly IPrototypeManager _prototype = default!; + + private ISawmill _logging = default!; + + public IPrototype? RepresentedPrototype { get; private set; } + + public GuideLawsetEmbed() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + MouseFilter = MouseFilterMode.Stop; + } + + public GuideLawsetEmbed(SiliconLawsetPrototype lawset) : this() + { + GenerateControl(lawset); + } + + private void GenerateControl(SiliconLawsetPrototype lawset) + { + RepresentedPrototype = lawset; + + var lawsetNameString = lawset.Name == null ? lawset.ID : Loc.GetString(lawset.Name); + LawsetName.SetMarkup($"[bold]{FormattedMessage.EscapeText(lawsetNameString)}[/bold]"); + + var i = 1; + foreach (var lawID in lawset.Laws) + { + var lawPrototype = _prototype.Index(lawID); + var locLawString = Loc.GetString(lawPrototype.LawString); + + RichTextLabel lawN = new() + { + Margin = new(0, 5, 0, 1) + }; + var locLawStatement = Loc.GetString("laws-number-wrapper", ("lawnumber", i), ("lawstring", locLawString)); + lawN.SetMarkup(locLawStatement); + LawsetContainer.AddChild(lawN); + + i++; + } + } + + public bool TryParseTag(Dictionary args, [NotNullWhen(true)] out Control? control) + { + control = null; + if (!args.TryGetValue("Lawset", out var id)) + { + _logging.Error("Lawset embed tag is missing lawset prototype argument"); + return false; + } + + if (!_prototype.TryIndex(id, out var lawset)) + { + _logging.Error($"Specified SiliconLawsetPrototype \"{id}\" is not a valid Lawset prototype"); + return false; + } + + GenerateControl(lawset); + + control = this; + return true; + } + + public bool CheckMatchesSearch(string query) + { + return this.ChildrenContainText(query); + } + + public void SetHiddenState(bool state, string query) + { + Visible = CheckMatchesSearch(query) ? state : !state; + } +} diff --git a/Content.Client/Guidebook/Controls/GuideLawsetListEmbed.xaml b/Content.Client/Guidebook/Controls/GuideLawsetListEmbed.xaml new file mode 100644 index 0000000000..59434cff0b --- /dev/null +++ b/Content.Client/Guidebook/Controls/GuideLawsetListEmbed.xaml @@ -0,0 +1,5 @@ + + + + diff --git a/Content.Client/Guidebook/Controls/GuideLawsetListEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideLawsetListEmbed.xaml.cs new file mode 100644 index 0000000000..108f066c39 --- /dev/null +++ b/Content.Client/Guidebook/Controls/GuideLawsetListEmbed.xaml.cs @@ -0,0 +1,40 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Client.Guidebook.Richtext; +using JetBrains.Annotations; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; + +using Content.Shared.Silicons.Laws; + +namespace Content.Client.Guidebook.Controls; + +/// +/// Control for iterating and embedding every SiliconLawsetPrototype into the guidebook. +/// +[UsedImplicitly, GenerateTypedNameReferences] +public sealed partial class GuideLawsetListEmbed : Control, IDocumentTag +{ + [Dependency] private readonly IPrototypeManager _prototype = default!; + + public GuideLawsetListEmbed() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + MouseFilter = MouseFilterMode.Stop; + } + + public bool TryParseTag(Dictionary args, [NotNullWhen(true)] out Control? control) + { + foreach (var lawset in _prototype.EnumeratePrototypes().OrderBy(x => x.ID)) + { + GuideLawsetEmbed embed = new(lawset); + GroupContainer.AddChild(embed); + } + + control = this; + return true; + } +} diff --git a/Content.Client/Guidebook/DocumentParsingManager.cs b/Content.Client/Guidebook/DocumentParsingManager.cs index 37ec1ac2de..1727ae0de0 100644 --- a/Content.Client/Guidebook/DocumentParsingManager.cs +++ b/Content.Client/Guidebook/DocumentParsingManager.cs @@ -7,6 +7,7 @@ using Robust.Client.UserInterface; using Robust.Shared.ContentPack; using Robust.Shared.Prototypes; using Robust.Shared.Reflection; +using Robust.Shared.Sandboxing; using Robust.Shared.Utility; using static Pidgin.Parser; @@ -20,7 +21,7 @@ public sealed partial class DocumentParsingManager [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IReflectionManager _reflectionManager = default!; [Dependency] private readonly IResourceManager _resourceManager = default!; - [Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!; + [Dependency] private readonly ISandboxHelper _sandboxHelper = default!; private readonly Dictionary> _tagControlParsers = new(); private Parser _controlParser = default!; @@ -42,7 +43,7 @@ public sealed partial class DocumentParsingManager foreach (var typ in _reflectionManager.GetAllChildren()) { - _tagControlParsers.Add(typ.Name, CreateTagControlParser(typ.Name, typ, _dynamicTypeFactory)); + _tagControlParsers.Add(typ.Name, CreateTagControlParser(typ.Name, typ, _sandboxHelper)); } ControlParser = whitespaceAndCommentParser.Then(_controlParser.Many()); @@ -86,14 +87,14 @@ public sealed partial class DocumentParsingManager return true; } - private Parser CreateTagControlParser(string tagId, Type tagType, IDynamicTypeFactory typeFactory) + private Parser CreateTagControlParser(string tagId, Type tagType, ISandboxHelper sandbox) { return Map( (args, controls) => { try { - var tag = (IDocumentTag) typeFactory.CreateInstance(tagType); + var tag = (IDocumentTag) sandbox.CreateInstance(tagType); if (!tag.TryParseTag(args, out var control)) { _sawmill.Error($"Failed to parse {tagId} args"); diff --git a/Content.Client/Guidebook/Richtext/Table.cs b/Content.Client/Guidebook/Richtext/Table.cs index 82b884aa96..b6923c3698 100644 --- a/Content.Client/Guidebook/Richtext/Table.cs +++ b/Content.Client/Guidebook/Richtext/Table.cs @@ -8,11 +8,6 @@ namespace Content.Client.Guidebook.Richtext; [UsedImplicitly] public sealed class Table : TableContainer, IDocumentTag { - [Dependency] private readonly ILogManager _logManager = default!; - - private ISawmill Sawmill => _sawmill ??= _logManager.GetSawmill("table"); - private ISawmill? _sawmill; - public bool TryParseTag(Dictionary args, [NotNullWhen(true)] out Control? control) { HorizontalExpand = true; @@ -20,7 +15,7 @@ public sealed class Table : TableContainer, IDocumentTag if (!args.TryGetValue("Columns", out var columns) || !int.TryParse(columns, out var columnsCount)) { - Sawmill.Error("Guidebook tag \"Table\" does not specify required property \"Columns.\""); + Logger.Error("Guidebook tag \"Table\" does not specify required property \"Columns.\""); control = null; return false; } diff --git a/Content.Client/Guidebook/Richtext/TextLinkTag.cs b/Content.Client/Guidebook/Richtext/TextLinkTag.cs index 5c0098d5b5..a551b18473 100644 --- a/Content.Client/Guidebook/Richtext/TextLinkTag.cs +++ b/Content.Client/Guidebook/Richtext/TextLinkTag.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; @@ -12,11 +12,6 @@ namespace Content.Client.Guidebook.RichText; [UsedImplicitly] public sealed class TextLinkTag : IMarkupTagHandler { - [Dependency] private readonly ILogManager _logManager = default!; - - private ISawmill Sawmill => _sawmill ??= _logManager.GetSawmill(Name); - private ISawmill? _sawmill; - public static Color LinkColor => Color.CornflowerBlue; public string Name => "textlink"; @@ -58,7 +53,7 @@ public sealed class TextLinkTag : IMarkupTagHandler if (control.TryGetParentHandler(out var handler)) handler.HandleClick(link); else - Sawmill.Warning("Warning! No valid ILinkClickHandler found."); + Logger.Warning("Warning! No valid ILinkClickHandler found."); } } diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs index 47b03351cb..4d98f227f2 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs @@ -1,29 +1,23 @@ using System.Linq; using System.Numerics; -using Content.Client.Message; using Content.Shared.Atmos; using Content.Client.UserInterface.Controls; using Content.Shared._Offbrand.Wounds; // Offbrand -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/Implants/UI/ChameleonControllerMenu.xaml.cs b/Content.Client/Implants/UI/ChameleonControllerMenu.xaml.cs index c12ddb9319..6f66990b4e 100644 --- a/Content.Client/Implants/UI/ChameleonControllerMenu.xaml.cs +++ b/Content.Client/Implants/UI/ChameleonControllerMenu.xaml.cs @@ -116,7 +116,7 @@ public sealed partial class ChameleonControllerMenu : FancyWindow var button = new Button { HorizontalExpand = true, - StyleClasses = {StyleBase.ButtonSquare}, + StyleClasses = {StyleClass.ButtonSquare}, ToolTip = Loc.GetString(name), Text = Loc.GetString(name), Margin = new Thickness(0, 0, 15, 0), diff --git a/Content.Client/Implants/UI/ImplanterStatusControl.cs b/Content.Client/Implants/UI/ImplanterStatusControl.cs index 24445eeecf..43579ae6be 100644 --- a/Content.Client/Implants/UI/ImplanterStatusControl.cs +++ b/Content.Client/Implants/UI/ImplanterStatusControl.cs @@ -19,7 +19,7 @@ public sealed class ImplanterStatusControl : Control { IoCManager.InjectDependencies(this); _parent = parent; - _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } }; + _label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } }; _label.MaxWidth = 350; AddChild(new ClipControl { Children = { _label } }); diff --git a/Content.Client/Info/RulesAndInfoWindow.cs b/Content.Client/Info/RulesAndInfoWindow.cs index b9131dcb3c..4f483cf26c 100644 --- a/Content.Client/Info/RulesAndInfoWindow.cs +++ b/Content.Client/Info/RulesAndInfoWindow.cs @@ -36,7 +36,7 @@ namespace Content.Client.Info PopulateTutorial(tutorialList); - Contents.AddChild(rootContainer); + ContentsContainer.AddChild(rootContainer); SetSize = new Vector2(650, 650); } diff --git a/Content.Client/Info/RulesPopup.xaml b/Content.Client/Info/RulesPopup.xaml index 80ed415550..c94f061865 100644 --- a/Content.Client/Info/RulesPopup.xaml +++ b/Content.Client/Info/RulesPopup.xaml @@ -16,7 +16,7 @@ Text="{Loc 'ui-rules-accept'}" Disabled="True" /> - + diff --git a/Content.Client/Voting/UI/VotePopup.xaml.cs b/Content.Client/Voting/UI/VotePopup.xaml.cs index 2a9a6b31f8..b9ff4dd7bd 100644 --- a/Content.Client/Voting/UI/VotePopup.xaml.cs +++ b/Content.Client/Voting/UI/VotePopup.xaml.cs @@ -27,7 +27,7 @@ namespace Content.Client.Voting.UI IoCManager.InjectDependencies(this); RobustXamlLoader.Load(this); - Stylesheet = IoCManager.Resolve().SheetSpace; + Stylesheet = IoCManager.Resolve().SheetSystem; if (_vote.TargetEntity != null && _vote.TargetEntity != 0) { 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 84eaa9af1b..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 + }; } } @@ -137,7 +142,7 @@ public sealed partial class GunSystem }), (_ammoCount = new Label { - StyleClasses = { StyleNano.StyleClassItemStatus }, + StyleClasses = { StyleClass.ItemStatus }, HorizontalAlignment = HAlignment.Right, VerticalAlignment = VAlignment.Bottom }), @@ -189,7 +194,7 @@ public sealed partial class GunSystem (_noMagazineLabel = new Label { Text = "No Magazine!", - StyleClasses = {StyleNano.StyleClassItemStatus} + StyleClasses = {StyleClass.ItemStatus} }) } }, @@ -202,7 +207,7 @@ public sealed partial class GunSystem { (_ammoCount = new Label { - StyleClasses = {StyleNano.StyleClassItemStatus}, + StyleClasses = {StyleClass.ItemStatus}, HorizontalAlignment = HAlignment.Right, }), (_chamberedBullet = new TextureRect @@ -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.Client/Weapons/Ranged/Systems/GunSystem.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs index c27e81b5c7..adef067b60 100644 --- a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs @@ -5,6 +5,8 @@ using Content.Client.Items; using Content.Client.Weapons.Ranged.Components; using Content.Shared.Camera; using Content.Shared.CombatMode; +using Content.Shared.Damage; +using Content.Shared.Weapons.Hitscan.Components; using Content.Shared.Weapons.Ranged; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Events; @@ -16,6 +18,7 @@ using Robust.Client.Input; using Robust.Client.Player; using Robust.Client.State; using Robust.Shared.Animations; +using Robust.Shared.Audio; using Robust.Shared.Input; using Robust.Shared.Map; using Robust.Shared.Map.Components; @@ -234,6 +237,7 @@ public sealed partial class GunSystem : SharedGunSystem continue; } + // TODO: Clean this up in a gun refactor at some point - too much copy pasting switch (shootable) { case CartridgeAmmoComponent cartridge: @@ -266,7 +270,7 @@ public sealed partial class GunSystem : SharedGunSystem else RemoveShootable(ent.Value); break; - case HitscanPrototype: + case HitscanAmmoComponent: Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user); Recoil(user, direction, gun.CameraRecoilScalarModified); break; @@ -404,4 +408,7 @@ public sealed partial class GunSystem : SharedGunSystem _animPlayer.Stop(gunUid, uidPlayer, "muzzle-flash-light"); _animPlayer.Play((gunUid, uidPlayer), animTwo, "muzzle-flash-light"); } + + // TODO: Move RangedDamageSoundComponent to shared so this can be predicted. + public override void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound) {} } diff --git a/Content.Client/Wires/UI/WiresMenu.cs b/Content.Client/Wires/UI/WiresMenu.cs index 01c88bcd0c..7aca78215f 100644 --- a/Content.Client/Wires/UI/WiresMenu.cs +++ b/Content.Client/Wires/UI/WiresMenu.cs @@ -159,8 +159,8 @@ namespace Content.Client.Wires.UI { Text = Loc.GetString("wires-menu-name-label"), FontOverride = font, - FontColorOverride = StyleNano.NanoGold, VerticalAlignment = VAlignment.Center, + StyleClasses = { StyleClass.LabelKeyText }, }), (_serialLabel = new Label { 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/Administration/Logs/LogWindowTest.cs b/Content.IntegrationTests/Tests/Administration/Logs/LogWindowTest.cs new file mode 100644 index 0000000000..80236c7d90 --- /dev/null +++ b/Content.IntegrationTests/Tests/Administration/Logs/LogWindowTest.cs @@ -0,0 +1,60 @@ +using System.Linq; +using Content.Client.Administration.UI; +using Content.Client.Administration.UI.CustomControls; +using Content.Client.Administration.UI.Logs; +using Content.Client.UserInterface.Controls; +using Content.Client.UserInterface.Systems.MenuBar.Widgets; +using Content.IntegrationTests.Tests.Interaction; +using Content.Server.Administration.Commands; +using Content.Server.Administration.Logs; +using Content.Shared.Database; + +namespace Content.IntegrationTests.Tests.Administration.Logs; + +public sealed class LogWindowTest : InteractionTest +{ + protected override PoolSettings Settings => new() { Connected = true, Dirty = true, AdminLogsEnabled = true, DummyTicker = false }; + + [Test] + public async Task TestAdminLogsWindow() + { + // First, generate a new log + var log = Server.Resolve(); + var guid = Guid.NewGuid(); + await Server.WaitPost(() => log.Add(LogType.Unknown, $"{SPlayer} test log 1: {guid}")); + + // Click the admin button in the menu bar + await ClickWidgetControl(nameof(GameTopMenuBar.AdminButton)); + var adminWindow = GetWindow(); + + // Find and click the "open logs" button. + Assert.That(TryGetControlFromChildren(x => x.Command == OpenAdminLogsCommand.Cmd, adminWindow, out var btn)); + await ClickControl(btn!); + var logWindow = GetWindow(); + + // Find the log search field and refresh buttons + var search = logWindow.Logs.LogSearch; + var refresh = logWindow.Logs.RefreshButton; + var cont = logWindow.Logs.LogsContainer; + + // Search for the log we added earlier. + await Client.WaitPost(() => search.Text = guid.ToString()); + await ClickControl(refresh); + await RunTicks(5); + var searchResult = cont.Children.Where(x => x.Visible && x is AdminLogLabel).Cast().ToArray(); + Assert.That(searchResult.Length, Is.EqualTo(1)); + Assert.That(searchResult[0].Log.Message, Contains.Substring($" test log 1: {guid}")); + + // Add a new log + guid = Guid.NewGuid(); + await Server.WaitPost(() => log.Add(LogType.Unknown, $"{SPlayer} test log 2: {guid}")); + + // Update the search and refresh + await Client.WaitPost(() => search.Text = guid.ToString()); + await ClickControl(refresh); + await RunTicks(5); + searchResult = cont.Children.Where(x => x.Visible && x is AdminLogLabel).Cast().ToArray(); + Assert.That(searchResult.Length, Is.EqualTo(1)); + Assert.That(searchResult[0].Log.Message, Contains.Substring($" test log 2: {guid}")); + } +} diff --git a/Content.IntegrationTests/Tests/Administration/Notes/NotesControlTest.cs b/Content.IntegrationTests/Tests/Administration/Notes/NotesControlTest.cs new file mode 100644 index 0000000000..c8dd637c05 --- /dev/null +++ b/Content.IntegrationTests/Tests/Administration/Notes/NotesControlTest.cs @@ -0,0 +1,56 @@ +using System.Linq; +using Content.Client.Administration.UI.Bwoink; +using Content.Client.Administration.UI.CustomControls; +using Content.Client.Administration.UI.Notes; +using Content.Client.UserInterface.Controls; +using Content.Client.UserInterface.Systems.MenuBar.Widgets; +using Content.IntegrationTests.Tests.Interaction; +using Content.Shared.Database; +using Robust.Shared.Utility; + +namespace Content.IntegrationTests.Tests.Administration.Notes; + +/// +/// Test that the admin notes UI can be used to add a new note. +/// +public sealed class NotesControlTest : InteractionTest +{ + protected override PoolSettings Settings => new() {Connected = true, Dirty = true, AdminLogsEnabled = true, DummyTicker = false}; + + [Test] + public async Task TestNotesControl() + { + // Click the ahelp button in the menu bar + await ClickWidgetControl(nameof(GameTopMenuBar.AHelpButton)); + var bwoink = GetWindow(); + + // Damn, if only I had an excuse to use bwoink.Bwoink.BwoinkArea + var players = bwoink.Bwoink.ChannelSelector.PlayerListContainer; + + // Check that the player is in the menu, and make sure it is selected + var entry = players.Data.Cast().Single(x => x.Info.SessionId == ServerSession.UserId); + await Client.WaitPost(() => players.Select(entry)); + + // Open their notes + await ClickControl(bwoink.Bwoink.Notes); + var noteCtrl = GetWindow().Notes; + Assert.That(noteCtrl.Notes.ChildCount, Is.EqualTo(0)); + + // Add a new note + await ClickControl(noteCtrl.NewNoteButton); + var addNoteWindow = GetWindow(); + var msg = $"note: {Guid.NewGuid()}"; + await Client.WaitPost(() => addNoteWindow.NoteTextEdit.TextRope = new Rope.Leaf(msg)); + addNoteWindow.NoteSeverity = NoteSeverity.None; + + // Have to click submit twice for confirmation? + await ClickControl(addNoteWindow.SubmitButton); + await ClickControl(addNoteWindow.SubmitButton); + + // Check that the new note exists + await RunTicks(5); + Assert.That(noteCtrl.Notes.ChildCount, Is.EqualTo(1)); + var note = (AdminNotesLine)noteCtrl.Notes.Children[0]; + Assert.That(note.Note.Message, Is.EqualTo(msg)); + } +} diff --git a/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs b/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs index 9dda130847..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; @@ -93,6 +94,12 @@ public sealed class DeltaPressureTest private readonly ResPath _testMap = new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml"); + // TODO ATMOS TESTS + // - Check for directional windows (partial airtight ents) properly computing pressure differences + // - Check for multi-tick damage (window with n damage threshold should take n ticks to destroy) + // - Check that all maps do not explode into a million pieces on load due to dP + // - Ensure that all tests work for a map that has an origin at a non zero coordinate + /// /// Asserts that an entity with a DeltaPressureComponent with autoJoinProcessingList /// set to true is automatically added to the DeltaPressure processing list diff --git a/Content.IntegrationTests/Tests/CargoTest.cs b/Content.IntegrationTests/Tests/CargoTest.cs index aad87b711a..df85e61550 100644 --- a/Content.IntegrationTests/Tests/CargoTest.cs +++ b/Content.IntegrationTests/Tests/CargoTest.cs @@ -215,13 +215,10 @@ public sealed class CargoTest [TestPrototypes] private const string StackProto = @" -- type: entity - id: A - - type: stack id: StackProto name: stack-steel - spawn: A + spawn: StackEnt - type: entity id: StackEnt 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 e4ec7e907a..87ff00691a 100644 --- a/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs +++ b/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs @@ -1,7 +1,8 @@ -using Content.Server.Administration.Commands; -using Content.Server.Administration.Systems; +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 9b54f0bb66..640149272f 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; @@ -282,7 +284,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(); @@ -358,7 +360,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/CraftingTests.cs b/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs index 05e8197c8d..a7b96bdd2f 100644 --- a/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs +++ b/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs @@ -95,8 +95,8 @@ public sealed class CraftingTests : InteractionTest Assert.That(sys.IsEntityInContainer(shard), Is.True); Assert.That(sys.IsEntityInContainer(rods), Is.False); Assert.That(sys.IsEntityInContainer(wires), Is.False); - Assert.That(rodStack, Has.Count.EqualTo(8)); - Assert.That(wireStack, Has.Count.EqualTo(7)); + Assert.That(rodStack.Count, Is.EqualTo(8)); + Assert.That(wireStack.Count, Is.EqualTo(7)); await FindEntity(Spear, shouldSucceed: false); 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 0da07ad5a1..99f68b3fa3 100644 --- a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs +++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs @@ -1,10 +1,10 @@ -using Content.Server.Destructible.Thresholds.Triggers; 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; -using Robust.Shared.IoC; -using Robust.Shared.Map; using Robust.Shared.Prototypes; using static Content.IntegrationTests.Tests.Destructible.DestructibleTestPrototypes; @@ -91,7 +91,7 @@ namespace Content.IntegrationTests.Tests.Destructible Assert.That(threshold.Trigger, Is.InstanceOf()); }); - var trigger = (AndTrigger) threshold.Trigger; + var trigger = (AndTrigger)threshold.Trigger; Assert.Multiple(() => { @@ -132,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); @@ -162,7 +162,7 @@ namespace Content.IntegrationTests.Tests.Destructible Assert.That(threshold.Trigger, Is.InstanceOf()); }); - trigger = (AndTrigger) threshold.Trigger; + trigger = (AndTrigger)threshold.Trigger; Assert.Multiple(() => { @@ -176,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 ccd459668b..70baaea95a 100644 --- a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs +++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs @@ -1,6 +1,8 @@ -using Content.Server.Destructible.Thresholds.Triggers; 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; using static Content.IntegrationTests.Tests.Destructible.DestructibleTestPrototypes; @@ -86,7 +88,7 @@ namespace Content.IntegrationTests.Tests.Destructible Assert.That(threshold.Trigger, Is.InstanceOf()); }); - var trigger = (AndTrigger) threshold.Trigger; + var trigger = (AndTrigger)threshold.Trigger; Assert.Multiple(() => { @@ -154,7 +156,7 @@ namespace Content.IntegrationTests.Tests.Destructible Assert.That(threshold.Trigger, Is.InstanceOf()); }); - trigger = (AndTrigger) threshold.Trigger; + trigger = (AndTrigger)threshold.Trigger; Assert.Multiple(() => { diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs index 4e169e7dfa..df98294ee9 100644 --- a/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs +++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs @@ -1,8 +1,8 @@ using System.Linq; -using Content.Server.Destructible.Thresholds; 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; @@ -59,7 +59,7 @@ namespace Content.IntegrationTests.Tests.Destructible Assert.That(threshold.Behaviors, Has.Count.EqualTo(3)); }); - var spawnEntitiesBehavior = (SpawnEntitiesBehavior) threshold.Behaviors.Single(b => b is SpawnEntitiesBehavior); + var spawnEntitiesBehavior = (SpawnEntitiesBehavior)threshold.Behaviors.Single(b => b is SpawnEntitiesBehavior); Assert.Multiple(() => { diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs index af86e406ef..1736d3d7e3 100644 --- a/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs +++ b/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs @@ -2,12 +2,14 @@ using System.Linq; using Content.Server.Destructible; using Content.Server.Destructible.Thresholds; using Content.Server.Destructible.Thresholds.Behaviors; -using Content.Server.Destructible.Thresholds.Triggers; 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; using Robust.Shared.Audio.Systems; +using Content.Shared.Destructible; +using Content.Shared.Destructible.Thresholds.Triggers; using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; using static Content.IntegrationTests.Tests.Destructible.DestructibleTestPrototypes; @@ -99,9 +101,9 @@ namespace Content.IntegrationTests.Tests.Destructible // Check that it matches the YAML prototype Assert.That(threshold.Behaviors, Has.Count.EqualTo(3)); - var soundThreshold = (PlaySoundBehavior) threshold.Behaviors[0]; - var spawnThreshold = (SpawnEntitiesBehavior) threshold.Behaviors[1]; - var actsThreshold = (DoActsBehavior) threshold.Behaviors[2]; + var soundThreshold = (PlaySoundBehavior)threshold.Behaviors[0]; + var spawnThreshold = (SpawnEntitiesBehavior)threshold.Behaviors[1]; + var actsThreshold = (DoActsBehavior)threshold.Behaviors[2]; Assert.Multiple(() => { @@ -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); @@ -164,9 +166,9 @@ namespace Content.IntegrationTests.Tests.Destructible // Check that it matches the YAML prototype Assert.That(threshold.Behaviors, Has.Count.EqualTo(3)); - soundThreshold = (PlaySoundBehavior) threshold.Behaviors[0]; - spawnThreshold = (SpawnEntitiesBehavior) threshold.Behaviors[1]; - actsThreshold = (DoActsBehavior) threshold.Behaviors[2]; + soundThreshold = (PlaySoundBehavior)threshold.Behaviors[0]; + spawnThreshold = (SpawnEntitiesBehavior)threshold.Behaviors[1]; + actsThreshold = (DoActsBehavior)threshold.Behaviors[2]; // Check that it matches the YAML prototype Assert.Multiple(() => @@ -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); @@ -201,11 +203,11 @@ namespace Content.IntegrationTests.Tests.Destructible // Verify the first one, should be the lowest one (20) msg = sTestThresholdListenerSystem.ThresholdsReached[0]; - var trigger = (DamageTrigger) msg.Threshold.Trigger; + var trigger = (DamageTrigger)msg.Threshold.Trigger; Assert.Multiple(() => { Assert.That(trigger, Is.Not.Null); - Assert.That(trigger.Damage, Is.EqualTo(20)); + Assert.That(trigger.Damage, Is.EqualTo(FixedPoint2.New(20))); }); threshold = msg.Threshold; @@ -215,20 +217,20 @@ namespace Content.IntegrationTests.Tests.Destructible // Verify the second one, should be the highest one (50) msg = sTestThresholdListenerSystem.ThresholdsReached[1]; - trigger = (DamageTrigger) msg.Threshold.Trigger; + trigger = (DamageTrigger)msg.Threshold.Trigger; Assert.Multiple(() => { Assert.That(trigger, Is.Not.Null); - Assert.That(trigger.Damage, Is.EqualTo(50)); + Assert.That(trigger.Damage, Is.EqualTo(FixedPoint2.New(50))); }); threshold = msg.Threshold; Assert.That(threshold.Behaviors, Has.Count.EqualTo(3)); - soundThreshold = (PlaySoundBehavior) threshold.Behaviors[0]; - spawnThreshold = (SpawnEntitiesBehavior) threshold.Behaviors[1]; - actsThreshold = (DoActsBehavior) threshold.Behaviors[2]; + soundThreshold = (PlaySoundBehavior)threshold.Behaviors[0]; + spawnThreshold = (SpawnEntitiesBehavior)threshold.Behaviors[1]; + actsThreshold = (DoActsBehavior)threshold.Behaviors[2]; // Check that it matches the YAML prototype Assert.Multiple(() => @@ -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.EntitySpecifier.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs index ca7445c359..37526f39a7 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs @@ -93,7 +93,7 @@ public abstract partial class InteractionTest await Server.WaitPost(() => { uid = SEntMan.SpawnEntity(stackProto.Spawn, coords); - Stack.SetCount(uid, spec.Quantity); + Stack.SetCount((uid, null), spec.Quantity); }); return uid; } diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs index eff0ed3a0c..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; @@ -156,10 +167,13 @@ public abstract partial class InteractionTest - type: CombatMode "; + protected static PoolSettings Default => new() { Connected = true, Dirty = true }; + protected virtual PoolSettings Settings => Default; + [SetUp] public virtual async Task Setup() { - Pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true, Dirty = true }); + Pair = await PoolManager.GetServerClient(Settings); // server dependencies SEntMan = Server.ResolveDependency(); @@ -196,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)); @@ -211,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); @@ -234,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 082baf7e64..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", - "SheetRGlass1", "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() @@ -469,7 +459,8 @@ public sealed class MaterialArbitrageTest await server.WaitPost(() => { var ent = entManager.SpawnEntity(id, testMap.GridCoords); - stackSys.SetCount(ent, 1); + if (entManager.TryGetComponent(ent, out var stackComp)) + stackSys.SetCount((ent, stackComp), 1); priceCache[id] = price = pricing.GetPrice(ent, false); entManager.DeleteEntity(ent); }); diff --git a/Content.IntegrationTests/Tests/Materials/MaterialTests.cs b/Content.IntegrationTests/Tests/Materials/MaterialTests.cs index 30800f358e..a177869e7f 100644 --- a/Content.IntegrationTests/Tests/Materials/MaterialTests.cs +++ b/Content.IntegrationTests/Tests/Materials/MaterialTests.cs @@ -54,7 +54,7 @@ namespace Content.IntegrationTests.Tests.Materials $"{proto.ID} material has no stack prototype"); if (stackProto != null) - Assert.That(proto.StackEntity, Is.EqualTo(stackProto.Spawn)); + Assert.That(proto.StackEntity, Is.EqualTo(stackProto.Spawn.Id)); } }); 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/Nutrition/HungerThirstTest.cs b/Content.IntegrationTests/Tests/Nutrition/HungerThirstTest.cs new file mode 100644 index 0000000000..7a5195800e --- /dev/null +++ b/Content.IntegrationTests/Tests/Nutrition/HungerThirstTest.cs @@ -0,0 +1,97 @@ +using Content.IntegrationTests.Tests.Interaction; +using Content.Shared.Chemistry.Components; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Nutrition; + +/// +/// Tests the mechanics of hunger and thirst. +/// +public sealed class HungerThirstTest : InteractionTest +{ + private readonly EntProtoId _drink = "DrinkLemonadeGlass"; + private readonly EntProtoId _food = "FoodCakeVanillaSlice"; + protected override string PlayerPrototype => "MobHuman"; + + /// + /// Tests that hunger and thirst values decrease over time (low means hungrier and thirstier). + /// Tests that hunger and thirst values increase when eating/drinking (high means less hungry and thirsty). + /// + [Test] + public async Task HungerThirstIncreaseDecreaseTest() + { + // Ensure that the player can breathe and not suffocate + await AddAtmosphere(); + + var hungerComponent = Comp(Player); + var thirstComponent = Comp(Player); + var hungerSystem = SEntMan.System(); + var thirstSystem = SEntMan.System(); + var ingestionSystem = SEntMan.System(); + + // Set initial value + hungerSystem.SetHunger(SPlayer, hungerComponent.Thresholds[HungerThreshold.Okay], hungerComponent); + thirstSystem.SetThirst(SPlayer, thirstComponent, thirstComponent.ThirstThresholds[ThirstThreshold.Okay]); + + // Ensure hunger and thirst value decrease over time (the Urist gets hungrier/thirstier) + var previousHungerValue = hungerSystem.GetHunger(hungerComponent); + var previousThirstValue = thirstComponent.CurrentThirst; // TODO: combined sation system with a sane API + + // Simulate long enough for both update loops to run + var runTime = Math.Max((float)hungerComponent.ThresholdUpdateRate.TotalSeconds, (float)thirstComponent.UpdateRate.TotalSeconds) + 1f; + await RunSeconds(runTime); + + var currentHungerValue = hungerSystem.GetHunger(hungerComponent); + Assert.That(currentHungerValue, Is.LessThan(previousHungerValue), "Hunger value did not decrease over time"); + previousHungerValue = currentHungerValue; + + var currentThirstValue = thirstComponent.CurrentThirst; + Assert.That(currentThirstValue, Is.LessThan(previousThirstValue), "Thirst value did not decrease over time"); + previousThirstValue = currentThirstValue; + + // Now we spawn food in the Urist's hand + await PlaceInHands(_food); + + // We eat the food in hand + await UseInHand(); + + // To see a change in hunger, we need to wait at least 30 seconds + await RunSeconds(30); + + // We ensure the food is fully eaten + var foodEaten = HandSys.GetActiveItem((SPlayer, Hands)); + Assert.That(foodEaten, Is.Null, "Food item did not disappear after eating it"); + + // Ensure that the hunger value has increased (The Urist is less hungry) + Assert.That(hungerSystem.GetHunger(hungerComponent), Is.GreaterThan(previousHungerValue), "Hunger value did not increase after eating food"); + + // Now we spawn a drink in the Urist's hand + var drink = await PlaceInHands(_drink); + + // Get the solution that can be consumed + Assert.That(ingestionSystem.CanConsume(SPlayer, SPlayer, ToServer(drink), out var solution, out _), + "Unable to get the solution or the entity can not be consumed"); + + // Find the initial amount of solution in the drink + var initialSolutionVolume = solution.Value.Comp.Solution.Volume; + + // We drink the drink in hand + await UseInHand(); + + // To see a change in thirst, we need to wait at least 30 seconds + await RunSeconds(30); + + // Ensure the solution volume has decreased + Assert.That(solution.Value.Comp.Solution.Volume, Is.LessThan(initialSolutionVolume), "Solution volume did not decrease after drinking"); + + // Ensure that the thirst value has increased (The Urist is less thirsty) + Assert.That(thirstComponent.CurrentThirst, Is.GreaterThan(previousThirstValue), "Thirst value did not increase after drinking"); + + // Make sure that the glass did not get deleted after drinking from it + var glass = HandSys.GetActiveItem((SPlayer, Hands)); + Assert.That(glass, Is.Not.Null, "Glass got deleted after drinking from it"); + } +} 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 new file mode 100644 index 0000000000..135e75f8be --- /dev/null +++ b/Content.IntegrationTests/Tests/Weapons/WeaponTests.cs @@ -0,0 +1,63 @@ +using Content.IntegrationTests.Tests.Interaction; +using Content.Shared.Damage.Components; +using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Systems; +using Content.Shared.Wieldable.Components; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Weapons; + +public sealed class WeaponTests : InteractionTest +{ + protected override string PlayerPrototype => "MobHuman"; // The default test mob only has one hand + private static readonly EntProtoId MobHuman = "MobHuman"; + private static readonly EntProtoId SniperMosin = "WeaponSniperMosin"; + + [Test] + public async Task GunRequiresWieldTest() + { + var gunSystem = SEntMan.System(); + + await AddAtmosphere(); // prevent the Urist from suffocating + + var urist = await SpawnTarget(MobHuman); + var damageComp = Comp(urist); + + var mosinNet = await PlaceInHands(SniperMosin); + var mosinEnt = ToServer(mosinNet); + + await Pair.RunSeconds(2f); // Guns have a cooldown when picking them up. + + Assert.That(HasComp(mosinNet), + "Looks like you've removed the 'GunRequiresWield' component from the mosin sniper." + + "If this was intentional, please update WeaponTests.cs to reflect this change!"); + + var startAmmo = gunSystem.GetAmmoCount(mosinEnt); + var wieldComp = Comp(mosinNet); + + Assert.That(startAmmo, Is.GreaterThan(0), "Mosin was spawned with no ammo!"); + Assert.That(wieldComp.Wielded, Is.False, "Mosin was spawned wielded!"); + + await AttemptShoot(urist, false); // should fail due to not being wielded + var updatedAmmo = gunSystem.GetAmmoCount(mosinEnt); + + Assert.That(updatedAmmo, + Is.EqualTo(startAmmo), + "Mosin discharged ammo when the weapon should not have fired!"); + Assert.That(damageComp.TotalDamage.Value, + Is.EqualTo(0), + "Urist took damage when the weapon should not have fired!"); + + await UseInHand(); + + Assert.That(wieldComp.Wielded, Is.True, "Mosin failed to wield when interacted with!"); + + await AttemptShoot(urist); + updatedAmmo = gunSystem.GetAmmoCount(mosinEnt); + + Assert.That(updatedAmmo, Is.EqualTo(startAmmo - 1), "Mosin failed to discharge appropriate amount of ammo!"); + Assert.That(damageComp.TotalDamage.Value, + Is.GreaterThan(0), + "Mosin was fired but urist sustained no damage!"); + } +} diff --git a/Content.Replay/Menu/ReplayMainMenu.cs b/Content.Replay/Menu/ReplayMainMenu.cs index 5adb769942..85c39c59da 100644 --- a/Content.Replay/Menu/ReplayMainMenu.cs +++ b/Content.Replay/Menu/ReplayMainMenu.cs @@ -33,9 +33,7 @@ public sealed class ReplayMainScreen : State [Dependency] private readonly IClientRobustSerializer _serializer = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; [Dependency] private readonly ContentReplayPlaybackManager _replayMan = default!; - [Dependency] private readonly ILogManager _logManager = default!; - private ISawmill _sawmill = default!; private ReplayMainMenuControl _mainMenuControl = default!; private SelectReplayWindow? _selectWindow; private ResPath _directory; @@ -44,8 +42,6 @@ public sealed class ReplayMainScreen : State protected override void Startup() { - _sawmill = _logManager.GetSawmill("replay.screen"); - _mainMenuControl = new(_resourceCache); _userInterfaceManager.StateRoot.AddChild(_mainMenuControl); @@ -267,7 +263,7 @@ public sealed class ReplayMainScreen : State } catch (Exception ex) { - _sawmill.Error($"Failed to load replay info. Exception: {ex}"); + Logger.Error($"Failed to load replay info. Exception: {ex}"); SelectReplay(null); return; } diff --git a/Content.Replay/Menu/ReplayMainMenuControl.xaml b/Content.Replay/Menu/ReplayMainMenuControl.xaml index 9eeafd95de..ef2f97ce3d 100644 --- a/Content.Replay/Menu/ReplayMainMenuControl.xaml +++ b/Content.Replay/Menu/ReplayMainMenuControl.xaml @@ -44,7 +44,7 @@ MinSize="300 150"> diff --git a/Content.Server.Database/ModelSqlite.cs b/Content.Server.Database/ModelSqlite.cs index 1ce9465847..5a993bdbfa 100644 --- a/Content.Server.Database/ModelSqlite.cs +++ b/Content.Server.Database/ModelSqlite.cs @@ -17,6 +17,9 @@ namespace Content.Server.Database { public SqliteServerDbContext(DbContextOptions options) : base(options) { +#if USE_SYSTEM_SQLITE + SQLitePCL.raw.SetProvider(new SQLitePCL.SQLite3Provider_sqlite3()); +#endif } protected override void OnConfiguring(DbContextOptionsBuilder options) diff --git a/Content.Server/Access/Systems/IdCardConsoleSystem.cs b/Content.Server/Access/Systems/IdCardConsoleSystem.cs index 13f2430b3d..9f21fd68d8 100644 --- a/Content.Server/Access/Systems/IdCardConsoleSystem.cs +++ b/Content.Server/Access/Systems/IdCardConsoleSystem.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Chat.Systems; using Content.Server.Containers; @@ -7,9 +8,11 @@ using static Content.Shared.Access.Components.IdCardConsoleComponent; using Content.Shared.Access.Systems; using Content.Shared.Access; using Content.Shared.Administration.Logs; +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; @@ -83,7 +86,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem { newState = new IdCardConsoleBoundUserInterfaceState( component.PrivilegedIdSlot.HasItem, - PrivilegedIdIsAuthorized(uid, component), + PrivilegedIdIsAuthorized(uid, component, out _), false, null, null, @@ -108,7 +111,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem newState = new IdCardConsoleBoundUserInterfaceState( component.PrivilegedIdSlot.HasItem, - PrivilegedIdIsAuthorized(uid, component), + PrivilegedIdIsAuthorized(uid, component, out _), true, targetIdComponent.FullName, targetIdComponent.LocalizedJobTitle, @@ -137,13 +140,13 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem if (!Resolve(uid, ref component)) return; - if (component.TargetIdSlot.Item is not { Valid: true } targetId || !PrivilegedIdIsAuthorized(uid, component)) + if (component.TargetIdSlot.Item is not { Valid: true } targetId || !PrivilegedIdIsAuthorized(uid, component, out var privilegedId)) return; _idCard.TryChangeFullName(targetId, newFullName, player: player); _idCard.TryChangeJobTitle(targetId, newJobTitle, player: player); - if (_prototype.TryIndex(newJobProto, out var job) + if (_prototype.Resolve(newJobProto, out var job) && _prototype.Resolve(job.Icon, out var jobIcon)) { _idCard.TryChangeJobIcon(targetId, jobIcon, player: player); @@ -165,10 +168,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem return; } - var oldTags = _access.TryGetTags(targetId) ?? new List>(); - oldTags = oldTags.ToList(); - - var privilegedId = component.PrivilegedIdSlot.Item; + var oldTags = _access.TryGetTags(targetId)?.ToList() ?? new List>(); if (oldTags.SequenceEqual(newAccessList)) return; @@ -176,8 +176,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem // I hate that C# doesn't have an option for this and don't desire to write this out the hard way. // var difference = newAccessList.Difference(oldTags); var difference = newAccessList.Union(oldTags).Except(newAccessList.Intersect(oldTags)).ToHashSet(); - // NULL SAFETY: PrivilegedIdIsAuthorized checked this earlier. - var privilegedPerms = _accessReader.FindAccessTags(privilegedId!.Value).ToHashSet(); + var privilegedPerms = _accessReader.FindAccessTags(privilegedId.Value); if (!difference.IsSubsetOf(privilegedPerms)) { _sawmill.Warning($"User {ToPrettyString(uid)} tried to modify permissions they could not give/take!"); @@ -190,26 +189,24 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem /*TODO: ECS SharedIdCardConsoleComponent and then log on card ejection, together with the save. This current implementation is pretty shit as it logs 27 entries (27 lines) if someone decides to give themselves AA*/ - _adminLogger.Add(LogType.Action, LogImpact.Medium, - $"{ToPrettyString(player):player} has modified {ToPrettyString(targetId):entity} with the following accesses: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]"); + _adminLogger.Add(LogType.Action, + $"{player} has modified {targetId} with the following accesses: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]"); } /// /// Returns true if there is an ID in and said ID satisfies the requirements of . /// - /// - /// Other code relies on the fact this returns false if privileged Id is null. Don't break that invariant. - /// - private bool PrivilegedIdIsAuthorized(EntityUid uid, IdCardConsoleComponent? component = null) + private bool PrivilegedIdIsAuthorized(EntityUid uid, IdCardConsoleComponent component, [NotNullWhen(true)] out EntityUid? id) { - if (!Resolve(uid, ref component)) - return true; + id = null; + if (component.PrivilegedIdSlot.Item == null) + return false; + id = component.PrivilegedIdSlot.Item; if (!TryComp(uid, out var reader)) return true; - var privilegedId = component.PrivilegedIdSlot.Item; - return privilegedId != null && _accessReader.IsAllowed(privilegedId.Value, uid, reader); + return _accessReader.IsAllowed(id.Value, uid, reader); } private void UpdateStationRecord(EntityUid uid, EntityUid targetId, string newFullName, ProtoId newJobTitle, JobPrototype? newJobProto) diff --git a/Content.Server/Access/Systems/IdCardSystem.cs b/Content.Server/Access/Systems/IdCardSystem.cs index 0fef62d970..f317e88f0f 100644 --- a/Content.Server/Access/Systems/IdCardSystem.cs +++ b/Content.Server/Access/Systems/IdCardSystem.cs @@ -6,6 +6,7 @@ using Content.Server.Popups; using Content.Shared.Access; using Content.Shared.Access.Components; using Content.Shared.Access.Systems; +using Content.Shared.Chat; using Content.Shared.Database; using Content.Shared.Popups; using Robust.Shared.Prototypes; diff --git a/Content.Server/Administration/Commands/DSay.cs b/Content.Server/Administration/Commands/DSay.cs index f5e0b32793..60f81fce5d 100644 --- a/Content.Server/Administration/Commands/DSay.cs +++ b/Content.Server/Administration/Commands/DSay.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Shared.Administration; +using Content.Shared.Chat; using Robust.Shared.Console; namespace Content.Server.Administration.Commands; diff --git a/Content.Server/Administration/Commands/OSay.cs b/Content.Server/Administration/Commands/OSay.cs index 2f17bd9d70..9c5a20ef69 100644 --- a/Content.Server/Administration/Commands/OSay.cs +++ b/Content.Server/Administration/Commands/OSay.cs @@ -2,6 +2,7 @@ using System.Linq; using Content.Server.Administration.Logs; using Content.Server.Chat.Systems; using Content.Shared.Administration; +using Content.Shared.Chat; using Content.Shared.Database; using Robust.Shared.Console; diff --git a/Content.Server/Administration/Commands/OpenAdminLogsCommand.cs b/Content.Server/Administration/Commands/OpenAdminLogsCommand.cs index 1beaaa928e..df437fcc2d 100644 --- a/Content.Server/Administration/Commands/OpenAdminLogsCommand.cs +++ b/Content.Server/Administration/Commands/OpenAdminLogsCommand.cs @@ -10,7 +10,8 @@ public sealed class OpenAdminLogsCommand : LocalizedEntityCommands { [Dependency] private readonly EuiManager _euiManager = default!; - public override string Command => "adminlogs"; + public override string Command => Cmd; + public const string Cmd = "adminlogs"; public override void Execute(IConsoleShell shell, string argStr, string[] args) { 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/PlayerPanelEui.cs b/Content.Server/Administration/PlayerPanelEui.cs index 31acd33bf1..7de62ac743 100644 --- a/Content.Server/Administration/PlayerPanelEui.cs +++ b/Content.Server/Administration/PlayerPanelEui.cs @@ -2,10 +2,10 @@ using System.Linq; using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; using Content.Server.Administration.Notes; -using Content.Server.Administration.Systems; using Content.Server.Database; using Content.Server.EUI; using Content.Shared.Administration; +using Content.Shared.Administration.Systems; using Content.Shared.Database; using Content.Shared.Eui; using Content.Shared.Follower; 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/Administration/Systems/AdminVerbSystem.Tools.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs index 6563d7b452..41228b5ac8 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs @@ -413,7 +413,7 @@ public sealed partial class AdminVerbSystem // Unbounded intentionally. _quickDialog.OpenDialog(player, Loc.GetString("admin-verbs-adjust-stack"), Loc.GetString("admin-verbs-dialog-adjust-stack-amount", ("max", _stackSystem.GetMaxCount(stack))), (int newAmount) => { - _stackSystem.SetCount(args.Target, newAmount, stack); + _stackSystem.SetCount((args.Target, stack), newAmount); }); }, Impact = LogImpact.Medium, @@ -429,7 +429,7 @@ public sealed partial class AdminVerbSystem Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/fill-stack.png")), Act = () => { - _stackSystem.SetCount(args.Target, _stackSystem.GetMaxCount(stack), stack); + _stackSystem.SetCount((args.Target, stack), _stackSystem.GetMaxCount(stack)); }, Impact = LogImpact.Medium, Message = Loc.GetString("admin-trick-fill-stack-description"), diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.cs b/Content.Server/Administration/Systems/AdminVerbSystem.cs index 61e3013bd9..4232b034f3 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.cs @@ -9,6 +9,7 @@ using Content.Server.Prayer; using Content.Server.Silicons.Laws; using Content.Server.Station.Systems; using Content.Shared.Administration; +using Content.Shared.Administration.Systems; using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Configurable; diff --git a/Content.Server/Administration/Systems/BwoinkSystem.cs b/Content.Server/Administration/Systems/BwoinkSystem.cs index 2df9aa9fcc..91211716b5 100644 --- a/Content.Server/Administration/Systems/BwoinkSystem.cs +++ b/Content.Server/Administration/Systems/BwoinkSystem.cs @@ -582,7 +582,7 @@ namespace Content.Server.Administration.Systems { GameRunLevel.PreRoundLobby => _gameTicker.RoundId == 0 ? "pre-round lobby after server restart" // first round after server restart has ID == 0 - : $"pre-round lobby for round {_gameTicker.RoundId + 1}", + : $"pre-round lobby for round {_gameTicker.RoundId}", GameRunLevel.InRound => $"round {_gameTicker.RoundId}", GameRunLevel.PostRound => $"post-round {_gameTicker.RoundId}", _ => throw new ArgumentOutOfRangeException(nameof(_gameTicker.RunLevel), diff --git a/Content.Server/Administration/Toolshed/RejuvenateCommand.cs b/Content.Server/Administration/Toolshed/RejuvenateCommand.cs index 3925badc58..b503e9cb96 100644 --- a/Content.Server/Administration/Toolshed/RejuvenateCommand.cs +++ b/Content.Server/Administration/Toolshed/RejuvenateCommand.cs @@ -1,5 +1,5 @@ -using Content.Server.Administration.Systems; -using Content.Shared.Administration; +using Content.Shared.Administration; +using Content.Shared.Administration.Systems; using Robust.Shared.Toolshed; using Robust.Shared.Toolshed.Errors; diff --git a/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs b/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs index 94b20c7b77..812a016b37 100644 --- a/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs +++ b/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Chat.Systems; using Content.Shared.Advertise.Components; using Content.Shared.Advertise.Systems; +using Content.Shared.Chat; using Content.Shared.UserInterface; using Robust.Shared.Prototypes; using Robust.Shared.Random; diff --git a/Content.Server/Ame/EntitySystems/AmeControllerSystem.cs b/Content.Server/Ame/EntitySystems/AmeControllerSystem.cs index 4276eafd60..f47c2def5b 100644 --- a/Content.Server/Ame/EntitySystems/AmeControllerSystem.cs +++ b/Content.Server/Ame/EntitySystems/AmeControllerSystem.cs @@ -150,7 +150,7 @@ public sealed class AmeControllerSystem : EntitySystem // how much power can be produced at the current settings, in kW // we don't use max. here since this is what is set in the Controller, not what the AME is actually producing float targetedPowerSupply = 0; - if (TryGetAMENodeGroup(uid, out var group)) + if (TryGetAMENodeGroup(uid, out var group) && group.CoreCount > 0) { coreCount = group.CoreCount; targetedPowerSupply = group.CalculatePower(controller.InjectionAmount, group.CoreCount) / 1000; diff --git a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.cs b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.cs index ae4c15f2db..d8acf44da3 100644 --- a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.cs +++ b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.cs @@ -243,7 +243,7 @@ public sealed partial class SpaceVillainGame UpdateUi( uid, Loc.GetString("space-villain-game-player-loses-message"), - Loc.GetString("space-villain-game-enemy-dies-with-player-message ", ("enemyName", _villainName)), + Loc.GetString("space-villain-game-enemy-dies-with-player-message", ("enemyName", _villainName)), true ); _audioSystem.PlayPvs(arcade.GameOverSound, uid, AudioParams.Default.WithVolume(-4f)); 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.API.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs index 87cfce135d..29f091f340 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs @@ -355,10 +355,6 @@ public partial class AtmosphereSystem grid.Comp.DeltaPressureEntityLookup[ent.Owner] = grid.Comp.DeltaPressureEntities.Count; grid.Comp.DeltaPressureEntities.Add(ent); - ent.Comp.CurrentPosition = _map.CoordinatesToTile(grid, - Comp(grid), - xform.Coordinates); - ent.Comp.GridUid = grid.Owner; ent.Comp.InProcessingList = true; 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 9d72b195fe..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; @@ -39,11 +40,13 @@ public sealed partial class AtmosphereSystem so simple vector operations like min/max/abs can be performed on them. */ + var airtightComp = _airtightQuery.Comp(ent); + var currentPos = airtightComp.LastPosition.Tile; var tiles = new TileAtmosphere?[Atmospherics.Directions]; for (var i = 0; i < Atmospherics.Directions; i++) { var direction = (AtmosDirection)(1 << i); - var offset = ent.Comp.CurrentPosition.Offset(direction); + var offset = currentPos.Offset(direction); tiles[i] = gridAtmosComp.Tiles.GetValueOrDefault(offset); } @@ -51,6 +54,21 @@ public sealed partial class AtmosphereSystem GetBulkTileAtmospherePressures(tiles, pressures); + // This entity could be airtight but still be able to contain air on the tile it's on (ex. directional windows). + // As such, substitute the pressure of the pressure on top of the entity for the directions that it can accept air from. + // (Or rather, don't do so for directions that it blocks air from.) + if (!airtightComp.NoAirWhenFullyAirBlocked) + { + for (var i = 0; i < Atmospherics.Directions; i++) + { + var direction = (AtmosDirection)(1 << i); + if (!airtightComp.AirBlockedDirection.HasFlag(direction)) + { + pressures[i] = gridAtmosComp.Tiles.GetValueOrDefault(currentPos)?.Air?.Pressure ?? 0f; + } + } + } + Span opposingGroupA = stackalloc float[DeltaPressurePairCount]; Span opposingGroupB = stackalloc float[DeltaPressurePairCount]; Span opposingGroupMax = stackalloc float[DeltaPressurePairCount]; @@ -158,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) @@ -167,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; } @@ -233,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 a6cbec0d0c..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,10 +14,9 @@ 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!; - [Dependency] private readonly SharedMapSystem _map = default!; public override void Initialize() { @@ -25,30 +24,16 @@ public sealed class DeltaPressureSystem : EntitySystem SubscribeLocalEvent(OnComponentInit); SubscribeLocalEvent(OnComponentShutdown); - SubscribeLocalEvent(OnExamined); - SubscribeLocalEvent(OnMoveEvent); - SubscribeLocalEvent(OnGridChanged); } - private void OnMoveEvent(Entity ent, ref MoveEvent args) - { - var xform = Transform(ent); - // May move off-grid, so, might as well protect against that. - if (!TryComp(xform.GridUid, out var mapGridComponent)) - { - return; - } - - ent.Comp.CurrentPosition = _map.CoordinatesToTile(xform.GridUid.Value, mapGridComponent, args.NewPosition); - } - private void OnComponentInit(Entity ent, ref ComponentInit args) { var xform = Transform(ent); if (xform.GridUid == null) return; + EnsureComp(ent); _atmosphereSystem.TryAddDeltaPressureEntity(xform.GridUid.Value, ent); } @@ -61,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 424f52ed58..071c63c500 100644 --- a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs +++ b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs @@ -1,14 +1,13 @@ using Content.Server.Administration.Logs; using Content.Server.Atmos.Components; using Content.Server.Stunnable; -using Content.Server.Temperature.Components; using Content.Server.Temperature.Systems; using Content.Server.Damage.Components; 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; @@ -24,6 +23,7 @@ using Content.Shared.Toggleable; using Content.Shared.Weapons.Melee.Events; using Content.Shared.FixedPoint; using Content.Shared.Hands; +using Content.Shared.Temperature.Components; using Robust.Server.Audio; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; 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/Rotting/RottingSystem.cs b/Content.Server/Atmos/Rotting/RottingSystem.cs index 6f14debc3d..5feb95e3c4 100644 --- a/Content.Server/Atmos/Rotting/RottingSystem.cs +++ b/Content.Server/Atmos/Rotting/RottingSystem.cs @@ -1,9 +1,9 @@ using Content.Server.Atmos.EntitySystems; -using Content.Server.Temperature.Components; 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; using Robust.Shared.Timing; 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/Components/MetabolizerComponent.cs b/Content.Server/Body/Components/MetabolizerComponent.cs index ad4104e13f..b5a1d49d3b 100644 --- a/Content.Server/Body/Components/MetabolizerComponent.cs +++ b/Content.Server/Body/Components/MetabolizerComponent.cs @@ -10,13 +10,13 @@ namespace Content.Server.Body.Components /// /// Handles metabolizing various reagents with given effects. /// - [RegisterComponent, Access(typeof(MetabolizerSystem))] + [RegisterComponent, AutoGenerateComponentPause, Access(typeof(MetabolizerSystem))] public sealed partial class MetabolizerComponent : Component { /// /// The next time that reagents will be metabolized. /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [DataField, AutoPausedField] public TimeSpan NextUpdate; /// diff --git a/Content.Server/Body/Systems/MetabolizerSystem.cs b/Content.Server/Body/Systems/MetabolizerSystem.cs index c6b5c6ad6a..b5b30ae74c 100644 --- a/Content.Server/Body/Systems/MetabolizerSystem.cs +++ b/Content.Server/Body/Systems/MetabolizerSystem.cs @@ -1,308 +1,282 @@ using Content.Server.Body.Components; -using Content.Shared.Administration.Logs; using Content.Shared.Body.Events; using Content.Shared.Body.Organ; +using Content.Shared.Body.Prototypes; using Content.Shared.Body.Systems; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Reagent; -using Content.Shared.Database; +using Content.Shared.EntityConditions; +using Content.Shared.EntityConditions.Conditions; +using Content.Shared.EntityConditions.Conditions.Body; using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Body; +using Content.Shared.EntityEffects.Effects.Solution; using Content.Shared.FixedPoint; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; +using Content.Shared.Random.Helpers; using Robust.Shared.Collections; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; -namespace Content.Server.Body.Systems +namespace Content.Server.Body.Systems; + +/// +public sealed class MetabolizerSystem : SharedMetabolizerSystem { - /// - public sealed class MetabolizerSystem : SharedMetabolizerSystem + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly MobStateSystem _mobStateSystem = default!; + [Dependency] private readonly SharedEntityConditionsSystem _entityConditions = default!; + [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; + + private EntityQuery _organQuery; + private EntityQuery _solutionQuery; + private static readonly ProtoId Gas = "Gas"; + + public override void Initialize() { - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; - [Dependency] private readonly MobStateSystem _mobStateSystem = default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; - [Dependency] private readonly Content.Shared.StatusEffectNew.StatusEffectsSystem _statusEffects = default!; + base.Initialize(); - private EntityQuery _organQuery; - private EntityQuery _solutionQuery; + _organQuery = GetEntityQuery(); + _solutionQuery = GetEntityQuery(); - public override void Initialize() + SubscribeLocalEvent(OnMetabolizerInit); + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnApplyMetabolicMultiplier); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.AdjustedUpdateInterval; + } + + private void OnMetabolizerInit(Entity entity, ref ComponentInit args) + { + if (!entity.Comp.SolutionOnBody) { - base.Initialize(); - - _organQuery = GetEntityQuery(); - _solutionQuery = GetEntityQuery(); - - SubscribeLocalEvent(OnMetabolizerInit); - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnUnpaused); - SubscribeLocalEvent(OnApplyMetabolicMultiplier); + _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.SolutionName, out _); } - - private void OnMapInit(Entity ent, ref MapInitEvent args) + else if (_organQuery.CompOrNull(entity)?.Body is { } body) { - ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.AdjustedUpdateInterval; - } - - private void OnUnpaused(Entity ent, ref EntityUnpausedEvent args) - { - ent.Comp.NextUpdate += args.PausedTime; - } - - private void OnMetabolizerInit(Entity entity, ref ComponentInit args) - { - if (!entity.Comp.SolutionOnBody) - { - _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.SolutionName, out _); - } - else if (_organQuery.CompOrNull(entity)?.Body is { } body) - { - _solutionContainerSystem.EnsureSolution(body, entity.Comp.SolutionName, out _); - } - } - - private void OnApplyMetabolicMultiplier(Entity ent, ref ApplyMetabolicMultiplierEvent args) - { - ent.Comp.UpdateIntervalMultiplier = args.Multiplier; - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var metabolizers = new ValueList<(EntityUid Uid, MetabolizerComponent Component)>(Count()); - var query = EntityQueryEnumerator(); - - while (query.MoveNext(out var uid, out var comp)) - { - metabolizers.Add((uid, comp)); - } - - foreach (var (uid, metab) in metabolizers) - { - // Only update as frequently as it should - if (_gameTiming.CurTime < metab.NextUpdate) - continue; - - metab.NextUpdate += metab.AdjustedUpdateInterval; - TryMetabolize((uid, metab)); - } - } - - private void TryMetabolize(Entity ent) - { - _organQuery.Resolve(ent, ref ent.Comp2, logMissing: false); - - // First step is get the solution we actually care about - var solutionName = ent.Comp1.SolutionName; - Solution? solution = null; - Entity? soln = default!; - EntityUid? solutionEntityUid = null; - - if (ent.Comp1.SolutionOnBody) - { - if (ent.Comp2?.Body is { } body) - { - if (!_solutionQuery.Resolve(body, ref ent.Comp3, logMissing: false)) - return; - - _solutionContainerSystem.TryGetSolution((body, ent.Comp3), solutionName, out soln, out solution); - solutionEntityUid = body; - } - } - else - { - if (!_solutionQuery.Resolve(ent, ref ent.Comp3, logMissing: false)) - return; - - _solutionContainerSystem.TryGetSolution((ent, ent), solutionName, out soln, out solution); - solutionEntityUid = ent; - } - - if (solutionEntityUid is null - || soln is null - || solution is null - || (solution.Contents.Count == 0 && ent.Comp1.MetabolizingReagents.Count == 0 && ent.Comp1.Metabolites.Count == 0)) // Offbrand - we need to ensure we clear out metabolizing reagents - { - return; - } - - // randomize the reagent list so we don't have any weird quirks - // like alphabetical order or insertion order mattering for processing - var list = solution.Contents.ToArray(); - _random.Shuffle(list); - - var metabolized = new HashSet>(); - int reagents = 0; - foreach (var (reagent, quantity) in list) - { - if (!_prototypeManager.TryIndex(reagent.Prototype, out var proto)) - continue; - - var mostToRemove = FixedPoint2.Zero; - if (proto.Metabolisms is null) - { - if (ent.Comp1.RemoveEmpty) - { - solution.RemoveReagent(reagent, FixedPoint2.New(1)); - } - - continue; - } - - // Begin Offbrand - No we're not - // we're done here entirely if this is true - // if (reagents >= ent.Comp1.MaxReagentsProcessable) - // return; - // End Offbrand - metabolized.Add(reagent.Prototype); // Offbrand - - // loop over all our groups and see which ones apply - if (ent.Comp1.MetabolismGroups is null) - continue; - - foreach (var group in ent.Comp1.MetabolismGroups) - { - if (!proto.Metabolisms.TryGetValue(group.Id, out var entry)) - continue; - - var rate = entry.MetabolismRate * group.MetabolismRateModifier; - - // Remove $rate, as long as there's enough reagent there to actually remove that much - mostToRemove = FixedPoint2.Clamp(rate, 0, quantity); - - float scale = (float) mostToRemove / (float) rate; - - // if it's possible for them to be dead, and they are, - // then we shouldn't process any effects, but should probably - // still remove reagents - if (TryComp(solutionEntityUid.Value, out var state)) - { - if (!proto.WorksOnTheDead && _mobStateSystem.IsDead(solutionEntityUid.Value, state)) - continue; - } - - var actualEntity = ent.Comp2?.Body ?? solutionEntityUid.Value; - var args = new EntityEffectReagentArgs(actualEntity, EntityManager, ent, solution, mostToRemove, proto, null, scale); - - // Begin Offbrand - foreach (var effect in entry.StatusEffects) - { - if (!effect.ShouldApplyStatusEffect(args)) - _statusEffects.TryRemoveStatusEffect(actualEntity, effect.StatusEffect); - else - _statusEffects.TryUpdateStatusEffectDuration(actualEntity, effect.StatusEffect, out _); - } - // End Offbrand - - // do all effects, if conditions apply - foreach (var effect in entry.Effects) - { - if (!effect.ShouldApply(args, _random)) - continue; - - if (effect.ShouldLog) - { - _adminLogger.Add( - LogType.ReagentEffect, - effect.LogImpact, - $"Metabolism effect {effect.GetType().Name:effect}" - + $" of reagent {proto.LocalizedName:reagent}" - + $" applied on entity {actualEntity:entity}" - + $" at {Transform(actualEntity).Coordinates:coordinates}" - ); - } - - effect.Effect(args); - } - } - - // remove a certain amount of reagent - if (mostToRemove > FixedPoint2.Zero) - { - var removed = solution.RemoveReagent(reagent, mostToRemove); // Offbrand - - // We have processed a reagant, so count it towards the cap - reagents += 1; - - // Begin Offbrand - if (!ent.Comp1.Metabolites.ContainsKey(reagent.Prototype)) - ent.Comp1.Metabolites[reagent.Prototype] = 0; - - ent.Comp1.Metabolites[reagent.Prototype] += removed; - // End Offbrand - } - } - - // Begin Offbrand - foreach (var reagent in ent.Comp1.MetabolizingReagents) - { - if (metabolized.Contains(reagent)) - continue; - - var proto = _prototypeManager.Index(reagent); - var actualEntity = ent.Comp2?.Body ?? solutionEntityUid.Value; - - if (ent.Comp1.MetabolismGroups is null) - continue; - - foreach (var group in ent.Comp1.MetabolismGroups) - { - if (proto.Metabolisms is null) - continue; - - if (!proto.Metabolisms.TryGetValue(group.Id, out var entry)) - continue; - - foreach (var effect in entry.StatusEffects) - { - _statusEffects.TryRemoveStatusEffect(actualEntity, effect.StatusEffect); - } - } - } - ent.Comp1.MetabolizingReagents = metabolized; - - foreach (var metaboliteReagent in ent.Comp1.Metabolites.Keys) - { - if (ent.Comp1.MetabolizingReagents.Contains(metaboliteReagent)) - continue; - - if (!_prototypeManager.Resolve(metaboliteReagent, out var proto) || proto.Metabolisms is not { } metabolisms) - continue; - - if (ent.Comp1.MetabolismGroups is null) - continue; - - ReagentEffectsEntry? entry = null; - var metabolismRateModifier = FixedPoint2.Zero; - foreach (var group in ent.Comp1.MetabolismGroups) - { - if (!proto.Metabolisms.TryGetValue(group.Id, out entry)) - continue; - - metabolismRateModifier = group.MetabolismRateModifier; - break; - } - - if (entry is not { } metabolismEntry) - continue; - - var rate = metabolismEntry.MetabolismRate * metabolismRateModifier * ent.Comp1.MetaboliteDecayFactor; - ent.Comp1.Metabolites[metaboliteReagent] -= rate; - - if (ent.Comp1.Metabolites[metaboliteReagent] <= 0) - ent.Comp1.Metabolites.Remove(metaboliteReagent); - } - // End Offbrand - - _solutionContainerSystem.UpdateChemicals(soln.Value); + _solutionContainerSystem.EnsureSolution(body, entity.Comp.SolutionName, out _); } } + + private void OnApplyMetabolicMultiplier(Entity ent, ref ApplyMetabolicMultiplierEvent args) + { + ent.Comp.UpdateIntervalMultiplier = args.Multiplier; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var metabolizers = new ValueList<(EntityUid Uid, MetabolizerComponent Component)>(Count()); + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var comp)) + { + metabolizers.Add((uid, comp)); + } + + foreach (var (uid, metab) in metabolizers) + { + // Only update as frequently as it should + if (_gameTiming.CurTime < metab.NextUpdate) + continue; + + metab.NextUpdate += metab.AdjustedUpdateInterval; + TryMetabolize((uid, metab)); + } + } + + private void TryMetabolize(Entity ent) + { + _organQuery.Resolve(ent, ref ent.Comp2, logMissing: false); + + // First step is get the solution we actually care about + var solutionName = ent.Comp1.SolutionName; + Solution? solution = null; + Entity? soln = default!; + EntityUid? solutionEntityUid = null; + + if (ent.Comp1.SolutionOnBody) + { + if (ent.Comp2?.Body is { } body) + { + if (!_solutionQuery.Resolve(body, ref ent.Comp3, logMissing: false)) + return; + + _solutionContainerSystem.TryGetSolution((body, ent.Comp3), solutionName, out soln, out solution); + solutionEntityUid = body; + } + } + else + { + if (!_solutionQuery.Resolve(ent, ref ent.Comp3, logMissing: false)) + return; + + _solutionContainerSystem.TryGetSolution((ent, ent), solutionName, out soln, out solution); + solutionEntityUid = ent; + } + + if (solutionEntityUid is null + || soln is null + || solution is null + || solution.Contents.Count == 0) + { + return; + } + + // randomize the reagent list so we don't have any weird quirks + // like alphabetical order or insertion order mattering for processing + var list = solution.Contents.ToArray(); + _random.Shuffle(list); + + int reagents = 0; + foreach (var (reagent, quantity) in list) + { + if (!_prototypeManager.TryIndex(reagent.Prototype, out var proto)) + continue; + + var mostToRemove = FixedPoint2.Zero; + if (proto.Metabolisms is null) + { + if (ent.Comp1.RemoveEmpty) + { + solution.RemoveReagent(reagent, FixedPoint2.New(1)); + } + + continue; + } + + // we're done here entirely if this is true + if (reagents >= ent.Comp1.MaxReagentsProcessable) + return; + + + // loop over all our groups and see which ones apply + if (ent.Comp1.MetabolismGroups is null) + continue; + + // TODO: Kill MetabolismGroups! + foreach (var group in ent.Comp1.MetabolismGroups) + { + if (!proto.Metabolisms.TryGetValue(group.Id, out var entry)) + continue; + + var rate = entry.MetabolismRate * group.MetabolismRateModifier; + + // Remove $rate, as long as there's enough reagent there to actually remove that much + mostToRemove = FixedPoint2.Clamp(rate, 0, quantity); + + var scale = (float) mostToRemove; + + // TODO: This is a very stupid workaround to lungs heavily relying on scale = reagent quantity. Needs lung and metabolism refactors to remove. + // TODO: Lungs just need to have their scale be equal to the mols consumed, scale needs to be not hardcoded either and configurable per metabolizer... + if (group.Id != Gas) + scale /= (float) entry.MetabolismRate; + + // if it's possible for them to be dead, and they are, + // then we shouldn't process any effects, but should probably + // still remove reagents + if (TryComp(solutionEntityUid.Value, out var state)) + { + if (!proto.WorksOnTheDead && _mobStateSystem.IsDead(solutionEntityUid.Value, state)) + continue; + } + + var actualEntity = ent.Comp2?.Body ?? solutionEntityUid.Value; + + // do all effects, if conditions apply + foreach (var effect in entry.Effects) + { + if (scale < effect.MinScale) + continue; + + if (effect.Probability < 1.0f && !_random.Prob(effect.Probability)) + continue; + + // See if conditions apply + if (effect.Conditions != null && !CanMetabolizeEffect(actualEntity, ent, soln.Value, effect.Conditions)) + continue; + + ApplyEffect(effect); + + } + + // TODO: We should have to do this with metabolism. ReagentEffect struct needs refactoring and so does metabolism! + void ApplyEffect(EntityEffect effect) + { + switch (effect) + { + case ModifyLungGas: + _entityEffects.ApplyEffect(ent, effect, scale); + break; + case AdjustReagent: + _entityEffects.ApplyEffect(soln.Value, effect, scale); + break; + default: + _entityEffects.ApplyEffect(actualEntity, effect, scale); + break; + } + } + } + + // remove a certain amount of reagent + if (mostToRemove > FixedPoint2.Zero) + { + solution.RemoveReagent(reagent, mostToRemove); + + // We have processed a reagant, so count it towards the cap + reagents += 1; + } + } + + _solutionContainerSystem.UpdateChemicals(soln.Value); + } + + /// + /// Public API to check if a certain metabolism effect can be applied to an entity. + /// TODO: With metabolism refactor make this logic smarter and unhardcode the old hardcoding entity effects used to have for metabolism! + /// + /// The body metabolizing the effects + /// The organ doing the metabolizing + /// The solution we are metabolizing from + /// The conditions that need to be met to metabolize + /// True if we can metabolize! False if we cannot! + public bool CanMetabolizeEffect(EntityUid body, EntityUid organ, Entity solution, EntityCondition[] conditions) + { + foreach (var condition in conditions) + { + switch (condition) + { + // Need specific handling of specific conditions since Metabolism is funny like that. + // TODO: MetabolizerTypes should be handled well before this stage by metabolism itself. + case MetabolizerTypeCondition: + if (_entityConditions.TryCondition(organ, condition)) + continue; + break; + case ReagentCondition: + if (_entityConditions.TryCondition(solution, condition)) + continue; + break; + default: + if (_entityConditions.TryCondition(body, condition)) + continue; + break; + } + + return false; + } + + return true; + } } + diff --git a/Content.Server/Body/Systems/RespiratorSystem.cs b/Content.Server/Body/Systems/RespiratorSystem.cs index 9de59a3fba..399be011f4 100644 --- a/Content.Server/Body/Systems/RespiratorSystem.cs +++ b/Content.Server/Body/Systems/RespiratorSystem.cs @@ -2,21 +2,23 @@ using Content.Server.Administration.Logs; using Content.Server.Atmos.EntitySystems; using Content.Server.Body.Components; using Content.Server.Chat.Systems; -using Content.Server.EntityEffects; using Content.Shared.Body.Systems; using Content.Shared.Alert; using Content.Shared.Atmos; using Content.Shared.Body.Components; using Content.Shared.Body.Events; using Content.Shared.Body.Prototypes; +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; using Content.Shared.EntityEffects; -using Content.Shared.EntityEffects.EffectConditions; using Content.Shared.EntityEffects.Effects; +using Content.Shared.EntityEffects.Effects.Body; using Content.Shared.Mobs.Systems; using JetBrains.Annotations; using Robust.Shared.Prototypes; @@ -30,16 +32,16 @@ public sealed class RespiratorSystem : EntitySystem { [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IPrototypeManager _protoMan = default!; [Dependency] private readonly AlertsSystem _alertsSystem = default!; [Dependency] private readonly AtmosphereSystem _atmosSys = default!; [Dependency] private readonly BodySystem _bodySystem = default!; + [Dependency] private readonly ChatSystem _chat = default!; [Dependency] private readonly DamageableSystem _damageableSys = default!; [Dependency] private readonly LungSystem _lungSystem = default!; [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly IPrototypeManager _protoMan = default!; + [Dependency] private readonly SharedEntityConditionsSystem _entityConditions = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; - [Dependency] private readonly ChatSystem _chat = default!; - [Dependency] private readonly EntityEffectSystem _entityEffect = default!; private static readonly ProtoId GasId = new("Gas"); @@ -357,7 +359,6 @@ public sealed class RespiratorSystem : EntitySystem } } - // TODO generalize condition checks // this is pretty janky, but I just want to bodge a method that checks if an entity can breathe a gas mixture // Applying actual reaction effects require a full ReagentEffectArgs struct. bool CanMetabolize(EntityEffect effect) @@ -365,9 +366,10 @@ public sealed class RespiratorSystem : EntitySystem if (effect.Conditions == null) return true; + // TODO: Use Metabolism Public API to do this instead, once that API has been built. foreach (var cond in effect.Conditions) { - if (cond is OrganType organ && !_entityEffect.OrganCondition(organ, lung)) + if (cond is MetabolizerTypeCondition organ && !_entityConditions.TryCondition(lung, organ)) return false; } @@ -382,7 +384,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; @@ -396,7 +398,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/Body/Systems/ThermalRegulatorSystem.cs b/Content.Server/Body/Systems/ThermalRegulatorSystem.cs index 3ba9e6af31..af7b17643e 100644 --- a/Content.Server/Body/Systems/ThermalRegulatorSystem.cs +++ b/Content.Server/Body/Systems/ThermalRegulatorSystem.cs @@ -1,7 +1,7 @@ using Content.Server.Body.Components; -using Content.Server.Temperature.Components; using Content.Server.Temperature.Systems; using Content.Shared.ActionBlocker; +using Content.Shared.Temperature.Components; using Robust.Shared.Timing; namespace Content.Server.Body.Systems; diff --git a/Content.Server/Botany/SeedPrototype.cs b/Content.Server/Botany/SeedPrototype.cs index ee7ca4f584..253eea2df9 100644 --- a/Content.Server/Botany/SeedPrototype.cs +++ b/Content.Server/Botany/SeedPrototype.cs @@ -1,8 +1,9 @@ using Content.Server.Botany.Components; using Content.Server.Botany.Systems; -using Content.Server.EntityEffects; +using Content.Server.EntityEffects.Effects.Botany; using Content.Shared.Atmos; using Content.Shared.Database; +using Content.Shared.EntityEffects; using Content.Shared.Random; using Robust.Shared.Audio; using Robust.Shared.Prototypes; @@ -79,9 +80,13 @@ public partial struct SeedChemQuantity [DataField("Inherent")] public bool Inherent = true; } -// TODO reduce the number of friends to a reasonable level. Requires ECS-ing things like plant holder component. +// TODO Make Botany ECS and give it a proper API. I removed the limited access of this class because it's egregious how many systems needed access to it due to a lack of an actual API. +/// +/// SeedData is no longer restricted because the number of friends is absolutely unreasonable. +/// This entire data definition is unreasonable. I felt genuine fear looking at this, this is horrific. Send help. +/// +// TODO: Hit Botany with hammers [Virtual, DataDefinition] -[Access(typeof(BotanySystem), typeof(PlantHolderSystem), typeof(SeedExtractorSystem), typeof(PlantHolderComponent), typeof(EntityEffectSystem), typeof(MutationSystem))] public partial class SeedData { #region Tracking diff --git a/Content.Server/Botany/Systems/BotanySystem.Produce.cs b/Content.Server/Botany/Systems/BotanySystem.Produce.cs index f6f3f99c09..7d8f8652c7 100644 --- a/Content.Server/Botany/Systems/BotanySystem.Produce.cs +++ b/Content.Server/Botany/Systems/BotanySystem.Produce.cs @@ -7,6 +7,8 @@ namespace Content.Server.Botany.Systems; public sealed partial class BotanySystem { + [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!; + public void ProduceGrown(EntityUid uid, ProduceComponent produce) { if (!TryGetSeed(produce, out var seed)) @@ -15,10 +17,7 @@ public sealed partial class BotanySystem foreach (var mutation in seed.Mutations) { if (mutation.AppliesToProduce) - { - var args = new EntityEffectBaseArgs(uid, EntityManager); - mutation.Effect.Effect(args); - } + _entityEffects.TryApplyEffect(uid, mutation.Effect); } if (!_solutionContainerSystem.EnsureSolution(uid, diff --git a/Content.Server/Botany/Systems/MutationSystem.cs b/Content.Server/Botany/Systems/MutationSystem.cs index ee35db48e3..834fd9e8ef 100644 --- a/Content.Server/Botany/Systems/MutationSystem.cs +++ b/Content.Server/Botany/Systems/MutationSystem.cs @@ -13,6 +13,7 @@ public sealed class MutationSystem : EntitySystem [Dependency] private readonly IRobustRandom _robustRandom = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!; private RandomPlantMutationListPrototype _randomMutations = default!; public override void Initialize() @@ -32,10 +33,8 @@ public sealed class MutationSystem : EntitySystem if (Random(Math.Min(mutation.BaseOdds * severity, 1.0f))) { if (mutation.AppliesToPlant) - { - var args = new EntityEffectBaseArgs(plantHolder, EntityManager); - mutation.Effect.Effect(args); - } + _entityEffects.TryApplyEffect(plantHolder, mutation.Effect); + // Stat adjustments do not persist by being an attached effect, they just change the stat. if (mutation.Persists && !seed.Mutations.Any(m => m.Name == mutation.Name)) seed.Mutations.Add(mutation); diff --git a/Content.Server/Botany/Systems/PlantHolderSystem.cs b/Content.Server/Botany/Systems/PlantHolderSystem.cs index caa796efe2..d5f331c157 100644 --- a/Content.Server/Botany/Systems/PlantHolderSystem.cs +++ b/Content.Server/Botany/Systems/PlantHolderSystem.cs @@ -23,9 +23,10 @@ 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; +using Content.Shared.EntityEffects; using Content.Shared.Kitchen.Components; using Content.Shared.Labels.Components; @@ -48,6 +49,7 @@ public sealed class PlantHolderSystem : EntitySystem [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!; public const float HydroponicsSpeedMultiplier = 1f; public const float HydroponicsConsumptionMultiplier = 2f; @@ -883,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); - reagentProto.ReactionPlant(uid, entry, solution, EntityManager, _random, _adminLogger); + _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/Cargo/Systems/CargoSystem.Funds.cs b/Content.Server/Cargo/Systems/CargoSystem.Funds.cs index 4a3fa5330e..bc6571c0cc 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Funds.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Funds.cs @@ -56,7 +56,7 @@ public sealed partial class CargoSystem if (args.Account == null) { var stackPrototype = _protoMan.Index(ent.Comp.CashType); - _stack.Spawn(args.Amount, stackPrototype, Transform(ent).Coordinates); + _stack.SpawnAtPosition(args.Amount, stackPrototype, Transform(ent).Coordinates); if (!_emag.CheckFlag(ent, EmagType.Interaction)) { diff --git a/Content.Server/Cargo/Systems/CargoSystem.cs b/Content.Server/Cargo/Systems/CargoSystem.cs index 18ae856fda..9f3a4d5bf3 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.cs @@ -1,20 +1,15 @@ using Content.Server.Cargo.Components; using Content.Server.DeviceLinking.Systems; using Content.Server.Popups; -using Content.Server.Shuttles.Systems; using Content.Server.Stack; using Content.Server.Station.Systems; using Content.Shared.Access.Systems; using Content.Shared.Administration.Logs; using Content.Server.Radio.EntitySystems; using Content.Shared.Cargo; -using Content.Shared.Cargo.Components; -using Content.Shared.Cargo.Prototypes; -using Content.Shared.CCVar; using Content.Shared.Containers.ItemSlots; using Content.Shared.Mobs.Components; using Content.Shared.Paper; -using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; @@ -76,49 +71,4 @@ public sealed partial class CargoSystem : SharedCargoSystem UpdateTelepad(frameTime); UpdateBounty(); } - - public void UpdateBankAccount( - Entity ent, - int balanceAdded, - ProtoId account, - bool dirty = true) - { - UpdateBankAccount( - ent, - balanceAdded, - new Dictionary, double> { {account, 1} }, - dirty: dirty); - } - - /// - /// Adds or removes funds from the . - /// - /// The station. - /// The amount of funds to add or remove. - /// The distribution between individual . - /// Whether to mark the bank account component as dirty. - [PublicAPI] - public void UpdateBankAccount( - Entity ent, - int balanceAdded, - Dictionary, double> accountDistribution, - bool dirty = true) - { - if (!Resolve(ent, ref ent.Comp)) - return; - - foreach (var (account, percent) in accountDistribution) - { - var accountBalancedAdded = (int) Math.Round(percent * balanceAdded); - ent.Comp.Accounts[account] += accountBalancedAdded; - } - - var ev = new BankBalanceUpdatedEvent(ent, ent.Comp.Accounts); - RaiseLocalEvent(ent, ref ev, true); - - if (!dirty) - return; - - Dirty(ent); - } } diff --git a/Content.Server/Chat/Commands/LOOCCommand.cs b/Content.Server/Chat/Commands/LOOCCommand.cs index e303b9766d..3737b5d518 100644 --- a/Content.Server/Chat/Commands/LOOCCommand.cs +++ b/Content.Server/Chat/Commands/LOOCCommand.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Shared.Administration; +using Content.Shared.Chat; using Robust.Shared.Console; using Robust.Shared.Enums; diff --git a/Content.Server/Chat/Commands/MeCommand.cs b/Content.Server/Chat/Commands/MeCommand.cs index 36acfa7a69..f5b845a2d7 100644 --- a/Content.Server/Chat/Commands/MeCommand.cs +++ b/Content.Server/Chat/Commands/MeCommand.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Shared.Administration; +using Content.Shared.Chat; using Robust.Shared.Console; using Robust.Shared.Enums; diff --git a/Content.Server/Chat/Commands/SayCommand.cs b/Content.Server/Chat/Commands/SayCommand.cs index 99ca4660f0..199cd69098 100644 --- a/Content.Server/Chat/Commands/SayCommand.cs +++ b/Content.Server/Chat/Commands/SayCommand.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Shared.Administration; +using Content.Shared.Chat; using Robust.Shared.Console; using Robust.Shared.Enums; diff --git a/Content.Server/Chat/Commands/WhisperCommand.cs b/Content.Server/Chat/Commands/WhisperCommand.cs index 0556dd8036..d31b21abb1 100644 --- a/Content.Server/Chat/Commands/WhisperCommand.cs +++ b/Content.Server/Chat/Commands/WhisperCommand.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Shared.Administration; +using Content.Shared.Chat; using Robust.Shared.Console; using Robust.Shared.Enums; diff --git a/Content.Server/Chat/Managers/ChatManager.cs b/Content.Server/Chat/Managers/ChatManager.cs index a37c05336f..f90e286d9e 100644 --- a/Content.Server/Chat/Managers/ChatManager.cs +++ b/Content.Server/Chat/Managers/ChatManager.cs @@ -1,3 +1,6 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.InteropServices; using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; using Content.Server.Administration.Systems; @@ -15,9 +18,6 @@ using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Replays; using Robust.Shared.Utility; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.InteropServices; namespace Content.Server.Chat.Managers; @@ -45,7 +45,6 @@ internal sealed partial class ChatManager : IChatManager [Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!; [Dependency] private readonly ISharedPlayerManager _player = default!; [Dependency] private readonly DiscordChatLink _discordLink = default!; - [Dependency] private readonly ILogManager _logManager = default!; /// /// The maximum length a player-sent message can be sent @@ -55,7 +54,6 @@ internal sealed partial class ChatManager : IChatManager private bool _oocEnabled = true; private bool _adminOocEnabled = true; - private ISawmill _sawmill = default!; private readonly Dictionary _players = new(); public void Initialize() @@ -66,8 +64,6 @@ internal sealed partial class ChatManager : IChatManager _configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true); _configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true); - _sawmill = _logManager.GetSawmill("SERVER"); - RegisterRateLimits(); } @@ -115,7 +111,7 @@ internal sealed partial class ChatManager : IChatManager { var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", FormattedMessage.EscapeText(message))); ChatMessageToAll(ChatChannel.Server, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride); - _sawmill.Info(message); + Logger.InfoS("SERVER", message); _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Server announcement: {message}"); } @@ -164,14 +160,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/AutoEmoteSystem.cs b/Content.Server/Chat/Systems/AutoEmoteSystem.cs index 3d6bd53540..ebb22f6314 100644 --- a/Content.Server/Chat/Systems/AutoEmoteSystem.cs +++ b/Content.Server/Chat/Systems/AutoEmoteSystem.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Shared.Chat; using Content.Shared.Chat.Prototypes; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -46,7 +47,11 @@ public sealed class AutoEmoteSystem : EntitySystem if (autoEmotePrototype.WithChat) { - _chatSystem.TryEmoteWithChat(uid, autoEmotePrototype.EmoteId, autoEmotePrototype.HiddenFromChatWindow ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal); + _chatSystem.TryEmoteWithChat(uid, + autoEmotePrototype.EmoteId, + autoEmotePrototype.HiddenFromChatWindow ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, + ignoreActionBlocker: autoEmotePrototype.IgnoreActionBlocker, + forceEmote: autoEmotePrototype.Force); } else { diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index b105e0c20b..fb37460446 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!; [Dependency] private readonly Content.Shared.StatusEffectNew.StatusEffectsSystem _statusEffects = default!; // Offbrand @@ -69,7 +68,7 @@ public sealed partial class ChatSystem : SharedChatSystem public override void Initialize() { base.Initialize(); - CacheEmotes(); + Subs.CVar(_configurationManager, CCVars.LoocEnabled, OnLoocEnabledChanged, true); Subs.CVar(_configurationManager, CCVars.DeadLoocEnabled, OnDeadLoocEnabledChanged, true); Subs.CVar(_configurationManager, CCVars.CritLoocEnabled, OnCritLoocEnabledChanged, true); @@ -121,42 +120,24 @@ public sealed partial class ChatSystem : SharedChatSystem } } - /// - /// Sends an in-character chat message to relevant clients. - /// - /// The entity that is speaking - /// The message being spoken or emoted - /// The chat type - /// Whether or not this message should appear in the chat window - /// Whether or not this message should appear in the adminlog window - /// - /// The player doing the speaking - /// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised. - public void TrySendInGameICMessage( + /// + public override void TrySendInGameICMessage( EntityUid source, string message, InGameICChatType desiredType, - bool hideChat, bool hideLog = false, + bool hideChat, + bool hideLog = false, IConsoleShell? shell = null, - ICommonSession? player = null, string? nameOverride = null, + ICommonSession? player = null, + string? nameOverride = null, bool checkRadioPrefix = true, bool ignoreActionBlocker = false) { TrySendInGameICMessage(source, message, desiredType, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, hideLog, shell, player, nameOverride, checkRadioPrefix, ignoreActionBlocker); } - /// - /// Sends an in-character chat message to relevant clients. - /// - /// The entity that is speaking - /// The message being spoken or emoted - /// The chat type - /// Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all... - /// - /// The player doing the speaking - /// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised. - /// If set to true, action blocker will not be considered for whether an entity can send this message. - public void TrySendInGameICMessage( + /// + public override void TrySendInGameICMessage( EntityUid source, string message, InGameICChatType desiredType, @@ -238,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; @@ -260,7 +241,8 @@ public sealed partial class ChatSystem : SharedChatSystem } } - public void TrySendInGameOOCMessage( + /// + public override void TrySendInGameOOCMessage( EntityUid source, string message, InGameOOCChatType type, @@ -309,14 +291,8 @@ public sealed partial class ChatSystem : SharedChatSystem #region Announcements - /// - /// Dispatches an announcement to all. - /// - /// The contents of the message - /// The sender (Communications Console in Communications Console Announcement) - /// Play the announcement sound - /// Optional color for the announcement message - public void DispatchGlobalAnnouncement( + /// + public override void DispatchGlobalAnnouncement( string message, string? sender = null, bool playSound = true, @@ -335,17 +311,8 @@ public sealed partial class ChatSystem : SharedChatSystem _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Global station announcement from {sender}: {message}"); } - /// - /// Dispatches an announcement to players selected by filter. - /// - /// Filter to select players who will recieve the announcement - /// The contents of the message - /// The entity making the announcement (used to determine the station) - /// The sender (Communications Console in Communications Console Announcement) - /// Play the announcement sound - /// Sound to play - /// Optional color for the announcement message - public void DispatchFilteredAnnouncement( + /// + public override void DispatchFilteredAnnouncement( Filter filter, string message, EntityUid? source = null, @@ -365,15 +332,8 @@ public sealed partial class ChatSystem : SharedChatSystem _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement from {sender}: {message}"); } - /// - /// Dispatches an announcement on a specific station - /// - /// The entity making the announcement (used to determine the station) - /// The contents of the message - /// The sender (Communications Console in Communications Console Announcement) - /// Play the announcement sound - /// Optional color for the announcement message - public void DispatchStationAnnouncement( + /// + public override void DispatchStationAnnouncement( EntityUid source, string message, string? sender = null, @@ -571,7 +531,7 @@ public sealed partial class ChatSystem : SharedChatSystem } } - private void SendEntityEmote( + protected override void SendEntityEmote( EntityUid source, string action, ChatTransmitRange range, @@ -912,89 +872,3 @@ public sealed partial class ChatSystem : SharedChatSystem public record ExpandICChatRecipientsEvent(EntityUid Source, float VoiceRange, Dictionary Recipients) { } - -/// -/// Raised broadcast in order to transform speech.transmit -/// -public sealed class TransformSpeechEvent : EntityEventArgs -{ - public EntityUid Sender; - public string Message; - - public TransformSpeechEvent(EntityUid sender, string message) - { - Sender = sender; - Message = message; - } -} - -public sealed class CheckIgnoreSpeechBlockerEvent : EntityEventArgs -{ - public EntityUid Sender; - public bool IgnoreBlocker; - - public CheckIgnoreSpeechBlockerEvent(EntityUid sender, bool ignoreBlocker) - { - Sender = sender; - IgnoreBlocker = ignoreBlocker; - } -} - -/// -/// Raised on an entity when it speaks, either through 'say' or 'whisper'. -/// -public sealed class EntitySpokeEvent : EntityEventArgs -{ - public readonly EntityUid Source; - public readonly string Message; - public readonly string? ObfuscatedMessage; // not null if this was a whisper - - /// - /// If the entity was trying to speak into a radio, this was the channel they were trying to access. If a radio - /// message gets sent on this channel, this should be set to null to prevent duplicate messages. - /// - public RadioChannelPrototype? Channel; - - public EntitySpokeEvent(EntityUid source, string message, RadioChannelPrototype? channel, string? obfuscatedMessage) - { - Source = source; - Message = message; - Channel = channel; - ObfuscatedMessage = obfuscatedMessage; - } -} - -/// -/// InGame IC chat is for chat that is specifically ingame (not lobby) but is also in character, i.e. speaking. -/// -// ReSharper disable once InconsistentNaming -public enum InGameICChatType : byte -{ - Speak, - Emote, - Whisper -} - -/// -/// InGame OOC chat is for chat that is specifically ingame (not lobby) but is OOC, like deadchat or LOOC. -/// -public enum InGameOOCChatType : byte -{ - Looc, - Dead -} - -/// -/// Controls transmission of chat. -/// -public enum ChatTransmitRange : byte -{ - /// Acts normal, ghosts can hear across the map, etc. - Normal, - /// Normal but ghosts are still range-limited. - GhostRangeLimit, - /// Hidden from the chat window. - HideChat, - /// Ghosts can't hear or see it at all. Regular players can if in-range. - NoGhosts -} diff --git a/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs b/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs index 878c517d92..2b2be1e869 100644 --- a/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs +++ b/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs @@ -1,5 +1,8 @@ +using Content.Shared.Damage.Systems; + namespace Content.Server.Chat.Systems; +using Content.Shared.Chat; using Content.Shared.Chat.Prototypes; using Content.Shared.Damage; using Robust.Shared.Prototypes; diff --git a/Content.Server/Chemistry/Commands/DumpReagentGuideText.cs b/Content.Server/Chemistry/Commands/DumpReagentGuideText.cs index a70c2196ab..58c86f058a 100644 --- a/Content.Server/Chemistry/Commands/DumpReagentGuideText.cs +++ b/Content.Server/Chemistry/Commands/DumpReagentGuideText.cs @@ -39,7 +39,7 @@ public sealed class DumpReagentGuideText : LocalizedEntityCommands { foreach (var effect in entry.Effects) { - shell.WriteLine(effect.GuidebookEffectDescription(_prototype, EntityManager.EntitySysManager) ?? + shell.WriteLine(reagent.GuidebookReagentEffectDescription(_prototype, EntityManager.EntitySysManager, effect, entry.MetabolismRate) ?? Loc.GetString($"cmd-dumpreagentguidetext-skipped", ("effect", effect.GetType()))); } } diff --git a/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs b/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs index 3cba02a599..2d751ba441 100644 --- a/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs @@ -146,10 +146,10 @@ namespace Content.Server.Chemistry.EntitySystems { // force open container, if applicable, to avoid confusing people on why it doesn't dispense _openable.SetOpen(storedContainer, true); - _solutionTransferSystem.Transfer(reagentDispenser, + _solutionTransferSystem.Transfer(new SolutionTransferData(reagentDispenser, storedContainer, src.Value, outputContainer.Value, dst.Value, - (int)reagentDispenser.Comp.DispenseAmount); + (int)reagentDispenser.Comp.DispenseAmount)); } UpdateUiState(reagentDispenser); diff --git a/Content.Server/Cloning/CloningPodSystem.cs b/Content.Server/Cloning/CloningPodSystem.cs index 588b0c75a5..67d6dc39a6 100644 --- a/Content.Server/Cloning/CloningPodSystem.cs +++ b/Content.Server/Cloning/CloningPodSystem.cs @@ -11,7 +11,8 @@ using Content.Shared.Atmos; using Content.Shared.CCVar; using Content.Shared.Chemistry.Components; using Content.Shared.Cloning; -using Content.Shared.Damage; +using Content.Shared.Chat; +using Content.Shared.Damage.Components; using Content.Shared.DeviceLinking.Events; using Content.Shared.Emag.Components; using Content.Shared.Emag.Systems; @@ -28,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; @@ -57,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() { @@ -301,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.Subscriptions.cs b/Content.Server/Cloning/CloningSystem.Subscriptions.cs index 84ef050305..a05c7069f0 100644 --- a/Content.Server/Cloning/CloningSystem.Subscriptions.cs +++ b/Content.Server/Cloning/CloningSystem.Subscriptions.cs @@ -60,7 +60,7 @@ public sealed partial class CloningSystem { // if the clone is a stack as well, adjust the count of the copy if (TryComp(args.CloneUid, out var cloneStackComp)) - _stack.SetCount(args.CloneUid, ent.Comp.Count, cloneStackComp); + _stack.SetCount((args.CloneUid, cloneStackComp), ent.Comp.Count); } private void OnCloneItemLabel(Entity ent, ref CloningItemEvent args) 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 e51a01a1d4..97d7005279 100644 --- a/Content.Server/Cluwne/CluwneSystem.cs +++ b/Content.Server/Cluwne/CluwneSystem.cs @@ -1,20 +1,21 @@ -using Content.Server.Popups; -using Content.Shared.Popups; -using Content.Shared.Mobs; using Content.Server.Chat; using Content.Server.Chat.Systems; -using Content.Server.Clothing.Systems; -using Content.Shared.Chat.Prototypes; -using Robust.Shared.Random; -using Content.Shared.Stunnable; -using Content.Shared.Damage; -using Robust.Shared.Prototypes; using Content.Server.Emoting.Systems; +using Content.Server.Clothing.Systems; +using Content.Server.Popups; using Content.Server.Speech.EntitySystems; -using Content.Shared.Cluwne; -using Robust.Shared.Audio.Systems; -using Content.Shared.NameModifier.EntitySystems; +using Content.Shared.Chat; +using Content.Shared.Chat.Prototypes; using Content.Shared.Clumsy; +using Content.Shared.Cluwne; +using Content.Shared.Damage.Systems; +using Content.Shared.Mobs; +using Content.Shared.NameModifier.EntitySystems; +using Content.Shared.Popups; +using Content.Shared.Stunnable; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Random; +using Robust.Shared.Prototypes; namespace Content.Server.Cluwne; @@ -75,7 +76,7 @@ public sealed class CluwneSystem : EntitySystem EnsureComp(ent.Owner); _autoEmote.AddEmote(ent.Owner, ent.Comp.AutoEmoteId); } - + EnsureComp(ent.Owner); var transformMessage = Loc.GetString(ent.Comp.TransformMessage, ("target", ent.Owner)); diff --git a/Content.Server/Construction/Completions/GivePrototype.cs b/Content.Server/Construction/Completions/GivePrototype.cs index f05feb70c0..22c5473c8d 100644 --- a/Content.Server/Construction/Completions/GivePrototype.cs +++ b/Content.Server/Construction/Completions/GivePrototype.cs @@ -27,14 +27,14 @@ public sealed partial class GivePrototype : IGraphAction if (EntityPrototypeHelpers.HasComponent(Prototype)) { var stackSystem = entityManager.EntitySysManager.GetEntitySystem(); - var stacks = stackSystem.SpawnMultiple(Prototype, Amount, userUid ?? uid); + var stacks = stackSystem.SpawnMultipleNextToOrDrop(Prototype, Amount, userUid ?? uid); if (userUid is null || !entityManager.TryGetComponent(userUid, out HandsComponent? handsComp)) return; foreach (var item in stacks) { - stackSystem.TryMergeToHands(item, userUid.Value, hands: handsComp); + stackSystem.TryMergeToHands(item, (userUid.Value, handsComp)); } } else diff --git a/Content.Server/Construction/Completions/SetStackCount.cs b/Content.Server/Construction/Completions/SetStackCount.cs index f1e3f9fb9e..409d6abf95 100644 --- a/Content.Server/Construction/Completions/SetStackCount.cs +++ b/Content.Server/Construction/Completions/SetStackCount.cs @@ -12,7 +12,7 @@ namespace Content.Server.Construction.Completions public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - entityManager.EntitySysManager.GetEntitySystem().SetCount(uid, Amount); + entityManager.EntitySysManager.GetEntitySystem().SetCount((uid, null), Amount); } } } diff --git a/Content.Server/Construction/Completions/SpawnPrototype.cs b/Content.Server/Construction/Completions/SpawnPrototype.cs index c42ecb611f..5aca29edf4 100644 --- a/Content.Server/Construction/Completions/SpawnPrototype.cs +++ b/Content.Server/Construction/Completions/SpawnPrototype.cs @@ -28,7 +28,7 @@ namespace Content.Server.Construction.Completions { var stackEnt = entityManager.SpawnEntity(Prototype, coordinates); var stack = entityManager.GetComponent(stackEnt); - entityManager.EntitySysManager.GetEntitySystem().SetCount(stackEnt, Amount, stack); + entityManager.EntitySysManager.GetEntitySystem().SetCount((stackEnt, stack), Amount); } else { 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/Construction/ConstructionSystem.Initial.cs b/Content.Server/Construction/ConstructionSystem.Initial.cs index 3739951a6f..98b1da034e 100644 --- a/Content.Server/Construction/ConstructionSystem.Initial.cs +++ b/Content.Server/Construction/ConstructionSystem.Initial.cs @@ -187,7 +187,7 @@ namespace Content.Server.Construction // TODO allow taking from several stacks. // Also update crafting steps to check if it works. - var splitStack = _stackSystem.Split(entity, materialStep.Amount, user.ToCoordinates(0, 0), stack); + var splitStack = _stackSystem.Split((entity, stack), materialStep.Amount, user.ToCoordinates(0, 0)); if (splitStack == null) continue; diff --git a/Content.Server/Construction/ConstructionSystem.Interactions.cs b/Content.Server/Construction/ConstructionSystem.Interactions.cs index 3a833f57a2..0ec47e2bd2 100644 --- a/Content.Server/Construction/ConstructionSystem.Interactions.cs +++ b/Content.Server/Construction/ConstructionSystem.Interactions.cs @@ -13,6 +13,7 @@ using Content.Shared.Prying.Systems; using Content.Shared.Radio.EntitySystems; using Content.Shared.Stacks; using Content.Shared.Temperature; +using Content.Shared.Temperature.Components; using Content.Shared.Tools.Systems; using Robust.Shared.Containers; using Robust.Shared.Utility; diff --git a/Content.Server/Construction/ConstructionSystem.Machine.cs b/Content.Server/Construction/ConstructionSystem.Machine.cs index eb922f198c..ce7f17f9b9 100644 --- a/Content.Server/Construction/ConstructionSystem.Machine.cs +++ b/Content.Server/Construction/ConstructionSystem.Machine.cs @@ -49,7 +49,7 @@ public sealed partial class ConstructionSystem foreach (var (stackType, amount) in machineBoard.StackRequirements) { - var stack = _stackSystem.Spawn(amount, stackType, xform.Coordinates); + var stack = _stackSystem.SpawnAtPosition(amount, stackType, xform.Coordinates); if (!_container.Insert(stack, partContainer)) throw new Exception($"Couldn't insert machine material of type {stackType} to machine with prototype {Prototype(uid)?.ID ?? "N/A"}"); } diff --git a/Content.Server/Construction/MachineFrameSystem.cs b/Content.Server/Construction/MachineFrameSystem.cs index b8624aeef2..3af1c6ab5f 100644 --- a/Content.Server/Construction/MachineFrameSystem.cs +++ b/Content.Server/Construction/MachineFrameSystem.cs @@ -182,7 +182,7 @@ public sealed class MachineFrameSystem : EntitySystem return true; } - var splitStack = _stack.Split(used, needed, Transform(uid).Coordinates, stack); + var splitStack = _stack.Split((used, stack), needed, Transform(uid).Coordinates); if (splitStack == null) return false; diff --git a/Content.Server/Crayon/CrayonSystem.cs b/Content.Server/Crayon/CrayonSystem.cs index 07b580fba5..e6f96c5abe 100644 --- a/Content.Server/Crayon/CrayonSystem.cs +++ b/Content.Server/Crayon/CrayonSystem.cs @@ -34,8 +34,8 @@ public sealed class CrayonSystem : SharedCrayonSystem SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnCrayonBoundUI); SubscribeLocalEvent(OnCrayonBoundUIColor); - SubscribeLocalEvent(OnCrayonUse, before: new[] { typeof(FoodSystem) }); - SubscribeLocalEvent(OnCrayonAfterInteract, after: new[] { typeof(FoodSystem) }); + SubscribeLocalEvent(OnCrayonUse); + SubscribeLocalEvent(OnCrayonAfterInteract, after: [typeof(IngestionSystem)]); SubscribeLocalEvent(OnCrayonDropped); } @@ -47,6 +47,7 @@ public sealed class CrayonSystem : SharedCrayonSystem Dirty(ent); } + // Runs after IngestionSystem so it doesn't bulldoze force-feeding private void OnCrayonAfterInteract(EntityUid uid, CrayonComponent component, AfterInteractEvent args) { if (args.Handled || !args.CanReach) diff --git a/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs b/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs index 1b7e50c651..a655556d49 100644 --- a/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs +++ b/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs @@ -87,7 +87,8 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS { // prevent malf client violating wanted/reason nullability if (msg.Status == SecurityStatus.Wanted != (msg.Reason != null) && - msg.Status == SecurityStatus.Suspected != (msg.Reason != null)) + msg.Status == SecurityStatus.Suspected != (msg.Reason != null) && + msg.Status == SecurityStatus.Hostile != (msg.Reason != null)) return; if (!CheckSelected(ent, msg.Actor, out var mob, out var key)) @@ -144,6 +145,8 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS // figure out which radio message to send depending on transition var statusString = (oldStatus, msg.Status) switch { + (_, SecurityStatus.Hostile) => "hostile", + (_, SecurityStatus.Eliminated) => "eliminated", // person has been detained (_, SecurityStatus.Detained) => "detained", // person did something sus @@ -154,6 +157,8 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS (_, SecurityStatus.Discharged) => "released", // going from any other state to wanted, AOS or prisonbreak / lazy secoff never set them to released and they reoffended (_, SecurityStatus.Wanted) => "wanted", + (SecurityStatus.Hostile, SecurityStatus.None) => "not-hostile", + (SecurityStatus.Eliminated, SecurityStatus.None) => "not-eliminated", // person is no longer sus (SecurityStatus.Suspected, SecurityStatus.None) => "not-suspected", // going from wanted to none, must have been a mistake 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/Delivery/DeliverySystem.cs b/Content.Server/Delivery/DeliverySystem.cs index 72d9427dff..496bc61a34 100644 --- a/Content.Server/Delivery/DeliverySystem.cs +++ b/Content.Server/Delivery/DeliverySystem.cs @@ -4,6 +4,7 @@ using Content.Server.Station.Systems; using Content.Server.StationRecords.Systems; using Content.Shared.Cargo.Components; using Content.Shared.Cargo.Prototypes; +using Content.Shared.Chat; using Content.Shared.Delivery; using Content.Shared.FingerprintReader; using Content.Shared.Labels.EntitySystems; 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 b4a79c6830..7ed736fffd 100644 --- a/Content.Server/Destructible/DestructibleSystem.cs +++ b/Content.Server/Destructible/DestructibleSystem.cs @@ -6,14 +6,15 @@ using Content.Server.Body.Systems; using Content.Server.Construction; using Content.Server.Destructible.Thresholds; using Content.Server.Destructible.Thresholds.Behaviors; -using Content.Server.Destructible.Thresholds.Triggers; using Content.Server.Explosion.EntitySystems; 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; using Content.Shared.FixedPoint; using Content.Shared.Humanoid; using Content.Shared.Trigger.Systems; @@ -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; @@ -42,24 +43,24 @@ namespace Content.Server.Destructible [Dependency] public readonly PuddleSystem PuddleSystem = default!; [Dependency] public readonly SharedContainerSystem ContainerSystem = default!; [Dependency] public readonly IPrototypeManager PrototypeManager = default!; - [Dependency] public readonly IAdminLogManager _adminLogger = default!; + [Dependency] public readonly IAdminLogManager AdminLogger = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(Execute); + SubscribeLocalEvent(OnDamageChanged); } /// - /// Check if any thresholds were reached. if they were, execute them. + /// Check if any thresholds were reached. if they were, execute them. /// - public void Execute(EntityUid uid, DestructibleComponent component, DamageChangedEvent args) + private void OnDamageChanged(EntityUid uid, DestructibleComponent component, DamageChangedEvent args) { component.IsBroken = false; foreach (var threshold in component.Thresholds) { - if (threshold.Reached(args.Damageable, this)) + if (Triggered(threshold, (uid, args.Damageable))) { RaiseLocalEvent(uid, new DamageThresholdReached(component, threshold), true); @@ -82,18 +83,18 @@ namespace Content.Server.Destructible if (args.Origin != null) { - _adminLogger.Add(LogType.Damaged, + AdminLogger.Add(LogType.Damaged, logImpact, $"{ToPrettyString(args.Origin.Value):actor} caused {ToPrettyString(uid):subject} to trigger [{triggeredBehaviors}]"); } else { - _adminLogger.Add(LogType.Damaged, + AdminLogger.Add(LogType.Damaged, logImpact, $"Unknown damage source caused {ToPrettyString(uid):subject} to trigger [{triggeredBehaviors}]"); } - threshold.Execute(uid, this, EntityManager, args.Origin); + Execute(threshold, uid, args.Origin); } if (threshold.OldTriggered) @@ -108,6 +109,61 @@ namespace Content.Server.Destructible } } + /// + /// Check if the given threshold should trigger. + /// + public bool Triggered(DamageThreshold threshold, Entity owner) + { + if (threshold.Trigger == null) + return false; + + if (threshold.Triggered && threshold.TriggersOnce) + return false; + + if (threshold.OldTriggered) + { + threshold.OldTriggered = threshold.Trigger.Reached(owner, this); + return false; + } + + if (!threshold.Trigger.Reached(owner, this)) + return false; + + threshold.OldTriggered = true; + return true; + } + + /// + /// Check if the conditions for the given threshold are currently true. + /// + public bool Reached(DamageThreshold threshold, Entity owner) + { + if (threshold.Trigger == null) + return false; + + return threshold.Trigger.Reached(owner, this); + } + + /// + /// Triggers this threshold. + /// + /// The entity that owns this threshold. + /// The entity that caused this threshold to trigger. + public void Execute(DamageThreshold threshold, EntityUid owner, EntityUid? cause = null) + { + threshold.Triggered = true; + + foreach (var behavior in threshold.Behaviors) + { + // The owner has been deleted. We stop execution of behaviors here. + if (!Exists(owner)) + return; + + // TODO: Replace with EntityEffects. + behavior.Execute(owner, this, cause); + } + } + public bool TryGetDestroyedAt(Entity ent, [NotNullWhen(true)] out FixedPoint2? destroyedAt) { destroyedAt = null; @@ -145,7 +201,7 @@ namespace Content.Server.Destructible if (behavior is DoActsBehavior actBehavior && actBehavior.HasAct(ThresholdActs.Destruction | ThresholdActs.Breakage)) { - damageNeeded = Math.Min(damageNeeded.Float(), trigger.Damage); + damageNeeded = FixedPoint2.Min(damageNeeded, trigger.Damage); } } } diff --git a/Content.Server/Destructible/Thresholds/ActsFlags.cs b/Content.Server/Destructible/Thresholds/ActsFlags.cs deleted file mode 100644 index 5193499aa9..0000000000 --- a/Content.Server/Destructible/Thresholds/ActsFlags.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Content.Server.Destructible.Thresholds -{ - public sealed class ActsFlags { } -} diff --git a/Content.Server/Destructible/Thresholds/Behaviors/DoActsBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/DoActsBehavior.cs index bfebae8f92..1086be4958 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/DoActsBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/DoActsBehavior.cs @@ -1,4 +1,6 @@ -namespace Content.Server.Destructible.Thresholds.Behaviors +using Content.Shared.Destructible; + +namespace Content.Server.Destructible.Thresholds.Behaviors { [Serializable] [DataDefinition] diff --git a/Content.Server/Destructible/Thresholds/Behaviors/DumpRestockInventory.cs b/Content.Server/Destructible/Thresholds/Behaviors/DumpRestockInventory.cs index a8448a1b7f..5646ce6d88 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/DumpRestockInventory.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/DumpRestockInventory.cs @@ -43,7 +43,7 @@ namespace Content.Server.Destructible.Thresholds.Behaviors if (EntityPrototypeHelpers.HasComponent(entityId, system.PrototypeManager, system.EntityManager.ComponentFactory)) { var spawned = system.EntityManager.SpawnEntity(entityId, xform.Coordinates.Offset(system.Random.NextVector2(-Offset, Offset))); - system.StackSystem.SetCount(spawned, toSpawn); + system.StackSystem.SetCount((spawned, null), toSpawn); system.EntityManager.GetComponent(spawned).LocalRotation = system.Random.NextAngle(); } else diff --git a/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs index 413991515b..13027a31fc 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs @@ -58,7 +58,7 @@ namespace Content.Server.Destructible.Thresholds.Behaviors var spawned = SpawnInContainer ? system.EntityManager.SpawnNextToOrDrop(entityId, owner) : system.EntityManager.SpawnEntity(entityId, position.Offset(getRandomVector())); - system.StackSystem.SetCount(spawned, count); + system.StackSystem.SetCount((spawned, null), count); TransferForensics(spawned, system, owner); } diff --git a/Content.Server/Destructible/Thresholds/DamageThreshold.cs b/Content.Server/Destructible/Thresholds/DamageThreshold.cs index e180f5c45c..de4362d677 100644 --- a/Content.Server/Destructible/Thresholds/DamageThreshold.cs +++ b/Content.Server/Destructible/Thresholds/DamageThreshold.cs @@ -1,96 +1,43 @@ using Content.Server.Destructible.Thresholds.Behaviors; -using Content.Server.Destructible.Thresholds.Triggers; -using Content.Shared.Damage; +using Content.Shared.Destructible.Thresholds.Triggers; -namespace Content.Server.Destructible.Thresholds +namespace Content.Server.Destructible.Thresholds; + +[DataDefinition] +public sealed partial class DamageThreshold { - [DataDefinition] - public sealed partial class DamageThreshold - { - [DataField("behaviors")] - private List _behaviors = new(); + /// + /// Whether or not this threshold was triggered in the previous call to + /// . + /// + [ViewVariables] public bool OldTriggered; - /// - /// Whether or not this threshold was triggered in the previous call to - /// . - /// - [ViewVariables] public bool OldTriggered { get; private set; } + /// + /// Whether or not this threshold has already been triggered. + /// + [DataField] + public bool Triggered; - /// - /// Whether or not this threshold has already been triggered. - /// - [DataField("triggered")] - public bool Triggered { get; private set; } + /// + /// Whether or not this threshold only triggers once. + /// If false, it will trigger again once the entity is healed + /// and then damaged to reach this threshold once again. + /// It will not repeatedly trigger as damage rises beyond that. + /// + [DataField] + public bool TriggersOnce; - /// - /// Whether or not this threshold only triggers once. - /// If false, it will trigger again once the entity is healed - /// and then damaged to reach this threshold once again. - /// It will not repeatedly trigger as damage rises beyond that. - /// - [DataField("triggersOnce")] - public bool TriggersOnce { get; set; } + /// + /// The condition that decides if this threshold has been reached. + /// Gets evaluated each time the entity's damage changes. + /// + [DataField] + public IThresholdTrigger? Trigger; - /// - /// The trigger that decides if this threshold has been reached. - /// - [DataField("trigger")] - public IThresholdTrigger? Trigger { get; set; } - - /// - /// Behaviors to activate once this threshold is triggered. - /// - [ViewVariables] public IReadOnlyList Behaviors => _behaviors; - - public bool Reached(DamageableComponent damageable, DestructibleSystem system) - { - if (Trigger == null) - { - return false; - } - - if (Triggered && TriggersOnce) - { - return false; - } - - if (OldTriggered) - { - OldTriggered = Trigger.Reached(damageable, system); - return false; - } - - if (!Trigger.Reached(damageable, system)) - { - return false; - } - - OldTriggered = true; - return true; - } - - /// - /// Triggers this threshold. - /// - /// The entity that owns this threshold. - /// - /// An instance of to get dependency and - /// system references from, if relevant. - /// - /// - /// - public void Execute(EntityUid owner, DestructibleSystem system, IEntityManager entityManager, EntityUid? cause) - { - Triggered = true; - - foreach (var behavior in Behaviors) - { - // The owner has been deleted. We stop execution of behaviors here. - if (!entityManager.EntityExists(owner)) - return; - - behavior.Execute(owner, system, cause); - } - } - } + /// + /// Behaviors to activate once this threshold is triggered. + /// TODO: Replace with EntityEffects. + /// + [DataField] + public List Behaviors = new(); } diff --git a/Content.Server/Destructible/Thresholds/ThresholdActs.cs b/Content.Server/Destructible/Thresholds/ThresholdActs.cs deleted file mode 100644 index d2ecd53742..0000000000 --- a/Content.Server/Destructible/Thresholds/ThresholdActs.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Robust.Shared.Serialization; - -namespace Content.Server.Destructible.Thresholds -{ - [Flags, FlagsFor(typeof(ActsFlags))] - [Serializable] - public enum ThresholdActs - { - None = 0, - Breakage, - Destruction - } -} diff --git a/Content.Server/Destructible/Thresholds/Triggers/AndTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/AndTrigger.cs deleted file mode 100644 index 70e0c3917c..0000000000 --- a/Content.Server/Destructible/Thresholds/Triggers/AndTrigger.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Content.Shared.Damage; - -namespace Content.Server.Destructible.Thresholds.Triggers -{ - /// - /// A trigger that will activate when all of its triggers have activated. - /// - [Serializable] - [DataDefinition] - public sealed partial class AndTrigger : IThresholdTrigger - { - [DataField("triggers")] - public List Triggers { get; set; } = new(); - - public bool Reached(DamageableComponent damageable, DestructibleSystem system) - { - foreach (var trigger in Triggers) - { - if (!trigger.Reached(damageable, system)) - { - return false; - } - } - - return true; - } - } -} diff --git a/Content.Server/Destructible/Thresholds/Triggers/DamageGroupTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/DamageGroupTrigger.cs deleted file mode 100644 index 09083105ca..0000000000 --- a/Content.Server/Destructible/Thresholds/Triggers/DamageGroupTrigger.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Content.Shared.Damage; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Content.Shared.Damage.Prototypes; - -namespace Content.Server.Destructible.Thresholds.Triggers -{ - /// - /// A trigger that will activate when the amount of damage received - /// of the specified class is above the specified threshold. - /// - [Serializable] - [DataDefinition] - public sealed partial class DamageGroupTrigger : IThresholdTrigger - { - [DataField("damageGroup", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string DamageGroup { get; set; } = default!; - - /// - /// The amount of damage at which this threshold will trigger. - /// - [DataField("damage", required: true)] - public int Damage { get; set; } = default!; - - public bool Reached(DamageableComponent damageable, DestructibleSystem system) - { - return damageable.DamagePerGroup[DamageGroup] >= Damage; - } - } -} diff --git a/Content.Server/Destructible/Thresholds/Triggers/DamageTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/DamageTrigger.cs deleted file mode 100644 index ce0106ee2b..0000000000 --- a/Content.Server/Destructible/Thresholds/Triggers/DamageTrigger.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Content.Shared.Damage; - -namespace Content.Server.Destructible.Thresholds.Triggers -{ - /// - /// A trigger that will activate when the amount of damage received - /// is above the specified threshold. - /// - [Serializable] - [DataDefinition] - public sealed partial class DamageTrigger : IThresholdTrigger - { - /// - /// The amount of damage at which this threshold will trigger. - /// - [DataField("damage", required: true)] - public int Damage { get; set; } = default!; - - public bool Reached(DamageableComponent damageable, DestructibleSystem system) - { - return damageable.TotalDamage >= Damage; - } - } -} diff --git a/Content.Server/Destructible/Thresholds/Triggers/DamageTypeTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/DamageTypeTrigger.cs deleted file mode 100644 index ed9fd27ab7..0000000000 --- a/Content.Server/Destructible/Thresholds/Triggers/DamageTypeTrigger.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Content.Shared.Damage; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Content.Shared.Damage.Prototypes; - -namespace Content.Server.Destructible.Thresholds.Triggers -{ - /// - /// A trigger that will activate when the amount of damage received - /// of the specified type is above the specified threshold. - /// - [Serializable] - [DataDefinition] - public sealed partial class DamageTypeTrigger : IThresholdTrigger - { - [DataField("damageType", required:true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string DamageType { get; set; } = default!; - - [DataField("damage", required: true)] - public int Damage { get; set; } = default!; - - public bool Reached(DamageableComponent damageable, DestructibleSystem system) - { - return damageable.Damage.DamageDict.TryGetValue(DamageType, out var damageReceived) && - damageReceived >= Damage; - } - } -} diff --git a/Content.Server/Destructible/Thresholds/Triggers/IThresholdTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/IThresholdTrigger.cs deleted file mode 100644 index dfceb31c9b..0000000000 --- a/Content.Server/Destructible/Thresholds/Triggers/IThresholdTrigger.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Content.Shared.Damage; - -namespace Content.Server.Destructible.Thresholds.Triggers -{ - public interface IThresholdTrigger - { - /// - /// Checks if this trigger has been reached. - /// - /// The damageable component to check with. - /// - /// An instance of to pull - /// dependencies from, if any. - /// - /// true if this trigger has been reached, false otherwise. - bool Reached(DamageableComponent damageable, DestructibleSystem system); - } -} diff --git a/Content.Server/Destructible/Thresholds/Triggers/OrTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/OrTrigger.cs deleted file mode 100644 index 179141dbf8..0000000000 --- a/Content.Server/Destructible/Thresholds/Triggers/OrTrigger.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Content.Shared.Damage; - -namespace Content.Server.Destructible.Thresholds.Triggers -{ - /// - /// A trigger that will activate when any of its triggers have activated. - /// - [Serializable] - [DataDefinition] - public sealed partial class OrTrigger : IThresholdTrigger - { - [DataField("triggers")] - public List Triggers { get; private set; } = new(); - - public bool Reached(DamageableComponent damageable, DestructibleSystem system) - { - foreach (var trigger in Triggers) - { - if (trigger.Reached(damageable, system)) - { - return true; - } - } - - return false; - } - } -} diff --git a/Content.Server/DeviceLinking/Systems/PowerSensorSystem.cs b/Content.Server/DeviceLinking/Systems/PowerSensorSystem.cs index b03cceda0f..1cb5091d24 100644 --- a/Content.Server/DeviceLinking/Systems/PowerSensorSystem.cs +++ b/Content.Server/DeviceLinking/Systems/PowerSensorSystem.cs @@ -98,28 +98,19 @@ public sealed class PowerSensorSystem : EntitySystem var nodeContainer = Comp(uid); var deviceNode = (CableDeviceNode) nodeContainer.Nodes[cable.Node]; - var charge = 0f; - var chargingState = false; - var dischargingState = false; - // update state based on the power stats retrieved from the selected power network var xform = _xformQuery.GetComponent(uid); if (!TryComp(xform.GridUid, out MapGridComponent? grid)) return; - var cables = deviceNode.GetReachableNodes(xform, _nodeQuery, _xformQuery, grid, EntityManager); - foreach (var node in cables) - { - if (node.NodeGroup == null) - continue; + if (deviceNode.NodeGroup == null) + return; - var group = (IBasePowerNet) node.NodeGroup; - var stats = _powerNet.GetNetworkStatistics(group.NetworkNode); - charge = comp.Output ? stats.OutStorageCurrent : stats.InStorageCurrent; - chargingState = charge > comp.LastCharge; - dischargingState = charge < comp.LastCharge; - break; - } + var group = (IBasePowerNet) deviceNode.NodeGroup; + var stats = _powerNet.GetNetworkStatistics(group.NetworkNode); + var charge = comp.Output ? stats.OutStorageCurrent : stats.InStorageCurrent; + var chargingState = charge > comp.LastCharge; + var dischargingState = charge < comp.LastCharge; comp.LastCharge = charge; 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/Emoting/Systems/BodyEmotesSystem.cs b/Content.Server/Emoting/Systems/BodyEmotesSystem.cs index aef79f1419..39217a8196 100644 --- a/Content.Server/Emoting/Systems/BodyEmotesSystem.cs +++ b/Content.Server/Emoting/Systems/BodyEmotesSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Server.Emoting.Components; +using Content.Shared.Chat; using Content.Shared.Chat.Prototypes; using Content.Shared.Hands.Components; using Robust.Shared.Prototypes; diff --git a/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs b/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs index 743646c92b..82e2d9c30d 100644 --- a/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs +++ b/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs @@ -63,8 +63,8 @@ namespace Content.Server.Engineering.EntitySystems if (component.Deleted || !IsTileClear()) return; - if (TryComp(uid, out StackComponent? stackComp) - && component.RemoveOnInteract && !_stackSystem.Use(uid, 1, stackComp)) + if (TryComp(uid, out var stackComp) + && component.RemoveOnInteract && !_stackSystem.TryUse((uid, stackComp), 1)) { return; } diff --git a/Content.Server/EntityConditions/Conditions/BreathingEntityConditionSystem.cs b/Content.Server/EntityConditions/Conditions/BreathingEntityConditionSystem.cs new file mode 100644 index 0000000000..e7b8aaf22b --- /dev/null +++ b/Content.Server/EntityConditions/Conditions/BreathingEntityConditionSystem.cs @@ -0,0 +1,19 @@ +using Content.Server.Body.Components; +using Content.Server.Body.Systems; +using Content.Shared.EntityConditions; +using Content.Shared.EntityConditions.Conditions.Body; + +namespace Content.Server.EntityConditions.Conditions; + +/// +/// Returns true if this entity is both able to breathe and is currently breathing. +/// +/// +public sealed partial class IsBreathingEntityConditionSystem : EntityConditionSystem +{ + [Dependency] private readonly RespiratorSystem _respirator = default!; + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + args.Result = _respirator.IsBreathing(entity.AsNullable()); + } +} diff --git a/Content.Server/EntityConditions/Conditions/MetabolizerTypesEntityConditionSystem.cs b/Content.Server/EntityConditions/Conditions/MetabolizerTypesEntityConditionSystem.cs new file mode 100644 index 0000000000..3b4fb5292b --- /dev/null +++ b/Content.Server/EntityConditions/Conditions/MetabolizerTypesEntityConditionSystem.cs @@ -0,0 +1,21 @@ +using System.Linq; +using Content.Server.Body.Components; +using Content.Shared.EntityConditions; +using Content.Shared.EntityConditions.Conditions.Body; + +namespace Content.Server.EntityConditions.Conditions; + +/// +/// Returns true if this entity has any of the listed metabolizer types. +/// +/// +public sealed partial class MetabolizerTypeEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + if (entity.Comp.MetabolizerTypes == null) + return; + + args.Result = entity.Comp.MetabolizerTypes.Overlaps(args.Condition.Type); + } +} diff --git a/Content.Server/EntityEffects/Effects/Atmos/CreateGasEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Atmos/CreateGasEntityEffectSystem.cs new file mode 100644 index 0000000000..033704ffcd --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Atmos/CreateGasEntityEffectSystem.cs @@ -0,0 +1,22 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Atmos; + +namespace Content.Server.EntityEffects.Effects.Atmos; + +/// +/// This effect adjusts a gas at the tile this entity is currently on. +/// The amount changed is modified by scale. +/// +/// +public sealed partial class CreateGasEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly AtmosphereSystem _atmosphere = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var tileMix = _atmosphere.GetContainingMixture(entity.AsNullable(), false, true); + + tileMix?.AdjustMoles(args.Effect.Gas, args.Scale * args.Effect.Moles); + } +} diff --git a/Content.Server/EntityEffects/Effects/Atmos/FlammableEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Atmos/FlammableEntityEffectSystem.cs new file mode 100644 index 0000000000..65c818f143 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Atmos/FlammableEntityEffectSystem.cs @@ -0,0 +1,25 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Shared.Atmos.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Atmos; + +namespace Content.Server.EntityEffects.Effects.Atmos; + +/// +/// Adds a number of FireStacks modified by scale to this entity. +/// The amount of FireStacks added is modified by scale. +/// +/// +public sealed partial class FlammableEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly FlammableSystem _flammable = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + // The multiplier is determined by if the entity is already on fire, and if the multiplier for existing FireStacks has a value. + // If both of these are true, we use the MultiplierOnExisting value, otherwise we use the standard Multiplier. + var multiplier = entity.Comp.FireStacks == 0f || args.Effect.MultiplierOnExisting == null ? args.Effect.Multiplier : args.Effect.MultiplierOnExisting.Value; + + _flammable.AdjustFireStacks(entity, args.Scale * multiplier, entity.Comp); + } +} diff --git a/Content.Server/EntityEffects/Effects/Atmos/IgniteEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Atmos/IgniteEntityEffectSystem.cs new file mode 100644 index 0000000000..de90656c66 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Atmos/IgniteEntityEffectSystem.cs @@ -0,0 +1,23 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Shared.Atmos.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Atmos; + +namespace Content.Server.EntityEffects.Effects.Atmos; + +/// +/// Sets this entity on fire. +/// +/// +public sealed partial class IngiteEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly FlammableSystem _flammable = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + // TODO: Proper BodySystem Metabolism Effect relay... + // TODO: If this fucks over downstream shitmed, I give you full approval to use whatever shitcode method you need to fix it. Metabolism is awful. + _flammable.Ignite(entity, entity, flammable: entity.Comp); + } +} + diff --git a/Content.Server/EntityEffects/Effects/Body/OxygenateEntityEffectsSystem.cs b/Content.Server/EntityEffects/Effects/Body/OxygenateEntityEffectsSystem.cs new file mode 100644 index 0000000000..0cbf0b3864 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Body/OxygenateEntityEffectsSystem.cs @@ -0,0 +1,20 @@ +using Content.Server.Body.Components; +using Content.Server.Body.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Body; + +namespace Content.Server.EntityEffects.Effects.Body; + +/// +/// This effect adjusts a respirator's saturation value. +/// The saturation adjustment is modified by scale. +/// +/// +public sealed partial class OxygenateEntityEffectsSystem : EntityEffectSystem +{ + [Dependency] private readonly RespiratorSystem _respirator = default!; + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _respirator.UpdateSaturation(entity, args.Scale * args.Effect.Factor, entity.Comp); + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustHealthEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustHealthEntityEffectSystem.cs new file mode 100644 index 0000000000..ab656b8aa4 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustHealthEntityEffectSystem.cs @@ -0,0 +1,20 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustHealthEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly PlantHolderSystem _plantHolder = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + entity.Comp.Health += args.Effect.Amount; + _plantHolder.CheckHealth(entity, entity.Comp); + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationLevelEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationLevelEntityEffectSystem.cs new file mode 100644 index 0000000000..4732465098 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationLevelEntityEffectSystem.cs @@ -0,0 +1,20 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustMutationLevelEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly PlantHolderSystem _plantHolder = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + entity.Comp.MutationLevel += args.Effect.Amount * entity.Comp.MutationMod; + _plantHolder.CheckHealth(entity, entity.Comp); + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationModEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationModEntityEffectSystem.cs new file mode 100644 index 0000000000..3163ee374c --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationModEntityEffectSystem.cs @@ -0,0 +1,16 @@ +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustMutationModEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + entity.Comp.MutationMod += args.Effect.Amount; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustNutritionEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustNutritionEntityEffectSystem.cs new file mode 100644 index 0000000000..56c016700d --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustNutritionEntityEffectSystem.cs @@ -0,0 +1,16 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustNutritionEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly PlantHolderSystem _plantHolder = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _plantHolder.AdjustNutrient(entity, args.Effect.Amount, entity); + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPestsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPestsEntityEffectSystem.cs new file mode 100644 index 0000000000..0495034b38 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPestsEntityEffectSystem.cs @@ -0,0 +1,16 @@ +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustPestsEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + entity.Comp.PestLevel += args.Effect.Amount; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotencyEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotencyEntityEffectSystem.cs new file mode 100644 index 0000000000..ebe5c83181 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotencyEntityEffectSystem.cs @@ -0,0 +1,19 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustPotencyEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly PlantHolderSystem _plantHolder = default!; + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + _plantHolder.EnsureUniqueSeed(entity, entity.Comp); + entity.Comp.Seed.Potency = Math.Max(entity.Comp.Seed.Potency + args.Effect.Amount, 1); + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustToxinsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustToxinsEntityEffectSystem.cs new file mode 100644 index 0000000000..31dc328977 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustToxinsEntityEffectSystem.cs @@ -0,0 +1,16 @@ +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustToxinsEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + entity.Comp.Toxins += args.Effect.Amount; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWaterEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWaterEntityEffectSystem.cs new file mode 100644 index 0000000000..706eeb2ffe --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWaterEntityEffectSystem.cs @@ -0,0 +1,16 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustWaterEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly PlantHolderSystem _plantHolder = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _plantHolder.AdjustWater(entity, args.Effect.Amount, entity.Comp); + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWeedsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWeedsEntityEffectSystem.cs new file mode 100644 index 0000000000..34aa51e4ff --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWeedsEntityEffectSystem.cs @@ -0,0 +1,16 @@ +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustWeedsEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + entity.Comp.WeedLevel += args.Effect.Amount; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowthEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowthEntityEffectSystem.cs new file mode 100644 index 0000000000..b0faa6255e --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowthEntityEffectSystem.cs @@ -0,0 +1,19 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAffectGrowthEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly PlantHolderSystem _plantHolder = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + _plantHolder.AffectGrowth(entity, (int)args.Effect.Amount, entity); + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStatEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStatEntityEffectSystem.cs new file mode 100644 index 0000000000..3d82f74b11 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStatEntityEffectSystem.cs @@ -0,0 +1,122 @@ +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; +using Robust.Shared.Random; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +/// +/// This system mutates an inputted stat for a PlantHolder, only works for floats, integers, and bools. +/// +/// +public sealed partial class PlantChangeStatEntityEffectSystem : EntityEffectSystem +{ + // TODO: This is awful. I do not have the strength to refactor this. I want it gone. + [Dependency] private readonly IRobustRandom _random = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + var effect = args.Effect; + var member = entity.Comp.Seed.GetType().GetField(args.Effect.TargetValue); + + if (member == null) + { + Log.Error($"{ effect.GetType().Name } Error: Member { args.Effect.TargetValue} not found on { entity.Comp.Seed.GetType().Name }. Did you misspell it?"); + return; + } + + var currentValObj = member.GetValue(entity.Comp.Seed); + if (currentValObj == null) + return; + + if (member.FieldType == typeof(float)) + { + var floatVal = (float)currentValObj; + MutateFloat(ref floatVal, args.Effect.MinValue, args.Effect.MaxValue, args.Effect.Steps); + member.SetValue(entity.Comp.Seed, floatVal); + } + else if (member.FieldType == typeof(int)) + { + var intVal = (int)currentValObj; + MutateInt(ref intVal, (int)args.Effect.MinValue, (int)args.Effect.MaxValue, args.Effect.Steps); + member.SetValue(entity.Comp.Seed, intVal); + } + else if (member.FieldType == typeof(bool)) + { + var boolVal = (bool)currentValObj; + boolVal = !boolVal; + member.SetValue(entity.Comp.Seed, boolVal); + } + } + + // Mutate reference 'val' between 'min' and 'max' by pretending the value + // is representable by a thermometer code with 'bits' number of bits and + // randomly flipping some of them. + private void MutateFloat(ref float val, float min, float max, int bits) + { + if (min == max) + { + val = min; + return; + } + + // Starting number of bits that are high, between 0 and bits. + // In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded. + int valInt = (int)MathF.Round((val - min) / (max - min) * bits); + // val may be outside the range of min/max due to starting prototype values, so clamp. + valInt = Math.Clamp(valInt, 0, bits); + + // Probability that the bit flip increases n. + // The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasive it it. + // In other words, it tends to go to the middle. + float probIncrease = 1 - (float)valInt / bits; + int valIntMutated; + if (_random.Prob(probIncrease)) + { + valIntMutated = valInt + 1; + } + else + { + valIntMutated = valInt - 1; + } + + // Set value based on mutated thermometer code. + float valMutated = Math.Clamp((float)valIntMutated / bits * (max - min) + min, min, max); + val = valMutated; + } + + private void MutateInt(ref int val, int min, int max, int bits) + { + if (min == max) + { + val = min; + return; + } + + // Starting number of bits that are high, between 0 and bits. + // In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded. + int valInt = (int)MathF.Round((val - min) / (max - min) * bits); + // val may be outside the range of min/max due to starting prototype values, so clamp. + valInt = Math.Clamp(valInt, 0, bits); + + // Probability that the bit flip increases n. + // The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasing it. + // In other words, it tends to go to the middle. + float probIncrease = 1 - (float)valInt / bits; + int valMutated; + if (_random.Prob(probIncrease)) + { + valMutated = val + 1; + } + else + { + valMutated = val - 1; + } + + valMutated = Math.Clamp(valMutated, min, max); + val = valMutated; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadoneEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadoneEntityEffectSystem.cs new file mode 100644 index 0000000000..710bce24dd --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadoneEntityEffectSystem.cs @@ -0,0 +1,30 @@ +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; +using Robust.Shared.Random; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantCryoxadoneEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + var deviation = 0; + var seed = entity.Comp.Seed; + if (seed == null) + return; + if (entity.Comp.Age > seed.Maturation) + deviation = (int) Math.Max(seed.Maturation - 1, entity.Comp.Age - _random.Next(7, 10)); + else + deviation = (int) (seed.Maturation / seed.GrowthStages); + entity.Comp.Age -= deviation; + entity.Comp.LastProduce = entity.Comp.Age; + entity.Comp.SkipAging++; + entity.Comp.ForceUpdate = true; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeedsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeedsEntityEffectSystem.cs new file mode 100644 index 0000000000..1661c501be --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeedsEntityEffectSystem.cs @@ -0,0 +1,31 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Server.Popups; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; +using Content.Shared.Popups; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantDestroySeedsEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly PlantHolderSystem _plantHolder = default!; + [Dependency] private readonly PopupSystem _popup = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead || entity.Comp.Seed.Immutable) + return; + + if (entity.Comp.Seed.Seedless) + return; + + _plantHolder.EnsureUniqueSeed(entity, entity.Comp); + _popup.PopupEntity( + Loc.GetString("botany-plant-seedsdestroyed"), + entity, + PopupType.SmallCaution + ); + entity.Comp.Seed.Seedless = true; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamineEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamineEntityEffectSystem.cs new file mode 100644 index 0000000000..f6aebde465 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamineEntityEffectSystem.cs @@ -0,0 +1,31 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; +using Robust.Shared.Random; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantDiethylamineEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly PlantHolderSystem _plantHolder = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead || entity.Comp.Seed.Immutable) + return; + + if (_random.Prob(0.1f)) + { + _plantHolder.EnsureUniqueSeed(entity, entity); + entity.Comp.Seed!.Lifespan++; + } + + if (_random.Prob(0.1f)) + { + _plantHolder.EnsureUniqueSeed(entity, entity); + entity.Comp.Seed!.Endurance++; + } + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximineEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximineEntityEffectSystem.cs new file mode 100644 index 0000000000..8a073392e1 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximineEntityEffectSystem.cs @@ -0,0 +1,16 @@ +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantPhalanximineEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead || entity.Comp.Seed.Immutable) + return; + + entity.Comp.Seed.Viable = true; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeedsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeedsEntityEffectSystem.cs new file mode 100644 index 0000000000..4d724be244 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeedsEntityEffectSystem.cs @@ -0,0 +1,26 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Server.Popups; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantRestoreSeedsEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly PlantHolderSystem _plantHolder = default!; + [Dependency] private readonly PopupSystem _popup = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead || entity.Comp.Seed.Immutable) + return; + + if (!entity.Comp.Seed.Seedless) + return; + + _plantHolder.EnsureUniqueSeed(entity, entity.Comp); + _popup.PopupEntity(Loc.GetString("botany-plant-seedsrestored"), entity); + entity.Comp.Seed.Seedless = false; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvestEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvestEntityEffectSystem.cs new file mode 100644 index 0000000000..68ea3319ef --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvestEntityEffectSystem.cs @@ -0,0 +1,41 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; +using Robust.Shared.Random; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +/// +/// This effect directly increases the potency of a PlantHolder's plant provided it exists and isn't dead. +/// Potency directly correlates to the size of the plant's produce. +/// +/// +public sealed partial class RobustHarvestEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly PlantHolderSystem _plantHolder = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + if (entity.Comp.Seed.Potency < args.Effect.PotencyLimit) + { + _plantHolder.EnsureUniqueSeed(entity, entity.Comp); + entity.Comp.Seed.Potency = Math.Min(entity.Comp.Seed.Potency + args.Effect.PotencyIncrease, args.Effect.PotencyLimit); + + if (entity.Comp.Seed.Potency > args.Effect.PotencySeedlessThreshold) + { + entity.Comp.Seed.Seedless = true; + } + } + else if (entity.Comp.Seed.Yield > 1 && _random.Prob(0.1f)) + { + // Too much of a good thing reduces yield + _plantHolder.EnsureUniqueSeed(entity, entity.Comp); + entity.Comp.Seed.Yield--; + } + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffectSystem.cs new file mode 100644 index 0000000000..120ae6e881 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffectSystem.cs @@ -0,0 +1,43 @@ +using Content.Server.Botany; +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.EntityEffects.Effects.Botany; + +public sealed partial class PlantMutateChemicalsEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null) + return; + + var chemicals = entity.Comp.Seed.Chemicals; + var randomChems = _proto.Index(args.Effect.RandomPickBotanyReagent).Fills; + + // Add a random amount of a random chemical to this set of chemicals + var pick = _random.Pick(randomChems); + var chemicalId = _random.Pick(pick.Reagents); + var amount = _random.Next(1, (int)pick.Quantity); + var seedChemQuantity = new SeedChemQuantity(); + if (chemicals.ContainsKey(chemicalId)) + { + seedChemQuantity.Min = chemicals[chemicalId].Min; + seedChemQuantity.Max = chemicals[chemicalId].Max + amount; + } + else + { + seedChemQuantity.Min = 1; + seedChemQuantity.Max = 1 + amount; + seedChemQuantity.Inherent = false; + } + var potencyDivisor = (int)Math.Ceiling(100.0f / seedChemQuantity.Max); + seedChemQuantity.PotencyDivisor = potencyDivisor; + chemicals[chemicalId] = seedChemQuantity; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffectSystem.cs new file mode 100644 index 0000000000..e2376ba186 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffectSystem.cs @@ -0,0 +1,53 @@ +using System.Linq; +using Content.Server.Botany.Components; +using Content.Shared.Atmos; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany; +using Robust.Shared.Random; + +namespace Content.Server.EntityEffects.Effects.Botany; + +public sealed partial class PlantMutateExudeGasesEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null) + return; + + var gasses = entity.Comp.Seed.ExudeGasses; + + // Add a random amount of a random gas to this gas dictionary + float amount = _random.NextFloat(args.Effect.MinValue, args.Effect.MaxValue); + var gas = _random.Pick(Enum.GetValues(typeof(Gas)).Cast().ToList()); + + if (!gasses.TryAdd(gas, amount)) + { + gasses[gas] += amount; + } + } +} + +public sealed partial class PlantMutateConsumeGasesEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null) + return; + + var gasses = entity.Comp.Seed.ConsumeGasses; + + // Add a random amount of a random gas to this gas dictionary + var amount = _random.NextFloat(args.Effect.MinValue, args.Effect.MaxValue); + var gas = _random.Pick(Enum.GetValues(typeof(Gas)).Cast().ToList()); + + if (!gasses.TryAdd(gas, amount)) + { + gasses[gas] += amount; + } + } +} + diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffectSystem.cs new file mode 100644 index 0000000000..95d7f97bbe --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffectSystem.cs @@ -0,0 +1,25 @@ +using Content.Server.Botany; +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany; + +namespace Content.Server.EntityEffects.Effects.Botany; + +public sealed partial class PlantMutateHarvestEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null) + return; + + switch (entity.Comp.Seed.HarvestRepeat) + { + case HarvestType.NoRepeat: + entity.Comp.Seed.HarvestRepeat = HarvestType.Repeat; + break; + case HarvestType.Repeat: + entity.Comp.Seed.HarvestRepeat = HarvestType.SelfHarvest; + break; + } + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantMutateSpeciesChangeEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantMutateSpeciesChangeEntityEffectSystem.cs new file mode 100644 index 0000000000..c26e1e08cf --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantMutateSpeciesChangeEntityEffectSystem.cs @@ -0,0 +1,31 @@ +using Content.Server.Botany; +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.EntityEffects.Effects.Botany; + +public sealed partial class PlantMutateSpeciesChangeEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Seed.MutationPrototypes.Count == 0) + return; + + var targetProto = _random.Pick(entity.Comp.Seed.MutationPrototypes); + _proto.TryIndex(targetProto, out SeedPrototype? protoSeed); + + if (protoSeed == null) + { + Log.Error($"Seed prototype could not be found: {targetProto}!"); + return; + } + + entity.Comp.Seed = entity.Comp.Seed.SpeciesChange(protoSeed); + } +} diff --git a/Content.Server/EntityEffects/Effects/ExplodeEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/ExplodeEntityEffectSystem.cs new file mode 100644 index 0000000000..2e2fadff32 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/ExplodeEntityEffectSystem.cs @@ -0,0 +1,20 @@ +using Content.Server.Explosion.EntitySystems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects; +using Content.Shared.Explosion.Components; + +namespace Content.Server.EntityEffects.Effects; + +/// +/// Makes this entity explode using its . +/// +/// +public sealed partial class ExplodeEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly ExplosionSystem _explosion = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _explosion.TriggerExplosive(entity, entity, args.Effect.Delete, args.Effect.Intensity, args.Effect.Radius, args.User); + } +} diff --git a/Content.Server/EntityEffects/Effects/MakeSentientEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/MakeSentientEntityEffectSystem.cs new file mode 100644 index 0000000000..c623b25857 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/MakeSentientEntityEffectSystem.cs @@ -0,0 +1,42 @@ +using Content.Server.Ghost.Roles.Components; +using Content.Server.Speech.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects; +using Content.Shared.Mind.Components; + +namespace Content.Server.EntityEffects.Effects; + +/// +/// Makes this entity sentient. Allows ghost to take it over if it's not already occupied. +/// Optionally also allows this entity to speak. +/// +/// +public sealed partial class MakeSentientEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + // Let affected entities speak normally to make this effect different from, say, the "random sentience" event + // This also works on entities that already have a mind + // We call this before the mind check to allow things like player-controlled mice to be able to benefit from the effect + if (args.Effect.AllowSpeech) + { + RemComp(entity); + // TODO: Make MonkeyAccent a replacement accent and remove MonkeyAccent code-smell. + RemComp(entity); + } + + // Stops from adding a ghost role to things like people who already have a mind + if (TryComp(entity, out var mindContainer) && mindContainer.HasMind) + return; + + // Don't add a ghost role to things that already have ghost roles + if (TryComp(entity, out GhostRoleComponent? ghostRole)) + return; + + ghostRole = AddComp(entity); + EnsureComp(entity); + + ghostRole.RoleName = entity.Comp.EntityName; + ghostRole.RoleDescription = Loc.GetString("ghost-role-information-cognizine-description"); + } +} diff --git a/Content.Server/EntityEffects/Effects/PolymorphEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/PolymorphEntityEffectSystem.cs new file mode 100644 index 0000000000..5f19bcc50b --- /dev/null +++ b/Content.Server/EntityEffects/Effects/PolymorphEntityEffectSystem.cs @@ -0,0 +1,19 @@ +using Content.Server.Polymorph.Components; +using Content.Server.Polymorph.Systems; +using Content.Shared.EntityEffects; + +namespace Content.Server.EntityEffects.Effects; + +/// +/// Polymorphs this entity into another entity. +/// +/// +public sealed partial class PolymorphEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly PolymorphSystem _polymorph = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _polymorph.PolymorphEntity(entity, args.Effect.Prototype); + } +} diff --git a/Content.Server/EntityEffects/Effects/Solution/AreaReactionEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Solution/AreaReactionEntityEffectSystem.cs new file mode 100644 index 0000000000..e5ef488de8 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Solution/AreaReactionEntityEffectSystem.cs @@ -0,0 +1,51 @@ +using Content.Server.Fluids.EntitySystems; +using Content.Server.Spreader; +using Content.Shared.Chemistry.Components; +using Content.Shared.Coordinates.Helpers; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Solution; +using Content.Shared.Maps; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map; + +namespace Content.Server.EntityEffects.Effects.Solution; + +/// +/// This effect creates smoke at this solution's position. +/// The amount of smoke created is modified by scale. +/// +/// +public sealed partial class AreaReactionEntityEffectsSystem : EntityEffectSystem +{ + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly SharedTransformSystem _xform = default!; + [Dependency] private readonly SmokeSystem _smoke = default!; + [Dependency] private readonly SpreaderSystem _spreader = default!; + [Dependency] private readonly TurfSystem _turf = default!; + + // TODO: A sane way to make Smoke without a solution. + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var xform = Transform(entity); + var mapCoords = _xform.GetMapCoordinates(entity); + var spreadAmount = (int) Math.Max(0, Math.Ceiling(args.Scale / args.Effect.OverflowThreshold)); + var effect = args.Effect; + + if (!_mapManager.TryFindGridAt(mapCoords, out var gridUid, out var grid) || + !_map.TryGetTileRef(gridUid, grid, xform.Coordinates, out var tileRef)) + return; + + if (_spreader.RequiresFloorToSpread(effect.PrototypeId.ToString()) && _turf.IsSpace(tileRef)) + return; + + var coords = _map.MapToGrid(gridUid, mapCoords); + var ent = Spawn(args.Effect.PrototypeId, coords.SnapToGrid()); + + _smoke.StartSmoke(ent, entity.Comp.Solution, args.Effect.Duration, spreadAmount); + + _audio.PlayPvs(args.Effect.Sound, entity, AudioParams.Default.WithVariation(0.25f)); + } +} diff --git a/Content.Server/EntityEffects/Effects/Transform/ExplosionEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Transform/ExplosionEntityEffectSystem.cs new file mode 100644 index 0000000000..fec56aa2d3 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Transform/ExplosionEntityEffectSystem.cs @@ -0,0 +1,28 @@ +using Content.Server.Explosion.EntitySystems; +using Content.Shared.EntityEffects; +using ExplosionEffect = Content.Shared.EntityEffects.Effects.Transform.Explosion; + +namespace Content.Server.EntityEffects.Effects.Transform; + +/// +/// Creates an explosion at this entity's position. +/// Intensity is modified by scale. +/// +/// +public sealed partial class ExplosionEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly ExplosionSystem _explosion = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var intensity = MathF.Min(args.Effect.IntensityPerUnit * args.Scale, args.Effect.MaxTotalIntensity); + + _explosion.QueueExplosion( + entity, + args.Effect.ExplosionType, + intensity, + args.Effect.IntensitySlope, + args.Effect.MaxIntensity, + args.Effect.TileBreakScale); + } +} diff --git a/Content.Server/EntityEffects/EntityEffectSystem.cs b/Content.Server/EntityEffects/EntityEffectSystem.cs deleted file mode 100644 index 238ef4849d..0000000000 --- a/Content.Server/EntityEffects/EntityEffectSystem.cs +++ /dev/null @@ -1,976 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Content.Server.Atmos.EntitySystems; -using Content.Server.Body.Components; -using Content.Server.Body.Systems; -using Content.Server.Botany.Components; -using Content.Server.Botany.Systems; -using Content.Server.Botany; -using Content.Server.Chat.Systems; -using Content.Server.Emp; -using Content.Server.Explosion.EntitySystems; -using Content.Server.Fluids.EntitySystems; -using Content.Server.Ghost.Roles.Components; -using Content.Server.Polymorph.Components; -using Content.Server.Polymorph.Systems; -using Content.Server.Speech.Components; -using Content.Server.Spreader; -using Content.Server.Temperature.Components; -using Content.Server.Temperature.Systems; -using Content.Server.Zombies; -using Content.Shared.Atmos; -using Content.Shared.Atmos.Components; -using Content.Shared.Body.Components; -using Content.Shared.Coordinates.Helpers; -using Content.Shared.EntityEffects.EffectConditions; -using Content.Shared.EntityEffects.Effects.PlantMetabolism; -using Content.Shared.EntityEffects.Effects; -using Content.Shared.EntityEffects; -using Content.Shared.Flash; -using Content.Shared.Maps; -using Content.Shared.Medical; -using Content.Shared.Mind.Components; -using Content.Shared.Popups; -using Content.Shared.Random; -using Content.Shared.Traits.Assorted; -using Content.Shared.Zombies; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -using TemperatureCondition = Content.Shared.EntityEffects.EffectConditions.Temperature; // disambiguate the namespace -using PolymorphEffect = Content.Shared.EntityEffects.Effects.Polymorph; - -namespace Content.Server.EntityEffects; - -public sealed class EntityEffectSystem : EntitySystem -{ - private static readonly ProtoId RandomPickBotanyReagent = "RandomPickBotanyReagent"; - - [Dependency] private readonly AtmosphereSystem _atmosphere = default!; - [Dependency] private readonly BloodstreamSystem _bloodstream = default!; - [Dependency] private readonly ChatSystem _chat = default!; - [Dependency] private readonly EmpSystem _emp = default!; - [Dependency] private readonly ExplosionSystem _explosion = default!; - [Dependency] private readonly FlammableSystem _flammable = default!; - [Dependency] private readonly SharedFlashSystem _flash = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IPrototypeManager _protoManager = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly SharedMapSystem _map = default!; - [Dependency] private readonly MutationSystem _mutation = default!; - [Dependency] private readonly NarcolepsySystem _narcolepsy = default!; - [Dependency] private readonly PlantHolderSystem _plantHolder = default!; - [Dependency] private readonly PolymorphSystem _polymorph = default!; - [Dependency] private readonly RespiratorSystem _respirator = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedPointLightSystem _pointLight = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly SmokeSystem _smoke = default!; - [Dependency] private readonly SpreaderSystem _spreader = default!; - [Dependency] private readonly TemperatureSystem _temperature = default!; - [Dependency] private readonly SharedTransformSystem _xform = default!; - [Dependency] private readonly VomitSystem _vomit = default!; - [Dependency] private readonly TurfSystem _turf = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent>(OnCheckTemperature); - SubscribeLocalEvent>(OnCheckBreathing); - SubscribeLocalEvent>(OnCheckOrganType); - SubscribeLocalEvent>(OnExecutePlantAdjustHealth); - SubscribeLocalEvent>(OnExecutePlantAdjustMutationLevel); - SubscribeLocalEvent>(OnExecutePlantAdjustMutationMod); - SubscribeLocalEvent>(OnExecutePlantAdjustNutrition); - SubscribeLocalEvent>(OnExecutePlantAdjustPests); - SubscribeLocalEvent>(OnExecutePlantAdjustPotency); - SubscribeLocalEvent>(OnExecutePlantAdjustToxins); - SubscribeLocalEvent>(OnExecutePlantAdjustWater); - SubscribeLocalEvent>(OnExecutePlantAdjustWeeds); - SubscribeLocalEvent>(OnExecutePlantAffectGrowth); - SubscribeLocalEvent>(OnExecutePlantChangeStat); - SubscribeLocalEvent>(OnExecutePlantCryoxadone); - SubscribeLocalEvent>(OnExecutePlantDestroySeeds); - SubscribeLocalEvent>(OnExecutePlantDiethylamine); - SubscribeLocalEvent>(OnExecutePlantPhalanximine); - SubscribeLocalEvent>(OnExecutePlantRestoreSeeds); - SubscribeLocalEvent>(OnExecuteRobustHarvest); - SubscribeLocalEvent>(OnExecuteAdjustTemperature); - SubscribeLocalEvent>(OnExecuteAreaReactionEffect); - SubscribeLocalEvent>(OnExecuteCauseZombieInfection); - SubscribeLocalEvent>(OnExecuteChemCleanBloodstream); - SubscribeLocalEvent>(OnExecuteChemVomit); - SubscribeLocalEvent>(OnExecuteCreateEntityReactionEffect); - SubscribeLocalEvent>(OnExecuteCreateGas); - SubscribeLocalEvent>(OnExecuteCureZombieInfection); - SubscribeLocalEvent>(OnExecuteEmote); - SubscribeLocalEvent>(OnExecuteEmpReactionEffect); - SubscribeLocalEvent>(OnExecuteExplosionReactionEffect); - SubscribeLocalEvent>(OnExecuteFlammableReaction); - SubscribeLocalEvent>(OnExecuteFlashReactionEffect); - SubscribeLocalEvent>(OnExecuteIgnite); - SubscribeLocalEvent>(OnExecuteMakeSentient); - SubscribeLocalEvent>(OnExecuteModifyBleedAmount); - SubscribeLocalEvent>(OnExecuteModifyBloodLevel); - SubscribeLocalEvent>(OnExecuteModifyLungGas); - SubscribeLocalEvent>(OnExecuteOxygenate); - SubscribeLocalEvent>(OnExecutePlantMutateChemicals); - SubscribeLocalEvent>(OnExecutePlantMutateConsumeGasses); - SubscribeLocalEvent>(OnExecutePlantMutateExudeGasses); - SubscribeLocalEvent>(OnExecutePlantMutateHarvest); - SubscribeLocalEvent>(OnExecutePlantSpeciesChange); - SubscribeLocalEvent>(OnExecutePolymorph); - SubscribeLocalEvent>(OnExecuteResetNarcolepsy); - } - - private void OnCheckTemperature(ref CheckEntityEffectConditionEvent args) - { - args.Result = false; - if (TryComp(args.Args.TargetEntity, out TemperatureComponent? temp)) - { - if (temp.CurrentTemperature >= args.Condition.Min && temp.CurrentTemperature <= args.Condition.Max) - args.Result = true; - } - } - - private void OnCheckBreathing(ref CheckEntityEffectConditionEvent args) - { - if (!TryComp(args.Args.TargetEntity, out RespiratorComponent? respiratorComp)) - { - args.Result = !args.Condition.IsBreathing; - return; - } - - var breathingState = _respirator.IsBreathing((args.Args.TargetEntity, respiratorComp)); - args.Result = args.Condition.IsBreathing == breathingState; - } - - private void OnCheckOrganType(ref CheckEntityEffectConditionEvent args) - { - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - if (reagentArgs.OrganEntity == null) - { - args.Result = false; - return; - } - - args.Result = OrganCondition(args.Condition, reagentArgs.OrganEntity.Value); - return; - } - - // TODO: Someone needs to figure out how to do this for non-reagent effects. - throw new NotImplementedException(); - } - - public bool OrganCondition(OrganType condition, Entity metabolizer) - { - metabolizer.Comp ??= EntityManager.GetComponentOrNull(metabolizer.Owner); - if (metabolizer.Comp != null - && metabolizer.Comp.MetabolizerTypes != null - && metabolizer.Comp.MetabolizerTypes.Contains(condition.Type)) - return condition.ShouldHave; - return !condition.ShouldHave; - } - - /// - /// Checks if the plant holder can metabolize the reagent or not. Checks if it has an alive plant by default. - /// - /// The entity holding the plant - /// The plant holder component - /// The entity manager - /// Whether to check if it has an alive plant or not - /// - private bool CanMetabolizePlant(EntityUid plantHolder, [NotNullWhen(true)] out PlantHolderComponent? plantHolderComponent, - bool mustHaveAlivePlant = true, bool mustHaveMutableSeed = false) - { - plantHolderComponent = null; - - if (!TryComp(plantHolder, out plantHolderComponent)) - return false; - - if (mustHaveAlivePlant && (plantHolderComponent.Seed == null || plantHolderComponent.Dead)) - return false; - - if (mustHaveMutableSeed && (plantHolderComponent.Seed == null || plantHolderComponent.Seed.Immutable)) - return false; - - return true; - } - - private void OnExecutePlantAdjustHealth(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - plantHolderComp.Health += args.Effect.Amount; - _plantHolder.CheckHealth(args.Args.TargetEntity, plantHolderComp); - } - - private void OnExecutePlantAdjustMutationLevel(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - plantHolderComp.MutationLevel += args.Effect.Amount * plantHolderComp.MutationMod; - } - - private void OnExecutePlantAdjustMutationMod(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - plantHolderComp.MutationMod += args.Effect.Amount; - } - - private void OnExecutePlantAdjustNutrition(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveAlivePlant: false)) - return; - - _plantHolder.AdjustNutrient(args.Args.TargetEntity, args.Effect.Amount, plantHolderComp); - } - - private void OnExecutePlantAdjustPests(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - plantHolderComp.PestLevel += args.Effect.Amount; - } - - private void OnExecutePlantAdjustPotency(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - if (plantHolderComp.Seed == null) - return; - - _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp); - plantHolderComp.Seed.Potency = Math.Max(plantHolderComp.Seed.Potency + args.Effect.Amount, 1); - } - - private void OnExecutePlantAdjustToxins(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - plantHolderComp.Toxins += args.Effect.Amount; - } - - private void OnExecutePlantAdjustWater(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveAlivePlant: false)) - return; - - _plantHolder.AdjustWater(args.Args.TargetEntity, args.Effect.Amount, plantHolderComp); - } - - private void OnExecutePlantAdjustWeeds(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - plantHolderComp.WeedLevel += args.Effect.Amount; - } - - private void OnExecutePlantAffectGrowth(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - _plantHolder.AffectGrowth(args.Args.TargetEntity, (int) args.Effect.Amount, plantHolderComp); - } - - // Mutate reference 'val' between 'min' and 'max' by pretending the value - // is representable by a thermometer code with 'bits' number of bits and - // randomly flipping some of them. - private void MutateFloat(ref float val, float min, float max, int bits) - { - if (min == max) - { - val = min; - return; - } - - // Starting number of bits that are high, between 0 and bits. - // In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded. - int valInt = (int)MathF.Round((val - min) / (max - min) * bits); - // val may be outside the range of min/max due to starting prototype values, so clamp. - valInt = Math.Clamp(valInt, 0, bits); - - // Probability that the bit flip increases n. - // The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasive it it. - // In other words, it tends to go to the middle. - float probIncrease = 1 - (float)valInt / bits; - int valIntMutated; - if (_random.Prob(probIncrease)) - { - valIntMutated = valInt + 1; - } - else - { - valIntMutated = valInt - 1; - } - - // Set value based on mutated thermometer code. - float valMutated = Math.Clamp((float)valIntMutated / bits * (max - min) + min, min, max); - val = valMutated; - } - - private void MutateInt(ref int val, int min, int max, int bits) - { - if (min == max) - { - val = min; - return; - } - - // Starting number of bits that are high, between 0 and bits. - // In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded. - int valInt = (int)MathF.Round((val - min) / (max - min) * bits); - // val may be outside the range of min/max due to starting prototype values, so clamp. - valInt = Math.Clamp(valInt, 0, bits); - - // Probability that the bit flip increases n. - // The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasing it. - // In other words, it tends to go to the middle. - float probIncrease = 1 - (float)valInt / bits; - int valMutated; - if (_random.Prob(probIncrease)) - { - valMutated = val + 1; - } - else - { - valMutated = val - 1; - } - - valMutated = Math.Clamp(valMutated, min, max); - val = valMutated; - } - - private void OnExecutePlantChangeStat(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - if (plantHolderComp.Seed == null) - return; - - var member = plantHolderComp.Seed.GetType().GetField(args.Effect.TargetValue); - - if (member == null) - { - _mutation.Log.Error(args.Effect.GetType().Name + " Error: Member " + args.Effect.TargetValue + " not found on " + plantHolderComp.Seed.GetType().Name + ". Did you misspell it?"); - return; - } - - var currentValObj = member.GetValue(plantHolderComp.Seed); - if (currentValObj == null) - return; - - if (member.FieldType == typeof(float)) - { - var floatVal = (float)currentValObj; - MutateFloat(ref floatVal, args.Effect.MinValue, args.Effect.MaxValue, args.Effect.Steps); - member.SetValue(plantHolderComp.Seed, floatVal); - } - else if (member.FieldType == typeof(int)) - { - var intVal = (int)currentValObj; - MutateInt(ref intVal, (int)args.Effect.MinValue, (int)args.Effect.MaxValue, args.Effect.Steps); - member.SetValue(plantHolderComp.Seed, intVal); - } - else if (member.FieldType == typeof(bool)) - { - var boolVal = (bool)currentValObj; - boolVal = !boolVal; - member.SetValue(plantHolderComp.Seed, boolVal); - } - } - - private void OnExecutePlantCryoxadone(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - var deviation = 0; - var seed = plantHolderComp.Seed; - if (seed == null) - return; - if (plantHolderComp.Age > seed.Maturation) - deviation = (int) Math.Max(seed.Maturation - 1, plantHolderComp.Age - _random.Next(7, 10)); - else - deviation = (int) (seed.Maturation / seed.GrowthStages); - plantHolderComp.Age -= deviation; - plantHolderComp.LastProduce = plantHolderComp.Age; - plantHolderComp.SkipAging++; - plantHolderComp.ForceUpdate = true; - } - - private void OnExecutePlantDestroySeeds(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveMutableSeed: true)) - return; - - if (plantHolderComp.Seed!.Seedless == false) - { - _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp); - _popup.PopupEntity( - Loc.GetString("botany-plant-seedsdestroyed"), - args.Args.TargetEntity, - PopupType.SmallCaution - ); - plantHolderComp.Seed.Seedless = true; - } - } - - private void OnExecutePlantDiethylamine(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveMutableSeed: true)) - return; - - if (_random.Prob(0.1f)) - { - _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp); - plantHolderComp.Seed!.Lifespan++; - } - - if (_random.Prob(0.1f)) - { - _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp); - plantHolderComp.Seed!.Endurance++; - } - } - - private void OnExecutePlantPhalanximine(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveMutableSeed: true)) - return; - - plantHolderComp.Seed!.Viable = true; - } - - private void OnExecutePlantRestoreSeeds(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveMutableSeed: true)) - return; - - if (plantHolderComp.Seed!.Seedless) - { - _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp); - _popup.PopupEntity(Loc.GetString("botany-plant-seedsrestored"), args.Args.TargetEntity); - plantHolderComp.Seed.Seedless = false; - } - } - - private void OnExecuteRobustHarvest(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - if (plantHolderComp.Seed == null) - return; - - if (plantHolderComp.Seed.Potency < args.Effect.PotencyLimit) - { - _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp); - plantHolderComp.Seed.Potency = Math.Min(plantHolderComp.Seed.Potency + args.Effect.PotencyIncrease, args.Effect.PotencyLimit); - - if (plantHolderComp.Seed.Potency > args.Effect.PotencySeedlessThreshold) - { - plantHolderComp.Seed.Seedless = true; - } - } - else if (plantHolderComp.Seed.Yield > 1 && _random.Prob(0.1f)) - { - // Too much of a good thing reduces yield - _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp); - plantHolderComp.Seed.Yield--; - } - } - - private void OnExecuteAdjustTemperature(ref ExecuteEntityEffectEvent args) - { - if (TryComp(args.Args.TargetEntity, out TemperatureComponent? temp)) - { - var amount = args.Effect.Amount; - - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - amount *= reagentArgs.Scale.Float(); - } - - _temperature.ChangeHeat(args.Args.TargetEntity, amount, true, temp); - } - } - - private void OnExecuteAreaReactionEffect(ref ExecuteEntityEffectEvent args) - { - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - if (reagentArgs.Source == null) - return; - - var spreadAmount = (int) Math.Max(0, Math.Ceiling((reagentArgs.Quantity / args.Effect.OverflowThreshold).Float())); - var splitSolution = reagentArgs.Source.SplitSolution(reagentArgs.Source.Volume); - var transform = Comp(reagentArgs.TargetEntity); - var mapCoords = _xform.GetMapCoordinates(reagentArgs.TargetEntity, xform: transform); - - if (!_mapManager.TryFindGridAt(mapCoords, out var gridUid, out var grid) || - !_map.TryGetTileRef(gridUid, grid, transform.Coordinates, out var tileRef)) - { - return; - } - - if (_spreader.RequiresFloorToSpread(args.Effect.PrototypeId) && _turf.IsSpace(tileRef)) - return; - - var coords = _map.MapToGrid(gridUid, mapCoords); - var ent = Spawn(args.Effect.PrototypeId, coords.SnapToGrid()); - - _smoke.StartSmoke(ent, splitSolution, args.Effect.Duration, spreadAmount); - - _audio.PlayPvs(args.Effect.Sound, reagentArgs.TargetEntity, AudioParams.Default.WithVariation(0.25f)); - return; - } - - // TODO: Someone needs to figure out how to do this for non-reagent effects. - throw new NotImplementedException(); - } - - private void OnExecuteCauseZombieInfection(ref ExecuteEntityEffectEvent args) - { - EnsureComp(args.Args.TargetEntity); - EnsureComp(args.Args.TargetEntity); - } - - private void OnExecuteChemCleanBloodstream(ref ExecuteEntityEffectEvent args) - { - var cleanseRate = args.Effect.CleanseRate; - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - if (reagentArgs.Source == null || reagentArgs.Reagent == null) - return; - - cleanseRate *= reagentArgs.Scale.Float(); - _bloodstream.FlushChemicals(args.Args.TargetEntity, reagentArgs.Reagent, cleanseRate); - } - else - { - _bloodstream.FlushChemicals(args.Args.TargetEntity, null, cleanseRate); - } - } - - private void OnExecuteChemVomit(ref ExecuteEntityEffectEvent args) - { - if (args.Args is EntityEffectReagentArgs reagentArgs) - if (reagentArgs.Scale != 1f) - return; - - _vomit.Vomit(args.Args.TargetEntity, args.Effect.ThirstAmount, args.Effect.HungerAmount); - } - - private void OnExecuteCreateEntityReactionEffect(ref ExecuteEntityEffectEvent args) - { - var transform = Comp(args.Args.TargetEntity); - var quantity = (int)args.Effect.Number; - if (args.Args is EntityEffectReagentArgs reagentArgs) - quantity *= reagentArgs.Quantity.Int(); - - for (var i = 0; i < quantity; i++) - { - var uid = Spawn(args.Effect.Entity, _xform.GetMapCoordinates(args.Args.TargetEntity, xform: transform)); - _xform.AttachToGridOrMap(uid); - - // TODO figure out how to properly spawn inside of containers - // e.g. cheese: - // if the user is holding a bowl milk & enzyme, should drop to floor, not attached to the user. - // if reaction happens in a backpack, should insert cheese into backpack. - // --> if it doesn't fit, iterate through parent storage until it attaches to the grid (again, DON'T attach to players). - // if the reaction happens INSIDE a stomach? the bloodstream? I have no idea how to handle that. - // presumably having cheese materialize inside of your blood would have "disadvantages". - } - } - - private void OnExecuteCreateGas(ref ExecuteEntityEffectEvent args) - { - var tileMix = _atmosphere.GetContainingMixture(args.Args.TargetEntity, false, true); - - if (tileMix != null) - { - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - tileMix.AdjustMoles(args.Effect.Gas, reagentArgs.Quantity.Float() * args.Effect.Multiplier); - } - else - { - tileMix.AdjustMoles(args.Effect.Gas, args.Effect.Multiplier); - } - } - } - - private void OnExecuteCureZombieInfection(ref ExecuteEntityEffectEvent args) - { - if (HasComp(args.Args.TargetEntity)) - return; - - RemComp(args.Args.TargetEntity); - RemComp(args.Args.TargetEntity); - - if (args.Effect.Innoculate) - { - EnsureComp(args.Args.TargetEntity); - } - } - - private void OnExecuteEmote(ref ExecuteEntityEffectEvent args) - { - if (args.Effect.EmoteId == null) - return; - - if (args.Effect.ShowInChat) - _chat.TryEmoteWithChat(args.Args.TargetEntity, args.Effect.EmoteId, ChatTransmitRange.GhostRangeLimit, forceEmote: args.Effect.Force); - else - _chat.TryEmoteWithoutChat(args.Args.TargetEntity, args.Effect.EmoteId); - } - - private void OnExecuteEmpReactionEffect(ref ExecuteEntityEffectEvent args) - { - var transform = Comp(args.Args.TargetEntity); - - var range = args.Effect.EmpRangePerUnit; - - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - range = MathF.Min((float) (reagentArgs.Quantity * args.Effect.EmpRangePerUnit), args.Effect.EmpMaxRange); - } - - _emp.EmpPulse(_xform.GetMapCoordinates(args.Args.TargetEntity, xform: transform), - range, - args.Effect.EnergyConsumption, - args.Effect.DisableDuration); - } - - private void OnExecuteExplosionReactionEffect(ref ExecuteEntityEffectEvent args) - { - var intensity = args.Effect.IntensityPerUnit; - - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - intensity = MathF.Min((float) reagentArgs.Quantity * args.Effect.IntensityPerUnit, args.Effect.MaxTotalIntensity); - } - - _explosion.QueueExplosion( - args.Args.TargetEntity, - args.Effect.ExplosionType, - intensity, - args.Effect.IntensitySlope, - args.Effect.MaxIntensity, - args.Effect.TileBreakScale); - } - - private void OnExecuteFlammableReaction(ref ExecuteEntityEffectEvent args) - { - if (!TryComp(args.Args.TargetEntity, out FlammableComponent? flammable)) - return; - - // Sets the multiplier for FireStacks to MultiplierOnExisting is 0 or greater and target already has FireStacks - var multiplier = flammable.FireStacks != 0f && args.Effect.MultiplierOnExisting >= 0 ? args.Effect.MultiplierOnExisting : args.Effect.Multiplier; - var quantity = 1f; - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - quantity = reagentArgs.Quantity.Float(); - _flammable.AdjustFireStacks(args.Args.TargetEntity, quantity * multiplier, flammable); - if (reagentArgs.Reagent != null) - reagentArgs.Source?.RemoveReagent(reagentArgs.Reagent.ID, reagentArgs.Quantity); - } - else - { - _flammable.AdjustFireStacks(args.Args.TargetEntity, multiplier, flammable); - } - } - - private void OnExecuteFlashReactionEffect(ref ExecuteEntityEffectEvent args) - { - var transform = Comp(args.Args.TargetEntity); - - var range = 1f; - - if (args.Args is EntityEffectReagentArgs reagentArgs) - range = MathF.Min((float)(reagentArgs.Quantity * args.Effect.RangePerUnit), args.Effect.MaxRange); - - _flash.FlashArea( - args.Args.TargetEntity, - null, - range, - args.Effect.Duration, - slowTo: args.Effect.SlowTo, - sound: args.Effect.Sound); - - if (args.Effect.FlashEffectPrototype == null) - return; - - var uid = EntityManager.SpawnEntity(args.Effect.FlashEffectPrototype, _xform.GetMapCoordinates(transform)); - _xform.AttachToGridOrMap(uid); - - if (!TryComp(uid, out var pointLightComp)) - return; - - _pointLight.SetRadius(uid, MathF.Max(1.1f, range), pointLightComp); - } - - private void OnExecuteIgnite(ref ExecuteEntityEffectEvent args) - { - if (!TryComp(args.Args.TargetEntity, out FlammableComponent? flammable)) - return; - - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - _flammable.Ignite(reagentArgs.TargetEntity, reagentArgs.OrganEntity ?? reagentArgs.TargetEntity, flammable: flammable); - } - else - { - _flammable.Ignite(args.Args.TargetEntity, args.Args.TargetEntity, flammable: flammable); - } - } - - private void OnExecuteMakeSentient(ref ExecuteEntityEffectEvent args) - { - var uid = args.Args.TargetEntity; - - // Let affected entities speak normally to make this effect different from, say, the "random sentience" event - // This also works on entities that already have a mind - // We call this before the mind check to allow things like player-controlled mice to be able to benefit from the effect - RemComp(uid); - RemComp(uid); - - // Stops from adding a ghost role to things like people who already have a mind - if (TryComp(uid, out var mindContainer) && mindContainer.HasMind) - { - return; - } - - // Don't add a ghost role to things that already have ghost roles - if (TryComp(uid, out GhostRoleComponent? ghostRole)) - { - return; - } - - ghostRole = AddComp(uid); - EnsureComp(uid); - - var entityData = Comp(uid); - ghostRole.RoleName = entityData.EntityName; - ghostRole.RoleDescription = Loc.GetString("ghost-role-information-cognizine-description"); - } - - private void OnExecuteModifyBleedAmount(ref ExecuteEntityEffectEvent args) - { - if (TryComp(args.Args.TargetEntity, out var blood)) - { - var amt = args.Effect.Amount; - if (args.Args is EntityEffectReagentArgs reagentArgs) { - if (args.Effect.Scaled) - amt *= reagentArgs.Quantity.Float(); - amt *= reagentArgs.Scale.Float(); - } - - _bloodstream.TryModifyBleedAmount((args.Args.TargetEntity, blood), amt); - } - } - - private void OnExecuteModifyBloodLevel(ref ExecuteEntityEffectEvent args) - { - if (TryComp(args.Args.TargetEntity, out var blood)) - { - var amt = args.Effect.Amount; - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - if (args.Effect.Scaled) - amt *= reagentArgs.Quantity; - amt *= reagentArgs.Scale; - } - - _bloodstream.TryModifyBloodLevel((args.Args.TargetEntity, blood), amt); - } - } - - private void OnExecuteModifyLungGas(ref ExecuteEntityEffectEvent args) - { - LungComponent? lung; - float amount = 1f; - - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - if (!TryComp(reagentArgs.OrganEntity, out var organLung)) - return; - lung = organLung; - amount = reagentArgs.Quantity.Float(); - } - else - { - if (!TryComp(args.Args.TargetEntity, out var organLung)) //Likely needs to be modified to ensure it works correctly - return; - lung = organLung; - } - - if (lung != null) - { - foreach (var (gas, ratio) in args.Effect.Ratios) - { - var quantity = ratio * amount / Atmospherics.BreathMolesToReagentMultiplier; - if (quantity < 0) - quantity = Math.Max(quantity, -lung.Air[(int) gas]); - lung.Air.AdjustMoles(gas, quantity); - } - } - } - - private void OnExecuteOxygenate(ref ExecuteEntityEffectEvent args) - { - var multiplier = 1f; - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - multiplier = reagentArgs.Quantity.Float(); - } - - if (TryComp(args.Args.TargetEntity, out var resp)) - { - _respirator.UpdateSaturation(args.Args.TargetEntity, multiplier * args.Effect.Factor, resp); - } - } - - private void OnExecutePlantMutateChemicals(ref ExecuteEntityEffectEvent args) - { - var plantholder = Comp(args.Args.TargetEntity); - - if (plantholder.Seed == null) - return; - - var chemicals = plantholder.Seed.Chemicals; - var randomChems = _protoManager.Index(RandomPickBotanyReagent).Fills; - - // Add a random amount of a random chemical to this set of chemicals - if (randomChems != null) - { - var pick = _random.Pick(randomChems); - var chemicalId = _random.Pick(pick.Reagents); - var amount = _random.Next(1, (int)pick.Quantity); - var seedChemQuantity = new SeedChemQuantity(); - if (chemicals.ContainsKey(chemicalId)) - { - seedChemQuantity.Min = chemicals[chemicalId].Min; - seedChemQuantity.Max = chemicals[chemicalId].Max + amount; - } - else - { - seedChemQuantity.Min = 1; - seedChemQuantity.Max = 1 + amount; - seedChemQuantity.Inherent = false; - } - var potencyDivisor = (int)Math.Ceiling(100.0f / seedChemQuantity.Max); - seedChemQuantity.PotencyDivisor = potencyDivisor; - chemicals[chemicalId] = seedChemQuantity; - } - } - - private void OnExecutePlantMutateConsumeGasses(ref ExecuteEntityEffectEvent args) - { - var plantholder = Comp(args.Args.TargetEntity); - - if (plantholder.Seed == null) - return; - - var gasses = plantholder.Seed.ConsumeGasses; - - // Add a random amount of a random gas to this gas dictionary - float amount = _random.NextFloat(args.Effect.MinValue, args.Effect.MaxValue); - Gas gas = _random.Pick(Enum.GetValues(typeof(Gas)).Cast().ToList()); - if (gasses.ContainsKey(gas)) - { - gasses[gas] += amount; - } - else - { - gasses.Add(gas, amount); - } - } - - private void OnExecutePlantMutateExudeGasses(ref ExecuteEntityEffectEvent args) - { - var plantholder = Comp(args.Args.TargetEntity); - - if (plantholder.Seed == null) - return; - - var gasses = plantholder.Seed.ExudeGasses; - - // Add a random amount of a random gas to this gas dictionary - float amount = _random.NextFloat(args.Effect.MinValue, args.Effect.MaxValue); - Gas gas = _random.Pick(Enum.GetValues(typeof(Gas)).Cast().ToList()); - if (gasses.ContainsKey(gas)) - { - gasses[gas] += amount; - } - else - { - gasses.Add(gas, amount); - } - } - - private void OnExecutePlantMutateHarvest(ref ExecuteEntityEffectEvent args) - { - var plantholder = Comp(args.Args.TargetEntity); - - if (plantholder.Seed == null) - return; - - if (plantholder.Seed.HarvestRepeat == HarvestType.NoRepeat) - plantholder.Seed.HarvestRepeat = HarvestType.Repeat; - else if (plantholder.Seed.HarvestRepeat == HarvestType.Repeat) - plantholder.Seed.HarvestRepeat = HarvestType.SelfHarvest; - } - - private void OnExecutePlantSpeciesChange(ref ExecuteEntityEffectEvent args) - { - var plantholder = Comp(args.Args.TargetEntity); - if (plantholder.Seed == null) - return; - - if (plantholder.Seed.MutationPrototypes.Count == 0) - return; - - var targetProto = _random.Pick(plantholder.Seed.MutationPrototypes); - if (!_protoManager.TryIndex(targetProto, out SeedPrototype? protoSeed)) - { - Log.Error($"Seed prototype could not be found: {targetProto}!"); - return; - } - - plantholder.Seed = plantholder.Seed.SpeciesChange(protoSeed); - } - - private void OnExecutePolymorph(ref ExecuteEntityEffectEvent args) - { - // Make it into a prototype - EnsureComp(args.Args.TargetEntity); - _polymorph.PolymorphEntity(args.Args.TargetEntity, args.Effect.PolymorphPrototype); - } - - private void OnExecuteResetNarcolepsy(ref ExecuteEntityEffectEvent args) - { - if (args.Args is EntityEffectReagentArgs reagentArgs) - if (reagentArgs.Scale != 1f) - return; - - _narcolepsy.AdjustNarcolepsyTimer(args.Args.TargetEntity, args.Effect.TimerReset); - } -} 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/Fluids/EntitySystems/PuddleSystem.Evaporation.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Evaporation.cs deleted file mode 100644 index 245ab8308f..0000000000 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.Evaporation.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.FixedPoint; -using Content.Shared.Fluids.Components; - -namespace Content.Server.Fluids.EntitySystems; - -public sealed partial class PuddleSystem -{ - private static readonly TimeSpan EvaporationCooldown = TimeSpan.FromSeconds(1); - - private void OnEvaporationMapInit(Entity entity, ref MapInitEvent args) - { - entity.Comp.NextTick = _timing.CurTime + EvaporationCooldown; - } - - private void UpdateEvaporation(EntityUid uid, Solution solution) - { - if (HasComp(uid)) - { - return; - } - - if (solution.GetTotalPrototypeQuantity(GetEvaporatingReagents(solution)) > FixedPoint2.Zero) - { - var evaporation = AddComp(uid); - evaporation.NextTick = _timing.CurTime + EvaporationCooldown; - return; - } - - RemComp(uid); - } - - private void TickEvaporation() - { - var query = EntityQueryEnumerator(); - var xformQuery = GetEntityQuery(); - var curTime = _timing.CurTime; - while (query.MoveNext(out var uid, out var evaporation, out var puddle)) - { - if (evaporation.NextTick > curTime) - continue; - - evaporation.NextTick += EvaporationCooldown; - - if (!_solutionContainerSystem.ResolveSolution(uid, puddle.SolutionName, ref puddle.Solution, out var puddleSolution)) - continue; - - // Yes, this means that 50u water + 50u holy water evaporates twice as fast as 100u water. - foreach ((string evaporatingReagent, FixedPoint2 evaporatingSpeed) in GetEvaporationSpeeds(puddleSolution)) - { - var reagentTick = evaporation.EvaporationAmount * EvaporationCooldown.TotalSeconds * evaporatingSpeed; - puddleSolution.SplitSolutionWithOnly(reagentTick, evaporatingReagent); - } - - // Despawn if we're done - if (puddleSolution.Volume == FixedPoint2.Zero) - { - // Spawn a *sparkle* - Spawn("PuddleSparkle", xformQuery.GetComponent(uid).Coordinates); - QueueDel(uid); - } - - _solutionContainerSystem.UpdateChemicals(puddle.Solution.Value); - } - } -} diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs index 880a4395b4..01526d4ee5 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs @@ -1,20 +1,8 @@ -using Content.Server.Chemistry.Containers.EntitySystems; -using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Chemistry.Reaction; -using Content.Shared.Chemistry; -using Content.Shared.Clothing; -using Content.Shared.CombatMode.Pacification; using Content.Shared.Database; -using Content.Shared.FixedPoint; using Content.Shared.Fluids.Components; -using Content.Shared.IdentityManagement; -using Content.Shared.Nutrition.EntitySystems; -using Content.Shared.Popups; using Content.Shared.Spillable; using Content.Shared.Throwing; -using Content.Shared.Weapons.Melee.Events; -using Robust.Shared.Player; namespace Content.Server.Fluids.EntitySystems; @@ -26,10 +14,8 @@ public sealed partial class PuddleSystem SubscribeLocalEvent(SpillOnLand); // Openable handles the event if it's closed - SubscribeLocalEvent(SplashOnMeleeHit, after: [typeof(OpenableSystem)]); SubscribeLocalEvent(OnOverflow); SubscribeLocalEvent(OnDoAfter); - SubscribeLocalEvent(OnAttemptPacifiedThrow); } private void OnOverflow(Entity entity, ref SolutionContainerOverflowEvent args) @@ -41,66 +27,6 @@ public sealed partial class PuddleSystem args.Handled = true; } - private void SplashOnMeleeHit(Entity entity, ref MeleeHitEvent args) - { - if (args.Handled) - return; - - // When attacking someone reactive with a spillable entity, - // splash a little on them (touch react) - // If this also has solution transfer, then assume the transfer amount is how much we want to spill. - // Otherwise let's say they want to spill a quarter of its max volume. - - if (!_solutionContainerSystem.TryGetDrainableSolution(entity.Owner, out var soln, out var solution)) - return; - - var hitCount = args.HitEntities.Count; - - var totalSplit = FixedPoint2.Min(solution.MaxVolume * 0.25, solution.Volume); - if (TryComp(entity, out var transfer)) - { - totalSplit = FixedPoint2.Min(transfer.TransferAmount, solution.Volume); - } - - // a little lame, but reagent quantity is not very balanced and we don't want people - // spilling like 100u of reagent on someone at once! - totalSplit = FixedPoint2.Min(totalSplit, entity.Comp.MaxMeleeSpillAmount); - - if (totalSplit == 0) - return; - - // Optionally allow further melee handling occur - args.Handled = entity.Comp.PreventMelee; - - // First update the hit count so anything that is not reactive wont count towards the total! - foreach (var hit in args.HitEntities) - { - if (!HasComp(hit)) - hitCount -= 1; - } - - foreach (var hit in args.HitEntities) - { - if (!HasComp(hit)) - continue; - - var splitSolution = _solutionContainerSystem.SplitSolution(soln.Value, totalSplit / hitCount); - - _adminLogger.Add(LogType.MeleeHit, $"{ToPrettyString(args.User)} splashed {SharedSolutionContainerSystem.ToPrettyString(splitSolution):solution} from {ToPrettyString(entity.Owner):entity} onto {ToPrettyString(hit):target}"); - _reactive.DoEntityReaction(hit, splitSolution, ReactionMethod.Touch); - - _popups.PopupEntity( - Loc.GetString("spill-melee-hit-attacker", ("amount", totalSplit / hitCount), ("spillable", entity.Owner), - ("target", Identity.Entity(hit, EntityManager))), - hit, args.User); - - _popups.PopupEntity( - Loc.GetString("spill-melee-hit-others", ("attacker", args.User), ("spillable", entity.Owner), - ("target", Identity.Entity(hit, EntityManager))), - hit, Filter.PvsExcept(args.User), true, PopupType.SmallCaution); - } - } - private void SpillOnLand(Entity entity, ref LandEvent args) { if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution)) @@ -114,7 +40,7 @@ public sealed partial class PuddleSystem if (args.User != null) { - _adminLogger.Add(LogType.Landed, + AdminLogger.Add(LogType.Landed, $"{ToPrettyString(entity.Owner):entity} spilled a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution} on landing"); } @@ -122,22 +48,6 @@ public sealed partial class PuddleSystem TrySplashSpillAt(entity.Owner, Transform(entity).Coordinates, drainedSolution, out _); } - /// - /// Prevent Pacified entities from throwing items that can spill liquids. - /// - private void OnAttemptPacifiedThrow(Entity ent, ref AttemptPacifiedThrowEvent args) - { - // Don’t care about closed containers. - if (Openable.IsClosed(ent)) - return; - - // Don’t care about empty containers. - if (!_solutionContainerSystem.TryGetSolution(ent.Owner, ent.Comp.SolutionName, out _, out var solution) || solution.Volume <= 0) - return; - - args.Cancel("pacified-cannot-throw-spill"); - } - private void OnDoAfter(Entity entity, ref SpillDoAfterEvent args) { if (args.Handled || args.Cancelled || args.Args.Target == null) diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.Transfers.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Transfers.cs deleted file mode 100644 index e850f058a8..0000000000 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.Transfers.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Content.Shared.Chemistry.Components; -using Content.Shared.DragDrop; -using Content.Shared.FixedPoint; -using Content.Shared.Fluids; -using Content.Shared.Nutrition.EntitySystems; - -namespace Content.Server.Fluids.EntitySystems; - -public sealed partial class PuddleSystem -{ - [Dependency] private readonly OpenableSystem _openable = default!; - - private void InitializeTransfers() - { - SubscribeLocalEvent(OnRefillableDragged); - } - - private void OnRefillableDragged(Entity entity, ref DragDropDraggedEvent args) - { - if (!_actionBlocker.CanComplexInteract(args.User)) - { - _popups.PopupEntity(Loc.GetString("mopping-system-no-hands"), args.User, args.User); - return; - } - - if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.Solution, out var soln, out var solution) || solution.Volume == FixedPoint2.Zero) - { - _popups.PopupEntity(Loc.GetString("mopping-system-empty", ("used", entity.Owner)), entity, args.User); - return; - } - - // Dump reagents into DumpableSolution - if (TryComp(args.Target, out var dump)) - { - if (!_solutionContainerSystem.TryGetDumpableSolution((args.Target, dump, null), out var dumpableSoln, out var dumpableSolution)) - return; - - if (!_solutionContainerSystem.TryGetDrainableSolution(entity.Owner, out _, out _)) - return; - - if (_openable.IsClosed(entity)) - return; - - bool success = true; - if (dump.Unlimited) - { - var split = _solutionContainerSystem.SplitSolution(soln.Value, solution.Volume); - dumpableSolution.AddSolution(split, _prototypeManager); - } - else - { - var split = _solutionContainerSystem.SplitSolution(soln.Value, dumpableSolution.AvailableVolume); - success = _solutionContainerSystem.TryAddSolution(dumpableSoln.Value, split); - } - - if (success) - { - _audio.PlayPvs(AbsorbentComponent.DefaultTransferSound, args.Target); - } - else - { - _popups.PopupEntity(Loc.GetString("mopping-system-full", ("used", args.Target)), args.Target, args.User); - } - - return; - } - - // Take reagents from target - if (!TryComp(args.Target, out var drainable)) - { - if (!_solutionContainerSystem.TryGetDrainableSolution((args.Target, drainable, null), out var drainableSolution, out _)) - return; - - var split = _solutionContainerSystem.SplitSolution(drainableSolution.Value, solution.AvailableVolume); - - if (_solutionContainerSystem.TryAddSolution(soln.Value, split)) - { - _audio.PlayPvs(AbsorbentComponent.DefaultTransferSound, entity); - } - else - { - _popups.PopupEntity(Loc.GetString("mopping-system-full", ("used", entity.Owner)), entity, args.User); - } - } - } -} diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs index 63ee75618c..2f966354ec 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs @@ -1,38 +1,25 @@ -using System.Linq; -using Content.Server.Administration.Logs; -using Content.Server.Chemistry.TileReactions; -using Content.Server.DoAfter; using Content.Server.Fluids.Components; using Content.Server.Spreader; -using Content.Shared.ActionBlocker; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Reaction; -using Content.Shared.Chemistry.Reagent; using Content.Shared.Database; using Content.Shared.Effects; using Content.Shared.FixedPoint; using Content.Shared.Fluids; using Content.Shared.Fluids.Components; -using Content.Shared.Friction; using Content.Shared.IdentityManagement; using Content.Shared.Maps; -using Content.Shared.Movement.Components; -using Content.Shared.Movement.Systems; using Content.Shared.Popups; using Content.Shared.Slippery; -using Content.Shared.StepTrigger.Components; -using Content.Shared.StepTrigger.Systems; -using Robust.Server.Audio; using Robust.Shared.Collections; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; -using Robust.Shared.Timing; namespace Content.Server.Fluids.EntitySystems; @@ -41,28 +28,15 @@ namespace Content.Server.Fluids.EntitySystems; /// public sealed partial class PuddleSystem : SharedPuddleSystem { - [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; - [Dependency] private readonly IAdminLogManager _adminLogger = default!; - [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly SharedMapSystem _map = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; - [Dependency] private readonly ReactiveSystem _reactive = default!; [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; - [Dependency] private readonly SharedPopupSystem _popups = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; - [Dependency] private readonly StepTriggerSystem _stepTrigger = default!; - [Dependency] private readonly SpeedModifierContactsSystem _speedModContacts = default!; - [Dependency] private readonly TileFrictionController _tile = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly TurfSystem _turf = default!; - // Using local deletion queue instead of the standard queue so that we can easily "undelete" if a puddle - // loses & then gains reagents in a single tick. - private HashSet _deletionQueue = []; - private EntityQuery _puddleQuery; /* @@ -77,16 +51,11 @@ public sealed partial class PuddleSystem : SharedPuddleSystem _puddleQuery = GetEntityQuery(); - // Shouldn't need re-anchoring. - SubscribeLocalEvent(OnAnchorChanged); SubscribeLocalEvent(OnPuddleSpread); SubscribeLocalEvent(OnPuddleSlip); - - SubscribeLocalEvent(OnEvaporationMapInit); - - InitializeTransfers(); } + // TODO: This can be predicted once https://github.com/space-wizards/RobustToolbox/pull/5849 is merged private void OnPuddleSpread(Entity entity, ref SpreadNeighborsEvent args) { // Overflow is the source of the overflowing liquid. This contains the excess fluid above overflow limit (20u) @@ -273,6 +242,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem } } + // TODO: This can be predicted once https://github.com/space-wizards/RobustToolbox/pull/5849 is merged private void OnPuddleSlip(Entity entity, ref SlipEvent args) { // Reactive entities have a chance to get a touch reaction from slipping on a puddle @@ -289,168 +259,12 @@ public sealed partial class PuddleSystem : SharedPuddleSystem out var solution)) return; - _popups.PopupEntity(Loc.GetString("puddle-component-slipped-touch-reaction", ("puddle", entity.Owner)), + Popups.PopupEntity(Loc.GetString("puddle-component-slipped-touch-reaction", ("puddle", entity.Owner)), args.Slipped, args.Slipped, PopupType.SmallCaution); // Take 15% of the puddle solution var splitSol = _solutionContainerSystem.SplitSolution(entity.Comp.Solution.Value, solution.Volume * 0.15f); - _reactive.DoEntityReaction(args.Slipped, splitSol, ReactionMethod.Touch); - } - - /// - public override void Update(float frameTime) - { - base.Update(frameTime); - foreach (var ent in _deletionQueue) - { - Del(ent); - } - - _deletionQueue.Clear(); - - TickEvaporation(); - } - - protected override void OnSolutionUpdate(Entity entity, ref SolutionContainerChangedEvent args) - { - if (args.SolutionId != entity.Comp.SolutionName) - return; - - base.OnSolutionUpdate(entity, ref args); - - if (args.Solution.Volume <= 0) - { - _deletionQueue.Add(entity); - return; - } - - _deletionQueue.Remove(entity); - UpdateSlip((entity, entity.Comp), args.Solution); - UpdateSlow(entity, args.Solution); - UpdateEvaporation(entity, args.Solution); - } - - private void UpdateSlip(Entity entity, Solution solution) - { - if (!TryComp(entity, out var comp)) - return; - - // Ensure we actually have the component - EnsureComp(entity); - - EnsureComp(entity, out var slipComp); - - // This is the base amount of reagent needed before a puddle can be considered slippery. Is defined based on - // the sprite threshold for a puddle larger than 5 pixels. - var smallPuddleThreshold = FixedPoint2.New(entity.Comp.OverflowVolume.Float() * LowThreshold); - - // Stores how many units of slippery reagents a puddle has - var slipperyUnits = FixedPoint2.Zero; - // Stores how many units of super slippery reagents a puddle has - var superSlipperyUnits = FixedPoint2.Zero; - - // These three values will be averaged later and all start at zero so the calculations work - // A cumulative weighted amount of minimum speed to slip values - var puddleFriction = FixedPoint2.Zero; - // A cumulative weighted amount of minimum speed to slip values - var slipStepTrigger = FixedPoint2.Zero; - // A cumulative weighted amount of launch multipliers from slippery reagents - var launchMult = FixedPoint2.Zero; - // A cumulative weighted amount of stun times from slippery reagents - var stunTimer = TimeSpan.Zero; - // A cumulative weighted amount of knockdown times from slippery reagents - var knockdownTimer = TimeSpan.Zero; - - // Check if the puddle is big enough to slip in to avoid doing unnecessary logic - if (solution.Volume <= smallPuddleThreshold) - { - _stepTrigger.SetActive(entity, false, comp); - _tile.SetModifier(entity, 1f); - slipComp.SlipData.SlipFriction = 1f; - slipComp.AffectsSliding = false; - Dirty(entity, slipComp); - return; - } - - slipComp.AffectsSliding = true; - - foreach (var (reagent, quantity) in solution.Contents) - { - var reagentProto = _prototypeManager.Index(reagent.Prototype); - - // Calculate the minimum speed needed to slip in the puddle. Average the overall slip thresholds for all reagents - var deltaSlipTrigger = reagentProto.SlipData?.RequiredSlipSpeed ?? entity.Comp.DefaultSlippery; - slipStepTrigger += quantity * deltaSlipTrigger; - - // Aggregate Friction based on quantity - puddleFriction += reagentProto.Friction * quantity; - - if (reagentProto.SlipData == null) - continue; - - slipperyUnits += quantity; - // Aggregate launch speed based on quantity - launchMult += reagentProto.SlipData.LaunchForwardsMultiplier * quantity; - // Aggregate stun times based on quantity - stunTimer += reagentProto.SlipData.StunTime * (float)quantity; - knockdownTimer += reagentProto.SlipData.KnockdownTime * (float)quantity; - - if (reagentProto.SlipData.SuperSlippery) - superSlipperyUnits += quantity; - } - - // Turn on the step trigger if it's slippery - _stepTrigger.SetActive(entity, slipperyUnits > smallPuddleThreshold, comp); - - // This is based of the total volume and not just the slippery volume because there is a default - // slippery for all reagents even if they aren't technically slippery. - slipComp.SlipData.RequiredSlipSpeed = (float)(slipStepTrigger / solution.Volume); - _stepTrigger.SetRequiredTriggerSpeed(entity, slipComp.SlipData.RequiredSlipSpeed); - - // Divide these both by only total amount of slippery reagents. - // A puddle with 10 units of lube vs a puddle with 10 of lube and 20 catchup should stun and launch forward the same amount. - if (slipperyUnits > 0) - { - slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult/slipperyUnits); - slipComp.SlipData.StunTime = (stunTimer/(float)slipperyUnits); - slipComp.SlipData.KnockdownTime = (knockdownTimer/(float)slipperyUnits); - } - - // Only make it super slippery if there is enough super slippery units for its own puddle - slipComp.SlipData.SuperSlippery = superSlipperyUnits >= smallPuddleThreshold; - - // Lower tile friction based on how slippery it is, lets items slide across a puddle of lube - slipComp.SlipData.SlipFriction = (float)(puddleFriction / solution.Volume); - _tile.SetModifier(entity, slipComp.SlipData.SlipFriction); - - Dirty(entity, slipComp); - } - - private void UpdateSlow(EntityUid uid, Solution solution) - { - var maxViscosity = 0f; - foreach (var (reagent, _) in solution.Contents) - { - var reagentProto = _prototypeManager.Index(reagent.Prototype); - maxViscosity = Math.Max(maxViscosity, reagentProto.Viscosity); - } - - if (maxViscosity > 0) - { - var comp = EnsureComp(uid); - var speed = 1 - maxViscosity; - _speedModContacts.ChangeSpeedModifiers(uid, speed, comp); - } - else - { - RemComp(uid); - } - } - - private void OnAnchorChanged(Entity entity, ref AnchorStateChangedEvent args) - { - if (!args.Anchored) - QueueDel(entity); + Reactive.DoEntityReaction(args.Slipped, splitSol, ReactionMethod.Touch); } /// @@ -507,7 +321,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem return true; } - _audio.PlayPvs(puddleComponent.SpillSound, puddleUid); + Audio.PlayPvs(puddleComponent.SpillSound, puddleUid); return true; } @@ -553,6 +367,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem #region Spill + // TODO: This can be predicted once https://github.com/space-wizards/RobustToolbox/pull/5849 is merged /// public override bool TrySplashSpillAt(EntityUid uid, EntityCoordinates coordinates, @@ -582,13 +397,13 @@ public sealed partial class PuddleSystem : SharedPuddleSystem if (user != null) { - _adminLogger.Add(LogType.Landed, + AdminLogger.Add(LogType.Landed, $"{ToPrettyString(user.Value):user} threw {ToPrettyString(uid):entity} which splashed a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution} onto {ToPrettyString(owner):target}"); } targets.Add(owner); - _reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch); - _popups.PopupEntity( + Reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch); + Popups.PopupEntity( Loc.GetString("spill-land-spilled-on-other", ("spillable", uid), ("target", Identity.Entity(owner, EntityManager))), owner, PopupType.SmallCaution); } diff --git a/Content.Server/Fluids/EntitySystems/SmokeSystem.cs b/Content.Server/Fluids/EntitySystems/SmokeSystem.cs index 13695caff1..7c9d02c561 100644 --- a/Content.Server/Fluids/EntitySystems/SmokeSystem.cs +++ b/Content.Server/Fluids/EntitySystems/SmokeSystem.cs @@ -21,7 +21,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; using System.Linq; - +using Content.Shared.EntityEffects.Effects.Solution; using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent; namespace Content.Server.Fluids.EntitySystems; @@ -278,11 +278,10 @@ public sealed class SmokeSystem : EntitySystem { if (reagentQuantity.Quantity == FixedPoint2.Zero) continue; - var reagentProto = _prototype.Index(reagentQuantity.Reagent.Prototype); - _reactive.ReactionEntity(entity, ReactionMethod.Touch, reagentProto, reagentQuantity, transferSolution); + _reactive.ReactionEntity(entity, ReactionMethod.Touch, reagentQuantity); if (!blockIngestion) - _reactive.ReactionEntity(entity, ReactionMethod.Ingestion, reagentProto, reagentQuantity, transferSolution); + _reactive.ReactionEntity(entity, ReactionMethod.Ingestion, reagentQuantity); } if (blockIngestion) diff --git a/Content.Server/GameTicking/Commands/JoinGameCommand.cs b/Content.Server/GameTicking/Commands/JoinGameCommand.cs index bfb3d91464..a32a2f9495 100644 --- a/Content.Server/GameTicking/Commands/JoinGameCommand.cs +++ b/Content.Server/GameTicking/Commands/JoinGameCommand.cs @@ -13,14 +13,11 @@ namespace Content.Server.GameTicking.Commands [AnyCommand] sealed class JoinGameCommand : IConsoleCommand { - [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly IEntityManager _entManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IAdminManager _adminManager = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; - private readonly ISawmill _sawmill = default!; - public string Command => "joingame"; public string Description => ""; public string Help => ""; @@ -28,8 +25,6 @@ namespace Content.Server.GameTicking.Commands public JoinGameCommand() { IoCManager.InjectDependencies(this); - - _sawmill = _logManager.GetSawmill("security"); } public void Execute(IConsoleShell shell, string argStr, string[] args) { @@ -51,7 +46,7 @@ namespace Content.Server.GameTicking.Commands if (ticker.PlayerGameStatuses.TryGetValue(player.UserId, out var status) && status == PlayerGameStatus.JoinedGame) { - _sawmill.Info($"{player.Name} ({player.UserId}) attempted to latejoin while in-game."); + Logger.InfoS("security", $"{player.Name} ({player.UserId}) attempted to latejoin while in-game."); shell.WriteError($"{player.Name} is not in the lobby. This incident will be reported."); return; } 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/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs index e72e9d5f73..3568f17306 100644 --- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs @@ -170,12 +170,18 @@ public sealed class TraitorRuleSystem : GameRuleSystem Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#")))); return (code, briefing); } + + Log.Error($"MakeTraitor {ToPrettyString(traitor)} failed to generate an uplink code on {ToPrettyString(pda)}."); } else if (pda is null && uplinked) { Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink is implant"); briefing += "\n" + Loc.GetString("traitor-role-uplink-implant-short"); } + else + { + Log.Error($"MakeTraitor failed on {ToPrettyString(traitor)} - No uplink could be added"); + } return (null, briefing); } diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs index 1682e4cfb1..c90aeb2197 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/Ghost/SpookySpeakerSystem.cs b/Content.Server/Ghost/SpookySpeakerSystem.cs index ce3a2705a0..e16c2cf9b3 100644 --- a/Content.Server/Ghost/SpookySpeakerSystem.cs +++ b/Content.Server/Ghost/SpookySpeakerSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Server.Ghost.Components; +using Content.Shared.Chat; using Content.Shared.Random.Helpers; using Robust.Shared.Prototypes; using Robust.Shared.Random; 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/GuideGenerator/ChemistryJsonGenerator.cs b/Content.Server/GuideGenerator/ChemistryJsonGenerator.cs index 8fd088408c..930368255c 100644 --- a/Content.Server/GuideGenerator/ChemistryJsonGenerator.cs +++ b/Content.Server/GuideGenerator/ChemistryJsonGenerator.cs @@ -5,6 +5,7 @@ using System.Text.Json.Serialization; using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reagent; using Content.Shared.Damage; +using Content.Shared.EntityConditions; using Content.Shared.EntityEffects; using Content.Shared.FixedPoint; using Robust.Shared.Prototypes; @@ -42,7 +43,7 @@ public sealed class ChemistryJsonGenerator Converters = { new UniversalJsonConverter(), - new UniversalJsonConverter(), + new UniversalJsonConverter(), new UniversalJsonConverter(), new UniversalJsonConverter(), new FixedPointJsonConverter() diff --git a/Content.Server/GuideGenerator/ReagentEntry.cs b/Content.Server/GuideGenerator/ReagentEntry.cs index 8b597ad61b..59a212dbd2 100644 --- a/Content.Server/GuideGenerator/ReagentEntry.cs +++ b/Content.Server/GuideGenerator/ReagentEntry.cs @@ -76,7 +76,7 @@ public sealed class ReactionEntry proto.Products .Select(x => KeyValuePair.Create(x.Key, x.Value.Float())) .ToDictionary(x => x.Key, x => x.Value); - Effects = proto.Effects; + Effects = proto.Effects.ToList(); } } diff --git a/Content.Server/Hands/Systems/HandsSystem.cs b/Content.Server/Hands/Systems/HandsSystem.cs index 4d47ea4a78..7688d14ada 100644 --- a/Content.Server/Hands/Systems/HandsSystem.cs +++ b/Content.Server/Hands/Systems/HandsSystem.cs @@ -160,7 +160,7 @@ namespace Content.Server.Hands.Systems if (TryComp(throwEnt, out StackComponent? stack) && stack.Count > 1 && stack.ThrowIndividually) { - var splitStack = _stackSystem.Split(throwEnt.Value, 1, Comp(player).Coordinates, stack); + var splitStack = _stackSystem.Split((throwEnt.Value, stack), 1, Comp(player).Coordinates); if (splitStack is not {Valid: true}) return false; diff --git a/Content.Server/Holopad/HolopadSystem.cs b/Content.Server/Holopad/HolopadSystem.cs index 630141d003..c634d14f2f 100644 --- a/Content.Server/Holopad/HolopadSystem.cs +++ b/Content.Server/Holopad/HolopadSystem.cs @@ -4,6 +4,7 @@ using Content.Server.Power.EntitySystems; using Content.Server.Telephone; using Content.Shared.Access.Systems; using Content.Shared.Audio; +using Content.Shared.Chat; using Content.Shared.Chat.TypingIndicator; using Content.Shared.Holopad; using Content.Shared.IdentityManagement; 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 c2d2614a0a..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,8 +39,8 @@ using Robust.Shared.Timing; using Content.Shared.Stacks; using Content.Server.Construction.Components; using Content.Shared.Chat; -using Content.Shared.Damage; -using Robust.Shared.Utility; +using Content.Shared.Damage.Components; +using Content.Shared.Temperature.Components; namespace Content.Server.Kitchen.EntitySystems { @@ -241,7 +240,7 @@ namespace Content.Server.Kitchen.EntitySystems // If an entity has a stack component, use the stacktype instead of prototype id if (TryComp(item, out var stackComp)) { - itemID = _prototype.Index(stackComp.StackTypeId).Spawn; + itemID = _prototype.Index(stackComp.StackTypeId).Spawn; } else { @@ -264,7 +263,7 @@ namespace Content.Server.Kitchen.EntitySystems { _container.Remove(item, component.Storage); } - _stack.Use(item, 1, stackComp); + _stack.ReduceCount((item, stackComp), 1); break; } else diff --git a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs index cd0ce8f3a6..b850bc87fa 100644 --- a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs @@ -118,7 +118,7 @@ namespace Content.Server.Kitchen.EntitySystems scaledSolution.ScaleSolution(fitsCount); solution = scaledSolution; - _stackSystem.SetCount(item, stack.Count - fitsCount); // Setting to 0 will QueueDel + _stackSystem.ReduceCount((item, stack), fitsCount); // Setting to 0 will QueueDel } else { diff --git a/Content.Server/Light/EntitySystems/ExpendableLightSystem.cs b/Content.Server/Light/EntitySystems/ExpendableLightSystem.cs index f643bec73f..0436ea7d3c 100644 --- a/Content.Server/Light/EntitySystems/ExpendableLightSystem.cs +++ b/Content.Server/Light/EntitySystems/ExpendableLightSystem.cs @@ -136,13 +136,13 @@ namespace Content.Server.Light.EntitySystems component.StateExpiryTime = (float)component.RefuelMaterialTime.TotalSeconds; _nameModifier.RefreshNameModifiers(uid); - _stackSystem.SetCount(args.Used, stack.Count - 1, stack); + _stackSystem.ReduceCount((args.Used, stack), 1); UpdateVisualizer((uid, component)); return; } component.StateExpiryTime += (float)component.RefuelMaterialTime.TotalSeconds; - _stackSystem.SetCount(args.Used, stack.Count - 1, stack); + _stackSystem.ReduceCount((args.Used, stack), 1); args.Handled = true; } 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/Lube/LubedSystem.cs b/Content.Server/Lube/LubedSystem.cs index 3c536dcceb..01a2fa8dde 100644 --- a/Content.Server/Lube/LubedSystem.cs +++ b/Content.Server/Lube/LubedSystem.cs @@ -1,9 +1,11 @@ +using Content.Shared.Hands; +using Content.Shared.Hands.EntitySystems; using Content.Shared.IdentityManagement; +using Content.Shared.Item; using Content.Shared.Lube; using Content.Shared.NameModifier.EntitySystems; using Content.Shared.Popups; using Content.Shared.Throwing; -using Robust.Shared.Containers; using Robust.Shared.Random; namespace Content.Server.Lube; @@ -21,7 +23,7 @@ public sealed class LubedSystem : EntitySystem base.Initialize(); SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(OnHandPickUp); + SubscribeLocalEvent(OnHandPickUp); SubscribeLocalEvent(OnRefreshNameModifiers); } @@ -30,21 +32,38 @@ public sealed class LubedSystem : EntitySystem _nameMod.RefreshNameModifiers(uid); } - private void OnHandPickUp(EntityUid uid, LubedComponent component, ContainerGettingInsertedAttemptEvent args) + /// + /// Note to whoever makes this predicted—there is a mispredict here that + /// would be nice to keep! If this is in shared, the client will predict + /// this and not run the pickup animation in + /// which would (probably) make this effect look less funny. You will + /// probably want to either tweak + /// to be able to cancel but still run the animation or something—we do want + /// the event to run before the animation for stuff like + /// . + /// + private void OnHandPickUp(Entity ent, ref BeforeGettingEquippedHandEvent args) { - if (component.SlipsLeft <= 0) + if (args.Cancelled) + return; + + if (ent.Comp.SlipsLeft <= 0) { - RemComp(uid); - _nameMod.RefreshNameModifiers(uid); + RemComp(ent); + _nameMod.RefreshNameModifiers(ent.Owner); return; } - component.SlipsLeft--; - args.Cancel(); - var user = args.Container.Owner; - _transform.SetCoordinates(uid, Transform(user).Coordinates); - _transform.AttachToGridOrMap(uid); - _throwing.TryThrow(uid, _random.NextVector2(), baseThrowSpeed: component.SlipStrength); - _popup.PopupEntity(Loc.GetString("lube-slip", ("target", Identity.Entity(uid, EntityManager))), user, user, PopupType.MediumCaution); + + ent.Comp.SlipsLeft--; + args.Cancelled = true; + + _transform.SetCoordinates(ent, Transform(args.User).Coordinates); + _transform.AttachToGridOrMap(ent); + _throwing.TryThrow(ent, _random.NextVector2(), ent.Comp.SlipStrength); + _popup.PopupEntity(Loc.GetString("lube-slip", ("target", Identity.Entity(ent, EntityManager))), + args.User, + args.User, + PopupType.MediumCaution); } private void OnRefreshNameModifiers(Entity entity, ref RefreshNameModifiersEvent args) diff --git a/Content.Server/MagicMirror/MagicMirrorSystem.cs b/Content.Server/MagicMirror/MagicMirrorSystem.cs index 0fd00fca24..dbc258c02c 100644 --- a/Content.Server/MagicMirror/MagicMirrorSystem.cs +++ b/Content.Server/MagicMirror/MagicMirrorSystem.cs @@ -107,6 +107,8 @@ public sealed class MagicMirrorSystem : SharedMagicMirrorSystem private void OnSelectSlotDoAfter(EntityUid uid, MagicMirrorComponent component, MagicMirrorSelectDoAfterEvent args) { + component.DoAfter = null; + if (args.Handled || args.Target == null || args.Cancelled) return; @@ -185,6 +187,8 @@ public sealed class MagicMirrorSystem : SharedMagicMirrorSystem } private void OnChangeColorDoAfter(EntityUid uid, MagicMirrorComponent component, MagicMirrorChangeColorDoAfterEvent args) { + component.DoAfter = null; + if (args.Handled || args.Target == null || args.Cancelled) return; @@ -265,6 +269,8 @@ public sealed class MagicMirrorSystem : SharedMagicMirrorSystem private void OnRemoveSlotDoAfter(EntityUid uid, MagicMirrorComponent component, MagicMirrorRemoveSlotDoAfterEvent args) { + component.DoAfter = null; + if (args.Handled || args.Target == null || args.Cancelled) return; @@ -342,6 +348,8 @@ public sealed class MagicMirrorSystem : SharedMagicMirrorSystem } private void OnAddSlotDoAfter(EntityUid uid, MagicMirrorComponent component, MagicMirrorAddSlotDoAfterEvent args) { + component.DoAfter = null; + if (args.Handled || args.Target == null || args.Cancelled || !TryComp(component.Target, out HumanoidAppearanceComponent? humanoid)) return; diff --git a/Content.Server/Materials/MaterialStorageSystem.cs b/Content.Server/Materials/MaterialStorageSystem.cs index 3a462dd4d5..f6a1b6c4d8 100644 --- a/Content.Server/Materials/MaterialStorageSystem.cs +++ b/Content.Server/Materials/MaterialStorageSystem.cs @@ -73,7 +73,7 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem return; var volumePerSheet = composition.MaterialComposition.FirstOrDefault(kvp => kvp.Key == msg.Material).Value; - var sheetsToExtract = Math.Min(msg.SheetsToExtract, _stackSystem.GetMaxCount(material.StackEntity)); + var sheetsToExtract = Math.Min(msg.SheetsToExtract, _stackSystem.GetMaxCount(material.StackEntity.Value)); volume = sheetsToExtract * volumePerSheet; } @@ -183,7 +183,7 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem if (amountToSpawn == 0) return new List(); - return _stackSystem.SpawnMultiple(materialProto.StackEntity, amountToSpawn, coordinates); + return _stackSystem.SpawnMultipleAtPosition(materialProto.StackEntity.Value, amountToSpawn, coordinates); } /// 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/CryoPodSystem.cs b/Content.Server/Medical/CryoPodSystem.cs index 5bee8e3014..fb7ba2d42f 100644 --- a/Content.Server/Medical/CryoPodSystem.cs +++ b/Content.Server/Medical/CryoPodSystem.cs @@ -5,12 +5,12 @@ using Content.Server.Medical.Components; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.NodeGroups; using Content.Server.NodeContainer.Nodes; -using Content.Server.Temperature.Components; using Content.Shared.Atmos; using Content.Shared.Body.Components; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Medical.Cryogenics; using Content.Shared.MedicalScanner; +using Content.Shared.Temperature.Components; using Content.Shared.UserInterface; using Robust.Shared.Containers; using Content.Shared._Offbrand.Wounds; // Offbrand diff --git a/Content.Server/Medical/DefibrillatorSystem.cs b/Content.Server/Medical/DefibrillatorSystem.cs index a4ad7a2e51..26611d4a9c 100644 --- a/Content.Server/Medical/DefibrillatorSystem.cs +++ b/Content.Server/Medical/DefibrillatorSystem.cs @@ -7,20 +7,18 @@ using Content.Server.Ghost; using Content.Server.Popups; using Content.Server.PowerCell; using Content.Shared.Traits.Assorted; -using Content.Shared.Damage; +using Content.Shared.Chat; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; using Content.Shared.DoAfter; using Content.Shared.Interaction; -using Content.Shared.Interaction.Components; -using Content.Shared.Interaction.Events; using Content.Shared.Item.ItemToggle; using Content.Shared.Medical; using Content.Shared.Mind; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; -using Content.Shared.PowerCell; using Content.Shared.Timing; -using Content.Shared.Toggleable; using Robust.Shared.Audio.Systems; using Robust.Shared.Player; using Content.Shared._Offbrand.Wounds; // Offbrand diff --git a/Content.Server/Medical/HealthAnalyzerSystem.cs b/Content.Server/Medical/HealthAnalyzerSystem.cs index c7359d9f78..e8a9ac3059 100644 --- a/Content.Server/Medical/HealthAnalyzerSystem.cs +++ b/Content.Server/Medical/HealthAnalyzerSystem.cs @@ -1,9 +1,8 @@ using Content.Server.Medical.Components; using Content.Server.PowerCell; -using Content.Server.Temperature.Components; 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; @@ -13,6 +12,7 @@ using Content.Shared.Item.ItemToggle.Components; using Content.Shared.MedicalScanner; using Content.Shared.Mobs.Components; using Content.Shared.Popups; +using Content.Shared.Temperature.Components; using Content.Shared.Traits.Assorted; using Robust.Server.GameObjects; using Robust.Shared.Audio.Systems; 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/Mobs/CritMobActionsSystem.cs b/Content.Server/Mobs/CritMobActionsSystem.cs index c266037a8f..4a16fa19a3 100644 --- a/Content.Server/Mobs/CritMobActionsSystem.cs +++ b/Content.Server/Mobs/CritMobActionsSystem.cs @@ -1,7 +1,7 @@ using Content.Server.Administration; using Content.Server.Chat.Systems; using Content.Server.Popups; -using Content.Server.Speech.Muting; +using Content.Shared.Chat; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; diff --git a/Content.Server/Movement/Components/StressTestMovementComponent.cs b/Content.Server/Movement/Components/StressTestMovementComponent.cs deleted file mode 100644 index 051747a849..0000000000 --- a/Content.Server/Movement/Components/StressTestMovementComponent.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Numerics; - -namespace Content.Server.Movement.Components; - -[RegisterComponent] -public sealed partial class StressTestMovementComponent : Component -{ - public float Progress { get; set; } - public Vector2 Origin { get; set; } -} diff --git a/Content.Server/Movement/StressTestMovementSystem.cs b/Content.Server/Movement/StressTestMovementSystem.cs deleted file mode 100644 index 51916204da..0000000000 --- a/Content.Server/Movement/StressTestMovementSystem.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Numerics; -using Content.Server.Movement.Components; - -namespace Content.Server.Movement; - -public sealed class StressTestMovementSystem : EntitySystem -{ - [Dependency] private readonly SharedTransformSystem _transform = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnStressStartup); - } - - private void OnStressStartup(EntityUid uid, StressTestMovementComponent component, ComponentStartup args) - { - component.Origin = _transform.GetWorldPosition(uid); - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator(); - - while (query.MoveNext(out var uid, out var stressTest, out var transform)) - { - if (!transform.ParentUid.IsValid()) - continue; - - stressTest.Progress += frameTime; - - if (stressTest.Progress > 1) - { - stressTest.Progress -= 1; - } - - var x = MathF.Sin(stressTest.Progress * MathHelper.TwoPi); - var y = MathF.Cos(stressTest.Progress * MathHelper.TwoPi); - - _transform.SetWorldPosition((uid, transform), stressTest.Origin + new Vector2(x, y) * 5); - } - } -} diff --git a/Content.Server/NPC/HTN/Preconditions/InFriendlyContainerPrecondition.cs b/Content.Server/NPC/HTN/Preconditions/InFriendlyContainerPrecondition.cs new file mode 100644 index 0000000000..ffee9adaf3 --- /dev/null +++ b/Content.Server/NPC/HTN/Preconditions/InFriendlyContainerPrecondition.cs @@ -0,0 +1,50 @@ +using Content.Shared.NPC.Systems; +using Robust.Shared.Containers; +using Robust.Server.Containers; +using Robust.Shared.GameObjects; + +namespace Content.Server.NPC.HTN.Preconditions; + +/// +/// Checks if the owner is in a friendly container. +/// Recursively checks if container's container is friendly. +/// +public sealed partial class InFriendlyContainerPrecondition : HTNPrecondition +{ + private ContainerSystem _container = default!; + private NpcFactionSystem _npcFaction = default!; + + [DataField] public bool IsInFriendlyContainer = true; + + public override void Initialize(IEntitySystemManager sysManager) + { + base.Initialize(sysManager); + _container = sysManager.GetEntitySystem(); + _npcFaction = sysManager.GetEntitySystem(); + } + + public override bool IsMet(NPCBlackboard blackboard) + { + var owner = blackboard.GetValue(NPCBlackboard.Owner); + + if (!_container.TryGetContainingContainer(owner, out var container)) + return !IsInFriendlyContainer; + + return IsInFriendlyContainer == IsContainerOrParentFriendly(owner, container.Owner); + } + + /// + /// Recursively check if a container or any parent container is friendly. + /// + /// True if any container is friendly. + private bool IsContainerOrParentFriendly(EntityUid owner, EntityUid containerOwner) + { + if (_npcFaction.IsEntityFriendly(owner, containerOwner)) + return true; + + if (!_container.TryGetContainingContainer(containerOwner, out var nextContainer)) + return false; + + return IsContainerOrParentFriendly(owner, nextContainer.Owner); + } +} diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SayKeyOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SayKeyOperator.cs index 558b1fc04d..99227492af 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SayKeyOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SayKeyOperator.cs @@ -1,4 +1,5 @@ using Content.Server.Chat.Systems; +using Content.Shared.Chat; namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators; diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs index f69a0771f9..f3b977518b 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs @@ -1,4 +1,5 @@ using Content.Server.Chat.Systems; +using Content.Shared.Chat; using Content.Shared.Dataset; using Content.Shared.Random.Helpers; using JetBrains.Annotations; diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs index bac9cfcf40..20e3daec53 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs @@ -1,10 +1,7 @@ using Content.Server.Chat.Systems; -using Content.Shared.NPC.Components; +using Content.Shared.Chat; using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Damage; -using Content.Shared.Emag.Components; using Content.Shared.Interaction; -using Content.Shared.Mobs.Components; using Content.Shared.Popups; using Content.Shared.Silicons.Bots; using Robust.Shared.Audio.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 0fbbc43c16..0683bbf42e 100644 --- a/Content.Server/NPC/Systems/NPCUtilitySystem.cs +++ b/Content.Server/NPC/Systems/NPCUtilitySystem.cs @@ -5,9 +5,7 @@ using Content.Server.NPC.Queries.Considerations; using Content.Server.NPC.Queries.Curves; using Content.Server.NPC.Queries.Queries; using Content.Server.Nutrition.Components; -using Content.Server.Temperature.Components; using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Damage; using Content.Shared.Examine; using Content.Shared.Fluids.Components; using Content.Shared.Inventory; @@ -30,6 +28,8 @@ 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; using Content.Shared._Offbrand.Wounds; // Offbrand namespace Content.Server.NPC.Systems; @@ -187,7 +187,7 @@ public sealed class NPCUtilitySystem : EntitySystem return 0f; var nutrition = _ingestion.TotalNutrition(targetUid, owner); - if (nutrition <= 1.0f) + if (nutrition == 0.0f) return 0f; return 1f; 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/Components/BadFoodComponent.cs b/Content.Server/Nutrition/Components/BadFoodComponent.cs index 16f90533cb..40a805e437 100644 --- a/Content.Server/Nutrition/Components/BadFoodComponent.cs +++ b/Content.Server/Nutrition/Components/BadFoodComponent.cs @@ -6,7 +6,5 @@ namespace Content.Server.Nutrition.Components; /// This component prevents NPC mobs like mice from wanting to eat something that is edible but is not exactly food. /// Including but not limited to: uranium, death pills, insulation /// -[RegisterComponent, Access(typeof(FoodSystem))] -public sealed partial class BadFoodComponent : Component -{ -} +[RegisterComponent] +public sealed partial class BadFoodComponent : Component; diff --git a/Content.Server/Nutrition/Components/IgnoreBadFoodComponent.cs b/Content.Server/Nutrition/Components/IgnoreBadFoodComponent.cs index f5b3c326f3..c0f2fefce4 100644 --- a/Content.Server/Nutrition/Components/IgnoreBadFoodComponent.cs +++ b/Content.Server/Nutrition/Components/IgnoreBadFoodComponent.cs @@ -6,7 +6,5 @@ namespace Content.Server.Nutrition.Components; /// This component allows NPC mobs to eat food with BadFoodComponent. /// See MobMouseAdmeme for usage. /// -[RegisterComponent, Access(typeof(FoodSystem))] -public sealed partial class IgnoreBadFoodComponent : Component -{ -} +[RegisterComponent] +public sealed partial class IgnoreBadFoodComponent : Component; diff --git a/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs b/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs index 7164c701f5..33b619732d 100644 --- a/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs @@ -33,8 +33,6 @@ namespace Content.Server.Nutrition.EntitySystems { base.Initialize(); - // activate BEFORE entity is deleted and trash is spawned - SubscribeLocalEvent(OnConsume, before: [typeof(FoodSystem)]); SubscribeLocalEvent(OnSlice); SubscribeLocalEvent(OnRejuvenate); @@ -59,10 +57,10 @@ namespace Content.Server.Nutrition.EntitySystems QueueDel(entity); } - private void OnConsume(Entity entity, ref ConsumeDoAfterEvent args) - { - ActivatePayload(entity); - } + // TODO + // A regression occured here. Previously creampies would activate their hidden payload if you tried to eat them. + // However, the refactor to IngestionSystem caused the event to not be reached, + // because eating is blocked if an item is inside the food. private void OnSlice(Entity entity, ref SliceFoodEvent args) { 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/Nutrition/Events/DrinkEvents.cs b/Content.Server/Nutrition/Events/DrinkEvents.cs deleted file mode 100644 index b7a7403105..0000000000 --- a/Content.Server/Nutrition/Events/DrinkEvents.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Content.Shared.Chemistry.Components; - -namespace Content.Server.Nutrition.Events; - -/// -/// Raised on the entity drinking. This is right before they actually transfer the solution into the stomach. -/// -/// The drink that is being drank. -/// The solution that will be digested. -/// Whether the target was forced to drink the solution by somebody else. -[ByRefEvent] -public record struct BeforeIngestDrinkEvent(EntityUid Drink, Solution Solution, bool Forced); 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/Power/EntitySystems/CableSystem.Placer.cs b/Content.Server/Power/EntitySystems/CableSystem.Placer.cs index 79ea6b5285..d6fe1a8f85 100644 --- a/Content.Server/Power/EntitySystems/CableSystem.Placer.cs +++ b/Content.Server/Power/EntitySystems/CableSystem.Placer.cs @@ -49,7 +49,7 @@ public sealed partial class CableSystem return; } - if (TryComp(placer, out var stack) && !_stack.Use(placer, 1, stack)) + if (TryComp(placer, out var stack) && !_stack.TryUse((placer.Owner, stack), 1)) return; var newCable = Spawn(component.CablePrototypeId, _map.GridTileToLocal(gridUid, grid, snapPos)); diff --git a/Content.Server/PowerSink/PowerSinkSystem.cs b/Content.Server/PowerSink/PowerSinkSystem.cs index df0980883d..d1071d3e38 100644 --- a/Content.Server/PowerSink/PowerSinkSystem.cs +++ b/Content.Server/PowerSink/PowerSinkSystem.cs @@ -1,4 +1,4 @@ -using Content.Server.Chat.Systems; +using Content.Server.Chat.Systems; using Content.Server.Explosion.EntitySystems; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; 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/Radiation/Systems/RadiationSystem.cs b/Content.Server/Radiation/Systems/RadiationSystem.cs index 34dbec6f86..4a4d0f8930 100644 --- a/Content.Server/Radiation/Systems/RadiationSystem.cs +++ b/Content.Server/Radiation/Systems/RadiationSystem.cs @@ -49,9 +49,9 @@ public sealed partial class RadiationSystem : EntitySystem _accumulator = 0f; } - public void IrradiateEntity(EntityUid uid, float radsPerSecond, float time) + public void IrradiateEntity(EntityUid uid, float radsPerSecond, float time, EntityUid? origin = null) { - var msg = new OnIrradiatedEvent(time, radsPerSecond, uid); + var msg = new OnIrradiatedEvent(time, radsPerSecond, origin); RaiseLocalEvent(uid, msg); } diff --git a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs index f1e15bcc79..6f717b96cd 100644 --- a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs +++ b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs @@ -1,4 +1,4 @@ -using Content.Server.Chat.Systems; +using Content.Shared.Chat; using Content.Shared.Inventory.Events; using Content.Shared.Radio; using Content.Shared.Radio.Components; diff --git a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs index b0f38c92fa..68067ae6f5 100644 --- a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs @@ -3,16 +3,16 @@ using Content.Server.Chat.Systems; using Content.Server.Interaction; using Content.Server.Popups; using Content.Server.Power.EntitySystems; -using Content.Shared.Radio.Components; +using Content.Shared.Chat; using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.Power; using Content.Shared.Radio; +using Content.Shared.Radio.Components; +using Content.Shared.Radio.EntitySystems; using Content.Shared.Speech; using Content.Shared.Speech.Components; -using Content.Shared.Chat; using Robust.Shared.Prototypes; -using Content.Shared.Radio.EntitySystems; namespace Content.Server.Radio.EntitySystems; diff --git a/Content.Server/RatKing/RatKingSystem.cs b/Content.Server/RatKing/RatKingSystem.cs index eb3bef6c02..a7d6ecfd47 100644 --- a/Content.Server/RatKing/RatKingSystem.cs +++ b/Content.Server/RatKing/RatKingSystem.cs @@ -6,6 +6,7 @@ using Content.Server.NPC.HTN; using Content.Server.NPC.Systems; using Content.Server.Popups; using Content.Shared.Atmos; +using Content.Shared.Chat; using Content.Shared.Dataset; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; @@ -13,7 +14,6 @@ using Content.Shared.Pointing; using Content.Shared.Random.Helpers; using Content.Shared.RatKing; using Robust.Shared.Map; -using Robust.Shared.Random; namespace Content.Server.RatKing { diff --git a/Content.Server/Remotes/DoorRemoteSystem.cs b/Content.Server/Remotes/DoorRemoteSystem.cs index c3425f347a..6d2219bae1 100644 --- a/Content.Server/Remotes/DoorRemoteSystem.cs +++ b/Content.Server/Remotes/DoorRemoteSystem.cs @@ -1,108 +1,5 @@ -using Content.Server.Administration.Logs; -using Content.Server.Doors.Systems; -using Content.Server.Power.EntitySystems; -using Content.Shared.Access.Components; -using Content.Shared.Database; -using Content.Shared.Doors.Components; -using Content.Shared.Examine; -using Content.Shared.Interaction; -using Content.Shared.Remotes.Components; using Content.Shared.Remotes.EntitySystems; -namespace Content.Shared.Remotes -{ - public sealed class DoorRemoteSystem : SharedDoorRemoteSystem - { - [Dependency] private readonly IAdminLogManager _adminLogger = default!; - [Dependency] private readonly AirlockSystem _airlock = default!; - [Dependency] private readonly DoorSystem _doorSystem = default!; - [Dependency] private readonly ExamineSystemShared _examine = default!; +namespace Content.Server.Remotes; - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnBeforeInteract); - } - - private void OnBeforeInteract(Entity entity, ref BeforeRangedInteractEvent args) - { - bool isAirlock = TryComp(args.Target, out var airlockComp); - - if (args.Handled - || args.Target == null - || !TryComp(args.Target, out var doorComp) // If it isn't a door we don't use it - // Only able to control doors if they are within your vision and within your max range. - // Not affected by mobs or machines anymore. - || !_examine.InRangeUnOccluded(args.User, - args.Target.Value, - SharedInteractionSystem.MaxRaycastRange, - null)) - - { - return; - } - - args.Handled = true; - - if (!this.IsPowered(args.Target.Value, EntityManager)) - { - Popup.PopupEntity(Loc.GetString("door-remote-no-power"), args.User, args.User); - return; - } - - var accessTarget = args.Used; - // This covers the accesses the REMOTE has, and is not effected by the user's ID card. - if (entity.Comp.IncludeUserAccess) // Allows some door remotes to inherit the user's access. - { - accessTarget = args.User; - // This covers the accesses the USER has, which always includes the remote's access since holding a remote acts like holding an ID card. - } - - if (TryComp(args.Target, out var accessComponent) - && !_doorSystem.HasAccess(args.Target.Value, accessTarget, doorComp, accessComponent)) - { - if (isAirlock) - _doorSystem.Deny(args.Target.Value, doorComp, accessTarget); - Popup.PopupEntity(Loc.GetString("door-remote-denied"), args.User, args.User); - return; - } - - switch (entity.Comp.Mode) - { - case OperatingMode.OpenClose: - if (_doorSystem.TryToggleDoor(args.Target.Value, doorComp, accessTarget)) - _adminLogger.Add(LogType.Action, - LogImpact.Medium, - $"{ToPrettyString(args.User):player} used {ToPrettyString(args.Used)} on {ToPrettyString(args.Target.Value)}: {doorComp.State}"); - break; - case OperatingMode.ToggleBolts: - if (TryComp(args.Target, out var boltsComp)) - { - if (!boltsComp.BoltWireCut) - { - _doorSystem.SetBoltsDown((args.Target.Value, boltsComp), !boltsComp.BoltsDown, accessTarget); - _adminLogger.Add(LogType.Action, - LogImpact.Medium, - $"{ToPrettyString(args.User):player} used {ToPrettyString(args.Used)} on {ToPrettyString(args.Target.Value)} to {(boltsComp.BoltsDown ? "" : "un")}bolt it"); - } - } - - break; - case OperatingMode.ToggleEmergencyAccess: - if (airlockComp != null) - { - _airlock.SetEmergencyAccess((args.Target.Value, airlockComp), !airlockComp.EmergencyAccess); - _adminLogger.Add(LogType.Action, - LogImpact.Medium, - $"{ToPrettyString(args.User):player} used {ToPrettyString(args.Used)} on {ToPrettyString(args.Target.Value)} to set emergency access {(airlockComp.EmergencyAccess ? "on" : "off")}"); - } - - break; - default: - throw new InvalidOperationException( - $"{nameof(DoorRemoteComponent)} had invalid mode {entity.Comp.Mode}"); - } - } - } -} +public sealed class DoorRemoteSystem : SharedDoorRemoteSystem; diff --git a/Content.Server/Research/Systems/ResearchSystem.Client.cs b/Content.Server/Research/Systems/ResearchSystem.Client.cs index 3f75982425..509d1d20de 100644 --- a/Content.Server/Research/Systems/ResearchSystem.Client.cs +++ b/Content.Server/Research/Systems/ResearchSystem.Client.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Power.EntitySystems; using Content.Shared.Research.Components; +using Robust.Shared.Utility; namespace Content.Server.Research.Systems; @@ -62,10 +63,8 @@ public sealed partial class ResearchSystem private void OnClientMapInit(EntityUid uid, ResearchClientComponent component, MapInitEvent args) { - var allServers = GetServers(uid).ToList(); - - if (allServers.Count > 0) - RegisterClient(uid, allServers[0], component, allServers[0]); + if (GetServers(uid).FirstOrNull() is { } server) + RegisterClient(uid, server, component, server); } private void OnClientShutdown(EntityUid uid, ResearchClientComponent component, ComponentShutdown args) @@ -85,10 +84,8 @@ public sealed partial class ResearchSystem if (ent.Comp.Server is not null) return; - var allServers = GetServers(ent).ToList(); - - if (allServers.Count > 0) - RegisterClient(ent, allServers[0], ent, allServers[0]); + if (GetServers(ent).FirstOrNull() is { } server) + RegisterClient(ent, server, ent, server); } else { diff --git a/Content.Server/Research/Systems/ResearchSystem.cs b/Content.Server/Research/Systems/ResearchSystem.cs index 5cad4d51ba..c6a2a0125d 100644 --- a/Content.Server/Research/Systems/ResearchSystem.cs +++ b/Content.Server/Research/Systems/ResearchSystem.cs @@ -23,8 +23,6 @@ namespace Content.Server.Research.Systems [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly RadioSystem _radio = default!; - private static readonly HashSet> ClientLookup = new(); - public override void Initialize() { base.Initialize(); @@ -49,7 +47,7 @@ namespace Content.Server.Research.Systems serverUid = null; serverComponent = null; - var query = GetServers(client).ToList(); + var query = GetServers(client); foreach (var (uid, server) in query) { if (server.Id != id) @@ -67,15 +65,7 @@ namespace Content.Server.Research.Systems /// public string[] GetServerNames(EntityUid client) { - var allServers = GetServers(client).ToArray(); - var list = new string[allServers.Length]; - - for (var i = 0; i < allServers.Length; i++) - { - list[i] = allServers[i].Comp.ServerName; - } - - return list; + return GetServers(client).Select(x => x.Comp.ServerName).ToArray(); } /// @@ -84,27 +74,18 @@ namespace Content.Server.Research.Systems /// public int[] GetServerIds(EntityUid client) { - var allServers = GetServers(client).ToArray(); - var list = new int[allServers.Length]; - - for (var i = 0; i < allServers.Length; i++) - { - list[i] = allServers[i].Comp.Id; - } - - return list; + return GetServers(client).Select(x => x.Comp.Id).ToArray(); } public HashSet> GetServers(EntityUid client) { - ClientLookup.Clear(); - var clientXform = Transform(client); if (clientXform.GridUid is not { } grid) - return ClientLookup; + return []; - _lookup.GetGridEntities(grid, ClientLookup); - return ClientLookup; + var set = new HashSet>(); + _lookup.GetGridEntities(grid, set); + return set; } public override void Update(float frameTime) diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs index d1cb98f15c..c0d9fece0a 100644 --- a/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs +++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs @@ -211,7 +211,7 @@ public sealed partial class RevenantSystem //KILL THEMMMM - _damage.TryChangeDamage(args.Args.Target, component.HarvestDamage, true, origin: uid); // Offbrand - use a fixed amount + _damage.ChangeDamage(args.Args.Target.Value, component.HarvestDamage, 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.FasterThanLight.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs index a63359077f..4ca0b53035 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs @@ -801,11 +801,7 @@ public sealed partial class ShuttleSystem while (iteration < FTLProximityIterations) { grids.Clear(); - // We pass in an expanded offset here so we can safely do a random offset later. - // We don't include this in the actual targetAABB because then we would be double-expanding it. - // Once in this loop, then again when placing the shuttle later. - // Note that targetAABB already has expansionAmount factored in already. - _mapManager.FindGridsIntersecting(mapId, targetAABB.Enlarged(maxOffset), ref grids); + _mapManager.FindGridsIntersecting(mapId, targetAABB, ref grids); foreach (var grid in grids) { @@ -838,6 +834,10 @@ public sealed partial class ShuttleSystem if (nearbyGrids.Contains(uid)) continue; + // We pass in an expanded offset here so we can safely do a random offset later. + // We don't include this in the actual targetAABB because then we would be double-expanding it. + // Once in this loop, then again when placing the shuttle later. + // Note that targetAABB already has expansionAmount factored in already. targetAABB = targetAABB.Union( _transform.GetWorldMatrix(uid) .TransformBox(Comp(uid).LocalAABB.Enlarged(expansionAmount))); @@ -857,7 +857,7 @@ public sealed partial class ShuttleSystem // TODO: This should prefer the position's angle instead. // TODO: This is pretty crude for multiple landings. - if (nearbyGrids.Count > 1 || !HasComp(targetXform.GridUid)) + if (nearbyGrids.Count >= 1) { // Pick a random angle var offsetAngle = _random.NextAngle(); @@ -866,13 +866,9 @@ public sealed partial class ShuttleSystem var minRadius = MathF.Max(targetAABB.Width / 2f, targetAABB.Height / 2f); spawnPos = targetAABB.Center + offsetAngle.RotateVec(new Vector2(_random.NextFloat(minRadius + minOffset, minRadius + maxOffset), 0f)); } - else if (shuttleBody != null) - { - (spawnPos, angle) = _transform.GetWorldPositionRotation(targetXform); - } else { - spawnPos = _transform.GetWorldPosition(targetXform); + spawnPos = _transform.ToWorldPosition(targetCoordinates); } var offset = Vector2.Zero; @@ -893,10 +889,10 @@ public sealed partial class ShuttleSystem } // Rotate our localcenter around so we spawn exactly where we "think" we should (center of grid on the dot). - var transform = new Transform(spawnPos, angle); - spawnPos = Robust.Shared.Physics.Transform.Mul(transform, offset); + var transform = new Transform(_transform.ToWorldPosition(xform.Coordinates), angle); + var adjustedOffset = Robust.Shared.Physics.Transform.Mul(transform, offset); - coordinates = new EntityCoordinates(targetXform.MapUid.Value, spawnPos - offset); + coordinates = new EntityCoordinates(targetXform.MapUid.Value, spawnPos + adjustedOffset); return true; } 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 cea7fbfc09..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; @@ -104,7 +104,12 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem return; EnsureComp(ev.EntityUid); - EnsureComp(ev.EntityUid); + + // This and RoofComponent should be mutually exclusive, so ImplicitRoof should be removed if the grid has RoofComponent + if (HasComp(ev.EntityUid)) + RemComp(ev.EntityUid); + else + EnsureComp(ev.EntityUid); } private void OnShuttleStartup(EntityUid uid, ShuttleComponent component, ComponentStartup args) 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/Borgs/BorgSystem.cs b/Content.Server/Silicons/Borgs/BorgSystem.cs index 88d484b846..6ae61acc57 100644 --- a/Content.Server/Silicons/Borgs/BorgSystem.cs +++ b/Content.Server/Silicons/Borgs/BorgSystem.cs @@ -349,7 +349,6 @@ public sealed partial class BorgSystem : SharedBorgSystem /// /// Checks that a player has fulfilled the requirements for the borg job. - /// If they don't have enough hours, they cannot be placed into a chassis. /// public bool CanPlayerBeBorged(ICommonSession session) { 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/GravityWellSystem.cs b/Content.Server/Singularity/EntitySystems/GravityWellSystem.cs index 6f2137b0d0..bf639746ba 100644 --- a/Content.Server/Singularity/EntitySystems/GravityWellSystem.cs +++ b/Content.Server/Singularity/EntitySystems/GravityWellSystem.cs @@ -73,9 +73,6 @@ public sealed class GravityWellSystem : SharedGravityWellSystem /// The time elapsed since the last set of updates. public override void Update(float frameTime) { - if(!_timing.IsFirstTimePredicted) - return; - var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var gravWell, out var xform)) { diff --git a/Content.Server/Singularity/EntitySystems/SingularityAttractorSystem.cs b/Content.Server/Singularity/EntitySystems/SingularityAttractorSystem.cs index 4b31d5f814..099b2d89b0 100644 --- a/Content.Server/Singularity/EntitySystems/SingularityAttractorSystem.cs +++ b/Content.Server/Singularity/EntitySystems/SingularityAttractorSystem.cs @@ -36,9 +36,6 @@ public sealed class SingularityAttractorSystem : EntitySystem /// The time elapsed since the last set of updates. public override void Update(float frameTime) { - if (!_timing.IsFirstTimePredicted) - return; - var query = EntityQueryEnumerator(); var now = _timing.CurTime; while (query.MoveNext(out var uid, out var attractor, out var xform)) 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/Singularity/EntitySystems/SingularitySystem.cs b/Content.Server/Singularity/EntitySystems/SingularitySystem.cs index f86faf9ee4..81612153cc 100644 --- a/Content.Server/Singularity/EntitySystems/SingularitySystem.cs +++ b/Content.Server/Singularity/EntitySystems/SingularitySystem.cs @@ -5,11 +5,8 @@ using Content.Shared.Singularity.Components; using Content.Shared.Singularity.EntitySystems; using Content.Shared.Singularity.Events; using Robust.Server.GameStates; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.GameStates; -using Robust.Shared.Player; -using Robust.Shared.Timing; namespace Content.Server.Singularity.EntitySystems; @@ -22,7 +19,6 @@ namespace Content.Server.Singularity.EntitySystems; public sealed class SingularitySystem : SharedSingularitySystem { #region Dependencies - [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly PvsOverrideSystem _pvs = default!; #endregion Dependencies @@ -70,9 +66,6 @@ public sealed class SingularitySystem : SharedSingularitySystem /// The amount of time since the last set of updates. public override void Update(float frameTime) { - if(!_timing.IsFirstTimePredicted) - return; - var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var singularity)) { diff --git a/Content.Server/Species/Systems/NymphSystem.cs b/Content.Server/Species/Systems/NymphSystem.cs index 11493b65b3..f6477a7c8b 100644 --- a/Content.Server/Species/Systems/NymphSystem.cs +++ b/Content.Server/Species/Systems/NymphSystem.cs @@ -4,15 +4,13 @@ using Content.Shared.Body.Events; using Content.Shared.Zombies; using Content.Server.Zombies; using Robust.Shared.Prototypes; -using Robust.Shared.Timing; namespace Content.Server.Species.Systems; public sealed partial class NymphSystem : EntitySystem { - [Dependency] private readonly IPrototypeManager _protoManager= default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly MindSystem _mindSystem = default!; - [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly ZombieSystem _zombie = default!; public override void Initialize() @@ -24,9 +22,6 @@ public sealed partial class NymphSystem : EntitySystem private void OnRemovedFromPart(EntityUid uid, NymphComponent comp, ref OrganRemovedFromBodyEvent args) { - if (!_timing.IsFirstTimePredicted) - return; - if (TerminatingOrDeleted(uid) || TerminatingOrDeleted(args.OldBody)) return; diff --git a/Content.Server/Speech/AccentSystem.cs b/Content.Server/Speech/AccentSystem.cs index e413dd52f8..275a4175c8 100644 --- a/Content.Server/Speech/AccentSystem.cs +++ b/Content.Server/Speech/AccentSystem.cs @@ -1,5 +1,5 @@ using System.Text.RegularExpressions; -using Content.Server.Chat.Systems; +using Content.Shared.Chat; using Content.Shared.Speech; namespace Content.Server.Speech; diff --git a/Content.Server/Speech/EmotesMenuSystem.cs b/Content.Server/Speech/EmotesMenuSystem.cs index 5f91742eb1..6571587b91 100644 --- a/Content.Server/Speech/EmotesMenuSystem.cs +++ b/Content.Server/Speech/EmotesMenuSystem.cs @@ -1,5 +1,5 @@ -using Content.Server.Chat.Systems; -using Content.Shared.Chat; +using Content.Shared.Chat; +using Content.Server.Chat.Systems; using Robust.Shared.Prototypes; namespace Content.Server.Speech; diff --git a/Content.Server/Speech/EntitySystems/BarkAccentSystem.cs b/Content.Server/Speech/EntitySystems/BarkAccentSystem.cs index 927b1259ad..27e42c70ed 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; @@ -23,6 +24,7 @@ namespace Content.Server.Speech.EntitySystems public override void Initialize() { SubscribeLocalEvent(OnAccent); + SubscribeLocalEvent>(OnAccentRelayed); } public string Accentuate(string message) @@ -36,9 +38,14 @@ namespace Content.Server.Speech.EntitySystems .Replace("l", "r").Replace("L", "R"); } - 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/ListeningSystem.cs b/Content.Server/Speech/EntitySystems/ListeningSystem.cs index 17513d80e7..35cb7f0eb4 100644 --- a/Content.Server/Speech/EntitySystems/ListeningSystem.cs +++ b/Content.Server/Speech/EntitySystems/ListeningSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Chat.Systems; +using Content.Shared.Chat; using Content.Shared.Speech; using Content.Shared.Speech.Components; diff --git a/Content.Server/Speech/EntitySystems/MumbleAccentSystem.cs b/Content.Server/Speech/EntitySystems/MumbleAccentSystem.cs index 27679c9134..868e5b73c1 100644 --- a/Content.Server/Speech/EntitySystems/MumbleAccentSystem.cs +++ b/Content.Server/Speech/EntitySystems/MumbleAccentSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Server.Speech.Components; +using Content.Shared.Chat; using Content.Shared.Chat.Prototypes; using Content.Shared.Speech; using Content.Shared.Speech.Components; diff --git a/Content.Server/Speech/EntitySystems/OwOAccentSystem.cs b/Content.Server/Speech/EntitySystems/OwOAccentSystem.cs index ff930e524a..804c711ea7 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 @@ -20,6 +21,7 @@ namespace Content.Server.Speech.EntitySystems public override void Initialize() { SubscribeLocalEvent(OnAccent); + SubscribeLocalEvent>(OnAccentRelayed); } public string Accentuate(string message) @@ -34,9 +36,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/Speech/EntitySystems/ReplacementAccentSystem.cs b/Content.Server/Speech/EntitySystems/ReplacementAccentSystem.cs index c285063d2d..60b531caa5 100644 --- a/Content.Server/Speech/EntitySystems/ReplacementAccentSystem.cs +++ b/Content.Server/Speech/EntitySystems/ReplacementAccentSystem.cs @@ -128,7 +128,7 @@ namespace Content.Server.Speech.EntitySystems var firstLoc = _loc.GetString(first); var replaceLoc = _loc.GetString(replace); - var regex = new Regex($@"(?(OnAccent); + SubscribeLocalEvent>(OnAccentRelayed); } public string Accentuate(string message) @@ -41,9 +43,14 @@ namespace Content.Server.Speech.EntitySystems return msg; } - private void OnAccent(EntityUid uid, ScrambledAccentComponent 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/SlurredSystem.cs b/Content.Server/Speech/EntitySystems/SlurredSystem.cs index 8690079de1..34cd050439 100644 --- a/Content.Server/Speech/EntitySystems/SlurredSystem.cs +++ b/Content.Server/Speech/EntitySystems/SlurredSystem.cs @@ -15,6 +15,16 @@ public sealed class SlurredSystem : SharedSlurredSystem [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IGameTiming _timing = default!; + /// + /// Divisor applied to total seconds used to get the odds of slurred speech occuring. + /// + private const float SlurredModifier = 1100f; + + /// + /// Minimum amount of time on the slurred accent for it to start taking effect. + /// + private const float SlurredThreshold = 80f; + public override void Initialize() { SubscribeLocalEvent(OnAccent); @@ -23,23 +33,18 @@ public sealed class SlurredSystem : SharedSlurredSystem } /// - /// Slur chance scales with "drunkeness", which is just measured using the time remaining on the status effect. + /// Slur chance scales with the time remaining on any status effect with the SlurredAccentComponent. + /// Typically, this is equivalent to "drunkenness" on the DrunkStatusEffect /// private float GetProbabilityScale(EntityUid uid) { - if (!_status.TryGetMaxTime(uid, out var time)) + if (!_status.TryGetMaxTime(uid, out var time)) return 0; // This is a magic number. Why this value? No clue it was made 3 years before I refactored this. - var magic = SharedDrunkSystem.MagicNumber; + var magic = time.Item2 == null ? SlurredModifier : (float) (time.Item2 - _timing.CurTime).Value.TotalSeconds - SlurredThreshold; - if (time.Item2 != null) - { - var curTime = _timing.CurTime; - magic = (float) (time.Item2 - curTime).Value.TotalSeconds - 80f; - } - - return Math.Clamp(magic / SharedDrunkSystem.MagicNumber, 0f, 1f); + return Math.Clamp(magic / SlurredModifier, 0f, 1f); } private void OnAccent(Entity entity, ref AccentGetEvent args) diff --git a/Content.Server/Speech/EntitySystems/SpeakOnActionSystem.cs b/Content.Server/Speech/EntitySystems/SpeakOnActionSystem.cs index ba7043e10c..6b52e69d7a 100644 --- a/Content.Server/Speech/EntitySystems/SpeakOnActionSystem.cs +++ b/Content.Server/Speech/EntitySystems/SpeakOnActionSystem.cs @@ -1,10 +1,10 @@ using Content.Server.Chat.Systems; -using Content.Shared.Speech.Components; +using Content.Shared.Actions.Events; +using Content.Shared.Chat; using Content.Shared.Speech; +using Content.Shared.Speech.Components; using Content.Shared.Speech.EntitySystems; using Content.Shared.Speech.Muting; -using Content.Shared.Actions.Events; - namespace Content.Server.Speech.EntitySystems; diff --git a/Content.Server/Speech/EntitySystems/UnblockableSpeechSystem.cs b/Content.Server/Speech/EntitySystems/UnblockableSpeechSystem.cs index 35a77c31f6..6d5f165d6c 100644 --- a/Content.Server/Speech/EntitySystems/UnblockableSpeechSystem.cs +++ b/Content.Server/Speech/EntitySystems/UnblockableSpeechSystem.cs @@ -1,5 +1,5 @@ -using Content.Server.Chat.Systems; using Content.Server.Speech.Components; +using Content.Shared.Chat; namespace Content.Server.Speech.EntitySystems { diff --git a/Content.Server/Speech/EntitySystems/VocalSystem.cs b/Content.Server/Speech/EntitySystems/VocalSystem.cs index 275140ff5b..2c83108898 100644 --- a/Content.Server/Speech/EntitySystems/VocalSystem.cs +++ b/Content.Server/Speech/EntitySystems/VocalSystem.cs @@ -1,12 +1,10 @@ using Content.Server.Actions; using Content.Server.Chat.Systems; -using Content.Server.Speech.Components; +using Content.Shared.Chat; using Content.Shared.Chat.Prototypes; -using Content.Shared.Cloning.Events; using Content.Shared.Humanoid; using Content.Shared.Speech; using Content.Shared.Speech.Components; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Prototypes; using Robust.Shared.Random; diff --git a/Content.Server/Speech/Muting/MutingSystem.cs b/Content.Server/Speech/Muting/MutingSystem.cs index f588e2238d..9e2a0602a4 100644 --- a/Content.Server/Speech/Muting/MutingSystem.cs +++ b/Content.Server/Speech/Muting/MutingSystem.cs @@ -1,8 +1,7 @@ -using Content.Shared.Abilities.Mime; -using Content.Server.Chat.Systems; using Content.Server.Popups; -using Content.Server.Speech.Components; using Content.Server.Speech.EntitySystems; +using Content.Shared.Abilities.Mime; +using Content.Shared.Chat; using Content.Shared.Chat.Prototypes; using Content.Shared.Puppet; using Content.Shared.Speech; diff --git a/Content.Server/Speech/SpeechNoiseSystem.cs b/Content.Server/Speech/SpeechNoiseSystem.cs index 5530f3fe57..cc9d5feb60 100644 --- a/Content.Server/Speech/SpeechNoiseSystem.cs +++ b/Content.Server/Speech/SpeechNoiseSystem.cs @@ -1,12 +1,10 @@ -using Robust.Shared.Audio; -using Content.Server.Chat; -using Content.Server.Chat.Systems; +using Content.Shared.Chat; using Content.Shared.Speech; +using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; -using Robust.Shared.Player; using Robust.Shared.Prototypes; -using Robust.Shared.Timing; using Robust.Shared.Random; +using Robust.Shared.Timing; namespace Content.Server.Speech { diff --git a/Content.Server/SprayPainter/SprayPainterSystem.cs b/Content.Server/SprayPainter/SprayPainterSystem.cs index 24ab5e0ea2..f00ae1d7dd 100644 --- a/Content.Server/SprayPainter/SprayPainterSystem.cs +++ b/Content.Server/SprayPainter/SprayPainterSystem.cs @@ -56,7 +56,7 @@ public sealed class SprayPainterSystem : SharedSprayPainterSystem return; args.Handled = true; - if (TryComp(ent, out LimitedChargesComponent? charges) && charges.LastCharges < ent.Comp.DecalChargeCost) + if (TryComp(ent, out LimitedChargesComponent? charges) && _charges.GetCurrentCharges((ent, charges)) < ent.Comp.DecalChargeCost) { _popup.PopupEntity(Loc.GetString("spray-painter-interact-no-charges"), args.User, args.User); return; @@ -165,7 +165,7 @@ public sealed class SprayPainterSystem : SharedSprayPainterSystem return; if (TryComp(args.Used, out var charges) - && charges.LastCharges < painter.PipeChargeCost) + && _charges.GetCurrentCharges((args.Used, charges)) < painter.PipeChargeCost) { var msg = Loc.GetString("spray-painter-interact-no-charges"); _popup.PopupEntity(msg, args.User, args.User); 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/Stack/StackSystem.cs b/Content.Server/Stack/StackSystem.cs index aac5a23902..a0d923dd1e 100644 --- a/Content.Server/Stack/StackSystem.cs +++ b/Content.Server/Stack/StackSystem.cs @@ -1,6 +1,5 @@ using Content.Shared.Popups; using Content.Shared.Stacks; -using Content.Shared.Verbs; using JetBrains.Annotations; using Robust.Shared.Map; using Robust.Shared.Prototypes; @@ -8,148 +7,246 @@ using Robust.Shared.Prototypes; namespace Content.Server.Stack { /// - /// Entity system that handles everything relating to stacks. - /// This is a good example for learning how to code in an ECS manner. + /// Entity system that handles everything relating to stacks. + /// This is a good example for learning how to code in an ECS manner. /// [UsedImplicitly] public sealed class StackSystem : SharedStackSystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - public override void Initialize() - { - base.Initialize(); - } - - public override void SetCount(EntityUid uid, int amount, StackComponent? component = null) - { - if (!Resolve(uid, ref component, false)) - return; - - base.SetCount(uid, amount, component); - - // Queue delete stack if count reaches zero. - if (component.Count <= 0) - QueueDel(uid); - } + #region Spawning /// - /// Try to split this stack into two. Returns a non-null if successful. + /// Spawns a new entity and moves an amount to it from the stack. + /// Moves nothing if amount is greater than ent's stack count. /// - public EntityUid? Split(EntityUid uid, int amount, EntityCoordinates spawnPosition, StackComponent? stack = null) + /// How much to move to the new entity. + /// Null if StackComponent doesn't resolve, or amount to move is greater than ent has available. + [PublicAPI] + public EntityUid? Split(Entity ent, int amount, EntityCoordinates spawnPosition) { - if (!Resolve(uid, ref stack)) + if (!Resolve(ent.Owner, ref ent.Comp)) return null; // Try to remove the amount of things we want to split from the original stack... - if (!Use(uid, amount, stack)) + if (!TryUse(ent, amount)) return null; - // Get a prototype ID to spawn the new entity. Null is also valid, although it should rarely be picked... - var prototype = _prototypeManager.TryIndex(stack.StackTypeId, out var stackType) - ? stackType.Spawn.ToString() - : Prototype(uid)?.ID; + if (!_prototypeManager.Resolve(ent.Comp.StackTypeId, out var stackType)) + return null; // Set the output parameter in the event instance to the newly split stack. - var entity = Spawn(prototype, spawnPosition); + var newEntity = SpawnAtPosition(stackType.Spawn, spawnPosition); - if (TryComp(entity, out StackComponent? stackComp)) - { - // Set the split stack's count. - SetCount(entity, amount, stackComp); - // Don't let people dupe unlimited stacks - stackComp.Unlimited = false; - } + // There should always be a StackComponent + var stackComp = Comp(newEntity); - var ev = new StackSplitEvent(entity); - RaiseLocalEvent(uid, ref ev); + SetCount((newEntity, stackComp), amount); + stackComp.Unlimited = false; // Don't let people dupe unlimited stacks + Dirty(newEntity, stackComp); + var ev = new StackSplitEvent(newEntity); + RaiseLocalEvent(ent, ref ev); + + return newEntity; + } + + #region SpawnAtPosition + + /// + /// Spawns a stack of a certain stack type and sets its count. Won't set the stack over its max. + /// + /// The amount to set the spawned stack to. + [PublicAPI] + public EntityUid SpawnAtPosition(int count, StackPrototype prototype, EntityCoordinates spawnPosition) + { + var entity = SpawnAtPosition(prototype.Spawn, spawnPosition); // The real SpawnAtPosition + + SetCount((entity, null), count); return entity; } - /// - /// Spawns a stack of a certain stack type. See . - /// - public EntityUid Spawn(int amount, ProtoId id, EntityCoordinates spawnPosition) + /// + [PublicAPI] + public EntityUid SpawnAtPosition(int count, ProtoId id, EntityCoordinates spawnPosition) { var proto = _prototypeManager.Index(id); - return Spawn(amount, proto, spawnPosition); + return SpawnAtPosition(count, proto, spawnPosition); } /// - /// Spawns a stack of a certain stack type. See . + /// Say you want to spawn 97 units of something that has a max stack count of 30. + /// This would spawn 3 stacks of 30 and 1 stack of 7. /// - public EntityUid Spawn(int amount, StackPrototype prototype, EntityCoordinates spawnPosition) + /// The entities spawned. + /// If the entity to spawn doesn't have stack component this will spawn a bunch of single items. + private List SpawnMultipleAtPosition(EntProtoId entityPrototype, + List amounts, + EntityCoordinates spawnPosition) { - // Set the output result parameter to the new stack entity... - var entity = SpawnAtPosition(prototype.Spawn, spawnPosition); - var stack = Comp(entity); + if (amounts.Count <= 0) + { + Log.Error( + $"Attempted to spawn stacks of nothing: {entityPrototype}, {amounts}. Trace: {Environment.StackTrace}"); + return new(); + } - // And finally, set the correct amount! - SetCount(entity, amount, stack); + var spawnedEnts = new List(); + foreach (var count in amounts) + { + var entity = SpawnAtPosition(entityPrototype, spawnPosition); // The real SpawnAtPosition + spawnedEnts.Add(entity); + if (TryComp(entity, out var stackComp)) // prevent errors from the Resolve + SetCount((entity, stackComp), count); + } + + return spawnedEnts; + } + + /// + [PublicAPI] + public List SpawnMultipleAtPosition(EntProtoId entityPrototypeId, + int amount, + EntityCoordinates spawnPosition) + { + return SpawnMultipleAtPosition(entityPrototypeId, + CalculateSpawns(entityPrototypeId, amount), + spawnPosition); + } + + /// + [PublicAPI] + public List SpawnMultipleAtPosition(EntityPrototype entityProto, + int amount, + EntityCoordinates spawnPosition) + { + return SpawnMultipleAtPosition(entityProto.ID, + CalculateSpawns(entityProto, amount), + spawnPosition); + } + + /// + [PublicAPI] + public List SpawnMultipleAtPosition(StackPrototype stack, + int amount, + EntityCoordinates spawnPosition) + { + return SpawnMultipleAtPosition(stack.Spawn, + CalculateSpawns(stack, amount), + spawnPosition); + } + + /// + [PublicAPI] + public List SpawnMultipleAtPosition(ProtoId stackId, + int amount, + EntityCoordinates spawnPosition) + { + var stackProto = _prototypeManager.Index(stackId); + return SpawnMultipleAtPosition(stackProto.Spawn, + CalculateSpawns(stackProto, amount), + spawnPosition); + } + + #endregion + #region SpawnNextToOrDrop + + /// + [PublicAPI] + public EntityUid SpawnNextToOrDrop(int amount, StackPrototype prototype, EntityUid source) + { + var entity = SpawnNextToOrDrop(prototype.Spawn, source); // The real SpawnNextToOrDrop + SetCount((entity, null), amount); return entity; } - /// - /// Say you want to spawn 97 units of something that has a max stack count of 30. - /// This would spawn 3 stacks of 30 and 1 stack of 7. - /// - public List SpawnMultiple(string entityPrototype, int amount, EntityCoordinates spawnPosition) + /// + [PublicAPI] + public EntityUid SpawnNextToOrDrop(int amount, ProtoId id, EntityUid source) { - if (amount <= 0) + var proto = _prototypeManager.Index(id); + return SpawnNextToOrDrop(amount, proto, source); + } + + /// + private List SpawnMultipleNextToOrDrop(EntProtoId entityPrototype, + List amounts, + EntityUid target) + { + if (amounts.Count <= 0) { Log.Error( - $"Attempted to spawn an invalid stack: {entityPrototype}, {amount}. Trace: {Environment.StackTrace}"); + $"Attempted to spawn stacks of nothing: {entityPrototype}, {amounts}. Trace: {Environment.StackTrace}"); return new(); } - var spawns = CalculateSpawns(entityPrototype, amount); - var spawnedEnts = new List(); - foreach (var count in spawns) + foreach (var count in amounts) { - var entity = SpawnAtPosition(entityPrototype, spawnPosition); + var entity = SpawnNextToOrDrop(entityPrototype, target); // The real SpawnNextToOrDrop spawnedEnts.Add(entity); - SetCount(entity, count); + if (TryComp(entity, out var stackComp)) // prevent errors from the Resolve + SetCount((entity, stackComp), count); } return spawnedEnts; } - /// - public List SpawnMultiple(string entityPrototype, int amount, EntityUid target) + /// + [PublicAPI] + public List SpawnMultipleNextToOrDrop(EntProtoId stack, + int amount, + EntityUid target) { - if (amount <= 0) - { - Log.Error( - $"Attempted to spawn an invalid stack: {entityPrototype}, {amount}. Trace: {Environment.StackTrace}"); - return new(); - } - - var spawns = CalculateSpawns(entityPrototype, amount); - - var spawnedEnts = new List(); - foreach (var count in spawns) - { - var entity = SpawnNextToOrDrop(entityPrototype, target); - spawnedEnts.Add(entity); - SetCount(entity, count); - } - - return spawnedEnts; + return SpawnMultipleNextToOrDrop(stack, + CalculateSpawns(stack, amount), + target); } + /// + [PublicAPI] + public List SpawnMultipleNextToOrDrop(EntityPrototype stack, + int amount, + EntityUid target) + { + return SpawnMultipleNextToOrDrop(stack.ID, + CalculateSpawns(stack, amount), + target); + } + + /// + [PublicAPI] + public List SpawnMultipleNextToOrDrop(StackPrototype stack, + int amount, + EntityUid target) + { + return SpawnMultipleNextToOrDrop(stack.Spawn, + CalculateSpawns(stack, amount), + target); + } + + /// + [PublicAPI] + public List SpawnMultipleNextToOrDrop(ProtoId stackId, + int amount, + EntityUid target) + { + var stackProto = _prototypeManager.Index(stackId); + return SpawnMultipleNextToOrDrop(stackProto.Spawn, + CalculateSpawns(stackProto, amount), + target); + } + + #endregion + #region Calculate + /// /// Calculates how many stacks to spawn that total up to . /// - /// The stack to spawn. - /// The amount of pieces across all stacks. /// The list of stack counts per entity. - private List CalculateSpawns(string entityPrototype, int amount) + private List CalculateSpawns(int maxCountPerStack, int amount) { - var proto = _prototypeManager.Index(entityPrototype); - proto.TryGetComponent(out var stack, EntityManager.ComponentFactory); - var maxCountPerStack = GetMaxCount(stack); var amounts = new List(); while (amount > 0) { @@ -161,28 +258,47 @@ namespace Content.Server.Stack return amounts; } - protected override void UserSplit(EntityUid uid, EntityUid userUid, int amount, - StackComponent? stack = null, - TransformComponent? userTransform = null) + /// + private List CalculateSpawns(StackPrototype stackProto, int amount) { - if (!Resolve(uid, ref stack)) - return; + return CalculateSpawns(GetMaxCount(stackProto), amount); + } - if (!Resolve(userUid, ref userTransform)) + /// + private List CalculateSpawns(EntityPrototype entityPrototype, int amount) + { + return CalculateSpawns(GetMaxCount(entityPrototype), amount); + } + + /// + private List CalculateSpawns(EntProtoId entityId, int amount) + { + return CalculateSpawns(GetMaxCount(entityId), amount); + } + + #endregion + #endregion + #region Event Handlers + + /// + protected override void UserSplit(Entity stack, Entity user, int amount) + { + if (!Resolve(user.Owner, ref user.Comp, false)) return; if (amount <= 0) { - Popup.PopupCursor(Loc.GetString("comp-stack-split-too-small"), userUid, PopupType.Medium); + Popup.PopupCursor(Loc.GetString("comp-stack-split-too-small"), user.Owner, PopupType.Medium); return; } - if (Split(uid, amount, userTransform.Coordinates, stack) is not {} split) + if (Split(stack.AsNullable(), amount, user.Comp.Coordinates) is not { } split) return; - Hands.PickupOrDrop(userUid, split); + Hands.PickupOrDrop(user.Owner, split); - Popup.PopupCursor(Loc.GetString("comp-stack-split"), userUid); + Popup.PopupCursor(Loc.GetString("comp-stack-split"), user.Owner); } + #endregion } } diff --git a/Content.Server/Station/Commands/BankCommand.cs b/Content.Server/Station/Commands/BankCommand.cs new file mode 100644 index 0000000000..c24cacdffb --- /dev/null +++ b/Content.Server/Station/Commands/BankCommand.cs @@ -0,0 +1,93 @@ +using System.Linq; +using Content.Server.Administration; +using Content.Server.Cargo.Systems; +using Content.Shared.Administration; +using Content.Shared.Cargo.Components; +using Content.Shared.Cargo.Prototypes; +using Robust.Shared.Prototypes; +using Robust.Shared.Toolshed; + +namespace Content.Server.Station.Commands; + +[ToolshedCommand, AdminCommand(AdminFlags.Admin)] +public sealed class BankCommand : ToolshedCommand +{ + private CargoSystem? _cargo; + + [CommandImplementation("accounts")] + public IEnumerable Accounts([PipedArgument] EntityUid station) + { + _cargo ??= GetSys(); + + foreach (var (account, _) in _cargo.GetAccounts(station)) + { + yield return new BankAccount(account.Id, station, _cargo, EntityManager); + } + } + + [CommandImplementation("accounts")] + public IEnumerable Accounts([PipedArgument] IEnumerable stations) + => stations.SelectMany(Accounts); + + [CommandImplementation("account")] + public BankAccount Account([PipedArgument] EntityUid station, ProtoId account) + { + _cargo ??= GetSys(); + + return new BankAccount(account.Id, station, _cargo, EntityManager); + } + + [CommandImplementation("account")] + public IEnumerable Account([PipedArgument] IEnumerable stations, ProtoId account) + => stations.Select(x => Account(x, account)); + + [CommandImplementation("adjust")] + public IEnumerable Adjust([PipedArgument] IEnumerable @ref, int by) + { + _cargo ??= GetSys(); + var bankAccounts = @ref.ToList(); + foreach (var bankAccount in bankAccounts.ToList()) + { + _cargo.TryAdjustBankAccount(bankAccount.Station, bankAccount.Account, by, true); + } + return bankAccounts; + } + + [CommandImplementation("set")] + public IEnumerable Set([PipedArgument] IEnumerable @ref, int by) + { + _cargo ??= GetSys(); + var bankAccounts = @ref.ToList(); + foreach (var bankAccount in bankAccounts.ToList()) + { + _cargo.TrySetBankAccount(bankAccount.Station, bankAccount.Account, by, true); + } + return bankAccounts; + } + + [CommandImplementation("amount")] + public IEnumerable Amount([PipedArgument] IEnumerable @ref) + { + _cargo ??= GetSys(); + return @ref.Select(bankAccount => (success: _cargo.TryGetAccount(bankAccount.Station, bankAccount.Account, out var money), money)) + .Where(result => result.success) + .Select(result => result.money); + } +} + +public readonly record struct BankAccount( + string Account, + Entity Station, + CargoSystem Cargo, + IEntityManager EntityManager) +{ + public override string ToString() + { + if (!Cargo.TryGetAccount(Station, Account, out var money)) + { + return $"{EntityManager.ToPrettyString(Station)} Account {Account} : (not a account)"; + } + + return $"{EntityManager.ToPrettyString(Station)} Account {Account} : {money}"; + } +} diff --git a/Content.Server/Store/Systems/StoreSystem.Ui.cs b/Content.Server/Store/Systems/StoreSystem.Ui.cs index 742434ff23..c3d37a74b8 100644 --- a/Content.Server/Store/Systems/StoreSystem.Ui.cs +++ b/Content.Server/Store/Systems/StoreSystem.Ui.cs @@ -313,7 +313,7 @@ public sealed partial class StoreSystem { var cashId = proto.Cash[value]; var amountToSpawn = (int) MathF.Floor((float) (amountRemaining / value)); - var ents = _stack.SpawnMultiple(cashId, amountToSpawn, coordinates); + var ents = _stack.SpawnMultipleAtPosition(cashId, amountToSpawn, coordinates); if (ents.FirstOrDefault() is {} ent) _hands.PickupOrDrop(buyer, ent); amountRemaining -= value * amountToSpawn; diff --git a/Content.Server/Store/Systems/StoreSystem.cs b/Content.Server/Store/Systems/StoreSystem.cs index 10060dc7d3..279026c873 100644 --- a/Content.Server/Store/Systems/StoreSystem.cs +++ b/Content.Server/Store/Systems/StoreSystem.cs @@ -153,7 +153,7 @@ public sealed partial class StoreSystem : EntitySystem // same tick currency.Comp.Price.Clear(); if (stack != null) - _stack.SetCount(currency.Owner, 0, stack); + _stack.SetCount((currency.Owner, stack), 0); QueueDel(currency); return true; diff --git a/Content.Server/Teleportation/TeleportLocationsSystem.cs b/Content.Server/Teleportation/TeleportLocationsSystem.cs index 14211d9672..edda0859ee 100644 --- a/Content.Server/Teleportation/TeleportLocationsSystem.cs +++ b/Content.Server/Teleportation/TeleportLocationsSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Chat.Systems; +using Content.Shared.Chat; using Content.Shared.Teleportation; using Content.Shared.Teleportation.Components; using Content.Shared.Teleportation.Systems; diff --git a/Content.Server/Temperature/Systems/TemperatureSystem.cs b/Content.Server/Temperature/Systems/TemperatureSystem.cs index 6290e8231a..928b6ae9b5 100644 --- a/Content.Server/Temperature/Systems/TemperatureSystem.cs +++ b/Content.Server/Temperature/Systems/TemperatureSystem.cs @@ -5,19 +5,20 @@ 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; using Content.Shared.Temperature; -using Robust.Shared.Physics.Components; using Robust.Shared.Prototypes; -using Robust.Shared.Physics.Events; using Content.Shared.Projectiles; +using Content.Shared.Temperature.Components; +using Content.Shared.Temperature.Systems; namespace Content.Server.Temperature.Systems; -public sealed class TemperatureSystem : EntitySystem +public sealed class TemperatureSystem : SharedTemperatureSystem { [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly AtmosphereSystem _atmosphere = default!; @@ -40,6 +41,8 @@ public sealed class TemperatureSystem : EntitySystem public override void Initialize() { + base.Initialize(); + SubscribeLocalEvent(EnqueueDamage); SubscribeLocalEvent(OnAtmosExposedUpdate); SubscribeLocalEvent(OnRejuvenate); @@ -125,8 +128,7 @@ public sealed class TemperatureSystem : EntitySystem true); } - public void ChangeHeat(EntityUid uid, float heatAmount, bool ignoreHeatResistance = false, - TemperatureComponent? temperature = null) + public override void ChangeHeat(EntityUid uid, float heatAmount, bool ignoreHeatResistance = false, TemperatureComponent? temperature = null) { if (!Resolve(uid, ref temperature, false)) return; @@ -161,16 +163,6 @@ public sealed class TemperatureSystem : EntitySystem ChangeHeat(uid, heat * temperature.AtmosTemperatureTransferEfficiency, temperature: temperature); } - public float GetHeatCapacity(EntityUid uid, TemperatureComponent? comp = null, PhysicsComponent? physics = null) - { - if (!Resolve(uid, ref comp) || !Resolve(uid, ref physics, false) || physics.FixturesMass <= 0) - { - return Atmospherics.MinimumHeatCapacity; - } - - return comp.SpecificHeat * physics.FixturesMass; - } - private void OnInit(EntityUid uid, InternalTemperatureComponent comp, MapInitEvent args) { if (!TryComp(uid, out var temp)) diff --git a/Content.Server/Tiles/TileEntityEffectComponent.cs b/Content.Server/Tiles/TileEntityEffectComponent.cs index 4201af47f9..70f11bb060 100644 --- a/Content.Server/Tiles/TileEntityEffectComponent.cs +++ b/Content.Server/Tiles/TileEntityEffectComponent.cs @@ -14,6 +14,6 @@ public sealed partial class TileEntityEffectComponent : Component /// /// List of effects that should be applied. /// - [ViewVariables(VVAccess.ReadWrite), DataField] + [DataField] public List Effects = default!; } diff --git a/Content.Server/Tiles/TileEntityEffectSystem.cs b/Content.Server/Tiles/TileEntityEffectSystem.cs index 4d866cb254..1123781e7b 100644 --- a/Content.Server/Tiles/TileEntityEffectSystem.cs +++ b/Content.Server/Tiles/TileEntityEffectSystem.cs @@ -1,13 +1,11 @@ -using Content.Server.Atmos.Components; -using Content.Server.Atmos.EntitySystems; using Content.Shared.StepTrigger.Systems; -using Content.Shared.Chemistry.Reagent; using Content.Shared.EntityEffects; namespace Content.Server.Tiles; public sealed class TileEntityEffectSystem : EntitySystem { + [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!; public override void Initialize() { @@ -23,11 +21,7 @@ public sealed class TileEntityEffectSystem : EntitySystem private void OnTileStepTriggered(Entity ent, ref StepTriggeredOffEvent args) { var otherUid = args.Tripper; - var effectArgs = new EntityEffectBaseArgs(otherUid, EntityManager); - foreach (var effect in ent.Comp.Effects) - { - effect.Effect(effectArgs); - } + _entityEffects.ApplyEffects(otherUid, ent.Comp.Effects.ToArray(), user: otherUid); } } 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/Traitor/Uplink/UplinkSystem.cs b/Content.Server/Traitor/Uplink/UplinkSystem.cs index e4aa7b5ec1..e8ed868dfb 100644 --- a/Content.Server/Traitor/Uplink/UplinkSystem.cs +++ b/Content.Server/Traitor/Uplink/UplinkSystem.cs @@ -102,7 +102,10 @@ public sealed class UplinkSystem : EntitySystem var implant = _subdermalImplant.AddImplant(user, FallbackUplinkImplant); if (!HasComp(implant)) + { + Log.Error($"Implant does not have the store component {implant}"); return false; + } SetUplink(user, implant.Value, balance, giveDiscounts); return true; @@ -117,20 +120,19 @@ public sealed class UplinkSystem : EntitySystem // Try to find PDA in inventory if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator)) { - while (containerSlotEnumerator.MoveNext(out var pdaUid)) + while (containerSlotEnumerator.MoveNext(out var containerSlot)) { - if (!pdaUid.ContainedEntity.HasValue) - continue; + var pdaUid = containerSlot.ContainedEntity; - if (HasComp(pdaUid.ContainedEntity.Value) || HasComp(pdaUid.ContainedEntity.Value)) - return pdaUid.ContainedEntity.Value; + if (HasComp(pdaUid) && HasComp(pdaUid)) + return pdaUid; } } // Also check hands foreach (var item in _handsSystem.EnumerateHeld(user)) { - if (HasComp(item) || HasComp(item)) + if (HasComp(item) && HasComp(item)) return item; } diff --git a/Content.Server/Trigger/Systems/SpeakOnTriggerSystem.cs b/Content.Server/Trigger/Systems/SpeakOnTriggerSystem.cs index 1e08587ce7..48ead11161 100644 --- a/Content.Server/Trigger/Systems/SpeakOnTriggerSystem.cs +++ b/Content.Server/Trigger/Systems/SpeakOnTriggerSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Chat.Systems; +using Content.Shared.Chat; using Content.Shared.Trigger; using Content.Shared.Trigger.Components.Effects; using Robust.Shared.Prototypes; diff --git a/Content.Server/TurretController/DeployableTurretControllerSystem.cs b/Content.Server/TurretController/DeployableTurretControllerSystem.cs index f0b6881431..fbb802b679 100644 --- a/Content.Server/TurretController/DeployableTurretControllerSystem.cs +++ b/Content.Server/TurretController/DeployableTurretControllerSystem.cs @@ -9,6 +9,8 @@ using Content.Shared.Turrets; using Robust.Server.GameObjects; using Robust.Shared.Prototypes; using System.Linq; +using Content.Server.Administration.Logs; +using Content.Shared.Database; namespace Content.Server.TurretController; @@ -17,6 +19,7 @@ public sealed partial class DeployableTurretControllerSystem : SharedDeployableT { [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; [Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; /// Keys for the device network. See for further examples. public const string CmdSetArmamemtState = "set_armament_state"; @@ -109,6 +112,8 @@ public sealed partial class DeployableTurretControllerSystem : SharedDeployableT [CmdSetArmamemtState] = armamentState, }; + _adminLogger.Add(LogType.ItemConfigure, LogImpact.Medium, $"{ToPrettyString(user)} set {ToPrettyString(ent)} to {armamentState}"); + _deviceNetwork.QueuePacket(ent, null, payload, device: device); } @@ -132,6 +137,11 @@ public sealed partial class DeployableTurretControllerSystem : SharedDeployableT [CmdSetAccessExemptions] = turretTargetingSettings.ExemptAccessLevels, }; + foreach (var exemption in exemptions) + { + _adminLogger.Add(LogType.ItemConfigure, LogImpact.Medium, $"{ToPrettyString(user)} set {ToPrettyString(ent)} authorization of {exemption} to {enabled}"); + } + _deviceNetwork.QueuePacket(ent, null, payload, device: device); } 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/Vocalization/Systems/VocalizationSystem.cs b/Content.Server/Vocalization/Systems/VocalizationSystem.cs index 49dfaf4281..8801e057b7 100644 --- a/Content.Server/Vocalization/Systems/VocalizationSystem.cs +++ b/Content.Server/Vocalization/Systems/VocalizationSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Chat.Systems; using Content.Server.Power.Components; using Content.Server.Vocalization.Components; using Content.Shared.ActionBlocker; +using Content.Shared.Chat; using Robust.Shared.Random; using Robust.Shared.Timing; diff --git a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs index 468bfdc0f8..0dd1540933 100644 --- a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs +++ b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs @@ -456,7 +456,7 @@ namespace Content.Server.Voting.Managers (Loc.GetString("ui-vote-votekick-abstain"), "abstain") }, Duration = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VotekickTimer)), - InitiatorTimeout = TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.VotekickTimeout)), + InitiatorTimeout = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VotekickTimeout)), VoterEligibility = voterEligibility, DisplayVotes = false, TargetEntity = targetNetEntity @@ -471,7 +471,7 @@ namespace Content.Server.Voting.Managers var webhookState = _voteWebhooks.CreateWebhookIfConfigured(options, _cfg.GetCVar(CCVars.DiscordVotekickWebhook), Loc.GetString("votekick-webhook-name"), options.Title + "\n" + Loc.GetString("votekick-webhook-description", ("initiator", initiatorName), ("target", targetSession))); // Time out the vote now that we know it will happen - TimeoutStandardVote(StandardVoteType.Votekick); + TimeoutStandardVote(StandardVoteType.Votekick, TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VotekickTimeout))); vote.OnFinished += (_, eventArgs) => { @@ -578,9 +578,9 @@ namespace Content.Server.Voting.Managers } } - private void TimeoutStandardVote(StandardVoteType type) + private void TimeoutStandardVote(StandardVoteType type, TimeSpan? timeoutOverride = null) { - var timeout = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteSameTypeTimeout)); + var timeout = timeoutOverride ?? TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteSameTypeTimeout)); _standardVoteTimeout[type] = _timing.RealTime + timeout; DirtyCanCallVoteAll(); } diff --git a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs index 31d289217c..2efe94c20e 100644 --- a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs +++ b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Server.Movement.Systems; +using Content.Shared.Chat; using Content.Shared.Effects; using Content.Shared.Speech.Components; using Content.Shared.Weapons.Melee; 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 e04fa9b64c..8bbab9503e 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs @@ -1,37 +1,28 @@ -using System.Linq; using System.Numerics; using Content.Server.Cargo.Systems; using Content.Server.Weapons.Ranged.Components; using Content.Shared.Cargo; using Content.Shared.Damage; using Content.Shared.Damage.Systems; -using Content.Shared.Database; -using Content.Shared.Effects; using Content.Shared.Projectiles; using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Ranged; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Systems; -using Content.Shared.Weapons.Reflect; -using Content.Shared.Damage.Components; +using Content.Shared.Weapons.Hitscan.Components; +using Content.Shared.Weapons.Hitscan.Events; using Robust.Shared.Audio; using Robust.Shared.Map; -using Robust.Shared.Physics; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Utility; -using Robust.Shared.Containers; 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 SharedColorFlashEffectSystem _color = default!; - [Dependency] private readonly SharedStaminaSystem _stamina = default!; - [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedMapSystem _map = default!; private const float DamagePitchVariation = 0.05f; @@ -103,6 +94,7 @@ public sealed partial class GunSystem : SharedGunSystem continue; } + // TODO: Clean this up in a gun refactor at some point - too much copy pasting switch (shootable) { // Cartridge shoots something else @@ -141,107 +133,21 @@ public sealed partial class GunSystem : SharedGunSystem CreateAndFireProjectiles(ent.Value, newAmmo); break; - case HitscanPrototype hitscan: + case HitscanAmmoComponent: + if (ent == null) + break; - EntityUid? lastHit = null; - - var from = fromMap; - // can't use map coords above because funny FireEffects - var fromEffect = fromCoordinates; - var dir = mapDirection.Normalized(); - - //in the situation when user == null, means that the cannon fires on its own (via signals). And we need the gun to not fire by itself in this case - var lastUser = user ?? gunUid; - - if (hitscan.Reflective != ReflectType.None) + var hitscanEv = new HitscanTraceEvent { - for (var reflectAttempt = 0; reflectAttempt < 3; reflectAttempt++) - { - var ray = new CollisionRay(from.Position, dir, hitscan.CollisionMask); - var rayCastResults = - Physics.IntersectRay(from.MapId, ray, hitscan.MaxLength, lastUser, false).ToList(); - if (!rayCastResults.Any()) - break; + FromCoordinates = fromCoordinates, + ShotDirection = mapDirection.Normalized(), + Gun = gunUid, + Shooter = user, + Target = gun.Target, + }; + RaiseLocalEvent(ent.Value, ref hitscanEv); - var result = rayCastResults[0]; - - // Check if laser is shot from in a container - if (!_container.IsEntityOrParentInContainer(lastUser)) - { - // Checks if the laser should pass over unless targeted by its user - foreach (var collide in rayCastResults) - { - if (collide.HitEntity != gun.Target && - CompOrNull(collide.HitEntity)?.Active == true) - { - continue; - } - - result = collide; - break; - } - } - - var hit = result.HitEntity; - lastHit = hit; - - FireEffects(fromEffect, result.Distance, dir.Normalized().ToAngle(), hitscan, hit); - - var ev = new HitScanReflectAttemptEvent(user, gunUid, hitscan.Reflective, dir, false); - RaiseLocalEvent(hit, ref ev); - - if (!ev.Reflected) - break; - - fromEffect = Transform(hit).Coordinates; - from = TransformSystem.ToMapCoordinates(fromEffect); - dir = ev.Direction; - lastUser = hit; - } - } - - if (lastHit != null) - { - var hitEntity = lastHit.Value; - if (hitscan.StaminaDamage > 0f) - _stamina.TakeStaminaDamage(hitEntity, hitscan.StaminaDamage, source: user); - - var dmg = hitscan.Damage; - - var hitName = ToPrettyString(hitEntity); - if (dmg != null) - dmg = Damageable.TryChangeDamage(hitEntity, dmg * Damageable.UniversalHitscanDamageModifier, origin: user); - - // check null again, as TryChangeDamage returns modified damage values - if (dmg != null) - { - if (!Deleted(hitEntity)) - { - if (dmg.AnyPositive()) - { - _color.RaiseEffect(Color.Red, new List() { hitEntity }, Filter.Pvs(hitEntity, entityManager: EntityManager)); - } - - // TODO get fallback position for playing hit sound. - PlayImpactSound(hitEntity, dmg, hitscan.Sound, hitscan.ForceSound); - } - - if (user != null) - { - Logs.Add(LogType.HitScanHit, - $"{ToPrettyString(user.Value):user} hit {hitName:target} using hitscan and dealt {dmg.GetTotal():damage} damage"); - } - else - { - Logs.Add(LogType.HitScanHit, - $"{hitName:target} hit by hitscan dealing {dmg.GetTotal():damage} damage"); - } - } - } - else - { - FireEffects(fromEffect, hitscan.MaxLength, dir.ToAngle(), hitscan); - } + Del(ent); Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user); break; @@ -353,7 +259,7 @@ public sealed partial class GunSystem : SharedGunSystem RaiseNetworkEvent(message, filter); } - public void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound) + public override void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound) { DebugTools.Assert(!Deleted(otherEntity), "Impact sound entity was deleted"); @@ -384,69 +290,4 @@ public sealed partial class GunSystem : SharedGunSystem Audio.PlayPvs(weaponSound, otherEntity); } } - - // TODO: Pseudo RNG so the client can predict these. - #region Hitscan effects - - private void FireEffects(EntityCoordinates fromCoordinates, float distance, Angle angle, HitscanPrototype hitscan, EntityUid? hitEntity = null) - { - // Lord - // Forgive me for the shitcode I am about to do - // Effects tempt me not - var sprites = new List<(NetCoordinates coordinates, Angle angle, SpriteSpecifier sprite, float scale)>(); - var fromXform = Transform(fromCoordinates.EntityId); - - // We'll get the effects relative to the grid / map of the firer - // Look you could probably optimise this a bit with redundant transforms at this point. - - var gridUid = fromXform.GridUid; - if (gridUid != fromCoordinates.EntityId && TryComp(gridUid, out TransformComponent? gridXform)) - { - var (_, gridRot, gridInvMatrix) = TransformSystem.GetWorldPositionRotationInvMatrix(gridXform); - var map = TransformSystem.ToMapCoordinates(fromCoordinates); - fromCoordinates = new EntityCoordinates(gridUid.Value, Vector2.Transform(map.Position, gridInvMatrix)); - angle -= gridRot; - } - else - { - angle -= TransformSystem.GetWorldRotation(fromXform); - } - - if (distance >= 1f) - { - if (hitscan.MuzzleFlash != null) - { - var coords = fromCoordinates.Offset(angle.ToVec().Normalized() / 2); - var netCoords = GetNetCoordinates(coords); - - sprites.Add((netCoords, angle, hitscan.MuzzleFlash, 1f)); - } - - if (hitscan.TravelFlash != null) - { - var coords = fromCoordinates.Offset(angle.ToVec() * (distance + 0.5f) / 2); - var netCoords = GetNetCoordinates(coords); - - sprites.Add((netCoords, angle, hitscan.TravelFlash, distance - 1.5f)); - } - } - - if (hitscan.ImpactFlash != null) - { - var coords = fromCoordinates.Offset(angle.ToVec() * distance); - var netCoords = GetNetCoordinates(coords); - - sprites.Add((netCoords, angle.FlipPositive(), hitscan.ImpactFlash, 1f)); - } - - if (sprites.Count > 0) - { - RaiseNetworkEvent(new HitscanEvent - { - Sprites = sprites, - }, Filter.Pvs(fromCoordinates, entityMan: EntityManager)); - } - } - - #endregion } diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPortalSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPortalSystem.cs index acc36f0f8d..6bede34b68 100644 --- a/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPortalSystem.cs +++ b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPortalSystem.cs @@ -20,14 +20,10 @@ 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) { - if (!_timing.IsFirstTimePredicted) - return; - var map = Transform(ent).MapID; var validMinds = new ValueList(); var mindQuery = EntityQueryEnumerator(); diff --git a/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs b/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs index 05bb2327e6..a2cd1eb715 100644 --- a/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs +++ b/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs @@ -34,7 +34,7 @@ public sealed class ArtifactCrusherSystem : SharedArtifactCrusherSystem if (_whitelistSystem.IsWhitelistPass(crusher.CrushingWhitelist, contained)) { var amount = _random.Next(crusher.MinFragments, crusher.MaxFragments); - var stacks = _stack.SpawnMultiple(crusher.FragmentStackProtoId, amount, coords); + var stacks = _stack.SpawnMultipleAtPosition(crusher.FragmentStackProtoId, amount, coords); foreach (var stack in stacks) { ContainerSystem.Insert((stack, null, null, null), crusher.OutputContainer); diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs index be75306921..fa6005831e 100644 --- a/Content.Server/Zombies/ZombieSystem.Transform.cs +++ b/Content.Server/Zombies/ZombieSystem.Transform.cs @@ -13,12 +13,10 @@ using Content.Server.NPC.HTN; using Content.Server.NPC.Systems; using Content.Server.StationEvents.Components; using Content.Server.Speech.Components; -using Content.Server.Temperature.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.Humanoid; @@ -44,6 +42,7 @@ using Robust.Shared.Player; using Robust.Shared.Prototypes; using Content.Shared.NPC.Prototypes; using Content.Shared.Roles; +using Content.Shared.Temperature.Components; using Content.Shared._Offbrand.Wounds; // Offbrand namespace Content.Server.Zombies; @@ -191,7 +190,6 @@ public sealed partial class ZombieSystem var combat = EnsureComp(target); RemComp(target); _combat.SetCanDisarm(target, false, combat); - _combat.SetInCombatMode(target, true, combat); //This is the actual damage of the zombie. We assign the visual appearance //and range here because of stuff we'll find out later @@ -291,8 +289,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 ff47620f47..7c938d9466 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; @@ -10,7 +9,8 @@ using Content.Shared.Anomaly.Components; using Content.Shared.Armor; using Content.Shared.Bed.Sleep; using Content.Shared.Cloning.Events; -using Content.Shared.Damage; +using Content.Shared.Chat; +using Content.Shared.Damage.Systems; using Content.Shared.Humanoid; using Content.Shared.Inventory; using Content.Shared.Mind; @@ -119,7 +119,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 @@ -139,12 +139,12 @@ 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); _brainDamage.TryChangeBrainDamage(uid, multiplier / 2f); // Offbrand } // 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 @@ -161,7 +161,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); } } @@ -238,25 +238,26 @@ namespace Content.Server.Zombies return MathF.Max(chance, zombieComponent.MinZombieInfectionChance); } - private void OnMeleeHit(EntityUid uid, ZombieComponent component, MeleeHitEvent args) + private void OnMeleeHit(Entity entity, ref MeleeHitEvent args) { - if (!TryComp(args.User, out _)) + if (!args.IsHit) return; - if (!args.HitEntities.Any()) - return; + var cannotSpread = HasComp(args.User); - foreach (var entity in args.HitEntities) + foreach (var uid in args.HitEntities) { - if (args.User == entity) + if (args.User == uid) continue; - if (!TryComp(entity, out var mobState)) + if (!TryComp(uid, out var mobState)) continue; - if (HasComp(entity)) + if (HasComp(uid) || HasComp(uid)) { - args.BonusDamage = -args.BaseDamage; + // Don't infect, don't deal damage, do not heal from bites, don't pass go! + args.Handled = true; + continue; } else if (!HasComp(entity)) // Offbrand { @@ -267,14 +268,25 @@ namespace Content.Server.Zombies } } - if (_mobState.IsIncapacitated(entity, mobState) && !HasComp(entity) && !HasComp(entity) && !HasComp(entity)) // Offbrand + if (_mobState.IsAlive(uid, mobState)) { - ZombifyEntity(entity); - args.BonusDamage = -args.BaseDamage; + _damageable.TryChangeDamage(args.User, entity.Comp.HealingOnBite, true, false); + + // If we cannot infect the living target, the zed will just heal itself. + if (HasComp(uid) || cannotSpread || _random.Prob(GetZombieInfectionChance(uid, entity.Comp))) + continue; + + EnsureComp(uid); + EnsureComp(uid); } - else if (mobState.CurrentState == MobState.Alive) //heals when zombies bite live entities + else { - _damageable.TryChangeDamage(uid, component.HealingOnBite, true, false); + if (HasComp(uid) || cannotSpread) + continue; + + // If the target is dead and can be infected, infect. + ZombifyEntity(uid); + args.Handled = true; } } } diff --git a/Content.Shared.Database/LogType.cs b/Content.Shared.Database/LogType.cs index 58a41a5f7a..d905cc03ae 100644 --- a/Content.Shared.Database/LogType.cs +++ b/Content.Shared.Database/LogType.cs @@ -81,9 +81,9 @@ public enum LogType ChemicalReaction = 17, /// - /// Reagent effects related interactions. + /// EntityEffect related interactions. /// - ReagentEffect = 18, + EntityEffect = 18, /// /// Canister valve was opened or closed. diff --git a/Content.Shared/Access/Systems/AccessReaderSystem.cs b/Content.Shared/Access/Systems/AccessReaderSystem.cs index 801bfd4b1d..f8c6d49244 100644 --- a/Content.Shared/Access/Systems/AccessReaderSystem.cs +++ b/Content.Shared/Access/Systems/AccessReaderSystem.cs @@ -10,6 +10,7 @@ using Content.Shared.Hands.EntitySystems; using Content.Shared.IdentityManagement; using Content.Shared.Inventory; using Content.Shared.Localizations; +using Content.Shared.Lock; using Content.Shared.NameIdentifier; using Content.Shared.PDA; using Content.Shared.StationRecords; @@ -44,6 +45,8 @@ public sealed class AccessReaderSystem : EntitySystem SubscribeLocalEvent(OnEmagged); SubscribeLocalEvent(OnLinkAttempt); SubscribeLocalEvent(OnConfigurationAttempt); + SubscribeLocalEvent(OnFindAvailableLocks); + SubscribeLocalEvent(OnCheckLockAccess); SubscribeLocalEvent(OnGetState); SubscribeLocalEvent(OnHandleState); @@ -169,6 +172,22 @@ public sealed class AccessReaderSystem : EntitySystem ent.Comp.AccessListsOriginal ??= new(ent.Comp.AccessLists); } + private void OnFindAvailableLocks(Entity ent, ref FindAvailableLocksEvent args) + { + args.FoundReaders |= LockTypes.Access; + } + + private void OnCheckLockAccess(Entity ent, ref CheckUserHasLockAccessEvent args) + { + // Are we looking for an access lock? + if (!args.FoundReaders.HasFlag(LockTypes.Access)) + return; + + // If the user has access to this lock, we pass it into the event. + if (IsAllowed(args.User, ent)) + args.HasAccess |= LockTypes.Access; + } + /// /// Searches the source for access tags /// then compares it with the all targets accesses to see if it is allowed. diff --git a/Content.Shared/ActionBlocker/ActionBlockerSystem.cs b/Content.Shared/ActionBlocker/ActionBlockerSystem.cs index 08eac657c0..c256872cc7 100644 --- a/Content.Shared/ActionBlocker/ActionBlockerSystem.cs +++ b/Content.Shared/ActionBlocker/ActionBlockerSystem.cs @@ -167,15 +167,21 @@ namespace Content.Shared.ActionBlocker return !ev.Cancelled; } - public bool CanPickup(EntityUid user, EntityUid item) + /// + /// Whether a user can pickup the given item. + /// + /// The mob trying to pick up the item. + /// The item being picked up. + /// Whether or not to show a popup to the player telling them why the attempt failed. + public bool CanPickup(EntityUid user, EntityUid item, bool showPopup = false) { - var userEv = new PickupAttemptEvent(user, item); + var userEv = new PickupAttemptEvent(user, item, showPopup); RaiseLocalEvent(user, userEv); if (userEv.Cancelled) return false; - var itemEv = new GettingPickedUpAttemptEvent(user, item); + var itemEv = new GettingPickedUpAttemptEvent(user, item, showPopup); RaiseLocalEvent(item, itemEv); return !itemEv.Cancelled; diff --git a/Content.Server/Administration/Systems/RejuvenateSystem.cs b/Content.Shared/Administration/Systems/RejuvenateSystem.cs similarity index 54% rename from Content.Server/Administration/Systems/RejuvenateSystem.cs rename to Content.Shared/Administration/Systems/RejuvenateSystem.cs index d25db20bd9..d27ea63ace 100644 --- a/Content.Server/Administration/Systems/RejuvenateSystem.cs +++ b/Content.Shared/Administration/Systems/RejuvenateSystem.cs @@ -1,9 +1,12 @@ using Content.Shared.Rejuvenate; -namespace Content.Server.Administration.Systems; +namespace Content.Shared.Administration.Systems; public sealed class RejuvenateSystem : EntitySystem { + /// + /// Fully heals the target, removing all damage, debuffs or other negative status effects. + /// public void PerformRejuvenate(EntityUid target) { RaiseLocalEvent(target, new RejuvenateEvent()); 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 82% rename from Content.Server/Atmos/Components/DeltaPressureComponent.cs rename to Content.Shared/Atmos/Components/DeltaPressureComponent.cs index 8031cd876d..064d67f606 100644 --- a/Content.Server/Atmos/Components/DeltaPressureComponent.cs +++ b/Content.Shared/Atmos/Components/DeltaPressureComponent.cs @@ -1,52 +1,50 @@ -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; - /// - /// The current cached position of this entity on the grid. - /// Updated via MoveEvent. - /// - [DataField(readOnly: true)] - public Vector2i CurrentPosition = Vector2i.Zero; - /// /// The grid this entity is currently joined to for processing. /// Required for proper deletion, as we cannot reference the grid /// for removal while the entity is being deleted. /// - [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/Components/LungComponent.cs b/Content.Shared/Body/Components/LungComponent.cs index dd31de7722..ab1869f8b1 100644 --- a/Content.Shared/Body/Components/LungComponent.cs +++ b/Content.Shared/Body/Components/LungComponent.cs @@ -22,7 +22,7 @@ public sealed partial class LungComponent : Component /// The name/key of the solution on this entity which these lungs act on. /// [DataField] - public string SolutionName = LungSystem.LungSolutionName; + public string SolutionName = "Lung"; /// /// The solution on this entity that these lungs act on. diff --git a/Content.Shared/Body/Components/StomachComponent.cs b/Content.Shared/Body/Components/StomachComponent.cs index 1b2f587824..ca20cc544a 100644 --- a/Content.Shared/Body/Components/StomachComponent.cs +++ b/Content.Shared/Body/Components/StomachComponent.cs @@ -8,7 +8,7 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared.Body.Components { - [RegisterComponent, NetworkedComponent, Access(typeof(StomachSystem), typeof(FoodSystem))] + [RegisterComponent, NetworkedComponent, Access(typeof(StomachSystem))] public sealed partial class StomachComponent : Component { /// diff --git a/Content.Shared/Body/Systems/LungSystem.cs b/Content.Shared/Body/Systems/LungSystem.cs index 5f4c1ee4ef..a097a7752a 100644 --- a/Content.Shared/Body/Systems/LungSystem.cs +++ b/Content.Shared/Body/Systems/LungSystem.cs @@ -1,11 +1,14 @@ +using Content.Shared.Atmos; using Content.Shared.Atmos.Components; using Content.Shared.Atmos.EntitySystems; using Content.Shared.Body.Components; -using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Atmos; +using Content.Shared.Body.Prototypes; using Content.Shared.Chemistry.Components; -using Content.Shared.Clothing; +using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Inventory.Events; +using Robust.Shared.Prototypes; +using BreathToolComponent = Content.Shared.Atmos.Components.BreathToolComponent; +using InternalsComponent = Content.Shared.Body.Components.InternalsComponent; namespace Content.Shared.Body.Systems; @@ -15,8 +18,6 @@ public sealed class LungSystem : EntitySystem [Dependency] private readonly SharedInternalsSystem _internals = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; - public static string LungSolutionName = "Lung"; - public override void Initialize() { base.Initialize(); @@ -53,6 +54,7 @@ public sealed class LungSystem : EntitySystem } } + // TODO: JUST METABOLIZE GASES DIRECTLY DON'T CONVERT TO REAGENTS!!! (Needs Metabolism refactor :B) public void GasToReagent(EntityUid uid, LungComponent lung) { if (!_solutionContainerSystem.ResolveSolution(uid, lung.SolutionName, ref lung.Solution, out var solution)) diff --git a/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs b/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs index 074bf596ee..dc27d5541d 100644 --- a/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs +++ b/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs @@ -6,7 +6,8 @@ using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reagent; using Content.Shared.Damage; -using Content.Shared.EntityEffects.Effects; +using Content.Shared.Damage.Systems; +using Content.Shared.EntityEffects.Effects.Solution; using Content.Shared.FixedPoint; using Content.Shared.Fluids; using Content.Shared.Forensics.Components; @@ -110,8 +111,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. @@ -161,7 +161,9 @@ public abstract class SharedBloodstreamSystem : EntitySystem { switch (effect) { - case CreateEntityReactionEffect: // Prevent entities from spawning in the bloodstream + // TODO: Rather than this, ReactionAttempt should allow systems to remove effects from the list before the reaction. + // TODO: I think there's a PR up on the repo for this and if there isn't I'll make one -Princess + case EntityEffects.Effects.EntitySpawning.SpawnEntity: // Prevent entities from spawning in the bloodstream case AreaReactionEffect: // No spontaneous smoke or foam leaking out of blood vessels. args.Cancelled = true; return; @@ -226,7 +228,7 @@ public abstract class SharedBloodstreamSystem : EntitySystem // TODO: Replace with RandomPredicted once the engine PR is merged // Use both the receiver and the damage causing entity for the seed so that we have different results for multiple attacks in the same tick - var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(ent).Id, GetNetEntity(args.Origin)?.Id ?? 0 }); + var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(ent).Id, GetNetEntity(args.Origin)?.Id ?? 0 ); var rand = new System.Random(seed); var prob = Math.Clamp(totalFloat / 25, 0, 1); if (totalFloat > 0 && rand.Prob(prob)) 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/Buckle/SharedBuckleSystem.Buckle.cs b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs index 7dd4ea4d11..75680089fb 100644 --- a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs +++ b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs @@ -467,7 +467,7 @@ public abstract partial class SharedBuckleSystem // TODO: This is doing 4 moveevents this is why I left the warning in, if you're going to remove it make it only do 1 moveevent. if (strap.Comp.BuckleOffset != Vector2.Zero) { - buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strap.Comp.BuckleOffset); + _transform.SetCoordinates(buckle, buckleXform, oldBuckledXform.Coordinates.Offset(strap.Comp.BuckleOffset)); } } diff --git a/Content.Shared/Cabinet/ItemCabinetSystem.cs b/Content.Shared/Cabinet/ItemCabinetSystem.cs index 749065ac47..ea500aaeaa 100644 --- a/Content.Shared/Cabinet/ItemCabinetSystem.cs +++ b/Content.Shared/Cabinet/ItemCabinetSystem.cs @@ -37,7 +37,7 @@ public sealed class ItemCabinetSystem : EntitySystem private void OnMapInit(Entity ent, ref MapInitEvent args) { // update at mapinit to avoid copy pasting locked: true and locked: false for each closed/open prototype - SetSlotLock(ent, !_openable.IsOpen(ent)); + SetSlotLock(ent, _openable.IsClosed(ent, null)); } private void UpdateAppearance(Entity ent) diff --git a/Content.Shared/Cargo/SharedCargoSystem.cs b/Content.Shared/Cargo/SharedCargoSystem.cs index 8925ce0de1..9d044d1850 100644 --- a/Content.Shared/Cargo/SharedCargoSystem.cs +++ b/Content.Shared/Cargo/SharedCargoSystem.cs @@ -1,5 +1,6 @@ using Content.Shared.Cargo.Components; using Content.Shared.Cargo.Prototypes; +using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Timing; @@ -55,6 +56,151 @@ public abstract class SharedCargoSystem : EntitySystem } return distribution; } + + /// + /// Returns information about the given bank account. + /// + /// Station to get bank account info from. + /// Bank account prototype ID to get info for. + /// The amount of money in the account + /// Whether or not the bank account exists. + public bool TryGetAccount(Entity station, ProtoId accountPrototypeId, out int money) + { + money = 0; + + if (!Resolve(station, ref station.Comp)) + return false; + + return station.Comp.Accounts.TryGetValue(accountPrototypeId, out money); + } + + /// + /// Returns a readonly dictionary of all accounts and their money info. + /// + /// Station to get bank account info from. + /// Whether or not the bank account exists. + public IReadOnlyDictionary, int> GetAccounts(Entity station) + { + if (!Resolve(station, ref station.Comp)) + return new Dictionary, int>(); + + return station.Comp.Accounts; + } + + /// + /// Attempts to adjust the money of a certain bank account. + /// + /// Station where the bank account is from + /// the id of the bank account + /// how much money to set the account to + /// Whether or not it should create the account if it doesn't exist. + /// Whether to mark the bank account component as dirty. + /// Whether or not setting the value succeeded. + public bool TryAdjustBankAccount( + Entity station, + ProtoId accountPrototypeId, + int money, + bool createAccount = false, + bool dirty = true) + { + if (!Resolve(station, ref station.Comp)) + return false; + + var accounts = station.Comp.Accounts; + + if (!accounts.ContainsKey(accountPrototypeId) && !createAccount) + return false; + + accounts[accountPrototypeId] += money; + var ev = new BankBalanceUpdatedEvent(station, station.Comp.Accounts); + RaiseLocalEvent(station, ref ev, true); + + if (!dirty) + return true; + + Dirty(station); + return true; + } + + /// + /// Attempts to set the money of a certain bank account. + /// + /// Station where the bank account is from + /// the id of the bank account + /// how much money to set the account to + /// Whether or not it should create the account if it doesn't exist. + /// Whether to mark the bank account component as dirty. + /// Whether or not setting the value succeeded. + public bool TrySetBankAccount( + Entity station, + ProtoId accountPrototypeId, + int money, + bool createAccount = false, + bool dirty = true) + { + if (!Resolve(station, ref station.Comp)) + return false; + + var accounts = station.Comp.Accounts; + + if (!accounts.ContainsKey(accountPrototypeId) && !createAccount) + return false; + + accounts[accountPrototypeId] = money; + var ev = new BankBalanceUpdatedEvent(station, station.Comp.Accounts); + RaiseLocalEvent(station, ref ev, true); + + if (!dirty) + return true; + + Dirty(station); + return true; + } + + public void UpdateBankAccount( + Entity ent, + int balanceAdded, + ProtoId account, + bool dirty = true) + { + UpdateBankAccount( + ent, + balanceAdded, + new Dictionary, double> { {account, 1} }, + dirty: dirty); + } + + /// + /// Adds or removes funds from the . + /// + /// The station. + /// The amount of funds to add or remove. + /// The distribution between individual . + /// Whether to mark the bank account component as dirty. + [PublicAPI] + public void UpdateBankAccount( + Entity ent, + int balanceAdded, + Dictionary, double> accountDistribution, + bool dirty = true) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + foreach (var (account, percent) in accountDistribution) + { + var accountBalancedAdded = (int) Math.Round(percent * balanceAdded); + ent.Comp.Accounts[account] += accountBalancedAdded; + } + + var ev = new BankBalanceUpdatedEvent(ent, ent.Comp.Accounts); + RaiseLocalEvent(ent, ref ev, true); + + if (!dirty) + return; + + Dirty(ent); + } } [NetSerializable, Serializable] 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/EmotesEvents.cs b/Content.Shared/Chat/EmotesEvents.cs index 4479f8b2ab..196bbb43bd 100644 --- a/Content.Shared/Chat/EmotesEvents.cs +++ b/Content.Shared/Chat/EmotesEvents.cs @@ -1,9 +1,49 @@ -using Content.Shared.Chat.Prototypes; +using Content.Shared.Chat.Prototypes; +using Content.Shared.Inventory; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; namespace Content.Shared.Chat; +/// +/// An event raised just before an emote is performed, providing systems with an opportunity to cancel the emote's performance. +/// +[ByRefEvent] +public sealed class BeforeEmoteEvent(EntityUid source, EmotePrototype emote) + : CancellableEntityEventArgs, IInventoryRelayEvent +{ + public readonly EntityUid Source = source; + public readonly EmotePrototype Emote = emote; + + /// + /// The equipment that is blocking emoting. Should only be non-null if the event was canceled. + /// + public EntityUid? Blocker = null; + + public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET; +} + +/// +/// Raised by the chat system when an entity made some emote. +/// Use it to play sound, change sprite or something else. +/// +[ByRefEvent] +public record struct EmoteEvent(EmotePrototype Emote) +{ + /// + /// The used emote. + /// + public EmotePrototype Emote = Emote; + + /// + /// If this message has already been "handled" by a previous system. + /// + public bool Handled; +} + +/// +/// Sent by the client when requesting the server to play a specific emote selected from the emote radial menu. +/// [Serializable, NetSerializable] public sealed class PlayEmoteMessage(ProtoId protoId) : EntityEventArgs { 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/Prototypes/AutoEmotePrototype.cs b/Content.Shared/Chat/Prototypes/AutoEmotePrototype.cs index e6fdae4cf1..a3d6ccce7f 100644 --- a/Content.Shared/Chat/Prototypes/AutoEmotePrototype.cs +++ b/Content.Shared/Chat/Prototypes/AutoEmotePrototype.cs @@ -14,30 +14,44 @@ public sealed partial class AutoEmotePrototype : IPrototype /// The ID of the emote prototype. ///
[DataField("emote", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string EmoteId = String.Empty; + public string EmoteId = string.Empty; /// /// How often an attempt at the emote will be made. /// - [DataField("interval", required: true)] + [DataField(required: true)] public TimeSpan Interval; /// /// Probability of performing the emote each interval. - /// + /// [DataField("chance")] public float Chance = 1; /// /// Also send the emote in chat. - /// - [DataField("withChat")] + /// + [DataField] public bool WithChat = true; + /// + /// Should we ignore action blockers? + /// This does nothing if WithChat is false. + /// + [DataField] + public bool IgnoreActionBlocker; + + /// + /// Should we ignore whitelists and force the emote? + /// This does nothing if WithChat is false. + /// + [DataField] + public bool Force; + /// /// Hide the chat message from the chat window, only showing the popup. /// This does nothing if WithChat is false. - /// - [DataField("hiddenFromChatWindow")] - public bool HiddenFromChatWindow = false; + /// + [DataField] + public bool HiddenFromChatWindow; } diff --git a/Content.Shared/Chat/SharedChatEvents.cs b/Content.Shared/Chat/SharedChatEvents.cs index c187fd59a8..f9b706e57d 100644 --- a/Content.Shared/Chat/SharedChatEvents.cs +++ b/Content.Shared/Chat/SharedChatEvents.cs @@ -1,12 +1,13 @@ +using Content.Shared.Inventory; +using Content.Shared.Radio; using Content.Shared.Speech; using Robust.Shared.Prototypes; -using Content.Shared.Inventory; namespace Content.Shared.Chat; /// -/// This event should be sent everytime an entity talks (Radio, local chat, etc...). -/// The event is sent to both the entity itself, and all clothing (For stuff like voice masks). +/// This event should be sent everytime an entity talks (Radio, local chat, etc...). +/// The event is sent to both the entity itself, and all clothing (For stuff like voice masks). /// public sealed class TransformSpeakerNameEvent : EntityEventArgs, IInventoryRelayEvent { @@ -22,3 +23,54 @@ public sealed class TransformSpeakerNameEvent : EntityEventArgs, IInventoryRelay SpeechVerb = null; } } + +/// +/// Raised broadcast in order to transform speech.transmit +/// +public sealed class TransformSpeechEvent : EntityEventArgs +{ + public EntityUid Sender; + public string Message; + + public TransformSpeechEvent(EntityUid sender, string message) + { + Sender = sender; + Message = message; + } +} + +public sealed class CheckIgnoreSpeechBlockerEvent : EntityEventArgs +{ + public EntityUid Sender; + public bool IgnoreBlocker; + + public CheckIgnoreSpeechBlockerEvent(EntityUid sender, bool ignoreBlocker) + { + Sender = sender; + IgnoreBlocker = ignoreBlocker; + } +} + +/// +/// Raised on an entity when it speaks, either through 'say' or 'whisper'. +/// +public sealed class EntitySpokeEvent : EntityEventArgs +{ + public readonly EntityUid Source; + public readonly string Message; + public readonly string? ObfuscatedMessage; // not null if this was a whisper + + /// + /// If the entity was trying to speak into a radio, this was the channel they were trying to access. If a radio + /// message gets sent on this channel, this should be set to null to prevent duplicate messages. + /// + public RadioChannelPrototype? Channel; + + public EntitySpokeEvent(EntityUid source, string message, RadioChannelPrototype? channel, string? obfuscatedMessage) + { + Source = source; + Message = message; + Channel = channel; + ObfuscatedMessage = obfuscatedMessage; + } +} diff --git a/Content.Server/Chat/Systems/ChatSystem.Emote.cs b/Content.Shared/Chat/SharedChatSystem.Emote.cs similarity index 68% rename from Content.Server/Chat/Systems/ChatSystem.Emote.cs rename to Content.Shared/Chat/SharedChatSystem.Emote.cs index ee891e0870..d92a02a9de 100644 --- a/Content.Server/Chat/Systems/ChatSystem.Emote.cs +++ b/Content.Shared/Chat/SharedChatSystem.Emote.cs @@ -1,28 +1,15 @@ using System.Collections.Frozen; -using Content.Server.Popups; using Content.Shared.Chat.Prototypes; -using Content.Shared.Emoting; using Content.Shared.Speech; using Robust.Shared.Audio; -using Robust.Shared.Prototypes; using Robust.Shared.Random; -namespace Content.Server.Chat.Systems; +namespace Content.Shared.Chat; -// emotes using emote prototype -public partial class ChatSystem +public abstract partial class SharedChatSystem { - [Dependency] private readonly PopupSystem _popupSystem = default!; - private FrozenDictionary _wordEmoteDict = FrozenDictionary.Empty; - protected override void OnPrototypeReload(PrototypesReloadedEventArgs obj) - { - base.OnPrototypeReload(obj); - if (obj.WasModified()) - CacheEmotes(); - } - private void CacheEmotes() { var dict = new Dictionary(); @@ -47,15 +34,19 @@ public partial class ChatSystem } /// - /// Makes selected entity to emote using and sends message to chat. + /// Makes the selected entity emote using the given and sends a message to chat. /// /// The entity that is speaking - /// The id of emote prototype. Should has valid - /// Whether or not this message should appear in the adminlog window + /// The id of emote prototype. Should have valid + /// Whether this message should appear in the adminlog window, or not. /// Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all... - /// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised. + /// Whether emote action blocking should be ignored or not. + /// + /// The name to use for the speaking entity. Usually this should just be modified via . + /// If this is set, the event will not get raised. + /// /// Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote - /// True if an emote was performed. False if the emote is unvailable, cancelled, etc. + /// True if an emote was performed. False if the emote is unavailable, cancelled, etc. public bool TryEmoteWithChat( EntityUid source, string emoteId, @@ -64,24 +55,28 @@ public partial class ChatSystem string? nameOverride = null, bool ignoreActionBlocker = false, bool forceEmote = false - ) + ) { - if (!_prototypeManager.TryIndex(emoteId, out var proto)) + if (!_prototypeManager.Resolve(emoteId, out var proto)) return false; + return TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote); } /// - /// Makes selected entity to emote using and sends message to chat. + /// Makes the selected entity emote using the given and sends a message to chat. /// - /// The entity that is speaking - /// The emote prototype. Should has valid - /// Whether or not this message should appear in the adminlog window - /// Whether or not this message should appear in the chat window + /// The entity that is speaking. + /// The emote prototype. Should have valid . + /// Whether this message should appear in the adminlog window or not. + /// Whether emote action blocking should be ignored or not. /// Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all... - /// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised. + /// + /// The name to use for the speaking entity. Usually this should just be modified via . + /// If this is set, the event will not get raised. + /// /// Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote - /// True if an emote was performed. False if the emote is unvailable, cancelled, etc. + /// True if an emote was performed. False if the emote is unavailable, cancelled, etc. public bool TryEmoteWithChat( EntityUid source, EmotePrototype emote, @@ -109,21 +104,21 @@ public partial class ChatSystem } /// - /// Makes selected entity to emote using without sending any messages to chat. + /// Makes the selected entity emote using the given without sending any messages to chat. /// - /// True if an emote was performed. False if the emote is unvailable, cancelled, etc. + /// True if an emote was performed. False if the emote is unavailable, cancelled, etc. public bool TryEmoteWithoutChat(EntityUid uid, string emoteId, bool ignoreActionBlocker = false) { - if (!_prototypeManager.TryIndex(emoteId, out var proto)) + if (!_prototypeManager.Resolve(emoteId, out var proto)) return false; return TryEmoteWithoutChat(uid, proto, ignoreActionBlocker); } /// - /// Makes selected entity to emote using without sending any messages to chat. + /// Makes the selected entity emote using the given without sending any messages to chat. /// - /// True if an emote was performed. False if the emote is unvailable, cancelled, etc. + /// True if an emote was performed. False if the emote is unavailable, cancelled, etc. public bool TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto, bool ignoreActionBlocker = false) { if (!_actionBlocker.CanEmote(uid) && !ignoreActionBlocker) @@ -133,7 +128,7 @@ public partial class ChatSystem } /// - /// Tries to find and play relevant emote sound in emote sounds collection. + /// Tries to find and play the relevant emote sound in an emote sounds collection. /// /// True if emote sound was played. public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, EmotePrototype emote, AudioParams? audioParams = null) @@ -142,7 +137,7 @@ public partial class ChatSystem } /// - /// Tries to find and play relevant emote sound in emote sounds collection. + /// Tries to find and play the relevant emote sound in an emote sounds collection. /// /// True if emote sound was played. public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, string emoteId, AudioParams? audioParams = null) @@ -167,44 +162,27 @@ public partial class ChatSystem /// /// Checks if a valid emote was typed, to play sounds and etc and invokes an event. /// - /// - /// + /// The entity that is speaking + /// Formatted emote message. /// True if the chat message should be displayed (because the emote was explicitly cancelled), false if it should not be. - private bool TryEmoteChatInput(EntityUid uid, string textInput) + protected bool TryEmoteChatInput(EntityUid source, string textInput) { var actionTrimmedLower = TrimPunctuation(textInput.ToLower()); if (!_wordEmoteDict.TryGetValue(actionTrimmedLower, out var emote)) return true; - if (!AllowedToUseEmote(uid, emote)) + if (!AllowedToUseEmote(source, emote)) return true; - return TryInvokeEmoteEvent(uid, emote); + return TryInvokeEmoteEvent(source, emote); - static string TrimPunctuation(string textInput) - { - var trimEnd = textInput.Length; - while (trimEnd > 0 && char.IsPunctuation(textInput[trimEnd - 1])) - { - trimEnd--; - } - - var trimStart = 0; - while (trimStart < trimEnd && char.IsPunctuation(textInput[trimStart])) - { - trimStart++; - } - - return textInput[trimStart..trimEnd]; - } } /// - /// Checks if we can use this emote based on the emotes whitelist, blacklist, and availibility to the entity. + /// Checks if we can use this emote based on the emotes whitelist, blacklist, and availability to the entity. /// /// The entity that is speaking /// The emote being used - /// - private bool AllowedToUseEmote(EntityUid source, EmotePrototype emote) + public bool AllowedToUseEmote(EntityUid source, EmotePrototype emote) { // If emote is in AllowedEmotes, it will bypass whitelist and blacklist if (TryComp(source, out var speech) && @@ -214,8 +192,8 @@ public partial class ChatSystem } // Check the whitelist and blacklist - if (_whitelistSystem.IsWhitelistFail(emote.Whitelist, source) || - _whitelistSystem.IsBlacklistPass(emote.Blacklist, source)) + if (_whitelist.IsWhitelistFail(emote.Whitelist, source) || + _whitelist.IsBlacklistPass(emote.Blacklist, source)) { return false; } @@ -244,9 +222,13 @@ public partial class ChatSystem if (beforeEv.Cancelled) { + // Chat is not predicted anyways, so no need to predict this popup either. + if (_net.IsClient) + return false; + if (beforeEv.Blocker != null) { - _popupSystem.PopupEntity( + _popup.PopupEntity( Loc.GetString( "chat-system-emote-cancelled-blocked", ("emote", Loc.GetString(proto.Name).ToLower()), @@ -258,7 +240,7 @@ public partial class ChatSystem } else { - _popupSystem.PopupEntity( + _popup.PopupEntity( Loc.GetString("chat-system-emote-cancelled-generic", ("emote", Loc.GetString(proto.Name).ToLower())), uid, @@ -274,20 +256,21 @@ public partial class ChatSystem return true; } -} -/// -/// Raised by chat system when entity made some emote. -/// Use it to play sound, change sprite or something else. -/// -[ByRefEvent] -public sealed class EmoteEvent : HandledEntityEventArgs -{ - public readonly EmotePrototype Emote; - - public EmoteEvent(EmotePrototype emote) + private string TrimPunctuation(string textInput) { - Emote = emote; - Handled = false; + var trimEnd = textInput.Length; + while (trimEnd > 0 && char.IsPunctuation(textInput[trimEnd - 1])) + { + trimEnd--; + } + + var trimStart = 0; + while (trimStart < trimEnd && char.IsPunctuation(textInput[trimStart])) + { + trimStart++; + } + + return textInput[trimStart..trimEnd]; } } diff --git a/Content.Shared/Chat/SharedChatSystem.cs b/Content.Shared/Chat/SharedChatSystem.cs index d9f7f5fc57..e7efe39df9 100644 --- a/Content.Shared/Chat/SharedChatSystem.cs +++ b/Content.Shared/Chat/SharedChatSystem.cs @@ -1,15 +1,23 @@ using System.Collections.Frozen; using System.Text.RegularExpressions; +using Content.Shared.ActionBlocker; +using Content.Shared.Chat.Prototypes; using Content.Shared.Popups; using Content.Shared.Radio; using Content.Shared.Speech; +using Content.Shared.Whitelist; using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Console; +using Robust.Shared.Network; +using Robust.Shared.Player; using Robust.Shared.Prototypes; +using Robust.Shared.Random; using Robust.Shared.Utility; namespace Content.Shared.Chat; -public abstract class SharedChatSystem : EntitySystem +public abstract partial class SharedChatSystem : EntitySystem { public const char RadioCommonPrefix = ';'; public const char RadioChannelPrefix = ':'; @@ -38,6 +46,11 @@ public abstract class SharedChatSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; + [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly INetManager _net = default!; /// /// Cache of the keycodes for faster lookup. @@ -47,15 +60,21 @@ public abstract class SharedChatSystem : EntitySystem public override void Initialize() { base.Initialize(); + DebugTools.Assert(_prototypeManager.HasIndex(CommonChannel)); + SubscribeLocalEvent(OnPrototypeReload); CacheRadios(); + CacheEmotes(); } protected virtual void OnPrototypeReload(PrototypesReloadedEventArgs obj) { if (obj.WasModified()) CacheRadios(); + + if (obj.WasModified()) + CacheEmotes(); } private void CacheRadios() @@ -127,7 +146,7 @@ public abstract 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, @@ -293,4 +312,177 @@ public abstract class SharedChatSystem : EntitySystem tagStart += tag.Length + 2; return rawmsg.Substring(tagStart, tagEnd - tagStart); } + + protected virtual void SendEntityEmote( + EntityUid source, + string action, + ChatTransmitRange range, + string? nameOverride, + bool hideLog = false, + bool checkEmote = true, + bool ignoreActionBlocker = false, + NetUserId? author = null + ) + { } + + /// + /// Sends an in-character chat message to relevant clients. + /// + /// The entity that is speaking. + /// The message being spoken or emoted. + /// The chat type. + /// Whether or not this message should appear in the chat window. + /// Whether or not this message should appear in the adminlog window. + /// + /// The player doing the speaking. + /// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised. + /// Whether or not should be parsed with consideration of radio channel prefix text at start the start. + /// If set to true, action blocker will not be considered for whether an entity can send this message. + public virtual void TrySendInGameICMessage( + EntityUid source, + string message, + InGameICChatType desiredType, + bool hideChat, + bool hideLog = false, + IConsoleShell? shell = null, + ICommonSession? player = null, + string? nameOverride = null, + bool checkRadioPrefix = true, + bool ignoreActionBlocker = false) + { } + + /// + /// Sends an in-character chat message to relevant clients. + /// + /// The entity that is speaking. + /// The message being spoken or emoted. + /// The chat type. + /// Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all... + /// Disables the admin log for this message if true. Used for entities that are not players, like vendors, cloning, etc. + /// + /// The player doing the speaking. + /// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised. + /// If set to true, action blocker will not be considered for whether an entity can send this message. + public virtual void TrySendInGameICMessage( + EntityUid source, + string message, + InGameICChatType desiredType, + ChatTransmitRange range, + bool hideLog = false, + IConsoleShell? shell = null, + ICommonSession? player = null, + string? nameOverride = null, + bool checkRadioPrefix = true, + bool ignoreActionBlocker = false + ) + { } + + /// + /// Sends an out-of-character chat message to relevant clients. + /// + /// The entity that is speaking. + /// The message being spoken or emoted. + /// The chat type. + /// Whether or not to show the message in the chat window. + /// + /// The player doing the speaking. + public virtual void TrySendInGameOOCMessage( + EntityUid source, + string message, + InGameOOCChatType type, + bool hideChat, + IConsoleShell? shell = null, + ICommonSession? player = null + ) + { } + + /// + /// Dispatches an announcement to all. + /// + /// The contents of the message. + /// The sender (Communications Console in Communications Console Announcement). + /// Play the announcement sound. + /// Sound to play. + /// Optional color for the announcement message. + public virtual void DispatchGlobalAnnouncement( + string message, + string? sender = null, + bool playSound = true, + SoundSpecifier? announcementSound = null, + Color? colorOverride = null + ) + { } + + /// + /// Dispatches an announcement to players selected by filter. + /// + /// Filter to select players who will recieve the announcement. + /// The contents of the message. + /// The entity making the announcement (used to determine the station). + /// The sender (Communications Console in Communications Console Announcement). + /// Play the announcement sound. + /// Sound to play. + /// Optional color for the announcement message. + public virtual void DispatchFilteredAnnouncement( + Filter filter, + string message, + EntityUid? source = null, + string? sender = null, + bool playSound = true, + SoundSpecifier? announcementSound = null, + Color? colorOverride = null) + { } + + /// + /// Dispatches an announcement on a specific station. + /// + /// The entity making the announcement (used to determine the station). + /// The contents of the message. + /// The sender (Communications Console in Communications Console Announcement). + /// Play the announcement sound. + /// Sound to play. + /// Optional color for the announcement message. + public virtual void DispatchStationAnnouncement( + EntityUid source, + string message, + string? sender = null, + bool playDefaultSound = true, + SoundSpecifier? announcementSound = null, + Color? colorOverride = null) + { } +} + +/// +/// Controls transmission of chat. +/// +public enum ChatTransmitRange : byte +{ + /// Acts normal, ghosts can hear across the map, etc. + Normal, + /// Normal but ghosts are still range-limited. + GhostRangeLimit, + /// Hidden from the chat window. + HideChat, + /// Ghosts can't hear or see it at all. Regular players can if in-range. + NoGhosts +} + +/// +/// InGame IC chat is for chat that is specifically ingame (not lobby) but is also in character, i.e. speaking. +/// +// ReSharper disable once InconsistentNaming +public enum InGameICChatType : byte +{ + Speak, + Emote, + Whisper +} + +/// +/// InGame OOC chat is for chat that is specifically ingame (not lobby) but is OOC, like deadchat or LOOC. +/// +public enum InGameOOCChatType : byte +{ + Looc, + Dead } 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/Chemistry/Components/DrainableSolutionComponent.cs b/Content.Shared/Chemistry/Components/DrainableSolutionComponent.cs index 0a6ace0943..616f5a1299 100644 --- a/Content.Shared/Chemistry/Components/DrainableSolutionComponent.cs +++ b/Content.Shared/Chemistry/Components/DrainableSolutionComponent.cs @@ -3,8 +3,9 @@ using Robust.Shared.GameStates; namespace Content.Shared.Chemistry.Components; /// -/// Denotes the solution that can be easily removed through any reagent container. -/// Think pouring this or draining from a water tank. +/// Denotes a specific solution contained within this entity that can can be +/// easily "drained". This means things with taps/spigots, or easily poured +/// items. /// [RegisterComponent, NetworkedComponent] public sealed partial class DrainableSolutionComponent : Component @@ -12,6 +13,12 @@ public sealed partial class DrainableSolutionComponent : Component /// /// Solution name that can be drained. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public string Solution = "default"; + + /// + /// The drain doafter time required to transfer reagents from the solution. + /// + [DataField] + public TimeSpan DrainTime = TimeSpan.Zero; } diff --git a/Content.Shared/Chemistry/Components/DumpableSolutionComponent.cs b/Content.Shared/Chemistry/Components/DumpableSolutionComponent.cs index 43fbe137b6..5fab79b6b4 100644 --- a/Content.Shared/Chemistry/Components/DumpableSolutionComponent.cs +++ b/Content.Shared/Chemistry/Components/DumpableSolutionComponent.cs @@ -3,8 +3,11 @@ using Robust.Shared.GameStates; namespace Content.Shared.Chemistry.Components; /// -/// Denotes the solution that can be easily dumped into (completely removed from the dumping container into this one) -/// Think pouring a container fully into this. +/// Denotes that there is a solution contained in this entity that can be +/// easily dumped into (that is, completely removed from the dumping container +/// into this one). Think pouring a container fully into this. The action for this is represented via drag & drop. +/// +/// To represent it being possible to controllably pour volumes into the entity, see . /// [RegisterComponent, NetworkedComponent] public sealed partial class DumpableSolutionComponent : Component @@ -12,12 +15,13 @@ public sealed partial class DumpableSolutionComponent : Component /// /// Solution name that can be dumped into. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public string Solution = "default"; /// /// Whether the solution can be dumped into infinitely. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + /// Note that this is what makes the ChemMaster's buffer a stasis buffer as well! + [DataField] public bool Unlimited = false; } diff --git a/Content.Shared/Chemistry/Components/HyposprayComponent.cs b/Content.Shared/Chemistry/Components/HyposprayComponent.cs index ca20e1c22f..e1e4f21101 100644 --- a/Content.Shared/Chemistry/Components/HyposprayComponent.cs +++ b/Content.Shared/Chemistry/Components/HyposprayComponent.cs @@ -24,6 +24,13 @@ public sealed partial class HyposprayComponent : Component [DataField] public FixedPoint2 TransferAmount = FixedPoint2.New(5); + /// + /// The delay to draw reagents using the hypospray. + /// If set, RefillTime should probably have the same value. + /// + [DataField] + public float DrawTime = 0f; + /// /// Sound that will be played when injecting. /// diff --git a/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs b/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs index 245b7398a7..41d6d42938 100644 --- a/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs +++ b/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs @@ -4,9 +4,11 @@ using Robust.Shared.GameStates; namespace Content.Shared.Chemistry.Components; /// -/// Reagents that can be added easily. For example like -/// pouring something into another beaker, glass, or into the gas -/// tank of a car. +/// Denotes that the entity has a solution contained which can be easily added +/// to in controlled volumes. This should go on things that are meant to be refilled, including +/// pouring things into a beaker. The action for this is represented via clicking. +/// +/// To represent it being possible to just dump entire volumes at once into an entity, see . /// [RegisterComponent, NetworkedComponent] public sealed partial class RefillableSolutionComponent : Component @@ -14,12 +16,18 @@ public sealed partial class RefillableSolutionComponent : Component /// /// Solution name that can added to easily. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public string Solution = "default"; /// /// The maximum amount that can be transferred to the solution at once /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public FixedPoint2? MaxRefill = null; + + /// + /// The refill doafter time required to transfer reagents into the solution. + /// + [DataField] + public TimeSpan RefillTime = TimeSpan.Zero; } diff --git a/Content.Shared/Chemistry/Components/Solution.cs b/Content.Shared/Chemistry/Components/Solution.cs index 38be03226d..2179469c08 100644 --- a/Content.Shared/Chemistry/Components/Solution.cs +++ b/Content.Shared/Chemistry/Components/Solution.cs @@ -37,7 +37,6 @@ namespace Content.Shared.Chemistry.Components /// systems use this. /// [DataField("maxVol")] - [ViewVariables(VVAccess.ReadWrite)] public FixedPoint2 MaxVolume { get; set; } = FixedPoint2.Zero; public float FillFraction => MaxVolume == 0 ? 1 : Volume.Float() / MaxVolume.Float(); @@ -45,8 +44,7 @@ namespace Content.Shared.Chemistry.Components /// /// If reactions will be checked for when adding reagents to the container. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("canReact")] + [DataField] public bool CanReact { get; set; } = true; /// @@ -58,8 +56,7 @@ namespace Content.Shared.Chemistry.Components /// /// The temperature of the reagents in the solution. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("temperature")] + [DataField] public float Temperature { get; set; } = 293.15f; /// @@ -100,7 +97,7 @@ namespace Content.Shared.Chemistry.Components _heatCapacity = 0; foreach (var (reagent, quantity) in Contents) { - _heatCapacity += (float) quantity * + _heatCapacity += (float)quantity * protoMan.Index(reagent.Prototype).SpecificHeat; } @@ -148,7 +145,7 @@ namespace Content.Shared.Chemistry.Components /// /// The prototype ID of the reagent to add. /// The quantity in milli-units. - public Solution(string prototype, FixedPoint2 quantity, List? data = null) : this() + public Solution([ForbidLiteral] string prototype, FixedPoint2 quantity, List? data = null) : this() { AddReagent(new ReagentId(prototype, data), quantity); } @@ -190,7 +187,7 @@ namespace Content.Shared.Chemistry.Components public void ValidateSolution() { // sandbox forbids: [Conditional("DEBUG")] - #if DEBUG +#if DEBUG // Correct volume DebugTools.Assert(Contents.Select(x => x.Quantity).Sum() == Volume); @@ -208,7 +205,7 @@ namespace Content.Shared.Chemistry.Components UpdateHeatCapacity(null); DebugTools.Assert(MathHelper.CloseTo(_heatCapacity, cur, tolerance: 0.01)); } - #endif +#endif } void ISerializationHooks.AfterDeserialization() @@ -223,7 +220,7 @@ namespace Content.Shared.Chemistry.Components MaxVolume = Volume; } - public bool ContainsPrototype(string prototype) + public bool ContainsPrototype([ForbidLiteral] string prototype) { foreach (var (reagent, _) in Contents) { @@ -245,7 +242,7 @@ namespace Content.Shared.Chemistry.Components return false; } - public bool ContainsReagent(string reagentId, List? data) + public bool ContainsReagent([ForbidLiteral] string reagentId, List? data) => ContainsReagent(new(reagentId, data)); public bool TryGetReagent(ReagentId id, out ReagentQuantity quantity) @@ -352,7 +349,7 @@ namespace Content.Shared.Chemistry.Components /// /// The prototype ID of the reagent to add. /// The quantity in milli-units. - public void AddReagent(string prototype, FixedPoint2 quantity, bool dirtyHeatCap = true) + public void AddReagent([ForbidLiteral] string prototype, FixedPoint2 quantity, bool dirtyHeatCap = true) => AddReagent(new ReagentId(prototype, null), quantity, dirtyHeatCap); /// @@ -673,6 +670,12 @@ namespace Content.Shared.Chemistry.Components return sol; } + /// + /// Splits a solution into two by moving reagents from the given solution into a new one. + /// This modifies the original solution. + /// + /// The quantity of this solution to remove. + /// A new solution containing the removed reagents. public Solution SplitSolution(FixedPoint2 toTake) { if (toTake <= FixedPoint2.Zero) @@ -690,7 +693,7 @@ namespace Content.Shared.Chemistry.Components var origVol = Volume; var effVol = Volume.Value; newSolution = new Solution(Contents.Count) { Temperature = Temperature }; - var remaining = (long) toTake.Value; + var remaining = (long)toTake.Value; for (var i = Contents.Count - 1; i >= 0; i--) // iterate backwards because of remove swap. { @@ -706,7 +709,7 @@ namespace Content.Shared.Chemistry.Components continue; } - var splitQuantity = FixedPoint2.FromCents((int) split); + var splitQuantity = FixedPoint2.FromCents((int)split); var newQuantity = quantity - splitQuantity; DebugTools.Assert(newQuantity >= 0); @@ -753,7 +756,7 @@ namespace Content.Shared.Chemistry.Components var effVol = Volume.Value; Volume -= toTake; - var remaining = (long) toTake.Value; + var remaining = (long)toTake.Value; for (var i = Contents.Count - 1; i >= 0; i--)// iterate backwards because of remove swap. { var (reagent, quantity) = Contents[i]; @@ -768,7 +771,7 @@ namespace Content.Shared.Chemistry.Components continue; } - var splitQuantity = FixedPoint2.FromCents((int) split); + var splitQuantity = FixedPoint2.FromCents((int)split); var newQuantity = quantity - splitQuantity; if (newQuantity > FixedPoint2.Zero) diff --git a/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs b/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs index 324858afd7..e179fb5f43 100644 --- a/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs +++ b/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs @@ -1,10 +1,10 @@ +using System.Diagnostics.CodeAnalysis; using Content.Shared.Administration.Logs; -using Content.Shared.Body.Components; -using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.Hypospray.Events; using Content.Shared.Database; +using Content.Shared.DoAfter; using Content.Shared.FixedPoint; using Content.Shared.Forensics; using Content.Shared.IdentityManagement; @@ -16,6 +16,7 @@ using Content.Shared.Timing; using Content.Shared.Verbs; using Content.Shared.Weapons.Melee.Events; using Robust.Shared.Audio.Systems; +using Robust.Shared.Serialization; namespace Content.Shared.Chemistry.EntitySystems; @@ -27,6 +28,7 @@ public sealed class HypospraySystem : EntitySystem [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!; [Dependency] private readonly UseDelaySystem _useDelay = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; public override void Initialize() { @@ -36,6 +38,7 @@ public sealed class HypospraySystem : EntitySystem SubscribeLocalEvent(OnAttack); SubscribeLocalEvent(OnUseInHand); SubscribeLocalEvent>(AddToggleModeVerb); + SubscribeLocalEvent(OnDrawDoAfter); } #region Ref events @@ -63,6 +66,20 @@ public sealed class HypospraySystem : EntitySystem TryDoInject(entity, args.HitEntities[0], args.User); } + private void OnDrawDoAfter(Entity entity, ref HyposprayDrawDoAfterEvent args) + { + if (args.Cancelled) + return; + + if (entity.Comp.CanContainerDraw + && args.Target.HasValue + && !EligibleEntity(args.Target.Value, entity) + && _solutionContainers.TryGetDrawableSolution(args.Target.Value, out var drawableSolution, out _)) + { + TryDraw(entity, args.Target.Value, drawableSolution.Value, args.User); + } + } + #endregion #region Draw/Inject @@ -73,7 +90,7 @@ public sealed class HypospraySystem : EntitySystem && !EligibleEntity(target, entity) && _solutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _)) { - return TryDraw(entity, target, drawableSolution.Value, user); + return TryStartDraw(entity, target, drawableSolution.Value, user); } return TryDoInject(entity, target, user); @@ -186,17 +203,37 @@ public sealed class HypospraySystem : EntitySystem return true; } - private bool TryDraw(Entity entity, EntityUid target, Entity targetSolution, EntityUid user) + public bool TryStartDraw(Entity entity, EntityUid target, Entity targetSolution, EntityUid user) { - if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, - out var solution) || solution.AvailableVolume == 0) + if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln)) + return false; + + if (!TryGetDrawAmount(entity, target, targetSolution, user, soln.Value, out _)) + return false; + + var doAfterArgs = new DoAfterArgs(EntityManager, user, entity.Comp.DrawTime, new HyposprayDrawDoAfterEvent(), entity, target) + { + BreakOnDamage = true, + BreakOnMove = true, + NeedHand = true, + Hidden = true, + }; + + return _doAfter.TryStartDoAfter(doAfterArgs, out _); + } + + private bool TryGetDrawAmount(Entity entity, EntityUid target, Entity targetSolution, EntityUid user, Entity solutionEntity, [NotNullWhen(true)] out FixedPoint2? amount) + { + amount = null; + + if (solutionEntity.Comp.Solution.AvailableVolume == 0) { return false; } // Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector var realTransferAmount = FixedPoint2.Min(entity.Comp.TransferAmount, targetSolution.Comp.Solution.Volume, - solution.AvailableVolume); + solutionEntity.Comp.Solution.AvailableVolume); if (realTransferAmount <= 0) { @@ -207,7 +244,19 @@ public sealed class HypospraySystem : EntitySystem return false; } - var removedSolution = _solutionContainers.Draw(target, targetSolution, realTransferAmount); + amount = realTransferAmount; + return true; + } + + private bool TryDraw(Entity entity, EntityUid target, Entity targetSolution, EntityUid user) + { + if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln)) + return false; + + if (!TryGetDrawAmount(entity, target, targetSolution, user, soln.Value, out var amount)) + return false; + + var removedSolution = _solutionContainers.Draw(target, targetSolution, amount.Value); if (!_solutionContainers.TryAddSolution(soln.Value, removedSolution)) { @@ -275,3 +324,6 @@ public sealed class HypospraySystem : EntitySystem #endregion } + +[Serializable, NetSerializable] +public sealed partial class HyposprayDrawDoAfterEvent : SimpleDoAfterEvent {} diff --git a/Content.Shared/Chemistry/EntitySystems/ScoopableSolutionSystem.cs b/Content.Shared/Chemistry/EntitySystems/ScoopableSolutionSystem.cs index 86f9ffa390..a40c28b586 100644 --- a/Content.Shared/Chemistry/EntitySystems/ScoopableSolutionSystem.cs +++ b/Content.Shared/Chemistry/EntitySystems/ScoopableSolutionSystem.cs @@ -36,7 +36,7 @@ public sealed class ScoopableSolutionSystem : EntitySystem !_solution.TryGetRefillableSolution(beaker, out var target, out _)) return false; - var scooped = _solutionTransfer.Transfer(user, ent, src.Value, beaker, target.Value, srcSolution.Volume); + var scooped = _solutionTransfer.Transfer(new SolutionTransferData(user, ent, src.Value, beaker, target.Value, srcSolution.Volume)); if (scooped == 0) return false; diff --git a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs index f80aecd317..e675584c15 100644 --- a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs +++ b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs @@ -588,7 +588,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem /// Adds a solution to the container, if it can fully fit. /// /// entity holding targetSolution - /// entity holding targetSolution + /// entity holding targetSolution /// solution being added /// If the solution could be added. public bool TryAddSolution(Entity soln, Solution toAdd) @@ -606,40 +606,44 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem } /// - /// Adds as much of a solution to a container as can fit. + /// Adds as much of a solution to a container as can fit and updates the container. /// /// The entity containing /// The solution being added to. - /// The solution being added to + /// The solution being added to . This solution is not modified. /// The quantity of the solution actually added. public FixedPoint2 AddSolution(Entity soln, Solution toAdd) { - var (uid, comp) = soln; - var solution = comp.Solution; + var solution = soln.Comp.Solution; if (toAdd.Volume == FixedPoint2.Zero) return FixedPoint2.Zero; var quantity = FixedPoint2.Max(FixedPoint2.Zero, FixedPoint2.Min(toAdd.Volume, solution.AvailableVolume)); if (quantity < toAdd.Volume) - TryTransferSolution(soln, toAdd, quantity); + { + // TODO: This should be made into a function that directly transfers reagents. + // Currently this is quite inefficient. + solution.AddSolution(toAdd.Clone().SplitSolution(quantity), PrototypeManager); + } else - ForceAddSolution(soln, toAdd); + solution.AddSolution(toAdd, PrototypeManager); + UpdateChemicals(soln); return quantity; } /// /// Adds a solution to a container and updates the container. + /// This can exceed the maximum volume of the solution added to. /// /// The entity containing /// The solution being added to. - /// The solution being added to + /// The solution being added to . This solution is not modified. /// Whether any reagents were added to the solution. public bool ForceAddSolution(Entity soln, Solution toAdd) { - var (uid, comp) = soln; - var solution = comp.Solution; + var solution = soln.Comp.Solution; if (toAdd.Volume == FixedPoint2.Zero) return false; @@ -707,6 +711,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem } // Thermal energy and temperature management. + // TODO: ENERGY CONSERVATION!!! Nuke this once we have HeatContainers and use methods which properly conserve energy and model heat transfer correctly! #region Thermal Energy and Temperature @@ -763,6 +768,26 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem UpdateChemicals(soln); } + /// + /// Same as but clamps the value between two temperature values. + /// + /// Solution we're adjusting the energy of + /// Thermal energy we're adding or removing + /// Min desired temperature + /// Max desired temperature + public void AddThermalEnergyClamped(Entity soln, float thermalEnergy, float min, float max) + { + var solution = soln.Comp.Solution; + + if (thermalEnergy == 0.0f) + return; + + var heatCap = solution.GetHeatCapacity(PrototypeManager); + var deltaT = thermalEnergy / heatCap; + solution.Temperature = Math.Clamp(solution.Temperature + deltaT, min, max); + UpdateChemicals(soln); + } + #endregion Thermal Energy and Temperature #region Event Handlers diff --git a/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs b/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs index b0f55a3272..4d78ab4647 100644 --- a/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs +++ b/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs @@ -1,19 +1,19 @@ +using System.Diagnostics.CodeAnalysis; using Content.Shared.Administration.Logs; -using Content.Shared.Chemistry; using Content.Shared.Chemistry.Components; using Content.Shared.Database; +using Content.Shared.DoAfter; using Content.Shared.FixedPoint; using Content.Shared.Interaction; using Content.Shared.Popups; using Content.Shared.Verbs; -using Robust.Shared.Network; -using Robust.Shared.Player; +using Robust.Shared.Serialization; namespace Content.Shared.Chemistry.EntitySystems; /// -/// Allows an entity to transfer solutions with a customizable amount per click. -/// Also provides API for other systems. +/// Allows an entity to transfer solutions with a customizable amount -per click-. +/// Also provides , and API for other systems. /// public sealed class SolutionTransferSystem : EntitySystem { @@ -21,6 +21,10 @@ public sealed class SolutionTransferSystem : EntitySystem [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedSolutionContainerSystem _solution = default!; [Dependency] private readonly SharedUserInterfaceSystem _ui = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + + private EntityQuery _refillableQuery; + private EntityQuery _drainableQuery; /// /// Default transfer amounts for the set-transfer verb. @@ -32,28 +36,18 @@ public sealed class SolutionTransferSystem : EntitySystem base.Initialize(); SubscribeLocalEvent>(AddSetTransferVerbs); - SubscribeLocalEvent(OnAfterInteract); SubscribeLocalEvent(OnTransferAmountSetValueMessage); - } + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnSolutionDrainTransferDoAfter); + SubscribeLocalEvent(OnSolutionFillTransferDoAfter); - private void OnTransferAmountSetValueMessage(Entity ent, ref TransferAmountSetValueMessage message) - { - var (uid, comp) = ent; - - var newTransferAmount = FixedPoint2.Clamp(message.Value, comp.MinimumTransferAmount, comp.MaximumTransferAmount); - comp.TransferAmount = newTransferAmount; - - if (message.Actor is { Valid: true } user) - _popup.PopupEntity(Loc.GetString("comp-solution-transfer-set-amount", ("amount", newTransferAmount)), uid, user); - - Dirty(uid, comp); + _refillableQuery = GetEntityQuery(); + _drainableQuery = GetEntityQuery(); } private void AddSetTransferVerbs(Entity ent, ref GetVerbsEvent args) { - var (uid, comp) = ent; - - if (!args.CanAccess || !args.CanInteract || !comp.CanChangeTransferAmount || args.Hands == null) + if (!args.CanAccess || !args.CanInteract || !ent.Comp.CanChangeTransferAmount || args.Hands == null) return; // Custom transfer verb @@ -66,7 +60,7 @@ public sealed class SolutionTransferSystem : EntitySystem // TODO: remove server check when bui prediction is a thing Act = () => { - _ui.OpenUi(uid, TransferAmountUiKey.Key, @event.User); + _ui.OpenUi(ent.Owner, TransferAmountUiKey.Key, @event.User); }, Priority = 1 }); @@ -76,7 +70,7 @@ public sealed class SolutionTransferSystem : EntitySystem var user = args.User; foreach (var amount in DefaultTransferAmounts) { - if (amount < comp.MinimumTransferAmount || amount > comp.MaximumTransferAmount) + if (amount < ent.Comp.MinimumTransferAmount || amount > ent.Comp.MaximumTransferAmount) continue; AlternativeVerb verb = new(); @@ -84,11 +78,11 @@ public sealed class SolutionTransferSystem : EntitySystem verb.Category = VerbCategory.SetTransferAmount; verb.Act = () => { - comp.TransferAmount = amount; + ent.Comp.TransferAmount = amount; - _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), uid, user); + _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), ent.Owner, user); - Dirty(uid, comp); + Dirty(ent.Owner, ent.Comp); }; // we want to sort by size, not alphabetically by the verb text. @@ -99,117 +93,301 @@ public sealed class SolutionTransferSystem : EntitySystem } } + private void OnTransferAmountSetValueMessage(Entity ent, ref TransferAmountSetValueMessage message) + { + var newTransferAmount = FixedPoint2.Clamp(message.Value, ent.Comp.MinimumTransferAmount, ent.Comp.MaximumTransferAmount); + ent.Comp.TransferAmount = newTransferAmount; + + if (message.Actor is { Valid: true } user) + _popup.PopupEntity(Loc.GetString("comp-solution-transfer-set-amount", ("amount", newTransferAmount)), ent.Owner, user); + + Dirty(ent.Owner, ent.Comp); + } + private void OnAfterInteract(Entity ent, ref AfterInteractEvent args) { if (!args.CanReach || args.Target is not {} target) return; - var (uid, comp) = ent; + // We have two cases for interaction: + // Held Drainable --> Target Refillable + // Held Refillable <-- Target Drainable - //Special case for reagent tanks, because normally clicking another container will give solution, not take it. - if (comp.CanReceive - && !HasComp(target) // target must not be refillable (e.g. Reagent Tanks) - && _solution.TryGetDrainableSolution(target, out var targetSoln, out _) // target must be drainable - && TryComp(uid, out var refill) - && _solution.TryGetRefillableSolution((uid, refill, null), out var ownerSoln, out var ownerRefill)) + // In the case where the target has both Refillable and Drainable, Held --> Target takes priority. + + if (ent.Comp.CanSend + && _drainableQuery.TryComp(ent.Owner, out var heldDrainable) + && _refillableQuery.TryComp(target, out var targetRefillable) + && TryGetTransferrableSolutions((ent.Owner, heldDrainable), + (target, targetRefillable), + out var ownerSoln, + out var targetSoln, + out _)) { - var transferAmount = comp.TransferAmount; // This is the player-configurable transfer amount of "uid," not the target reagent tank. + args.Handled = true; //If we reach this point, the interaction counts as handled. - // if the receiver has a smaller transfer limit, use that instead - if (refill?.MaxRefill is {} maxRefill) + var transferAmount = ent.Comp.TransferAmount; + if (targetRefillable.MaxRefill is {} maxRefill) transferAmount = FixedPoint2.Min(transferAmount, maxRefill); - var transferred = Transfer(args.User, target, targetSoln.Value, uid, ownerSoln.Value, transferAmount); - args.Handled = true; - if (transferred > 0) - { - var toTheBrim = ownerRefill.AvailableVolume == 0; - var msg = toTheBrim - ? "comp-solution-transfer-fill-fully" - : "comp-solution-transfer-fill-normal"; + var transferData = new SolutionTransferData(args.User, ent.Owner, ownerSoln.Value, target, targetSoln.Value, transferAmount); + var transferTime = targetRefillable.RefillTime + heldDrainable.DrainTime; - _popup.PopupClient(Loc.GetString(msg, ("owner", args.Target), ("amount", transferred), ("target", uid)), uid, args.User); - return; + if (transferTime > TimeSpan.Zero) + { + if (!CanTransfer(transferData)) + return; + + var doAfterArgs = new DoAfterArgs(EntityManager, args.User, transferTime, new SolutionDrainTransferDoAfterEvent(transferAmount), ent.Owner, target) + { + BreakOnDamage = true, + BreakOnMove = true, + NeedHand = true, + Hidden = true, + }; + _doAfter.TryStartDoAfter(doAfterArgs); } + else + { + DrainTransfer(transferData); + } + + return; } - // if target is refillable, and owner is drainable - if (comp.CanSend - && TryComp(target, out var targetRefill) - && _solution.TryGetRefillableSolution((target, targetRefill, null), out targetSoln, out _) - && _solution.TryGetDrainableSolution(uid, out ownerSoln, out _)) + if (ent.Comp.CanReceive + && _refillableQuery.TryComp(ent.Owner, out var heldRefillable) + && _drainableQuery.TryComp(target, out var targetDrainable) + && TryGetTransferrableSolutions((target, targetDrainable), + (ent.Owner, heldRefillable), + out targetSoln, + out ownerSoln, + out var solution)) { - var transferAmount = comp.TransferAmount; + args.Handled = true; //If we reach this point, the interaction counts as handled. - if (targetRefill?.MaxRefill is {} maxRefill) + var transferAmount = ent.Comp.TransferAmount; // This is the player-configurable transfer amount of "uid," not the target drainable. + if (heldRefillable.MaxRefill is {} maxRefill) // if the receiver has a smaller transfer limit, use that instead transferAmount = FixedPoint2.Min(transferAmount, maxRefill); - var transferred = Transfer(args.User, uid, ownerSoln.Value, target, targetSoln.Value, transferAmount); - args.Handled = true; - if (transferred > 0) + var transferData = new SolutionTransferData(args.User, target, targetSoln.Value, ent.Owner, ownerSoln.Value, transferAmount); + var transferTime = heldRefillable.RefillTime + targetDrainable.DrainTime; + + if (transferTime > TimeSpan.Zero) { - var message = Loc.GetString("comp-solution-transfer-transfer-solution", ("amount", transferred), ("target", target)); - _popup.PopupClient(message, uid, args.User); + if (!CanTransfer(transferData)) + return; + + var doAfterArgs = new DoAfterArgs(EntityManager, args.User, transferTime, new SolutionRefillTransferDoAfterEvent(transferAmount), ent.Owner, target) + { + BreakOnDamage = true, + BreakOnMove = true, + NeedHand = true, + Hidden = true, + }; + _doAfter.TryStartDoAfter(doAfterArgs); + } + else + { + RefillTransfer(transferData, solution); } } } + private void OnSolutionDrainTransferDoAfter(Entity ent, ref SolutionDrainTransferDoAfterEvent args) + { + if (args.Cancelled || args.Target is not { } target) + return; + + // Have to check again, in case something has changed. + if (CanSend(ent, target, out var ownerSoln, out var targetSoln)) + { + DrainTransfer(new SolutionTransferData(args.User, ent.Owner, ownerSoln.Value, args.Target.Value, targetSoln.Value, args.Amount)); + } + } + + private void OnSolutionFillTransferDoAfter(Entity ent, ref SolutionRefillTransferDoAfterEvent args) + { + if (args.Cancelled || args.Target is not { } target) + return; + + // Have to check again, in case something has changed. + if (!CanRecieve(ent, target, out var ownerSoln, out var targetSoln, out var solution)) + return; + + RefillTransfer(new SolutionTransferData(args.User, target, targetSoln.Value, ent.Owner, ownerSoln.Value, args.Amount), solution); + } + + private bool CanSend(Entity ent, + Entity target, + [NotNullWhen(true)] out Entity? drainable, + [NotNullWhen(true)] out Entity? refillable) + { + drainable = null; + refillable = null; + + return ent.Comp1.CanReceive && TryGetTransferrableSolutions(ent.Owner, target, out drainable, out refillable, out _); + } + + private bool CanRecieve(Entity ent, + EntityUid source, + [NotNullWhen(true)] out Entity? drainable, + [NotNullWhen(true)] out Entity? refillable, + [NotNullWhen(true)] out Solution? solution) + { + drainable = null; + refillable = null; + solution = null; + + return ent.Comp.CanReceive && TryGetTransferrableSolutions(source, ent.Owner, out drainable, out refillable, out solution); + } + + private bool TryGetTransferrableSolutions(Entity source, + Entity target, + [NotNullWhen(true)] out Entity? drainable, + [NotNullWhen(true)] out Entity? refillable, + [NotNullWhen(true)] out Solution? solution) + { + drainable = null; + refillable = null; + solution = null; + + if (!_drainableQuery.Resolve(source, ref source.Comp) || !_refillableQuery.Resolve(target, ref target.Comp)) + return false; + + if (!_solution.TryGetDrainableSolution(source, out drainable, out _)) + return false; + + if (!_solution.TryGetRefillableSolution(target, out refillable, out solution)) + return false; + + return true; + } + /// - /// Transfer from a solution to another, allowing either entity to cancel it and show a popup. + /// Attempt to drain a solution into another, such as pouring a bottle into a glass. + /// Includes a pop-up if the transfer failed or succeeded + /// + /// The transfer data making up the transfer. + /// The actual amount transferred. + private void DrainTransfer(SolutionTransferData data) + { + var transferred = Transfer(data); + if (transferred <= 0) + return; + + var message = Loc.GetString("comp-solution-transfer-transfer-solution", ("amount", transferred), ("target", data.TargetEntity)); + _popup.PopupClient(message, data.SourceEntity, data.User); + } + + /// + /// Attempt to fill a solution from another container, such as tapping from a water tank. + /// Includes a pop-up if the transfer failed or succeeded. + /// + /// The transfer data making up the transfer. + /// The target solution,included for LoC pop-up purposes. + /// The actual amount transferred. + private void RefillTransfer(SolutionTransferData data, Solution targetSolution) + { + var transferred = Transfer(data); + if (transferred <= 0) + return; + + var toTheBrim = targetSolution.AvailableVolume == 0; + var msg = toTheBrim + ? "comp-solution-transfer-fill-fully" + : "comp-solution-transfer-fill-normal"; + + _popup.PopupClient(Loc.GetString(msg, ("owner", data.SourceEntity), ("amount", transferred), ("target", data.TargetEntity)), data.TargetEntity, data.User); + } + + /// + /// Transfer from a solution to another, allowing either entity to cancel. + /// Includes a pop-up if the transfer failed. /// /// The actual amount transferred. - public FixedPoint2 Transfer(EntityUid user, - EntityUid sourceEntity, - Entity source, - EntityUid targetEntity, - Entity target, - FixedPoint2 amount) + public FixedPoint2 Transfer(SolutionTransferData data) { - var transferAttempt = new SolutionTransferAttemptEvent(sourceEntity, targetEntity); + var sourceSolution = data.Source.Comp.Solution; + var targetSolution = data.Target.Comp.Solution; - // Check if the source is cancelling the transfer - RaiseLocalEvent(sourceEntity, ref transferAttempt); - if (transferAttempt.CancelReason is {} reason) - { - _popup.PopupClient(reason, sourceEntity, user); + if (!CanTransfer(data)) return FixedPoint2.Zero; - } - var sourceSolution = source.Comp.Solution; - if (sourceSolution.Volume == 0) - { - _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-empty", ("target", sourceEntity)), sourceEntity, user); - return FixedPoint2.Zero; - } + var actualAmount = FixedPoint2.Min(data.Amount, FixedPoint2.Min(sourceSolution.Volume, targetSolution.AvailableVolume)); - // Check if the target is cancelling the transfer - RaiseLocalEvent(targetEntity, ref transferAttempt); - if (transferAttempt.CancelReason is {} targetReason) - { - _popup.PopupClient(targetReason, targetEntity, user); - return FixedPoint2.Zero; - } + var solution = _solution.SplitSolution(data.Source, actualAmount); + _solution.AddSolution(data.Target, solution); - var targetSolution = target.Comp.Solution; - if (targetSolution.AvailableVolume == 0) - { - _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-full", ("target", targetEntity)), targetEntity, user); - return FixedPoint2.Zero; - } + var ev = new SolutionTransferredEvent(data.SourceEntity, data.TargetEntity, data.User, actualAmount); + RaiseLocalEvent(data.TargetEntity, ref ev); - var actualAmount = FixedPoint2.Min(amount, FixedPoint2.Min(sourceSolution.Volume, targetSolution.AvailableVolume)); - - var solution = _solution.SplitSolution(source, actualAmount); - _solution.AddSolution(target, solution); - - var ev = new SolutionTransferredEvent(sourceEntity, targetEntity, user, actualAmount); - RaiseLocalEvent(targetEntity, ref ev); - - _adminLogger.Add(LogType.Action, LogImpact.Medium, - $"{ToPrettyString(user):player} transferred {SharedSolutionContainerSystem.ToPrettyString(solution)} to {ToPrettyString(targetEntity):target}, which now contains {SharedSolutionContainerSystem.ToPrettyString(targetSolution)}"); + _adminLogger.Add(LogType.Action, + LogImpact.Medium, + $"{ToPrettyString(data.User):player} transferred {SharedSolutionContainerSystem.ToPrettyString(solution)} to {ToPrettyString(data.TargetEntity):target}, which now contains {SharedSolutionContainerSystem.ToPrettyString(targetSolution)}"); return actualAmount; } + + /// + /// Check if the source solution can transfer the amount to the target solution, and display a pop-up if it fails. + /// + private bool CanTransfer(SolutionTransferData data) + { + var transferAttempt = new SolutionTransferAttemptEvent(data.SourceEntity, data.TargetEntity); + + // Check if the source is cancelling the transfer + RaiseLocalEvent(data.SourceEntity, ref transferAttempt); + if (transferAttempt.CancelReason is {} reason) + { + _popup.PopupClient(reason, data.SourceEntity, data.User); + return false; + } + + var sourceSolution = data.Source.Comp.Solution; + if (sourceSolution.Volume == 0) + { + _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-empty", ("target", data.SourceEntity)), data.SourceEntity, data.User); + return false; + } + + // Check if the target is cancelling the transfer + RaiseLocalEvent(data.TargetEntity, ref transferAttempt); + if (transferAttempt.CancelReason is {} targetReason) + { + _popup.PopupClient(targetReason, data.TargetEntity, data.User); + return false; + } + + var targetSolution = data.Target.Comp.Solution; + if (targetSolution.AvailableVolume == 0) + { + _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-full", ("target", data.TargetEntity)), data.TargetEntity, data.User); + return false; + } + + return true; + } +} + + +/// +/// A collection of data containing relevant entities and values for transferring reagents. +/// +/// The user performing the transfer. +/// The entity holding the solution container which reagents are being moved from. +/// The entity holding the solution from which reagents are being moved away from. +/// The entity holding the solution container which reagents are being moved to. +/// The entity holding the solution which reagents are being moved to +/// The amount being moved. +public struct SolutionTransferData(EntityUid user, EntityUid sourceEntity, Entity source, EntityUid targetEntity, Entity target, FixedPoint2 amount) +{ + public EntityUid User = user; + public EntityUid SourceEntity = sourceEntity; + public Entity Source = source; + public EntityUid TargetEntity = targetEntity; + public Entity Target = target; + public FixedPoint2 Amount = amount; } /// @@ -234,3 +412,35 @@ public record struct SolutionTransferAttemptEvent(EntityUid From, EntityUid To, /// [ByRefEvent] public record struct SolutionTransferredEvent(EntityUid From, EntityUid To, EntityUid User, FixedPoint2 Amount); + +/// +/// Doafter event for solution transfers where the held item is drained into the target. Checks for validity both when initiating and when finishing the event. +/// +[Serializable, NetSerializable] +public sealed partial class SolutionDrainTransferDoAfterEvent : DoAfterEvent +{ + public FixedPoint2 Amount; + + public SolutionDrainTransferDoAfterEvent(FixedPoint2 amount) + { + Amount = amount; + } + + public override DoAfterEvent Clone() => this; +} + +/// +/// Doafter event for solution transfers where the held item is filled from the target. Checks for validity both when initiating and when finishing the event. +/// +[Serializable, NetSerializable] +public sealed partial class SolutionRefillTransferDoAfterEvent : DoAfterEvent +{ + public FixedPoint2 Amount; + + public SolutionRefillTransferDoAfterEvent(FixedPoint2 amount) + { + Amount = amount; + } + + public override DoAfterEvent Clone() => this; +} diff --git a/Content.Shared/Chemistry/Reaction/ChemicalReactionSystem.cs b/Content.Shared/Chemistry/Reaction/ChemicalReactionSystem.cs index 351a51ecc1..a995ef90f4 100644 --- a/Content.Shared/Chemistry/Reaction/ChemicalReactionSystem.cs +++ b/Content.Shared/Chemistry/Reaction/ChemicalReactionSystem.cs @@ -31,6 +31,7 @@ namespace Content.Shared.Chemistry.Reaction [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!; + [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!; /// /// A cache of all reactions indexed by at most ONE of their required reactants. @@ -205,27 +206,12 @@ namespace Content.Shared.Chemistry.Reaction private void OnReaction(Entity soln, ReactionPrototype reaction, ReagentPrototype? reagent, FixedPoint2 unitReactions) { - var args = new EntityEffectReagentArgs(soln, EntityManager, null, soln.Comp.Solution, unitReactions, reagent, null, 1f); - var posFound = _transformSystem.TryGetMapOrGridCoordinates(soln, out var gridPos); _adminLogger.Add(LogType.ChemicalReaction, reaction.Impact, $"Chemical reaction {reaction.ID:reaction} occurred with strength {unitReactions:strength} on entity {ToPrettyString(soln):metabolizer} at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not Found]")}"); - foreach (var effect in reaction.Effects) - { - if (!effect.ShouldApply(args)) - continue; - - if (effect.ShouldLog) - { - var entity = args.TargetEntity; - _adminLogger.Add(LogType.ReagentEffect, effect.LogImpact, - $"Reaction effect {effect.GetType().Name:effect} of reaction {reaction.ID:reaction} applied on entity {ToPrettyString(entity):entity} at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not Found")}"); - } - - effect.Effect(args); - } + _entityEffects.ApplyEffects(soln, reaction.Effects, unitReactions.Float()); // Someday, some brave soul will thread through an optional actor // argument in from every call of OnReaction up, all just to pass diff --git a/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs b/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs index 4bbb972572..c9a24ec550 100644 --- a/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs +++ b/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs @@ -60,7 +60,7 @@ namespace Content.Shared.Chemistry.Reaction /// /// Effects to be triggered when the reaction occurs. /// - [DataField("effects")] public List Effects = new(); + [DataField("effects")] public EntityEffect[] Effects = []; /// /// How dangerous is this effect? Stuff like bicaridine should be low, while things like methamphetamine diff --git a/Content.Shared/Chemistry/Reaction/ReactiveComponent.cs b/Content.Shared/Chemistry/Reaction/ReactiveComponent.cs index cabdee93c1..89fcca900e 100644 --- a/Content.Shared/Chemistry/Reaction/ReactiveComponent.cs +++ b/Content.Shared/Chemistry/Reaction/ReactiveComponent.cs @@ -34,7 +34,7 @@ public sealed partial class ReactiveReagentEffectEntry public HashSet? Reagents = null; [DataField("effects", required: true)] - public List Effects = default!; + public EntityEffect[] Effects = default!; [DataField("groups", readOnly: true, serverOnly: true, customTypeSerializer:typeof(PrototypeIdDictionarySerializer, ReactiveGroupPrototype>))] diff --git a/Content.Shared/Chemistry/ReactiveSystem.cs b/Content.Shared/Chemistry/ReactiveSystem.cs index 6306537324..2ffb848f8a 100644 --- a/Content.Shared/Chemistry/ReactiveSystem.cs +++ b/Content.Shared/Chemistry/ReactiveSystem.cs @@ -1,108 +1,35 @@ -using Content.Shared.Administration.Logs; using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reagent; -using Content.Shared.Database; -using Content.Shared.EntityEffects; +using Content.Shared.FixedPoint; using JetBrains.Annotations; using Robust.Shared.Prototypes; -using Robust.Shared.Random; namespace Content.Shared.Chemistry; [UsedImplicitly] public sealed class ReactiveSystem : EntitySystem { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IRobustRandom _robustRandom = default!; - [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; public void DoEntityReaction(EntityUid uid, Solution solution, ReactionMethod method) { foreach (var reagent in solution.Contents.ToArray()) { - ReactionEntity(uid, method, reagent, solution); + ReactionEntity(uid, method, reagent); } } - public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentQuantity reagentQuantity, Solution? source) + public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentQuantity reagentQuantity) { - // We throw if the reagent specified doesn't exist. - var proto = _prototypeManager.Index(reagentQuantity.Reagent.Prototype); - ReactionEntity(uid, method, proto, reagentQuantity, source); - } - - public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentPrototype proto, - ReagentQuantity reagentQuantity, Solution? source) - { - if (!TryComp(uid, out ReactiveComponent? reactive)) + if (reagentQuantity.Quantity == FixedPoint2.Zero) return; - // custom event for bypassing reactivecomponent stuff - var ev = new ReactionEntityEvent(method, proto, reagentQuantity, source); + // We throw if the reagent specified doesn't exist. + if (!_proto.Resolve(reagentQuantity.Reagent.Prototype, out var proto)) + return; + + var ev = new ReactionEntityEvent(method, reagentQuantity, proto); RaiseLocalEvent(uid, ref ev); - - // If we have a source solution, use the reagent quantity we have left. Otherwise, use the reaction volume specified. - var args = new EntityEffectReagentArgs(uid, EntityManager, null, source, source?.GetReagentQuantity(reagentQuantity.Reagent) ?? reagentQuantity.Quantity, proto, method, 1f); - - // First, check if the reagent wants to apply any effects. - if (proto.ReactiveEffects != null && reactive.ReactiveGroups != null) - { - foreach (var (key, val) in proto.ReactiveEffects) - { - if (!val.Methods.Contains(method)) - continue; - - if (!reactive.ReactiveGroups.ContainsKey(key)) - continue; - - if (!reactive.ReactiveGroups[key].Contains(method)) - continue; - - foreach (var effect in val.Effects) - { - if (!effect.ShouldApply(args, _robustRandom)) - continue; - - if (effect.ShouldLog) - { - var entity = args.TargetEntity; - _adminLogger.Add(LogType.ReagentEffect, effect.LogImpact, - $"Reactive effect {effect.GetType().Name:effect} of reagent {proto.ID:reagent} with method {method} applied on entity {ToPrettyString(entity):entity} at {Transform(entity).Coordinates:coordinates}"); - } - - effect.Effect(args); - } - } - } - - // Then, check if the prototype has any effects it can apply as well. - if (reactive.Reactions != null) - { - foreach (var entry in reactive.Reactions) - { - if (!entry.Methods.Contains(method)) - continue; - - if (entry.Reagents != null && !entry.Reagents.Contains(proto.ID)) - continue; - - foreach (var effect in entry.Effects) - { - if (!effect.ShouldApply(args, _robustRandom)) - continue; - - if (effect.ShouldLog) - { - var entity = args.TargetEntity; - _adminLogger.Add(LogType.ReagentEffect, effect.LogImpact, - $"Reactive effect {effect.GetType().Name:effect} of {ToPrettyString(entity):entity} using reagent {proto.ID:reagent} with method {method} at {Transform(entity).Coordinates:coordinates}"); - } - - effect.Effect(args); - } - } - } } } public enum ReactionMethod @@ -113,9 +40,4 @@ Ingestion, } [ByRefEvent] -public readonly record struct ReactionEntityEvent( - ReactionMethod Method, - ReagentPrototype Reagent, - ReagentQuantity ReagentQuantity, - Solution? Source -); +public readonly record struct ReactionEntityEvent(ReactionMethod Method, ReagentQuantity ReagentQuantity, ReagentPrototype Reagent); diff --git a/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs b/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs index e108535fda..a586569a99 100644 --- a/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs +++ b/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs @@ -2,21 +2,17 @@ using System.Collections.Frozen; using System.Linq; using Content.Shared.FixedPoint; using System.Text.Json.Serialization; -using Content.Shared.Administration.Logs; using Content.Shared.Body.Prototypes; -using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reaction; using Content.Shared.Contraband; using Content.Shared.EntityEffects; -using Content.Shared.Database; +using Content.Shared.Localizations; using Content.Shared.Nutrition; -using Content.Shared.Prototypes; using Content.Shared.Roles; using Content.Shared.Slippery; using Robust.Shared.Audio; using Robust.Shared.Map; using Robust.Shared.Prototypes; -using Robust.Shared.Random; using Robust.Shared.Serialization; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; using Robust.Shared.Utility; @@ -190,6 +186,7 @@ namespace Content.Shared.Chemistry.Reagent [DataField] public SoundSpecifier FootstepSound = new SoundCollectionSpecifier("FootstepPuddle"); + // TODO: Reaction tile doesn't work properly and destroys reagents way too quickly public FixedPoint2 ReactionTile(TileRef tile, FixedPoint2 reactVolume, IEntityManager entityManager, List? data) { var removed = FixedPoint2.Zero; @@ -211,33 +208,32 @@ namespace Content.Shared.Chemistry.Reagent return removed; } - public void ReactionPlant(EntityUid? plantHolder, - ReagentQuantity amount, - Solution solution, - EntityManager entityManager, - IRobustRandom random, - ISharedAdminLogManager logger) + public IEnumerable GuidebookReagentEffectsDescription(IPrototypeManager prototype, IEntitySystemManager entSys, IEnumerable effects, FixedPoint2? metabolism = null) { - if (plantHolder == null) - return; + return effects.Select(x => GuidebookReagentEffectDescription(prototype, entSys, x, metabolism)) + .Where(x => x is not null) + .Select(x => x!) + .ToArray(); + } - var args = new EntityEffectReagentArgs(plantHolder.Value, entityManager, null, solution, amount.Quantity, this, null, 1f); - foreach (var plantMetabolizable in PlantMetabolisms) - { - if (!plantMetabolizable.ShouldApply(args, random)) - continue; + public string? GuidebookReagentEffectDescription(IPrototypeManager prototype, IEntitySystemManager entSys, EntityEffect effect, FixedPoint2? metabolism) + { + if (effect.EntityEffectGuidebookText(prototype, entSys) is not { } description) + return null; - if (plantMetabolizable.ShouldLog) - { - var entity = args.TargetEntity; - logger.Add( - LogType.ReagentEffect, - plantMetabolizable.LogImpact, - $"Plant metabolism effect {plantMetabolizable.GetType().Name:effect} of reagent {ID} applied on entity {entity}"); - } + var quantity = metabolism == null ? 0f : (double) (effect.MinScale * metabolism); - plantMetabolizable.Effect(args); - } + return Loc.GetString( + "guidebook-reagent-effect-description", + ("reagent", LocalizedName), + ("quantity", quantity), + ("effect", description), + ("chance", effect.Probability), + ("conditionCount", effect.Conditions?.Length ?? 0), + ("conditions", + ContentLocalizationManager.FormatList( + effect.Conditions?.Select(x => x.EntityConditionGuidebookText(prototype)).ToList() ?? new List() + ))); } } @@ -246,6 +242,7 @@ namespace Content.Shared.Chemistry.Reagent { public string ReagentPrototype; + // TODO: Kill Metabolism groups! public Dictionary, ReagentEffectsGuideEntry>? GuideEntries; public List? PlantMetabolisms = null; @@ -254,15 +251,12 @@ namespace Content.Shared.Chemistry.Reagent { ReagentPrototype = proto.ID; GuideEntries = proto.Metabolisms? - .Select(x => (x.Key, x.Value.MakeGuideEntry(prototype, entSys))) + .Select(x => (x.Key, x.Value.MakeGuideEntry(prototype, entSys, proto))) .ToDictionary(x => x.Key, x => x.Item2); if (proto.PlantMetabolisms.Count > 0) { - PlantMetabolisms = new List(proto.PlantMetabolisms - .Select(x => x.GuidebookEffectDescription(prototype, entSys)) - .Where(x => x is not null) - .Select(x => x!) - .ToArray()); + PlantMetabolisms = + new List(proto.GuidebookReagentEffectsDescription(prototype, entSys, proto.PlantMetabolisms)); } } } @@ -291,15 +285,11 @@ namespace Content.Shared.Chemistry.Reagent [DataField("effects", required: true)] public EntityEffect[] Effects = default!; - public ReagentEffectsGuideEntry MakeGuideEntry(IPrototypeManager prototype, IEntitySystemManager entSys) + public string EntityEffectFormat => "guidebook-reagent-effect-description"; + + public ReagentEffectsGuideEntry MakeGuideEntry(IPrototypeManager prototype, IEntitySystemManager entSys, ReagentPrototype proto) { - return new ReagentEffectsGuideEntry(MetabolismRate, - Effects - .Select(x => x.GuidebookEffectDescription(prototype, entSys)) // hate. - .Concat(StatusEffects.Select(x => x.Describe(prototype, entSys))) // Offbrand - .Where(x => x is not null) - .Select(x => x!) - .ToArray()); + return new ReagentEffectsGuideEntry(MetabolismRate, proto.GuidebookReagentEffectsDescription(prototype, entSys, Effects, MetabolismRate).ToArray()); } } 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/EntitySystems/MaskSystem.cs b/Content.Shared/Clothing/EntitySystems/MaskSystem.cs index 30e00faf0a..0c53349934 100644 --- a/Content.Shared/Clothing/EntitySystems/MaskSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/MaskSystem.cs @@ -62,27 +62,7 @@ public sealed class MaskSystem : EntitySystem private void OnGotUnequipped(EntityUid uid, MaskComponent mask, GotUnequippedEvent args) { - if (!mask.IsToggled || !mask.IsToggleable) - return; - - mask.IsToggled = false; - ToggleMaskComponents(uid, mask, args.Equipee, mask.EquippedPrefix, true); - } - - /// - /// Called after setting IsToggled, raises events and dirties. - /// - private void ToggleMaskComponents(EntityUid uid, MaskComponent mask, EntityUid wearer, string? equippedPrefix = null, bool isEquip = false) - { - Dirty(uid, mask); - if (mask.ToggleActionEntity is { } action) - _actionSystem.SetToggled(action, mask.IsToggled); - - var maskEv = new ItemMaskToggledEvent((uid, mask), wearer); - RaiseLocalEvent(uid, ref maskEv); - - var wearerEv = new WearerMaskToggledEvent((uid, mask)); - RaiseLocalEvent(wearer, ref wearerEv); + SetToggled(uid, false); } private void OnFolded(Entity ent, ref FoldedEvent args) 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 2506359c25..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; @@ -49,7 +49,7 @@ public sealed class ClumsySystem : EntitySystem return; // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(ent).Id }); + var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(ent).Id); var rand = new System.Random(seed); if (!rand.Prob(ent.Comp.ClumsyDefaultCheck)) return; @@ -68,7 +68,7 @@ public sealed class ClumsySystem : EntitySystem return; // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(ent).Id }); + var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(ent).Id); var rand = new System.Random(seed); if (!rand.Prob(ent.Comp.ClumsyDefaultCheck)) return; @@ -87,7 +87,7 @@ public sealed class ClumsySystem : EntitySystem return; // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(args.Item).Id }); + var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(args.Item).Id); var rand = new System.Random(seed); if (!rand.Prob(ent.Comp.ClumsyDefaultCheck)) return; @@ -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. @@ -121,13 +121,13 @@ public sealed class ClumsySystem : EntitySystem return; // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(args.Gun).Id }); + var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(args.Gun).Id); var rand = new System.Random(seed); if (!rand.Prob(ent.Comp.ClumsyDefaultCheck)) 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); @@ -146,7 +146,7 @@ public sealed class ClumsySystem : EntitySystem return; // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(ent).Id }); + var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(ent).Id); var rand = new System.Random(seed); if (!_cfg.GetCVar(CCVars.GameTableBonk) && !rand.Prob(ent.Comp.ClumsyDefaultCheck)) return; @@ -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/Containers/ItemSlot/ItemSlotsSystem.cs b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs index 88e6ca44dc..718d2a0524 100644 --- a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs +++ b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs @@ -573,7 +573,7 @@ namespace Content.Shared.Containers.ItemSlots item = slot.Item; // This handles user logic - if (user != null && item != null && !_actionBlockerSystem.CanPickup(user.Value, item.Value)) + if (user != null && item != null && !_actionBlockerSystem.CanPickup(user.Value, item.Value, showPopup: true)) return false; Eject(uid, slot, item!.Value, user, excludeUserAudio); diff --git a/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsSystem.cs b/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsSystem.cs index d665d32f1e..3e2e103501 100644 --- a/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsSystem.cs +++ b/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsSystem.cs @@ -45,6 +45,8 @@ public abstract class SharedCriminalRecordsSystem : EntitySystem SecurityStatus.Detained => "SecurityIconIncarcerated", SecurityStatus.Discharged => "SecurityIconDischarged", SecurityStatus.Suspected => "SecurityIconSuspected", + SecurityStatus.Hostile => "SecurityIconHostile", + SecurityStatus.Eliminated => "SecurityIconEliminated", _ => record.StatusIcon }; 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 8fe0b17c57..58fe00a447 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 ent, ref ComponentHandleState args) + { + if (args.Current is not DamageableComponentState state) + return; + + ent.Comp.DamageContainerID = state.DamageContainerId; + ent.Comp.DamageModifierSetId = state.ModifierSetId; + ent.Comp.HealthBarThreshold = state.HealthBarThreshold; + + // Has the damage actually changed? + DamageSpecifier newDamage = new() { DamageDict = new Dictionary(state.DamageDict) }; + var delta = newDamage - ent.Comp.Damage; + delta.TrimZeros(); + + if (delta.Empty) + return; + + ent.Comp.Damage = newDamage; + + OnEntityDamageChanged(ent, delta); + } +} + +/// +/// Raised before damage is done, so stuff can cancel it if necessary. +/// +[ByRefEvent] +public record struct BeforeDamageChangedEvent(DamageSpecifier Damage, EntityUid? Origin = null, bool Cancelled = false); + +/// +/// Raised on an entity when damage is about to be dealt, +/// in case anything else needs to modify it other than the base +/// damageable component. +/// +/// For example, armor. +/// +public sealed class DamageModifyEvent(DamageSpecifier damage, EntityUid? origin = null) + : EntityEventArgs, IInventoryRelayEvent +{ + // Whenever locational damage is a thing, this should just check only that bit of armour. + public SlotFlags TargetSlots => ~SlotFlags.POCKET; + + public readonly DamageSpecifier OriginalDamage = damage; + public DamageSpecifier Damage = damage; +} + +public sealed class DamageChangedEvent : EntityEventArgs +{ + /// + /// This is the component whose damage was changed. + /// + /// + /// Given that nearly every component that cares about a change in the damage, needs to know the + /// current damage values, directly passing this information prevents a lot of duplicate + /// Owner.TryGetComponent() calls. + /// + public readonly DamageableComponent Damageable; + + /// + /// The amount by which the damage has changed. If the damage was set directly to some number, this will be + /// null. + /// + public readonly DamageSpecifier? DamageDelta; + + /// + /// Was any of the damage change dealing damage, or was it all healing? + /// + public readonly bool DamageIncreased; + + /// + /// Does this event interrupt DoAfters? + /// Note: As provided in the constructor, this *does not* account for DamageIncreased. + /// As written into the event, this *does* account for DamageIncreased. + /// + public readonly bool InterruptsDoAfters; + + /// + /// Contains the entity which caused the change in damage, if any was responsible. + /// + public readonly EntityUid? Origin; + + public DamageChangedEvent( + DamageableComponent damageable, + DamageSpecifier? damageDelta, + bool interruptsDoAfters, + EntityUid? origin + ) + { + Damageable = damageable; + DamageDelta = damageDelta; + Origin = origin; + + if (DamageDelta is null) + return; + + foreach (var damageChange in DamageDelta.DamageDict.Values) + { + if (damageChange <= 0) + continue; + + DamageIncreased = true; + + break; + } + + InterruptsDoAfters = interruptsDoAfters && DamageIncreased; + } +} diff --git a/Content.Shared/Damage/Systems/DamageableSystem.cs b/Content.Shared/Damage/Systems/DamageableSystem.cs index ed495c749a..2b4c5ad360 100644 --- a/Content.Shared/Damage/Systems/DamageableSystem.cs +++ b/Content.Shared/Damage/Systems/DamageableSystem.cs @@ -1,484 +1,97 @@ using System.Linq; -using Content.Shared.CCVar; using Content.Shared.Chemistry; -using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Components; using Content.Shared.Explosion.EntitySystems; -using Content.Shared.FixedPoint; -using Content.Shared.Inventory; -using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; -using Content.Shared.Radiation.Events; -using Content.Shared.Rejuvenate; using Robust.Shared.Configuration; using Robust.Shared.GameStates; using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Utility; -namespace Content.Shared.Damage +namespace Content.Shared.Damage.Systems; + +public sealed partial class DamageableSystem : EntitySystem { - public sealed class DamageableSystem : EntitySystem - { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly INetManager _netMan = default!; - [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; - [Dependency] private readonly IConfigurationManager _config = default!; - [Dependency] private readonly SharedChemistryGuideDataSystem _chemistryGuideData = default!; - [Dependency] private readonly SharedExplosionSystem _explosion = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly INetManager _netMan = default!; + [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; + [Dependency] private readonly IConfigurationManager _config = default!; + [Dependency] private readonly SharedChemistryGuideDataSystem _chemistryGuideData = default!; + [Dependency] private readonly SharedExplosionSystem _explosion = default!; - private EntityQuery _appearanceQuery; - private EntityQuery _damageableQuery; + private EntityQuery _appearanceQuery; + private EntityQuery _damageableQuery; - public float UniversalAllDamageModifier { get; private set; } = 1f; - public float UniversalAllHealModifier { get; private set; } = 1f; - public float UniversalMeleeDamageModifier { get; private set; } = 1f; - public float UniversalProjectileDamageModifier { get; private set; } = 1f; - public float UniversalHitscanDamageModifier { get; private set; } = 1f; - public float UniversalReagentDamageModifier { get; private set; } = 1f; - public float UniversalReagentHealModifier { get; private set; } = 1f; - public float UniversalExplosionDamageModifier { get; private set; } = 1f; - public float UniversalThrownDamageModifier { get; private set; } = 1f; - public float UniversalTopicalsHealModifier { get; private set; } = 1f; - public float UniversalMobDamageModifier { get; private set; } = 1f; - - public override void Initialize() - { - SubscribeLocalEvent(DamageableInit); - SubscribeLocalEvent(DamageableHandleState); - SubscribeLocalEvent(DamageableGetState); - SubscribeLocalEvent(OnIrradiated); - SubscribeLocalEvent(OnRejuvenate, after: [typeof(Content.Shared.StatusEffectNew.StatusEffectsSystem)]); // Offbrand - - _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(EntityUid uid, DamageableComponent component, ComponentInit _) - { - if (component.DamageContainerID != null && - _prototypeManager.Resolve(component.DamageContainerID, - out var damageContainerPrototype)) - { - // Initialize damage dictionary, using the types and groups from the damage - // container prototype - foreach (var type in damageContainerPrototype.SupportedTypes) - { - component.Damage.DamageDict.TryAdd(type, FixedPoint2.Zero); - } - - foreach (var groupId in damageContainerPrototype.SupportedGroups) - { - var group = _prototypeManager.Index(groupId); - foreach (var type in group.DamageTypes) - { - component.Damage.DamageDict.TryAdd(type, FixedPoint2.Zero); - } - } - } - else - { - // No DamageContainerPrototype was given. So we will allow the container to support all damage types - foreach (var type in _prototypeManager.EnumeratePrototypes()) - { - component.Damage.DamageDict.TryAdd(type.ID, FixedPoint2.Zero); - } - } - - component.Damage.GetDamagePerGroup(_prototypeManager, component.DamagePerGroup); - component.TotalDamage = component.Damage.GetTotal(); - } - - /// - /// 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(EntityUid uid, DamageableComponent damageable, DamageSpecifier damage) - { - damageable.Damage = damage; - DamageChanged(uid, damageable); - } - - /// - /// If the damage in a DamageableComponent was changed, this function should be called. - /// - /// - /// This updates cached damage information, flags the component as dirty, and raises a damage changed event. - /// The damage changed event is used by other systems, such as damage thresholds. - /// - public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSpecifier? damageDelta = null, - bool interruptsDoAfters = true, EntityUid? origin = null, bool forcedRefresh = false) // Offbrand - { - component.Damage.GetDamagePerGroup(_prototypeManager, component.DamagePerGroup); - component.TotalDamage = component.Damage.GetTotal(); - Dirty(uid, component); - - if (_appearanceQuery.TryGetComponent(uid, out var appearance) && damageDelta != null) - { - var data = new DamageVisualizerGroupData(component.DamagePerGroup.Keys.ToList()); - _appearance.SetData(uid, DamageVisualizerKeys.DamageUpdateGroups, data, appearance); - } - - // TODO DAMAGE - // byref struct event. - RaiseLocalEvent(uid, new DamageChangedEvent(component, damageDelta, interruptsDoAfters, origin, forcedRefresh)); // Offbrand - } - - /// - /// 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 . - /// - /// - /// Returns a with information about the actual damage changes. This will be - /// null if the user had no applicable components that can take damage. - /// - /// If true, this will ignore the entity's damage modifier ( and skip raising a . - /// Whether the damage should cancel any damage sensitive do-afters - /// The entity that is causing this damage - /// If true, this will skip over applying the universal damage modifiers (see ). - /// - public DamageSpecifier? TryChangeDamage( - EntityUid? uid, - DamageSpecifier damage, - bool ignoreResistances = false, - bool interruptsDoAfters = true, - DamageableComponent? damageable = null, - EntityUid? origin = null, - bool ignoreGlobalModifiers = false, - bool forceRefresh = false) // Offbrand - { - if (!uid.HasValue || !_damageableQuery.Resolve(uid.Value, ref damageable, false)) - { - // TODO BODY SYSTEM pass damage onto body system - // BOBBY WHEN? - return null; - } - - if (damage.Empty && !forceRefresh) // Offbrand - { - return damage; - } - - var before = new BeforeDamageChangedEvent(damage, origin); - RaiseLocalEvent(uid.Value, ref before); - - if (before.Cancelled) - return null; - - // Apply resistances - if (!ignoreResistances) - { - if (damageable.DamageModifierSetId != null && - _prototypeManager.Resolve(damageable.DamageModifierSetId, out var modifierSet)) - { - damage = DamageSpecifier.ApplyModifierSet(damage, modifierSet); - } - - // TODO DAMAGE - // byref struct event. - var ev = new DamageModifyEvent(damage, origin); - RaiseLocalEvent(uid.Value, ev); - damage = ev.Damage; - - if (damage.Empty) - { - return damage; - } - } - - if (!ignoreGlobalModifiers) - damage = ApplyUniversalAllModifiers(damage); - - // Begin Offbrand - var beforeCommit = new Content.Shared._Offbrand.Wounds.BeforeDamageCommitEvent(damage, forceRefresh); - RaiseLocalEvent(uid.Value, ref beforeCommit); - damage = beforeCommit.Damage; - // End Offbrand - - var delta = new DamageSpecifier(); - delta.DamageDict.EnsureCapacity(damage.DamageDict.Count); - - var dict = damageable.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; - delta.DamageDict[type] = newValue - oldValue; - } - - if (delta.DamageDict.Count > 0) - DamageChanged(uid.Value, damageable, delta, interruptsDoAfters, origin, forceRefresh); // Offbrand - - return delta; - } - - /// - /// Applies the two univeral "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 (UniversalAllDamageModifier == 1f && 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; - } - - /// - /// Sets all damage types supported by a to the specified value. - /// - /// - /// Does nothing If the given damage value is negative. - /// - public void SetAllDamage(EntityUid uid, DamageableComponent component, FixedPoint2 newValue) - { - if (newValue < 0) - { - // invalid value - return; - } - - foreach (var type in component.Damage.DamageDict.Keys) - { - component.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. - DamageChanged(uid, component, new DamageSpecifier()); - } - - public void SetDamageModifierSetId(EntityUid uid, string? damageModifierSetId, DamageableComponent? comp = null) - { - if (!_damageableQuery.Resolve(uid, ref comp)) - return; - - comp.DamageModifierSetId = damageModifierSetId; - Dirty(uid, comp); - } - - private void DamageableGetState(EntityUid uid, DamageableComponent component, ref ComponentGetState args) - { - if (_netMan.IsServer) - { - args.State = new DamageableComponentState(component.Damage.DamageDict, component.DamageContainerID, component.DamageModifierSetId, component.HealthBarThreshold); - } - else - { - // avoid mispredicting damage on newly spawned entities. - args.State = new DamageableComponentState(component.Damage.DamageDict.ShallowClone(), component.DamageContainerID, component.DamageModifierSetId, component.HealthBarThreshold); - } - } - - private void OnIrradiated(EntityUid uid, DamageableComponent component, 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 component.RadiationDamageTypeIDs) - { - damage.DamageDict.Add(typeId, damageValue); - } - - TryChangeDamage(uid, damage, interruptsDoAfters: false, origin: args.Origin); - } - - private void OnRejuvenate(EntityUid uid, DamageableComponent component, RejuvenateEvent args) - { - Log.Debug("rejuvenate damage"); - TryComp(uid, out var thresholds); - _mobThreshold.SetAllowRevives(uid, true, thresholds); // do this so that the state changes when we set the damage - SetAllDamage(uid, component, 0); - _mobThreshold.SetAllowRevives(uid, false, thresholds); - } - - private void DamageableHandleState(EntityUid uid, DamageableComponent component, ref ComponentHandleState args) - { - if (args.Current is not DamageableComponentState state) - { - return; - } - - component.DamageContainerID = state.DamageContainerId; - component.DamageModifierSetId = state.ModifierSetId; - component.HealthBarThreshold = state.HealthBarThreshold; - - // Has the damage actually changed? - DamageSpecifier newDamage = new() { DamageDict = new(state.DamageDict) }; - var delta = newDamage - component.Damage; - delta.TrimZeros(); - - if (!delta.Empty) - { - component.Damage = newDamage; - DamageChanged(uid, component, delta); - } - } - } + public float UniversalAllDamageModifier { get; private set; } = 1f; + public float UniversalAllHealModifier { get; private set; } = 1f; + public float UniversalMeleeDamageModifier { get; private set; } = 1f; + public float UniversalProjectileDamageModifier { get; private set; } = 1f; + public float UniversalHitscanDamageModifier { get; private set; } = 1f; + public float UniversalReagentDamageModifier { get; private set; } = 1f; + public float UniversalReagentHealModifier { get; private set; } = 1f; + public float UniversalExplosionDamageModifier { get; private set; } = 1f; + public float UniversalThrownDamageModifier { get; private set; } = 1f; + public float UniversalTopicalsHealModifier { get; private set; } = 1f; + public float UniversalMobDamageModifier { get; private set; } = 1f; /// - /// Raised before damage is done, so stuff can cancel it if necessary. + /// If the damage in a DamageableComponent was changed this function should be called. /// - [ByRefEvent] - public record struct BeforeDamageChangedEvent(DamageSpecifier Damage, EntityUid? Origin = null, bool Cancelled = false); - - /// - /// Raised on an entity when damage is about to be dealt, - /// in case anything else needs to modify it other than the base - /// damageable component. - /// - /// For example, armor. - /// - public sealed class DamageModifyEvent : EntityEventArgs, IInventoryRelayEvent + /// + /// This updates cached damage information, flags the component as dirty, and raises a damage changed event. + /// The damage changed event is used by other systems, such as damage thresholds. + /// + private void OnEntityDamageChanged( + Entity ent, + DamageSpecifier? damageDelta = null, + bool interruptsDoAfters = true, + EntityUid? origin = null + ) { - // Whenever locational damage is a thing, this should just check only that bit of armour. - public SlotFlags TargetSlots { get; } = ~SlotFlags.POCKET; + ent.Comp.Damage.GetDamagePerGroup(_prototypeManager, ent.Comp.DamagePerGroup); + ent.Comp.TotalDamage = ent.Comp.Damage.GetTotal(); + Dirty(ent); - public readonly DamageSpecifier OriginalDamage; - public DamageSpecifier Damage; - public EntityUid? Origin; - - public DamageModifyEvent(DamageSpecifier damage, EntityUid? origin = null) + if (damageDelta != null && _appearanceQuery.TryGetComponent(ent, out var appearance)) { - OriginalDamage = damage; - Damage = damage; - Origin = origin; + _appearance.SetData( + ent, + DamageVisualizerKeys.DamageUpdateGroups, + new DamageVisualizerGroupData(ent.Comp.DamagePerGroup.Keys.ToList()), + appearance + ); } + + // TODO DAMAGE + // byref struct event. + RaiseLocalEvent(ent, new DamageChangedEvent(ent.Comp, damageDelta, interruptsDoAfters, origin)); } - public sealed class DamageChangedEvent : EntityEventArgs + private void DamageableGetState(Entity ent, ref ComponentGetState args) { - /// - /// This is the component whose damage was changed. - /// - /// - /// Given that nearly every component that cares about a change in the damage, needs to know the - /// current damage values, directly passing this information prevents a lot of duplicate - /// Owner.TryGetComponent() calls. - /// - public readonly DamageableComponent Damageable; - - /// - /// The amount by which the damage has changed. If the damage was set directly to some number, this will be - /// null. - /// - public readonly DamageSpecifier? DamageDelta; - - /// - /// Was any of the damage change dealing damage, or was it all healing? - /// - public readonly bool DamageIncreased; - - /// - /// Does this event interrupt DoAfters? - /// Note: As provided in the constructor, this *does not* account for DamageIncreased. - /// As written into the event, this *does* account for DamageIncreased. - /// - public readonly bool InterruptsDoAfters; - - /// - /// Contains the entity which caused the change in damage, if any was responsible. - /// - public readonly EntityUid? Origin; - - // Offbrand - /// - /// If this damage changed happened as part of a forced refresh - /// - public readonly bool ForcedRefresh; - - public DamageChangedEvent(DamageableComponent damageable, DamageSpecifier? damageDelta, bool interruptsDoAfters, EntityUid? origin, bool forcedRefresh) // Offbrand + if (_netMan.IsServer) { - Damageable = damageable; - DamageDelta = damageDelta; - Origin = origin; - ForcedRefresh = forcedRefresh; // Offbrand + args.State = new DamageableComponentState( + ent.Comp.Damage.DamageDict, + ent.Comp.DamageContainerID, + ent.Comp.DamageModifierSetId, + ent.Comp.HealthBarThreshold + ); + // TODO BODY SYSTEM pass damage onto body system + // BOBBY WHEN? 😭 + // BOBBY SOON 🫡 - if (DamageDelta == null) - return; - - foreach (var damageChange in DamageDelta.DamageDict.Values) - { - if (damageChange > 0) - { - DamageIncreased = true; - break; - } - } - InterruptsDoAfters = interruptsDoAfters && DamageIncreased; + return; } + + // avoid mispredicting damage on newly spawned entities. + args.State = new DamageableComponentState( + ent.Comp.Damage.DamageDict.ShallowClone(), + ent.Comp.DamageContainerID, + ent.Comp.DamageModifierSetId, + ent.Comp.HealthBarThreshold + ); } } diff --git a/Content.Shared/Damage/Systems/PassiveDamageSystem.cs b/Content.Shared/Damage/Systems/PassiveDamageSystem.cs index e750863e24..fddf7dc975 100644 --- a/Content.Shared/Damage/Systems/PassiveDamageSystem.cs +++ b/Content.Shared/Damage/Systems/PassiveDamageSystem.cs @@ -1,10 +1,8 @@ using Content.Shared.Damage.Components; -using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Components; -using Content.Shared.FixedPoint; using Robust.Shared.Timing; -namespace Content.Shared.Damage; +namespace Content.Shared.Damage.Systems; public sealed class PassiveDamageSystem : EntitySystem { @@ -47,7 +45,7 @@ public sealed class PassiveDamageSystem : EntitySystem foreach (var allowedState in comp.AllowedStates) { if(allowedState == mobState.CurrentState) - _damageable.TryChangeDamage(uid, comp.Damage, true, false, damage); + _damageable.ChangeDamage((uid, damage), comp.Damage, true, false); } } } diff --git a/Content.Shared/Damage/Systems/RequireProjectileTargetSystem.cs b/Content.Shared/Damage/Systems/RequireProjectileTargetSystem.cs index 66b1de65e8..63b690a67c 100644 --- a/Content.Shared/Damage/Systems/RequireProjectileTargetSystem.cs +++ b/Content.Shared/Damage/Systems/RequireProjectileTargetSystem.cs @@ -1,10 +1,11 @@ +using Content.Shared.Damage.Components; using Content.Shared.Projectiles; -using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Standing; -using Robust.Shared.Physics.Events; +using Content.Shared.Weapons.Ranged.Components; using Robust.Shared.Containers; +using Robust.Shared.Physics.Events; -namespace Content.Shared.Damage.Components; +namespace Content.Shared.Damage.Systems; public sealed class RequireProjectileTargetSystem : EntitySystem { diff --git a/Content.Shared/Damage/Systems/SharedGodmodeSystem.cs b/Content.Shared/Damage/Systems/SharedGodmodeSystem.cs index 4bf762c479..2a020732c2 100644 --- a/Content.Shared/Damage/Systems/SharedGodmodeSystem.cs +++ b/Content.Shared/Damage/Systems/SharedGodmodeSystem.cs @@ -85,9 +85,9 @@ public abstract class SharedGodmodeSystem : EntitySystem if (!Resolve(uid, ref godmode, false)) return; - if (TryComp(uid, out var damageable) && godmode.OldDamage != null) + if (godmode.OldDamage != null) { - _damageable.SetDamage(uid, damageable, godmode.OldDamage); + _damageable.SetDamage(uid, godmode.OldDamage); } RemComp(uid); diff --git a/Content.Shared/Damage/Systems/SlowOnDamageSystem.cs b/Content.Shared/Damage/Systems/SlowOnDamageSystem.cs index e339727200..fef7b6ae06 100644 --- a/Content.Shared/Damage/Systems/SlowOnDamageSystem.cs +++ b/Content.Shared/Damage/Systems/SlowOnDamageSystem.cs @@ -5,109 +5,108 @@ using Content.Shared.FixedPoint; using Content.Shared.Inventory; using Content.Shared.Movement.Systems; -namespace Content.Shared.Damage +namespace Content.Shared.Damage.Systems; + +public sealed class SlowOnDamageSystem : EntitySystem { - public sealed class SlowOnDamageSystem : EntitySystem + [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifierSystem = default!; + + public override void Initialize() { - [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifierSystem = default!; + base.Initialize(); - public override void Initialize() + SubscribeLocalEvent(OnDamageChanged); + SubscribeLocalEvent(OnRefreshMovespeed); + + SubscribeLocalEvent>(OnModifySpeed); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnGotEquipped); + SubscribeLocalEvent(OnGotUnequipped); + + SubscribeLocalEvent(OnIgnoreStartup); + SubscribeLocalEvent(OnIgnoreShutdown); + SubscribeLocalEvent(OnIgnoreModifySpeed); + } + + private void OnRefreshMovespeed(EntityUid uid, SlowOnDamageComponent component, RefreshMovementSpeedModifiersEvent args) + { + if (!TryComp(uid, out var damage)) + return; + + if (damage.TotalDamage == FixedPoint2.Zero) + return; + + // Get closest threshold + FixedPoint2 closest = FixedPoint2.Zero; + var total = damage.TotalDamage; + foreach (var thres in component.SpeedModifierThresholds) { - base.Initialize(); - - SubscribeLocalEvent(OnDamageChanged); - SubscribeLocalEvent(OnRefreshMovespeed); - - SubscribeLocalEvent>(OnModifySpeed); - SubscribeLocalEvent(OnExamined); - SubscribeLocalEvent(OnGotEquipped); - SubscribeLocalEvent(OnGotUnequipped); - - SubscribeLocalEvent(OnIgnoreStartup); - SubscribeLocalEvent(OnIgnoreShutdown); - SubscribeLocalEvent(OnIgnoreModifySpeed); + if (total >= thres.Key && thres.Key > closest) + closest = thres.Key; } - private void OnRefreshMovespeed(EntityUid uid, SlowOnDamageComponent component, RefreshMovementSpeedModifiersEvent args) + if (closest != FixedPoint2.Zero) { - if (!TryComp(uid, out var damage)) - return; + var speed = component.SpeedModifierThresholds[closest]; - if (damage.TotalDamage == FixedPoint2.Zero) - return; - - // Get closest threshold - FixedPoint2 closest = FixedPoint2.Zero; - var total = damage.TotalDamage; - foreach (var thres in component.SpeedModifierThresholds) - { - if (total >= thres.Key && thres.Key > closest) - closest = thres.Key; - } - - if (closest != FixedPoint2.Zero) - { - var speed = component.SpeedModifierThresholds[closest]; - - var ev = new ModifySlowOnDamageSpeedEvent(speed); - RaiseLocalEvent(uid, ref ev); - args.ModifySpeed(ev.Speed, ev.Speed); - } - } - - private void OnDamageChanged(EntityUid uid, SlowOnDamageComponent component, DamageChangedEvent args) - { - // We -could- only refresh if it crossed a threshold but that would kind of be a lot of duplicated - // code and this isn't a super hot path anyway since basically only humans have this - - _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(uid); - } - - private void OnModifySpeed(Entity ent, ref InventoryRelayedEvent args) - { - var dif = 1 - args.Args.Speed; - if (dif <= 0) - return; - - // reduces the slowness modifier by the given coefficient - args.Args.Speed += dif * ent.Comp.Modifier; - } - - private void OnExamined(Entity ent, ref ExaminedEvent args) - { - var msg = Loc.GetString("slow-on-damage-modifier-examine", ("mod", (1 - ent.Comp.Modifier) * 100)); - args.PushMarkup(msg); - } - - private void OnGotEquipped(Entity ent, ref ClothingGotEquippedEvent args) - { - _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(args.Wearer); - } - - private void OnGotUnequipped(Entity ent, ref ClothingGotUnequippedEvent args) - { - _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(args.Wearer); - } - - private void OnIgnoreStartup(Entity ent, ref ComponentStartup args) - { - _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(ent); - } - - private void OnIgnoreShutdown(Entity ent, ref ComponentShutdown args) - { - _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(ent); - } - - private void OnIgnoreModifySpeed(Entity ent, ref ModifySlowOnDamageSpeedEvent args) - { - args.Speed = 1f; + var ev = new ModifySlowOnDamageSpeedEvent(speed); + RaiseLocalEvent(uid, ref ev); + args.ModifySpeed(ev.Speed, ev.Speed); } } - [ByRefEvent] - public record struct ModifySlowOnDamageSpeedEvent(float Speed) : IInventoryRelayEvent + private void OnDamageChanged(EntityUid uid, SlowOnDamageComponent component, DamageChangedEvent args) { - public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET; + // We -could- only refresh if it crossed a threshold but that would kind of be a lot of duplicated + // code and this isn't a super hot path anyway since basically only humans have this + + _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(uid); + } + + private void OnModifySpeed(Entity ent, ref InventoryRelayedEvent args) + { + var dif = 1 - args.Args.Speed; + if (dif <= 0) + return; + + // reduces the slowness modifier by the given coefficient + args.Args.Speed += dif * ent.Comp.Modifier; + } + + private void OnExamined(Entity ent, ref ExaminedEvent args) + { + var msg = Loc.GetString("slow-on-damage-modifier-examine", ("mod", (1 - ent.Comp.Modifier) * 100)); + args.PushMarkup(msg); + } + + private void OnGotEquipped(Entity ent, ref ClothingGotEquippedEvent args) + { + _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(args.Wearer); + } + + private void OnGotUnequipped(Entity ent, ref ClothingGotUnequippedEvent args) + { + _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(args.Wearer); + } + + private void OnIgnoreStartup(Entity ent, ref ComponentStartup args) + { + _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(ent); + } + + private void OnIgnoreShutdown(Entity ent, ref ComponentShutdown args) + { + _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(ent); + } + + private void OnIgnoreModifySpeed(Entity ent, ref ModifySlowOnDamageSpeedEvent args) + { + args.Speed = 1f; } } + +[ByRefEvent] +public record struct ModifySlowOnDamageSpeedEvent(float Speed) : IInventoryRelayEvent +{ + public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET; +} diff --git a/Content.Shared/Delivery/SharedDeliverySystem.cs b/Content.Shared/Delivery/SharedDeliverySystem.cs index d7fc40dcc6..71baa92ec6 100644 --- a/Content.Shared/Delivery/SharedDeliverySystem.cs +++ b/Content.Shared/Delivery/SharedDeliverySystem.cs @@ -162,7 +162,7 @@ public abstract class SharedDeliverySystem : EntitySystem private bool TryUnlockDelivery(Entity ent, EntityUid user, bool rewardMoney = true, bool force = false) { // Check fingerprint access if there is a reader on the mail - if (!force && TryComp(ent, out var reader) && !_fingerprintReader.IsAllowed((ent, reader), user)) + if (!force && !_fingerprintReader.IsAllowed(ent.Owner, user, out _)) return false; var deliveryName = _nameModifier.GetBaseName(ent.Owner); diff --git a/Content.Shared/Destructible/SharedDestructibleSystem.cs b/Content.Shared/Destructible/SharedDestructibleSystem.cs index 572ed9d560..6b7148a4c9 100644 --- a/Content.Shared/Destructible/SharedDestructibleSystem.cs +++ b/Content.Shared/Destructible/SharedDestructibleSystem.cs @@ -1,11 +1,11 @@ -namespace Content.Shared.Destructible; +namespace Content.Shared.Destructible; public abstract class SharedDestructibleSystem : EntitySystem { /// - /// Force entity to be destroyed and deleted. + /// Force entity to be destroyed and deleted. /// - public bool DestroyEntity(EntityUid owner) + public bool DestroyEntity(Entity owner) { var ev = new DestructionAttemptEvent(); RaiseLocalEvent(owner, ev); @@ -15,12 +15,12 @@ public abstract class SharedDestructibleSystem : EntitySystem var eventArgs = new DestructionEventArgs(); RaiseLocalEvent(owner, eventArgs); - QueueDel(owner); + PredictedQueueDel(owner); return true; } /// - /// Force entity to break. + /// Force entity to break. /// public void BreakEntity(EntityUid owner) { @@ -30,7 +30,7 @@ public abstract class SharedDestructibleSystem : EntitySystem } /// -/// Raised before an entity is about to be destroyed and deleted +/// Raised before an entity is about to be destroyed and deleted /// public sealed class DestructionAttemptEvent : CancellableEntityEventArgs { @@ -38,7 +38,7 @@ public sealed class DestructionAttemptEvent : CancellableEntityEventArgs } /// -/// Raised when entity is destroyed and about to be deleted. +/// Raised when entity is destroyed and about to be deleted. /// public sealed class DestructionEventArgs : EntityEventArgs { @@ -46,7 +46,7 @@ public sealed class DestructionEventArgs : EntityEventArgs } /// -/// Raised when entity was heavy damage and about to break. +/// Raised when entity was heavy damage and about to break. /// public sealed class BreakageEventArgs : EntityEventArgs { diff --git a/Content.Shared/Destructible/ThresholdActs.cs b/Content.Shared/Destructible/ThresholdActs.cs new file mode 100644 index 0000000000..281d58092f --- /dev/null +++ b/Content.Shared/Destructible/ThresholdActs.cs @@ -0,0 +1,12 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Destructible; + +[Flags] +[Serializable, NetSerializable] +public enum ThresholdActs : byte +{ + None = 0, + Breakage = 1 << 0, + Destruction = 1 << 1, +} diff --git a/Content.Shared/Destructible/Triggers/AndTrigger.cs b/Content.Shared/Destructible/Triggers/AndTrigger.cs new file mode 100644 index 0000000000..938f125b01 --- /dev/null +++ b/Content.Shared/Destructible/Triggers/AndTrigger.cs @@ -0,0 +1,28 @@ +using Content.Shared.Damage.Components; +using Robust.Shared.Serialization; + +namespace Content.Shared.Destructible.Thresholds.Triggers; + +/// +/// A trigger that will activate when all of its triggers have activated. +/// +[Serializable, NetSerializable] +[DataDefinition] +public sealed partial class AndTrigger : IThresholdTrigger +{ + [DataField] + public List Triggers = new(); + + public bool Reached(Entity damageable, SharedDestructibleSystem system) + { + foreach (var trigger in Triggers) + { + if (!trigger.Reached(damageable, system)) + { + return false; + } + } + + return true; + } +} diff --git a/Content.Shared/Destructible/Triggers/DamageGroupTrigger.cs b/Content.Shared/Destructible/Triggers/DamageGroupTrigger.cs new file mode 100644 index 0000000000..c1add2e891 --- /dev/null +++ b/Content.Shared/Destructible/Triggers/DamageGroupTrigger.cs @@ -0,0 +1,33 @@ +using Content.Shared.Damage.Components; +using Content.Shared.FixedPoint; +using Content.Shared.Damage.Prototypes; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Destructible.Thresholds.Triggers; + +/// +/// A trigger that will activate when the amount of damage received +/// of the specified class is above the specified threshold. +/// +[Serializable, NetSerializable] +[DataDefinition] +public sealed partial class DamageGroupTrigger : IThresholdTrigger +{ + /// + /// The damage group to check for. + /// + [DataField(required: true)] + public ProtoId DamageGroup = default!; + + /// + /// The amount of damage at which this threshold will trigger. + /// + [DataField(required: true)] + public FixedPoint2 Damage = default!; + + public bool Reached(Entity damageable, SharedDestructibleSystem system) + { + return damageable.Comp.DamagePerGroup[DamageGroup] >= Damage; + } +} diff --git a/Content.Shared/Destructible/Triggers/DamageTrigger.cs b/Content.Shared/Destructible/Triggers/DamageTrigger.cs new file mode 100644 index 0000000000..7a2d612f75 --- /dev/null +++ b/Content.Shared/Destructible/Triggers/DamageTrigger.cs @@ -0,0 +1,25 @@ +using Content.Shared.Damage.Components; +using Content.Shared.FixedPoint; +using Robust.Shared.Serialization; + +namespace Content.Shared.Destructible.Thresholds.Triggers; + +/// +/// A trigger that will activate when the total amount of damage received +/// is above the specified threshold. +/// +[Serializable, NetSerializable] +[DataDefinition] +public sealed partial class DamageTrigger : IThresholdTrigger +{ + /// + /// The amount of damage at which this threshold will trigger. + /// + [DataField(required: true)] + public FixedPoint2 Damage = default!; + + public bool Reached(Entity damageable, SharedDestructibleSystem system) + { + return damageable.Comp.TotalDamage >= Damage; + } +} diff --git a/Content.Shared/Destructible/Triggers/DamageTypeTrigger.cs b/Content.Shared/Destructible/Triggers/DamageTypeTrigger.cs new file mode 100644 index 0000000000..60bd9baf67 --- /dev/null +++ b/Content.Shared/Destructible/Triggers/DamageTypeTrigger.cs @@ -0,0 +1,34 @@ +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Destructible.Thresholds.Triggers; + +/// +/// A trigger that will activate when the amount of damage received +/// of the specified type is above the specified threshold. +/// +[Serializable, NetSerializable] +[DataDefinition] +public sealed partial class DamageTypeTrigger : IThresholdTrigger +{ + /// + /// The damage type to check for. + /// + [DataField(required: true)] + public ProtoId DamageType = default!; + + /// + /// The amount of damage at which this threshold will trigger. + /// + [DataField(required: true)] + public FixedPoint2 Damage = default!; + + public bool Reached(Entity damageable, SharedDestructibleSystem system) + { + return damageable.Comp.Damage.DamageDict.TryGetValue(DamageType, out var damageReceived) && + damageReceived >= Damage; + } +} diff --git a/Content.Shared/Destructible/Triggers/IThresholdTrigger.cs b/Content.Shared/Destructible/Triggers/IThresholdTrigger.cs new file mode 100644 index 0000000000..d97394c406 --- /dev/null +++ b/Content.Shared/Destructible/Triggers/IThresholdTrigger.cs @@ -0,0 +1,27 @@ +using Content.Shared.Damage.Components; + +namespace Content.Shared.Destructible.Thresholds.Triggers; + +/// +/// A condition for triggering a . +/// +/// +/// I decided against converting these into EntityEffectConditions for performance reasons +/// (although I did not do any benchmarks, so it might be fine). +/// Entity effects will raise a separate event for each entity and each condition, which can become a huge number +/// for cases like nuke explosions or shuttle collisions where there are lots of DamageChangedEvents at once. +/// IThresholdTriggers on the other hand are directly checked in a foreach loop without raising events. +/// And there are only few of these conditions, so there is only a minor amount of code duplication. +/// +public interface IThresholdTrigger +{ + /// + /// Checks if this trigger has been reached. + /// + /// The damageable component to check with. + /// + /// An instance of to pull dependencies from, if any. + /// + /// true if this trigger has been reached, false otherwise. + bool Reached(Entity damageable, SharedDestructibleSystem system); +} diff --git a/Content.Shared/Destructible/Triggers/OrTrigger.cs b/Content.Shared/Destructible/Triggers/OrTrigger.cs new file mode 100644 index 0000000000..cdf72bed7f --- /dev/null +++ b/Content.Shared/Destructible/Triggers/OrTrigger.cs @@ -0,0 +1,28 @@ +using Content.Shared.Damage.Components; +using Robust.Shared.Serialization; + +namespace Content.Shared.Destructible.Thresholds.Triggers; + +/// +/// A trigger that will activate when any of its triggers have activated. +/// +[Serializable, NetSerializable] +[DataDefinition] +public sealed partial class OrTrigger : IThresholdTrigger +{ + [DataField] + public List Triggers = new(); + + public bool Reached(Entity damageable, SharedDestructibleSystem system) + { + foreach (var trigger in Triggers) + { + if (trigger.Reached(damageable, system)) + { + return true; + } + } + + return false; + } +} diff --git a/Content.Shared/DoAfter/SharedDoAfterSystem.cs b/Content.Shared/DoAfter/SharedDoAfterSystem.cs index c1a3d6ecee..d80f65755e 100644 --- a/Content.Shared/DoAfter/SharedDoAfterSystem.cs +++ b/Content.Shared/DoAfter/SharedDoAfterSystem.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Content.Shared.ActionBlocker; using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Hands.Components; using Content.Shared.Tag; using Robust.Shared.GameStates; diff --git a/Content.Shared/Doors/Components/DoorComponent.cs b/Content.Shared/Doors/Components/DoorComponent.cs index 020a7a4c9b..e66dff2611 100644 --- a/Content.Shared/Doors/Components/DoorComponent.cs +++ b/Content.Shared/Doors/Components/DoorComponent.cs @@ -325,4 +325,5 @@ public enum DoorVisualLayers : byte BaseUnlit, BaseBolted, BaseEmergencyAccess, + BaseEmagging, } diff --git a/Content.Shared/Doors/Systems/SharedDoorSystem.cs b/Content.Shared/Doors/Systems/SharedDoorSystem.cs index 50132e42dd..c031dcc7bd 100644 --- a/Content.Shared/Doors/Systems/SharedDoorSystem.cs +++ b/Content.Shared/Doors/Systems/SharedDoorSystem.cs @@ -2,7 +2,7 @@ using System.Linq; using Content.Shared.Access.Components; using Content.Shared.Access.Systems; using Content.Shared.Administration.Logs; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.Doors.Components; using Content.Shared.Emag.Systems; diff --git a/Content.Shared/Drunk/SharedDrunkSystem.cs b/Content.Shared/Drunk/SharedDrunkSystem.cs index 96aff82fa0..9faeb9419d 100644 --- a/Content.Shared/Drunk/SharedDrunkSystem.cs +++ b/Content.Shared/Drunk/SharedDrunkSystem.cs @@ -1,4 +1,3 @@ -using Content.Shared.Speech.EntitySystems; using Content.Shared.StatusEffectNew; using Content.Shared.Traits.Assorted; using Robust.Shared.Prototypes; @@ -8,12 +7,6 @@ namespace Content.Shared.Drunk; public abstract class SharedDrunkSystem : EntitySystem { public static EntProtoId Drunk = "StatusEffectDrunk"; - public static EntProtoId Woozy = "StatusEffectWoozy"; - - /* I have no clue why this magic number was chosen, I copied it from slur system and needed it for the overlay - If you have a more intelligent magic number be my guest to completely explode this value. - There were no comments as to why this value was chosen three years ago. */ - public static float MagicNumber = 1100f; [Dependency] protected readonly StatusEffectsSystem Status = default!; diff --git a/Content.Shared/Emoting/EmoteEvents.cs b/Content.Shared/Emoting/EmoteEvents.cs index ea3073f336..931395ee7e 100644 --- a/Content.Shared/Emoting/EmoteEvents.cs +++ b/Content.Shared/Emoting/EmoteEvents.cs @@ -1,27 +1,6 @@ -using Content.Shared.Chat.Prototypes; -using Content.Shared.Inventory; - -namespace Content.Shared.Emoting; +namespace Content.Shared.Emoting; public sealed class EmoteAttemptEvent(EntityUid uid) : CancellableEntityEventArgs { public EntityUid Uid { get; } = uid; } - -/// -/// An event raised just before an emote is performed, providing systems with an opportunity to cancel the emote's performance. -/// -[ByRefEvent] -public sealed class BeforeEmoteEvent(EntityUid source, EmotePrototype emote) - : CancellableEntityEventArgs, IInventoryRelayEvent -{ - public readonly EntityUid Source = source; - public readonly EmotePrototype Emote = emote; - - /// - /// The equipment that is blocking emoting. Should only be non-null if the event was canceled. - /// - public EntityUid? Blocker = null; - - public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET; -} diff --git a/Content.Shared/EntityConditions/Conditions/Body/BreathingEntityCondition.cs b/Content.Shared/EntityConditions/Conditions/Body/BreathingEntityCondition.cs new file mode 100644 index 0000000000..ece979a6a8 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/Body/BreathingEntityCondition.cs @@ -0,0 +1,10 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions.Body; + +/// +public sealed partial class BreathingCondition : EntityConditionBase +{ + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => + Loc.GetString("entity-condition-guidebook-breathing", ("isBreathing", !Inverted)); +} diff --git a/Content.Shared/EntityConditions/Conditions/Body/HungerEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/Body/HungerEntityConditionSystem.cs new file mode 100644 index 0000000000..f00707f907 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/Body/HungerEntityConditionSystem.cs @@ -0,0 +1,33 @@ +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions.Body; + +/// +/// Returns true if this entity's hunger is within a specified minimum and maximum. +/// +/// +public sealed partial class TotalHungerEntityConditionSystem : EntityConditionSystem +{ + [Dependency] private readonly HungerSystem _hunger = default!; + + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + var total = _hunger.GetHunger(entity.Comp); + args.Result = total >= args.Condition.Min && total <= args.Condition.Max; + } +} + +/// +public sealed partial class HungerCondition : EntityConditionBase +{ + [DataField] + public float Min; + + [DataField] + public float Max = float.PositiveInfinity; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => + Loc.GetString("entity-condition-guidebook-total-hunger", ("max", float.IsPositiveInfinity(Max) ? int.MaxValue : Max), ("min", Min)); +} diff --git a/Content.Shared/EntityConditions/Conditions/Body/InternalsEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/Body/InternalsEntityConditionSystem.cs new file mode 100644 index 0000000000..cabf731939 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/Body/InternalsEntityConditionSystem.cs @@ -0,0 +1,23 @@ +using Content.Shared.Body.Components; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions.Body; + +/// +/// Returns true if this entity is using internals. False if they are not or cannot use internals. +/// +/// +public sealed partial class InternalsOnEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + args.Result = entity.Comp.GasTankEntity != null; + } +} + +/// +public sealed partial class InternalsCondition : EntityConditionBase +{ + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => + Loc.GetString("entity-condition-guidebook-internals", ("usingInternals", !Inverted)); +} diff --git a/Content.Shared/EntityConditions/Conditions/Body/MetabolizerTypeEntityCondition.cs b/Content.Shared/EntityConditions/Conditions/Body/MetabolizerTypeEntityCondition.cs new file mode 100644 index 0000000000..5fb63d008f --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/Body/MetabolizerTypeEntityCondition.cs @@ -0,0 +1,31 @@ +using Content.Shared.Body.Prototypes; +using Content.Shared.Localizations; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions.Body; + +/// +public sealed partial class MetabolizerTypeCondition : EntityConditionBase +{ + [DataField(required: true)] + public ProtoId[] Type = default!; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) + { + var typeList = new List(); + + foreach (var type in Type) + { + if (!prototype.Resolve(type, out var proto)) + continue; + + typeList.Add(proto.LocalizedName); + } + + var names = ContentLocalizationManager.FormatListToOr(typeList); + + return Loc.GetString("entity-condition-guidebook-organ-type", + ("name", names), + ("shouldhave", !Inverted)); + } +} diff --git a/Content.Shared/EntityConditions/Conditions/Body/MobStateEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/Body/MobStateEntityConditionSystem.cs new file mode 100644 index 0000000000..735720c0ea --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/Body/MobStateEntityConditionSystem.cs @@ -0,0 +1,28 @@ +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions.Body; + +/// +/// Returns true if this entity's current mob state matches the condition's specified mob state. +/// +/// +public sealed partial class MobStateEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + if (entity.Comp.CurrentState == args.Condition.Mobstate) + args.Result = true; + } +} + +/// +public sealed partial class MobStateCondition : EntityConditionBase +{ + [DataField] + public MobState Mobstate = MobState.Alive; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => + Loc.GetString("entity-condition-guidebook-mob-state-condition", ("state", Mobstate)); +} diff --git a/Content.Shared/EntityConditions/Conditions/DamageGroupEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/DamageGroupEntityConditionSystem.cs new file mode 100644 index 0000000000..3691039189 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/DamageGroupEntityConditionSystem.cs @@ -0,0 +1,38 @@ +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions; + +/// +/// Returns true if this entity can take damage and if its damage of a given damage group is within a specified minimum and maximum. +/// +/// +public sealed partial class DamageGroupEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + var value = entity.Comp.DamagePerGroup[args.Condition.DamageGroup]; + args.Result = value >= args.Condition.Min && value <= args.Condition.Max; + } +} + +/// +public sealed partial class DamageGroupCondition : EntityConditionBase +{ + [DataField] + public FixedPoint2 Max = FixedPoint2.MaxValue; + + [DataField] + public FixedPoint2 Min = FixedPoint2.Zero; + + [DataField(required: true)] + public ProtoId DamageGroup; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => + Loc.GetString("entity-condition-guidebook-group-damage", + ("max", Max == FixedPoint2.MaxValue ? int.MaxValue : Max.Float()), + ("min", Min.Float()), + ("type", prototype.Index(DamageGroup).LocalizedName)); +} diff --git a/Content.Shared/EntityConditions/Conditions/DamageTypeEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/DamageTypeEntityConditionSystem.cs new file mode 100644 index 0000000000..f2611a8200 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/DamageTypeEntityConditionSystem.cs @@ -0,0 +1,39 @@ +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions; + +/// +/// Returns true if this entity can take damage and if its damage of a given damage type is within a specified minimum and maximum. +/// +/// +public sealed partial class DamageTypeEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + var value = entity.Comp.Damage.DamageDict.GetValueOrDefault(args.Condition.DamageType); + args.Result = value >= args.Condition.Min && value <= args.Condition.Max; + } +} + +/// +public sealed partial class DamageTypeCondition : EntityConditionBase +{ + [DataField] + public FixedPoint2 Max = FixedPoint2.MaxValue; + + [DataField] + public FixedPoint2 Min = FixedPoint2.Zero; + + [DataField(required: true)] + public ProtoId DamageType; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => + Loc.GetString("entity-condition-guidebook-type-damage", + ("max", Max == FixedPoint2.MaxValue ? int.MaxValue : Max.Float()), + ("min", Min.Float()), + ("type", prototype.Index(DamageType).LocalizedName)); +} diff --git a/Content.Shared/EntityConditions/Conditions/JobEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/JobEntityConditionSystem.cs new file mode 100644 index 0000000000..a698efa032 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/JobEntityConditionSystem.cs @@ -0,0 +1,59 @@ +using System.Linq; +using Content.Shared.Localizations; +using Content.Shared.Mind; +using Content.Shared.Mind.Components; +using Content.Shared.Roles; +using Content.Shared.Roles.Components; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions; + +/// +/// Returns true if this entity has any of the specified jobs. False if the entity has no mind, none of the specified jobs, or is jobless. +/// +/// +public sealed partial class HasJobEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + // We need a mind in our mind container... + if (!TryComp(entity.Comp.Mind, out var mind)) + return; + + foreach (var roleId in mind.MindRoleContainer.ContainedEntities) + { + if (!HasComp(roleId)) + continue; + + if (!TryComp(roleId, out var mindRole)) + { + Log.Error($"Encountered job mind role entity {roleId} without a {nameof(MindRoleComponent)}"); + continue; + } + + if (mindRole.JobPrototype == null) + { + Log.Error($"Encountered job mind role entity {roleId} without a {nameof(JobPrototype)}"); + continue; + } + + if (!args.Condition.Jobs.Contains(mindRole.JobPrototype.Value)) + continue; + + args.Result = true; + return; + } + } +} + +/// +public sealed partial class JobCondition : EntityConditionBase +{ + [DataField(required: true)] public List> Jobs = []; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) + { + var localizedNames = Jobs.Select(jobId => prototype.Index(jobId).LocalizedName).ToList(); + return Loc.GetString("entity-condition-guidebook-job-condition", ("job", ContentLocalizationManager.FormatListToOr(localizedNames))); + } +} diff --git a/Content.Shared/EntityConditions/Conditions/ReagentEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/ReagentEntityConditionSystem.cs new file mode 100644 index 0000000000..dbe585e631 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/ReagentEntityConditionSystem.cs @@ -0,0 +1,44 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions; + +/// +/// Returns true if this solution entity has an amount of reagent in it within a specified minimum and maximum. +/// +/// +public sealed partial class ReagentThresholdEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + var quant = entity.Comp.Solution.GetTotalPrototypeQuantity(args.Condition.Reagent); + + args.Result = quant >= args.Condition.Min && quant <= args.Condition.Max; + } +} + +/// +public sealed partial class ReagentCondition : EntityConditionBase +{ + [DataField] + public FixedPoint2 Min = FixedPoint2.Zero; + + [DataField] + public FixedPoint2 Max = FixedPoint2.MaxValue; + + [DataField(required: true)] + public ProtoId Reagent; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) + { + if (!prototype.Resolve(Reagent, out var reagentProto)) + return String.Empty; + + return Loc.GetString("entity-condition-guidebook-reagent-threshold", + ("reagent", reagentProto.LocalizedName), + ("max", Max == FixedPoint2.MaxValue ? int.MaxValue : Max.Float()), + ("min", Min.Float())); + } +} diff --git a/Content.Shared/EntityConditions/Conditions/Tags/HasAllTagsEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/Tags/HasAllTagsEntityConditionSystem.cs new file mode 100644 index 0000000000..36f4f51bf5 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/Tags/HasAllTagsEntityConditionSystem.cs @@ -0,0 +1,43 @@ +using Content.Shared.Localizations; +using Content.Shared.Tag; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions.Tags; + +/// +/// Returns true if this entity has all the listed tags. +/// +/// +public sealed partial class HasAllTagsEntityConditionSystem : EntityConditionSystem +{ + [Dependency] private readonly TagSystem _tag = default!; + + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + args.Result = _tag.HasAllTags(entity.Comp, args.Condition.Tags); + } +} + +/// +public sealed partial class AllTagsCondition : EntityConditionBase +{ + [DataField(required: true)] + public ProtoId[] Tags = []; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) + { + var tagList = new List(); + + foreach (var type in Tags) + { + if (!prototype.Resolve(type, out var proto)) + continue; + + tagList.Add(proto.ID); + } + + var names = ContentLocalizationManager.FormatList(tagList); + + return Loc.GetString("entity-condition-guidebook-has-tag", ("tag", names), ("invert", Inverted)); + } +} diff --git a/Content.Shared/EntityConditions/Conditions/Tags/HasAnyTagEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/Tags/HasAnyTagEntityConditionSystem.cs new file mode 100644 index 0000000000..dedce491fd --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/Tags/HasAnyTagEntityConditionSystem.cs @@ -0,0 +1,43 @@ +using Content.Shared.Localizations; +using Content.Shared.Tag; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions.Tags; + +/// +/// Returns true if this entity have any of the listed tags. +/// +/// +public sealed partial class HasAnyTagEntityConditionSystem : EntityConditionSystem +{ + [Dependency] private readonly TagSystem _tag = default!; + + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + args.Result = _tag.HasAnyTag(entity.Comp, args.Condition.Tags); + } +} + +/// +public sealed partial class AnyTagCondition : EntityConditionBase +{ + [DataField(required: true)] + public ProtoId[] Tags = []; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) + { + var tagList = new List(); + + foreach (var type in Tags) + { + if (!prototype.Resolve(type, out var proto)) + continue; + + tagList.Add(proto.ID); + } + + var names = ContentLocalizationManager.FormatListToOr(tagList); + + return Loc.GetString("entity-condition-guidebook-has-tag", ("tag", names), ("invert", Inverted)); + } +} diff --git a/Content.Shared/EntityConditions/Conditions/Tags/HasTagEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/Tags/HasTagEntityConditionSystem.cs new file mode 100644 index 0000000000..6145982a03 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/Tags/HasTagEntityConditionSystem.cs @@ -0,0 +1,28 @@ +using Content.Shared.Tag; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions.Tags; + +/// +/// Returns true if this entity has the listed tag. +/// +/// +public sealed partial class HasTagEntityConditionSystem : EntityConditionSystem +{ + [Dependency] private readonly TagSystem _tag = default!; + + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + args.Result = _tag.HasTag(entity.Comp, args.Condition.Tag); + } +} + +/// +public sealed partial class TagCondition : EntityConditionBase +{ + [DataField(required: true)] + public ProtoId Tag; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => + Loc.GetString("entity-condition-guidebook-has-tag", ("tag", Tag), ("invert", Inverted)); +} diff --git a/Content.Shared/EntityConditions/Conditions/TemperatureEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/TemperatureEntityConditionSystem.cs new file mode 100644 index 0000000000..f87bbecf8c --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/TemperatureEntityConditionSystem.cs @@ -0,0 +1,52 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Temperature.Components; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions; + +/// +/// Returns true if this entity has an amount of reagent in it within a specified minimum and maximum. +/// +/// +public sealed partial class TemperatureEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + if (entity.Comp.CurrentTemperature >= args.Condition.Min && entity.Comp.CurrentTemperature <= args.Condition.Max) + args.Result = true; + } +} + +/// +/// Returns true if this solution entity has an amount of reagent in it within a specified minimum and maximum. +/// +/// +public sealed partial class SolutionTemperatureEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + if (entity.Comp.Solution.Temperature >= args.Condition.Min && entity.Comp.Solution.Temperature <= args.Condition.Max) + args.Result = true; + } +} + +/// +public sealed partial class TemperatureCondition : EntityConditionBase +{ + /// + /// Minimum allowed temperature + /// + [DataField] + public float Min = 0; + + /// + /// Maximum allowed temperature + /// + [DataField] + public float Max = float.PositiveInfinity; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => + Loc.GetString("entity-condition-guidebook-body-temperature", + ("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max), + ("min", Min)); +} diff --git a/Content.Shared/EntityConditions/Conditions/TemplateEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/TemplateEntityConditionSystem.cs new file mode 100644 index 0000000000..fbb659b54f --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/TemplateEntityConditionSystem.cs @@ -0,0 +1,20 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions; +/// +/// A basic summary of this condition. +/// +/// +public sealed partial class TemplateEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + // Condition goes here. + } +} + +/// +public sealed partial class TemplateCondition : EntityConditionBase +{ + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => String.Empty; +} diff --git a/Content.Shared/EntityConditions/Conditions/TotalDamageEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/TotalDamageEntityConditionSystem.cs new file mode 100644 index 0000000000..306398b4eb --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/TotalDamageEntityConditionSystem.cs @@ -0,0 +1,33 @@ +using Content.Shared.Damage.Components; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions; + +/// +/// Returns true if this entity can take damage and if its total damage is within a specified minimum and maximum. +/// +/// +public sealed partial class TotalDamageEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + var total = entity.Comp.TotalDamage; + args.Result = total >= args.Condition.Min && total <= args.Condition.Max; + } +} + +/// +public sealed partial class TotalDamageCondition : EntityConditionBase +{ + [DataField] + public FixedPoint2 Max = FixedPoint2.MaxValue; + + [DataField] + public FixedPoint2 Min = FixedPoint2.Zero; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => + Loc.GetString("entity-condition-guidebook-total-damage", + ("max", Max == FixedPoint2.MaxValue ? int.MaxValue : Max.Float()), + ("min", Min.Float())); +} diff --git a/Content.Shared/EntityConditions/SharedEntityConditionsSystem.cs b/Content.Shared/EntityConditions/SharedEntityConditionsSystem.cs new file mode 100644 index 0000000000..090a422641 --- /dev/null +++ b/Content.Shared/EntityConditions/SharedEntityConditionsSystem.cs @@ -0,0 +1,152 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions; + +/// +/// This handles entity effects. +/// Specifically it handles the receiving of events for causing entity effects, and provides +/// public API for other systems to take advantage of entity effects. +/// +public sealed partial class SharedEntityConditionsSystem : EntitySystem, IEntityConditionRaiser +{ + /// + /// Checks a list of conditions to verify that they all return true. + /// + /// Target entity we're checking conditions on + /// Conditions we're checking + /// Returns true if all conditions return true, false if any fail + public bool TryConditions(EntityUid target, EntityCondition[]? conditions) + { + // If there's no conditions we can't fail any of them... + if (conditions == null) + return true; + + foreach (var condition in conditions) + { + if (!TryCondition(target, condition)) + return false; + } + + return true; + } + + /// + /// Checks a list of conditions to see if any are true. + /// + /// Target entity we're checking conditions on + /// Conditions we're checking + /// Returns true if any conditions return true + public bool TryAnyCondition(EntityUid target, EntityCondition[]? conditions) + { + // If there's no conditions we can't meet any of them... + if (conditions == null) + return false; + + foreach (var condition in conditions) + { + if (TryCondition(target, condition)) + return true; + } + + return false; + } + + /// + /// Checks a single on an entity. + /// + /// Target entity we're checking conditions on + /// Condition we're checking + /// Returns true if we meet the condition and false otherwise + public bool TryCondition(EntityUid target, EntityCondition condition) + { + return condition.Inverted != condition.RaiseEvent(target, this); + } + + /// + /// Raises a condition to an entity. You should not be calling this unless you know what you're doing. + /// + public bool RaiseConditionEvent(EntityUid target, T effect) where T : EntityConditionBase + { + var effectEv = new EntityConditionEvent(effect); + RaiseLocalEvent(target, ref effectEv); + return effectEv.Result; + } +} + +/// +/// This is a basic abstract entity effect containing all the data an entity effect needs to affect entities with effects... +/// +/// The Component that is required for the effect +/// The Condition we're testing +public abstract partial class EntityConditionSystem : EntitySystem where T : Component where TCon : EntityConditionBase +{ + /// + public override void Initialize() + { + SubscribeLocalEvent>(Condition); + } + protected abstract void Condition(Entity entity, ref EntityConditionEvent args); +} + +/// +/// Used to raise an EntityCondition without losing the type of condition. +/// +public interface IEntityConditionRaiser +{ + bool RaiseConditionEvent(EntityUid target, T effect) where T : EntityConditionBase; +} + +/// +/// Used to store an so it can be raised without losing the type of the condition. +/// +/// The Condition wer are raising. +public abstract partial class EntityConditionBase : EntityCondition where T : EntityConditionBase +{ + public override bool RaiseEvent(EntityUid target, IEntityConditionRaiser raiser) + { + if (this is not T type) + return false; + + // If the result of the event matches the result we're looking for then we pass. + return raiser.RaiseConditionEvent(target, type); + } +} + +/// +/// A basic condition which can be checked for on an entity via events. +/// +[ImplicitDataDefinitionForInheritors] +public abstract partial class EntityCondition +{ + public abstract bool RaiseEvent(EntityUid target, IEntityConditionRaiser raiser); + + /// + /// If true, invert the result. So false returns true and true returns false! + /// + [DataField] + public bool Inverted; + + /// + /// A basic description of this condition, which displays in the guidebook. + /// + public abstract string EntityConditionGuidebookText(IPrototypeManager prototype); +} + +/// +/// An Event carrying an entity effect. +/// +/// The Condition we're checking +[ByRefEvent] +public record struct EntityConditionEvent(T Condition) where T : EntityConditionBase +{ + /// + /// The result of our check, defaults to false if nothing handles it. + /// + [DataField] + public bool Result; + + /// + /// The Condition being raised in this event + /// + public readonly T Condition = Condition; +} diff --git a/Content.Shared/EntityEffects/EffectConditions/BodyTemperature.cs b/Content.Shared/EntityEffects/EffectConditions/BodyTemperature.cs deleted file mode 100644 index 351e4ee12c..0000000000 --- a/Content.Shared/EntityEffects/EffectConditions/BodyTemperature.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.EffectConditions; - -/// -/// Requires the target entity to be above or below a certain temperature. -/// Used for things like cryoxadone and pyroxadone. -/// -public sealed partial class Temperature : EventEntityEffectCondition -{ - [DataField] - public float Min = 0; - - [DataField] - public float Max = float.PositiveInfinity; - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-body-temperature", - ("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max), - ("min", Min)); - } -} diff --git a/Content.Shared/EntityEffects/EffectConditions/BreathingCondition.cs b/Content.Shared/EntityEffects/EffectConditions/BreathingCondition.cs deleted file mode 100644 index 9de1bfdbf6..0000000000 --- a/Content.Shared/EntityEffects/EffectConditions/BreathingCondition.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.EffectConditions; - -/// -/// Condition for if the entity is successfully breathing. -/// -public sealed partial class Breathing : EventEntityEffectCondition -{ - /// - /// If true, the entity must not have trouble breathing to pass. - /// - [DataField] - public bool IsBreathing = true; - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-breathing", - ("isBreathing", IsBreathing)); - } -} diff --git a/Content.Shared/EntityEffects/EffectConditions/HasTagCondition.cs b/Content.Shared/EntityEffects/EffectConditions/HasTagCondition.cs deleted file mode 100644 index 379a248027..0000000000 --- a/Content.Shared/EntityEffects/EffectConditions/HasTagCondition.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Content.Shared.Tag; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.EntityEffects.EffectConditions; - -public sealed partial class HasTag : EntityEffectCondition -{ - [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] - public string Tag = default!; - - [DataField] - public bool Invert = false; - - public override bool Condition(EntityEffectBaseArgs args) - { - if (args.EntityManager.TryGetComponent(args.TargetEntity, out var tag)) - return args.EntityManager.System().HasTag(tag, Tag) ^ Invert; - - return false; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - // this should somehow be made (much) nicer. - return Loc.GetString("reagent-effect-condition-guidebook-has-tag", ("tag", Tag), ("invert", Invert)); - } -} diff --git a/Content.Shared/EntityEffects/EffectConditions/InternalsCondition.cs b/Content.Shared/EntityEffects/EffectConditions/InternalsCondition.cs deleted file mode 100644 index cb30ef70c7..0000000000 --- a/Content.Shared/EntityEffects/EffectConditions/InternalsCondition.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Content.Shared.Body.Components; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.EffectConditions; - -/// -/// Condition for if the entity is or isn't wearing internals. -/// -public sealed partial class Internals : EntityEffectCondition -{ - /// - /// To pass, the entity's internals must have this same state. - /// - [DataField] - public bool UsingInternals = true; - - public override bool Condition(EntityEffectBaseArgs args) - { - if (!args.EntityManager.TryGetComponent(args.TargetEntity, out InternalsComponent? internalsComp)) - return !UsingInternals; // They have no internals to wear. - - var internalsState = internalsComp.GasTankEntity != null; // If gas tank is not null, they are wearing internals - return UsingInternals == internalsState; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-internals", ("usingInternals", UsingInternals)); - } -} diff --git a/Content.Shared/EntityEffects/EffectConditions/JobCondition.cs b/Content.Shared/EntityEffects/EffectConditions/JobCondition.cs deleted file mode 100644 index 96f3be64c6..0000000000 --- a/Content.Shared/EntityEffects/EffectConditions/JobCondition.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Linq; -using Content.Shared.Localizations; -using Content.Shared.Mind; -using Content.Shared.Mind.Components; -using Content.Shared.Roles; -using Content.Shared.Roles.Components; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.EffectConditions; - -public sealed partial class JobCondition : EntityEffectCondition -{ - [DataField(required: true)] public List> Job; - - public override bool Condition(EntityEffectBaseArgs args) - { - args.EntityManager.TryGetComponent(args.TargetEntity, out var mindContainer); - - if (mindContainer is null - || !args.EntityManager.TryGetComponent(mindContainer.Mind, out var mind)) - return false; - - foreach (var roleId in mind.MindRoleContainer.ContainedEntities) - { - if (!args.EntityManager.HasComponent(roleId)) - continue; - - if (!args.EntityManager.TryGetComponent(roleId, out var mindRole)) - { - Logger.Error($"Encountered job mind role entity {roleId} without a {nameof(MindRoleComponent)}"); - continue; - } - - if (mindRole.JobPrototype == null) - { - Logger.Error($"Encountered job mind role entity {roleId} without a {nameof(JobPrototype)}"); - continue; - } - - if (Job.Contains(mindRole.JobPrototype.Value)) - return true; - } - - return false; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - var localizedNames = Job.Select(jobId => prototype.Index(jobId).LocalizedName).ToList(); - return Loc.GetString("reagent-effect-condition-guidebook-job-condition", ("job", ContentLocalizationManager.FormatListToOr(localizedNames))); - } -} diff --git a/Content.Shared/EntityEffects/EffectConditions/MobStateCondition.cs b/Content.Shared/EntityEffects/EffectConditions/MobStateCondition.cs deleted file mode 100644 index e02343f66a..0000000000 --- a/Content.Shared/EntityEffects/EffectConditions/MobStateCondition.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Content.Shared.Mobs; -using Content.Shared.Mobs.Components; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.EffectConditions; - -public sealed partial class MobStateCondition : EntityEffectCondition -{ - [DataField] - public MobState Mobstate = MobState.Alive; - - public override bool Condition(EntityEffectBaseArgs args) - { - if (args.EntityManager.TryGetComponent(args.TargetEntity, out MobStateComponent? mobState)) - { - if (mobState.CurrentState == Mobstate) - return true; - } - // Begin Offbrand - if (Mobstate == MobState.Critical) - { - if (args.EntityManager.System() - .IsCritical(args.TargetEntity)) - { - return true; - } - } - // End Offbrand - - return false; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-mob-state-condition", ("state", Mobstate)); - } -} - diff --git a/Content.Shared/EntityEffects/EffectConditions/OrganType.cs b/Content.Shared/EntityEffects/EffectConditions/OrganType.cs deleted file mode 100644 index f99eb5cc77..0000000000 --- a/Content.Shared/EntityEffects/EffectConditions/OrganType.cs +++ /dev/null @@ -1,28 +0,0 @@ -// using Content.Server.Body.Components; -using Content.Shared.Body.Prototypes; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.EntityEffects.EffectConditions; - -/// -/// Requires that the metabolizing organ is or is not tagged with a certain MetabolizerType -/// -public sealed partial class OrganType : EventEntityEffectCondition -{ - [DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string Type = default!; - - /// - /// Does this condition pass when the organ has the type, or when it doesn't have the type? - /// - [DataField] - public bool ShouldHave = true; - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-organ-type", - ("name", prototype.Index(Type).LocalizedName), - ("shouldhave", ShouldHave)); - } -} diff --git a/Content.Shared/EntityEffects/EffectConditions/ReagentThreshold.cs b/Content.Shared/EntityEffects/EffectConditions/ReagentThreshold.cs deleted file mode 100644 index af71f20c8e..0000000000 --- a/Content.Shared/EntityEffects/EffectConditions/ReagentThreshold.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Content.Shared.FixedPoint; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.EffectConditions; - -/// -/// Used for implementing reagent effects that require a certain amount of reagent before it should be applied. -/// For instance, overdoses. -/// -/// This can also trigger on -other- reagents, not just the one metabolizing. By default, it uses the -/// one being metabolized. -/// -public sealed partial class ReagentThreshold : EntityEffectCondition -{ - [DataField] - public FixedPoint2 Min = FixedPoint2.Zero; - - [DataField] - public FixedPoint2 Max = FixedPoint2.MaxValue; - - // TODO use ReagentId - [DataField] - public string? Reagent; - - public override bool Condition(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) - { - var reagent = Reagent ?? reagentArgs.Reagent?.ID; - if (reagent == null) - return true; // No condition to apply. - - var quant = FixedPoint2.Zero; - if (reagentArgs.Source != null) - quant = reagentArgs.Source.GetTotalPrototypeQuantity(reagent); - - return quant >= Min && quant <= Max; - } - - // TODO: Someone needs to figure out how to do this for non-reagent effects. - throw new NotImplementedException(); - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - ReagentPrototype? reagentProto = null; - if (Reagent is not null) - prototype.TryIndex(Reagent, out reagentProto); - - return Loc.GetString("reagent-effect-condition-guidebook-reagent-threshold", - ("reagent", reagentProto?.LocalizedName ?? Loc.GetString("reagent-effect-condition-guidebook-this-reagent")), - ("max", Max == FixedPoint2.MaxValue ? (float) int.MaxValue : Max.Float()), - ("min", Min.Float())); - } -} diff --git a/Content.Shared/EntityEffects/EffectConditions/SolutionTemperature.cs b/Content.Shared/EntityEffects/EffectConditions/SolutionTemperature.cs deleted file mode 100644 index e2febd8f48..0000000000 --- a/Content.Shared/EntityEffects/EffectConditions/SolutionTemperature.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.EffectConditions; - -/// -/// Requires the solution to be above or below a certain temperature. -/// Used for things like explosives. -/// -public sealed partial class SolutionTemperature : EntityEffectCondition -{ - [DataField] - public float Min = 0.0f; - - [DataField] - public float Max = float.PositiveInfinity; - - public override bool Condition(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) - { - return reagentArgs?.Source != null && - reagentArgs.Source.Temperature >= Min && - reagentArgs.Source.Temperature <= Max; - } - - // TODO: Someone needs to figure out how to do this for non-reagent effects. - throw new NotImplementedException(); - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-solution-temperature", - ("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max), - ("min", Min)); - } -} diff --git a/Content.Shared/EntityEffects/EffectConditions/TotalDamage.cs b/Content.Shared/EntityEffects/EffectConditions/TotalDamage.cs deleted file mode 100644 index a4baeb634a..0000000000 --- a/Content.Shared/EntityEffects/EffectConditions/TotalDamage.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Content.Shared.EntityEffects; -using Content.Shared.Damage; -using Content.Shared.FixedPoint; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.EffectConditions; - -public sealed partial class TotalDamage : EntityEffectCondition -{ - [DataField] - public FixedPoint2 Max = FixedPoint2.MaxValue; - - [DataField] - public FixedPoint2 Min = FixedPoint2.Zero; - - public override bool Condition(EntityEffectBaseArgs args) - { - if (args.EntityManager.TryGetComponent(args.TargetEntity, out DamageableComponent? damage)) - { - var total = damage.TotalDamage; - return total >= Min && total <= Max; - } - - return false; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-total-damage", - ("max", Max == FixedPoint2.MaxValue ? (float) int.MaxValue : Max.Float()), - ("min", Min.Float())); - } -} diff --git a/Content.Shared/EntityEffects/EffectConditions/TotalHunger.cs b/Content.Shared/EntityEffects/EffectConditions/TotalHunger.cs deleted file mode 100644 index cbeb334c47..0000000000 --- a/Content.Shared/EntityEffects/EffectConditions/TotalHunger.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Content.Shared.EntityEffects; -using Content.Shared.Nutrition.Components; -using Content.Shared.Nutrition.EntitySystems; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.EffectConditions; - -public sealed partial class Hunger : EntityEffectCondition -{ - [DataField] - public float Max = float.PositiveInfinity; - - [DataField] - public float Min = 0; - - public override bool Condition(EntityEffectBaseArgs args) - { - if (args.EntityManager.TryGetComponent(args.TargetEntity, out HungerComponent? hunger)) - { - var total = args.EntityManager.System().GetHunger(hunger); - return total >= Min && total <= Max; - } - - return false; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-total-hunger", - ("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max), - ("min", Min)); - } -} diff --git a/Content.Shared/EntityEffects/Effects/AddToSolutionReaction.cs b/Content.Shared/EntityEffects/Effects/AddToSolutionReaction.cs deleted file mode 100644 index 0f2d35d369..0000000000 --- a/Content.Shared/EntityEffects/Effects/AddToSolutionReaction.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Content.Shared.Chemistry.EntitySystems; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects -{ - public sealed partial class AddToSolutionReaction : EntityEffect - { - [DataField("solution")] - private string _solution = "reagents"; - - public override void Effect(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) { - if (reagentArgs.Reagent == null) - return; - - // TODO see if this is correct - var solutionContainerSystem = reagentArgs.EntityManager.System(); - if (!solutionContainerSystem.TryGetSolution(reagentArgs.TargetEntity, _solution, out var solutionContainer)) - return; - - if (solutionContainerSystem.TryAddReagent(solutionContainer.Value, reagentArgs.Reagent.ID, reagentArgs.Quantity, out var accepted)) - reagentArgs.Source?.RemoveReagent(reagentArgs.Reagent.ID, accepted); - - return; - } - - // TODO: Someone needs to figure out how to do this for non-reagent effects. - throw new NotImplementedException(); - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => - Loc.GetString("reagent-effect-guidebook-add-to-solution-reaction", ("chance", Probability)); - } -} diff --git a/Content.Shared/EntityEffects/Effects/AdjustAlert.cs b/Content.Shared/EntityEffects/Effects/AdjustAlert.cs deleted file mode 100644 index 282de0a06c..0000000000 --- a/Content.Shared/EntityEffects/Effects/AdjustAlert.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Content.Shared.Alert; -using Robust.Shared.Prototypes; -using Robust.Shared.Timing; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class AdjustAlert : EntityEffect -{ - /// - /// The specific Alert that will be adjusted - /// - [DataField(required: true)] - public ProtoId AlertType; - - /// - /// If true, the alert is removed after Time seconds. If Time was not specified the alert is removed immediately. - /// - [DataField] - public bool Clear; - - /// - /// Visually display cooldown progress over the alert icon. - /// - [DataField] - public bool ShowCooldown; - - /// - /// The length of the cooldown or delay before removing the alert (in seconds). - /// - [DataField] - public float Time; - - //JUSTIFICATION: This just changes some visuals, doesn't need to be documented. - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => null; - - public override void Effect(EntityEffectBaseArgs args) - { - var alertSys = args.EntityManager.EntitySysManager.GetEntitySystem(); - if (!args.EntityManager.HasComponent(args.TargetEntity)) - return; - - if (Clear && Time <= 0) - { - alertSys.ClearAlert(args.TargetEntity, AlertType); - } - else - { - var timing = IoCManager.Resolve(); - (TimeSpan, TimeSpan)? cooldown = null; - - if ((ShowCooldown || Clear) && Time > 0) - cooldown = (timing.CurTime, timing.CurTime + TimeSpan.FromSeconds(Time)); - - alertSys.ShowAlert(args.TargetEntity, AlertType, cooldown: cooldown, autoRemove: Clear, showCooldown: ShowCooldown); - } - - } -} diff --git a/Content.Shared/EntityEffects/Effects/AdjustAlertEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/AdjustAlertEntityEffectSystem.cs new file mode 100644 index 0000000000..633cde0877 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/AdjustAlertEntityEffectSystem.cs @@ -0,0 +1,65 @@ +using Content.Shared.Alert; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// Adjusts a given alert on this entity. +/// +/// +public sealed partial class AdjustAlertEntityEffectSysten : EntityEffectSystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly AlertsSystem _alerts = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var time = args.Effect.Time; + var clear = args.Effect.Clear; + var type = args.Effect.AlertType; + + if (clear && time <= TimeSpan.Zero) + { + _alerts.ClearAlert(entity.AsNullable(), type); + } + else + { + (TimeSpan, TimeSpan)? cooldown = null; + + if ((args.Effect.ShowCooldown || clear) && args.Effect.Time >= TimeSpan.Zero) + cooldown = (_timing.CurTime, _timing.CurTime + time); + + _alerts.ShowAlert(entity.AsNullable(), type, cooldown: cooldown, autoRemove: clear, showCooldown: args.Effect.ShowCooldown); + } + + } +} + +/// +public sealed partial class AdjustAlert : EntityEffectBase +{ + /// + /// The specific Alert that will be adjusted + /// + [DataField(required: true)] + public ProtoId AlertType; + + /// + /// If true, the alert is removed after Time seconds. If Time was not specified the alert is removed immediately. + /// + [DataField] + public bool Clear; + + /// + /// Visually display cooldown progress over the alert icon. + /// + [DataField] + public bool ShowCooldown; + + /// + /// The length of the cooldown or delay before removing the alert (in seconds). + /// + [DataField] + public TimeSpan Time; +} diff --git a/Content.Shared/EntityEffects/Effects/AdjustReagent.cs b/Content.Shared/EntityEffects/Effects/AdjustReagent.cs deleted file mode 100644 index bb655b46bc..0000000000 --- a/Content.Shared/EntityEffects/Effects/AdjustReagent.cs +++ /dev/null @@ -1,90 +0,0 @@ -using Content.Shared.Body.Prototypes; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.FixedPoint; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.EntityEffects.Effects -{ - public sealed partial class AdjustReagent : EntityEffect - { - /// - /// The reagent ID to remove. Only one of this and should be active. - /// - [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? Reagent = null; - // TODO use ReagentId - - /// - /// The metabolism group to remove, if the reagent satisfies any. - /// Only one of this and should be active. - /// - [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? Group = null; - - [DataField(required: true)] - public FixedPoint2 Amount = default!; - - public override void Effect(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) - { - if (reagentArgs.Source == null) - return; - - var amount = Amount; - amount *= reagentArgs.Scale; - - if (Reagent != null) - { - if (amount < 0 && reagentArgs.Source.ContainsPrototype(Reagent)) - reagentArgs.Source.RemoveReagent(Reagent, -amount); - if (amount > 0) - reagentArgs.Source.AddReagent(Reagent, amount); - } - else if (Group != null) - { - var prototypeMan = IoCManager.Resolve(); - foreach (var quant in reagentArgs.Source.Contents.ToArray()) - { - var proto = prototypeMan.Index(quant.Reagent.Prototype); - if (proto.Metabolisms != null && proto.Metabolisms.ContainsKey(Group)) - { - if (amount < 0) - reagentArgs.Source.RemoveReagent(quant.Reagent, amount); - if (amount > 0) - reagentArgs.Source.AddReagent(quant.Reagent, amount); - } - } - } - return; - } - - // TODO: Someone needs to figure out how to do this for non-reagent effects. - throw new NotImplementedException(); - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - if (Reagent is not null && prototype.TryIndex(Reagent, out ReagentPrototype? reagentProto)) - { - return Loc.GetString("reagent-effect-guidebook-adjust-reagent-reagent", - ("chance", Probability), - ("deltasign", MathF.Sign(Amount.Float())), - ("reagent", reagentProto.LocalizedName), - ("amount", MathF.Abs(Amount.Float()))); - } - else if (Group is not null && prototype.TryIndex(Group, out MetabolismGroupPrototype? groupProto)) - { - return Loc.GetString("reagent-effect-guidebook-adjust-reagent-group", - ("chance", Probability), - ("deltasign", MathF.Sign(Amount.Float())), - ("group", groupProto.LocalizedName), - ("amount", MathF.Abs(Amount.Float()))); - } - - throw new NotImplementedException(); - } - } -} - diff --git a/Content.Shared/EntityEffects/Effects/AdjustTemperature.cs b/Content.Shared/EntityEffects/Effects/AdjustTemperature.cs deleted file mode 100644 index 03dc226e93..0000000000 --- a/Content.Shared/EntityEffects/Effects/AdjustTemperature.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class AdjustTemperature : EventEntityEffect -{ - [DataField] - public float Amount; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-adjust-temperature", - ("chance", Probability), - ("deltasign", MathF.Sign(Amount)), - ("amount", MathF.Abs(Amount))); -} diff --git a/Content.Shared/EntityEffects/Effects/AdjustTemperatureEntityEffectsSystem.cs b/Content.Shared/EntityEffects/Effects/AdjustTemperatureEntityEffectsSystem.cs new file mode 100644 index 0000000000..adc465f341 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/AdjustTemperatureEntityEffectsSystem.cs @@ -0,0 +1,30 @@ +using Content.Shared.Temperature.Components; +using Content.Shared.Temperature.Systems; + +namespace Content.Shared.EntityEffects.Effects; + +// TODO: When we get a proper temperature/energy struct combine this with the solution temperature effect!!! +/// +/// Adjusts the temperature of this entity. +/// +/// +public sealed partial class AdjustTemperatureEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedTemperatureSystem _temperature = default!; + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var amount = args.Effect.Amount * args.Scale; + + _temperature.ChangeHeat(entity, amount, true, entity.Comp); + } +} + +/// +public sealed partial class AdjustTemperature : EntityEffectBase +{ + /// + /// Amount we're adjusting temperature by. + /// + [DataField] + public float Amount; +} diff --git a/Content.Shared/EntityEffects/Effects/AreaReactionEffect.cs b/Content.Shared/EntityEffects/Effects/AreaReactionEffect.cs deleted file mode 100644 index 45ed261a35..0000000000 --- a/Content.Shared/EntityEffects/Effects/AreaReactionEffect.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Content.Shared.Database; -using Content.Shared.FixedPoint; -using Robust.Shared.Audio; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Basically smoke and foam reactions. -/// -public sealed partial class AreaReactionEffect : EventEntityEffect -{ - /// - /// How many seconds will the effect stay, counting after fully spreading. - /// - [DataField("duration")] public float Duration = 10; - - /// - /// How many units of reaction for 1 smoke entity. - /// - [DataField] public FixedPoint2 OverflowThreshold = FixedPoint2.New(2.5); - - /// - /// The entity prototype that will be spawned as the effect. - /// - [DataField("prototypeId", required: true, customTypeSerializer:typeof(PrototypeIdSerializer))] - public string PrototypeId = default!; - - /// - /// Sound that will get played when this reaction effect occurs. - /// - [DataField("sound", required: true)] public SoundSpecifier Sound = default!; - - public override bool ShouldLog => true; - - protected override string ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-area-reaction", - ("duration", Duration) - ); - - public override LogImpact LogImpact => LogImpact.High; -} diff --git a/Content.Shared/EntityEffects/Effects/ArtifactDurabilityRestore.cs b/Content.Shared/EntityEffects/Effects/ArtifactDurabilityRestore.cs deleted file mode 100644 index 45ca740363..0000000000 --- a/Content.Shared/EntityEffects/Effects/ArtifactDurabilityRestore.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Content.Shared.Xenoarchaeology.Artifact.Components; -using Content.Shared.Xenoarchaeology.Artifact; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Restores durability in active artefact nodes. -/// -public sealed partial class ArtifactDurabilityRestore : EntityEffect -{ - /// - /// Amount of durability that will be restored per effect interaction. - /// - [DataField] - public int RestoredDurability = 1; - - public override void Effect(EntityEffectBaseArgs args) - { - var entMan = args.EntityManager; - var xenoArtifactSys = entMan.System(); - - if (!entMan.TryGetComponent(args.TargetEntity, out var xenoArtifact)) - return; - - foreach (var node in xenoArtifactSys.GetActiveNodes((args.TargetEntity, xenoArtifact))) - { - xenoArtifactSys.AdjustNodeDurability(node.Owner, RestoredDurability); - } - } - - protected override string ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return Loc.GetString("reagent-effect-guidebook-artifact-durability-restore", ("restored", RestoredDurability)); - } -} diff --git a/Content.Shared/EntityEffects/Effects/ArtifactEntityEffectsSystem.cs b/Content.Shared/EntityEffects/Effects/ArtifactEntityEffectsSystem.cs new file mode 100644 index 0000000000..563c053b36 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/ArtifactEntityEffectsSystem.cs @@ -0,0 +1,72 @@ +using Content.Shared.Popups; +using Content.Shared.Xenoarchaeology.Artifact; +using Content.Shared.Xenoarchaeology.Artifact.Components; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// Restores durability on this artifact +/// +/// +public sealed partial class ArtifactDurabilityRestoreEntityEffectsSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedXenoArtifactSystem _xenoArtifact = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var durability = args.Effect.RestoredDurability; + + foreach (var node in _xenoArtifact.GetActiveNodes(entity)) + { + _xenoArtifact.AdjustNodeDurability(node.Owner, durability); + } + } +} + +/// +/// Unlocks a node on this artifact. Only works this effect hasn't been applied before. +/// +/// +public sealed partial class ArtifactUnlockEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedXenoArtifactSystem _xenoArtifact = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (EnsureComp(entity, out var unlocking)) + { + if (unlocking.ArtifexiumApplied) + return; + + _popup.PopupEntity(Loc.GetString("artifact-activation-artifexium"), entity, PopupType.Medium); + } + else + { + _xenoArtifact.TriggerXenoArtifact(entity, null, force: true); + } + + _xenoArtifact.SetArtifexiumApplied((entity, unlocking), true); + } +} + +/// +public sealed partial class ArtifactDurabilityRestore : EntityEffectBase +{ + /// + /// Amount of durability that will be restored per effect interaction. + /// + [DataField] + public int RestoredDurability = 1; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-artifact-durability-restore", ("restored", RestoredDurability)); +} + +/// +public sealed partial class ArtifactUnlock : EntityEffectBase +{ + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-artifact-unlock", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/ArtifactUnlock.cs b/Content.Shared/EntityEffects/Effects/ArtifactUnlock.cs deleted file mode 100644 index 077e1ebfd2..0000000000 --- a/Content.Shared/EntityEffects/Effects/ArtifactUnlock.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Content.Shared.Xenoarchaeology.Artifact; -using Content.Shared.EntityEffects; -using Content.Shared.Popups; -using Content.Shared.Xenoarchaeology.Artifact.Components; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Sets an artifact into the unlocking state and marks the artifexium effect as true. -/// This is a very specific behavior intended for a specific chem. -/// -public sealed partial class ArtifactUnlock : EntityEffect -{ - public override void Effect(EntityEffectBaseArgs args) - { - var entMan = args.EntityManager; - var xenoArtifactSys = entMan.System(); - var popupSys = entMan.System(); - - if (!entMan.TryGetComponent(args.TargetEntity, out var xenoArtifact)) - return; - - if (!entMan.TryGetComponent(args.TargetEntity, out var unlocking)) - { - xenoArtifactSys.TriggerXenoArtifact((args.TargetEntity, xenoArtifact), null, force: true); - unlocking = entMan.EnsureComponent(args.TargetEntity); - } - else if (!unlocking.ArtifexiumApplied) - { - popupSys.PopupEntity(Loc.GetString("artifact-activation-artifexium"), args.TargetEntity, PopupType.Medium); - } - - if (unlocking.ArtifexiumApplied) - return; - - xenoArtifactSys.SetArtifexiumApplied((args.TargetEntity, unlocking), true); - } - - protected override string ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return Loc.GetString("reagent-effect-guidebook-artifact-unlock", ("chance", Probability)); - } -} diff --git a/Content.Shared/EntityEffects/Effects/Atmos/CreateGasEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Atmos/CreateGasEntityEffect.cs new file mode 100644 index 0000000000..aa5132e596 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Atmos/CreateGasEntityEffect.cs @@ -0,0 +1,35 @@ +using Content.Shared.Atmos; +using Content.Shared.Atmos.EntitySystems; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Atmos; + +/// +/// See serverside system. +/// +/// +public sealed partial class CreateGas : EntityEffectBase +{ + /// + /// The gas we're creating + /// + [DataField] + public Gas Gas; + + /// + /// Amount of moles we're creating + /// + [DataField] + public float Moles = 3f; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + var atmos = entSys.GetEntitySystem(); + var gasProto = atmos.GetGas(Gas); + + return Loc.GetString("entity-effect-guidebook-create-gas", + ("chance", Probability), + ("moles", Moles), + ("gas", gasProto.Name)); + } +} diff --git a/Content.Shared/EntityEffects/Effects/Atmos/ExtinguishEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Atmos/ExtinguishEntityEffectSystem.cs new file mode 100644 index 0000000000..b4b475ec59 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Atmos/ExtinguishEntityEffectSystem.cs @@ -0,0 +1,36 @@ +using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Atmos; + +/// +/// This raises an extinguish event on a given entity, reducing FireStacks. +/// The amount of FireStacks reduced is modified by scale. +/// +/// +public sealed partial class ExtinguishEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var ev = new ExtinguishEvent + { + FireStacksAdjustment = args.Effect.FireStacksAdjustment * args.Scale, + }; + + RaiseLocalEvent(entity, ref ev); + } +} + +/// +public sealed partial class Extinguish : EntityEffectBase +{ + /// + /// Amount of FireStacks reduced. + /// + [DataField] + public float FireStacksAdjustment = -1.5f; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-extinguish-reaction", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Atmos/FlammableEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Atmos/FlammableEntityEffect.cs new file mode 100644 index 0000000000..563201a40f --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Atmos/FlammableEntityEffect.cs @@ -0,0 +1,29 @@ +using Content.Shared.Database; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Atmos; + +/// +/// See serverside system. +/// +/// +public sealed partial class Flammable : EntityEffectBase +{ + /// + /// Fire stack multiplier applied on an entity, + /// unless that entity is already on fire and is not null. + /// + [DataField] + public float Multiplier = 0.05f; + + /// + /// Fire stack multiplier applied if the entity is already on fire. Defaults to if null. + /// + [DataField] + public float? MultiplierOnExisting; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-flammable-reaction", ("chance", Probability)); + + public override LogImpact? Impact => LogImpact.Low; +} diff --git a/Content.Shared/EntityEffects/Effects/Atmos/IgniteEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Atmos/IgniteEntityEffect.cs new file mode 100644 index 0000000000..7ca93e7c18 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Atmos/IgniteEntityEffect.cs @@ -0,0 +1,16 @@ +using Content.Shared.Database; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Atmos; + +/// +/// See serverside system +/// +/// +public sealed partial class Ignite : EntityEffectBase +{ + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-ignite", ("chance", Probability)); + + public override LogImpact? Impact => LogImpact.Medium; +} diff --git a/Content.Shared/EntityEffects/Effects/Body/CleanBloodstreamEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/CleanBloodstreamEntityEffectSystem.cs new file mode 100644 index 0000000000..402a50538a --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Body/CleanBloodstreamEntityEffectSystem.cs @@ -0,0 +1,43 @@ +using Content.Shared.Body.Components; +using Content.Shared.Body.Systems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Body; + +/// +/// Removes a given amount of chemicals from the bloodstream modified by scale. +/// Optionally ignores a given chemical. +/// +/// +public sealed partial class CleanBloodstreamEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedBloodstreamSystem _bloodstream = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var scale = args.Scale * args.Effect.CleanseRate; + + _bloodstream.FlushChemicals((entity, entity), args.Effect.Excluded, scale); + } +} + +/// +public sealed partial class CleanBloodstream : EntityEffectBase +{ + /// + /// Amount of reagent we're cleaning out of our bloodstream. + /// + [DataField] + public FixedPoint2 CleanseRate = 3.0f; + + /// + /// An optional chemical to ignore when doing removal. + /// + [DataField] + public ProtoId? Excluded; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-clean-bloodstream", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Body/EyeDamageEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/EyeDamageEntityEffectSystem.cs new file mode 100644 index 0000000000..29fd994e8c --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Body/EyeDamageEntityEffectSystem.cs @@ -0,0 +1,32 @@ +using Content.Shared.Eye.Blinding.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Body; + +/// +/// Modifies eye damage by a given amount, modified by scale, floored to an integer. +/// +/// +public sealed partial class EyeDamageEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly BlindableSystem _blindable = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var amount = (int) Math.Floor(args.Effect.Amount * args.Scale); + _blindable.AdjustEyeDamage(entity.Owner, amount); + } +} + +/// +public sealed partial class EyeDamage : EntityEffectBase +{ + /// + /// The amount of eye damage we're adding or removing + /// + [DataField] + public int Amount = -1; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-eye-damage", ("chance", Probability), ("deltasign", MathF.Sign(Amount))); +} diff --git a/Content.Shared/EntityEffects/Effects/Body/ModifyBleedEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/ModifyBleedEntityEffectSystem.cs new file mode 100644 index 0000000000..c684ffcdf2 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Body/ModifyBleedEntityEffectSystem.cs @@ -0,0 +1,32 @@ +using Content.Shared.Body.Components; +using Content.Shared.Body.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Body; + +/// +/// Modifies bleed by a given amount multiplied by scale. This can increase or decrease bleed. +/// +/// +public sealed partial class ModifyBleedEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedBloodstreamSystem _bloodstream = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _bloodstream.TryModifyBleedAmount(entity.AsNullable(), args.Effect.Amount * args.Scale); + } +} + +/// +public sealed partial class ModifyBleed : EntityEffectBase +{ + /// + /// Amount of bleed we're applying or removing if negative. + /// + [DataField] + public float Amount = -1.0f; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-modify-bleed-amount", ("chance", Probability), ("deltasign", MathF.Sign(Amount))); +} diff --git a/Content.Shared/EntityEffects/Effects/Body/ModifyBloodLevelEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/ModifyBloodLevelEntityEffectSystem.cs new file mode 100644 index 0000000000..43098c9ddd --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Body/ModifyBloodLevelEntityEffectSystem.cs @@ -0,0 +1,34 @@ +using Content.Shared.Body.Components; +using Content.Shared.Body.Systems; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Body; + +/// +/// Modifies the amount of blood in this entity's bloodstream by a given amount multiplied by scale. +/// This effect can increase or decrease blood level. +/// +/// +public sealed partial class ModifyBloodLevelEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedBloodstreamSystem _bloodstream = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _bloodstream.TryModifyBloodLevel(entity.AsNullable(), args.Effect.Amount * args.Scale); + } +} + +/// +public sealed partial class ModifyBloodLevel : EntityEffectBase +{ + /// + /// Amount of bleed we're applying or removing if negative. + /// + [DataField] + public FixedPoint2 Amount = 1.0f; + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-modify-blood-level", ("chance", Probability), ("deltasign", MathF.Sign(Amount.Float()))); +} diff --git a/Content.Shared/EntityEffects/Effects/Body/ModifyLungGasEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/ModifyLungGasEntityEffectSystem.cs new file mode 100644 index 0000000000..29fa1de98d --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Body/ModifyLungGasEntityEffectSystem.cs @@ -0,0 +1,33 @@ +using Content.Shared.Atmos; +using Content.Shared.Body.Components; + +namespace Content.Shared.EntityEffects.Effects.Body; + +/// +/// Adjust the amount of Moles stored in this set of lungs based on a given dictionary of gasses and ratios. +/// The amount of gas adjusted is modified by scale. +/// +/// +public sealed partial class ModifyLungGasEntityEffectSystem : EntityEffectSystem +{ + // TODO: This shouldn't be an entity effect, gasses should just metabolize and make a byproduct by default... + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var amount = args.Scale; + + foreach (var (gas, ratio) in args.Effect.Ratios) + { + var quantity = ratio * amount / Atmospherics.BreathMolesToReagentMultiplier; + if (quantity < 0) + quantity = Math.Max(quantity, -entity.Comp.Air[(int) gas]); + entity.Comp.Air.AdjustMoles(gas, quantity); + } + } +} + +/// +public sealed partial class ModifyLungGas : EntityEffectBase +{ + [DataField(required: true)] + public Dictionary Ratios = default!; +} diff --git a/Content.Shared/EntityEffects/Effects/Body/OxygenateEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Body/OxygenateEntityEffect.cs new file mode 100644 index 0000000000..9790fced4e --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Body/OxygenateEntityEffect.cs @@ -0,0 +1,14 @@ +namespace Content.Shared.EntityEffects.Effects.Body; + +/// +/// See serverside system. +/// +/// +public sealed partial class Oxygenate : EntityEffectBase +{ + /// + /// Factor of oxygenation per metabolized quantity. Lungs metabolize at about 50u per tick so we need an equal multiplier to cancel that out! + /// + [DataField] + public float Factor = 1f; +} diff --git a/Content.Shared/EntityEffects/Effects/Body/ReduceRottingEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/ReduceRottingEntityEffectSystem.cs new file mode 100644 index 0000000000..4fb41bd010 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Body/ReduceRottingEntityEffectSystem.cs @@ -0,0 +1,36 @@ +using Content.Shared.Atmos.Rotting; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Body; + +/// +/// Reduces the rotting timer on an entity by a number of seconds, modified by scale. +/// This cannot increase the amount of seconds a body has rotted. +/// +/// +public sealed partial class ReduceRottingEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedRottingSystem _rotting = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var amount = args.Effect.Seconds * args.Scale; + + _rotting.ReduceAccumulator(entity, amount); + } +} + +/// +public sealed partial class ReduceRotting : EntityEffectBase +{ + /// + /// Number of seconds removed from the rotting timer. + /// + [DataField] + public TimeSpan Seconds = TimeSpan.FromSeconds(10); + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-reduce-rotting", + ("chance", Probability), + ("time", Seconds.TotalSeconds)); +} diff --git a/Content.Shared/EntityEffects/Effects/Body/SatiateEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/SatiateEntityEffectSystem.cs new file mode 100644 index 0000000000..167e7e540d --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Body/SatiateEntityEffectSystem.cs @@ -0,0 +1,63 @@ +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Body; + +// TODO: These systems are in the same file since satiation should be one system instead of two. Combine these when that happens. +// TODO: Arguably oxygen saturation should also be added here... +/// +/// Modifies the thirst level of a given entity, multiplied by scale. +/// +/// +public sealed partial class SatiateThirstEntityEffectsSystem : EntityEffectSystem +{ + [Dependency] private readonly ThirstSystem _thirst = default!; + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _thirst.ModifyThirst(entity, entity.Comp, args.Effect.Factor * args.Scale); + } +} + +/// +/// Modifies the hunger level of a given entity, multiplied by scale. +/// +/// +public sealed partial class SatiateHungerEntityEffectsSystem : EntityEffectSystem +{ + [Dependency] private readonly HungerSystem _hunger = default!; + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _hunger.ModifyHunger(entity, args.Effect.Factor * args.Scale, entity.Comp); + } +} + +/// +/// A type of made for satiation effects. +/// +/// The effect inheriting this BaseEffect +/// +public abstract partial class Satiate : EntityEffectBase where T : EntityEffectBase +{ + public const float AverageSatiation = 3f; // Magic number. Not sure how it was calculated since I didn't make it. + + /// + /// Change in satiation. + /// + [DataField] + public float Factor = 1.5f; +} + +/// +public sealed partial class SatiateThirst : Satiate +{ + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-satiate-thirst", ("chance", Probability), ("relative", Factor / AverageSatiation)); +} + +/// +public sealed partial class SatiateHunger : Satiate +{ + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-satiate-hunger", ("chance", Probability), ("relative", Factor / AverageSatiation)); +} diff --git a/Content.Shared/EntityEffects/Effects/Body/VomitEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/VomitEntityEffectSystem.cs new file mode 100644 index 0000000000..02747057c5 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Body/VomitEntityEffectSystem.cs @@ -0,0 +1,37 @@ +using Content.Shared.Medical; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Body; + +/// +/// Makes an entity vomit and reduces hunger and thirst by a given amount, modified by scale. +/// +/// +public sealed partial class VomitEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly VomitSystem _vomit = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _vomit.Vomit(entity.Owner, args.Effect.ThirstAmount * args.Scale, args.Effect.HungerAmount * args.Scale); + } +} + +/// +public sealed partial class Vomit : EntityEffectBase +{ + /// + /// How much we adjust our thirst after vomiting. + /// + [DataField] + public float ThirstAmount = -8f; + + /// + /// How much we adjust our hunger after vomiting. + /// + [DataField] + public float HungerAmount = -8f; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-vomit", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/BasePlantAdjustAttributeEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/BasePlantAdjustAttributeEntityEffect.cs new file mode 100644 index 0000000000..9b235e68d4 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/BasePlantAdjustAttributeEntityEffect.cs @@ -0,0 +1,37 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +/// +/// A type of which modifies the attribute of a Seed in a PlantHolder. +/// These are not modified by scale as botany has no concept of scale. +/// +/// The effect inheriting this BaseEffect +/// +public abstract partial class BasePlantAdjustAttribute : EntityEffectBase where T : BasePlantAdjustAttribute +{ + /// + /// How much we're adjusting the given attribute by. + /// + [DataField] + public float Amount { get; protected set; } = 1; + + /// + /// Localisation key for the name of the adjusted attribute. Used for guidebook descriptions. + /// + [DataField] + public abstract string GuidebookAttributeName { get; set; } + + /// + /// Whether the attribute in question is a good thing. Used for guidebook descriptions to determine the color of the number. + /// + [DataField] + public virtual bool GuidebookIsAttributePositive { get; protected set; } = true; + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-plant-attribute", + ("attribute", Loc.GetString(GuidebookAttributeName)), + ("amount", Amount.ToString("0.00")), + ("positive", GuidebookIsAttributePositive), + ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustHealth.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustHealth.cs new file mode 100644 index 0000000000..069cb320a9 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustHealth.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustHealth : BasePlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-health"; +} + diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationLevel.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationLevel.cs new file mode 100644 index 0000000000..32e419e291 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationLevel.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustMutationLevel : BasePlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-mutation-level"; +} + diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationMod.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationMod.cs new file mode 100644 index 0000000000..4ea695d135 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationMod.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustMutationMod : BasePlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-mutation-mod"; +} + diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustNutrition.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustNutrition.cs new file mode 100644 index 0000000000..6f53076fb3 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustNutrition.cs @@ -0,0 +1,6 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustNutrition : BasePlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-nutrition"; +} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustPests.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPests.cs similarity index 53% rename from Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustPests.cs rename to Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPests.cs index 18c00550d5..c1661ec89e 100644 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustPests.cs +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPests.cs @@ -1,6 +1,6 @@ -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; -public sealed partial class PlantAdjustPests : PlantAdjustAttribute +public sealed partial class PlantAdjustPests : BasePlantAdjustAttribute { public override string GuidebookAttributeName { get; set; } = "plant-attribute-pests"; public override bool GuidebookIsAttributePositive { get; protected set; } = false; diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotency.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotency.cs new file mode 100644 index 0000000000..4f42adf3e6 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotency.cs @@ -0,0 +1,9 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +/// +/// Handles increase or decrease of plant potency. +/// +public sealed partial class PlantAdjustPotency : BasePlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-potency"; +} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustToxins.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustToxins.cs similarity index 53% rename from Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustToxins.cs rename to Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustToxins.cs index 9123b5847d..04eccb03ec 100644 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustToxins.cs +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustToxins.cs @@ -1,8 +1,9 @@ -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; -public sealed partial class PlantAdjustToxins : PlantAdjustAttribute +public sealed partial class PlantAdjustToxins : BasePlantAdjustAttribute { public override string GuidebookAttributeName { get; set; } = "plant-attribute-toxins"; + public override bool GuidebookIsAttributePositive { get; protected set; } = false; } diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWater.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWater.cs new file mode 100644 index 0000000000..1d6ef8ea09 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWater.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustWater : BasePlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-water"; +} + diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustWeeds.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWeeds.cs similarity index 53% rename from Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustWeeds.cs rename to Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWeeds.cs index 70ff0747dd..e6be25d8c9 100644 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustWeeds.cs +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWeeds.cs @@ -1,6 +1,6 @@ -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; -public sealed partial class PlantAdjustWeeds : PlantAdjustAttribute +public sealed partial class PlantAdjustWeeds : BasePlantAdjustAttribute { public override string GuidebookAttributeName { get; set; } = "plant-attribute-weeds"; public override bool GuidebookIsAttributePositive { get; protected set; } = false; diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowth.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowth.cs new file mode 100644 index 0000000000..76466f544d --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowth.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAffectGrowth : BasePlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-growth"; +} + diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStat.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStat.cs new file mode 100644 index 0000000000..dcea56baad --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStat.cs @@ -0,0 +1,21 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantChangeStat : EntityEffectBase +{ + /// + /// This is the worst thing in the code base. + /// It's meant to be generic and expandable I guess? But it's looking for a specific datafield and then + /// sending it into an if else if else if statement that filters by object type and randomly flips bits. + /// + [DataField (required: true)] + public string TargetValue = string.Empty; + + [DataField] + public float MinValue; + + [DataField] + public float MaxValue; + + [DataField] + public int Steps; +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadone.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadone.cs new file mode 100644 index 0000000000..7dcea240a6 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadone.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantCryoxadone : EntityEffectBase +{ + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-plant-cryoxadone", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeeds.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeeds.cs new file mode 100644 index 0000000000..d45c0369f7 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeeds.cs @@ -0,0 +1,12 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +/// +/// Handles removal of seeds on a plant. +/// +public sealed partial class PlantDestroySeeds : EntityEffectBase +{ + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-plant-seeds-remove", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamine.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamine.cs new file mode 100644 index 0000000000..4355a44593 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamine.cs @@ -0,0 +1,11 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantDiethylamine : EntityEffectBase +{ + /// + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-plant-diethylamine", ("chance", Probability)); +} + diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximine.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximine.cs new file mode 100644 index 0000000000..b56b99038b --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximine.cs @@ -0,0 +1,10 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantPhalanximine : EntityEffectBase +{ + /// + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-plant-phalanximine", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeeds.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeeds.cs new file mode 100644 index 0000000000..63b06b0ad1 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeeds.cs @@ -0,0 +1,12 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +/// +/// Handles restoral of seeds on a plant. +/// +public sealed partial class PlantRestoreSeeds : EntityEffectBase +{ + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-plant-seeds-add", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvest.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvest.cs new file mode 100644 index 0000000000..77bde39b12 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvest.cs @@ -0,0 +1,22 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class RobustHarvest : EntityEffectBase +{ + [DataField] + public int PotencyLimit = 50; + + [DataField] + public int PotencyIncrease = 3; + + [DataField] + public int PotencySeedlessThreshold = 30; + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-plant-robust-harvest", + ("seedlesstreshold", PotencySeedlessThreshold), + ("limit", PotencyLimit), + ("increase", PotencyIncrease), + ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffect.cs new file mode 100644 index 0000000000..6a3d13e0c6 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffect.cs @@ -0,0 +1,16 @@ +using Content.Shared.Random; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Botany; + +/// +/// See serverside system. +/// +public sealed partial class PlantMutateChemicals : EntityEffectBase +{ + /// + /// The Reagent list this mutation draws from. + /// + [DataField] + public ProtoId RandomPickBotanyReagent = "RandomPickBotanyReagent"; +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffect.cs new file mode 100644 index 0000000000..c617c05b33 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffect.cs @@ -0,0 +1,22 @@ +namespace Content.Shared.EntityEffects.Effects.Botany; + +/// +/// See serverside system. +/// +public sealed partial class PlantMutateConsumeGases : EntityEffectBase +{ + [DataField] + public float MinValue = 0.01f; + + [DataField] + public float MaxValue = 0.5f; +} + +public sealed partial class PlantMutateExudeGases : EntityEffectBase +{ + [DataField] + public float MinValue = 0.01f; + + [DataField] + public float MaxValue = 0.5f; +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffect.cs new file mode 100644 index 0000000000..3602453b16 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffect.cs @@ -0,0 +1,6 @@ +namespace Content.Shared.EntityEffects.Effects.Botany; + +/// +/// See serverside system. +/// +public sealed partial class PlantMutateHarvest : EntityEffectBase; diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantMutateSpeciesChangeEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantMutateSpeciesChangeEntityEffect.cs new file mode 100644 index 0000000000..91e8947044 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantMutateSpeciesChangeEntityEffect.cs @@ -0,0 +1,6 @@ +namespace Content.Shared.EntityEffects.Effects.Botany; + +/// +/// See serverside system. +/// +public sealed partial class PlantMutateSpeciesChange : EntityEffectBase; diff --git a/Content.Shared/EntityEffects/Effects/CauseZombieInfection.cs b/Content.Shared/EntityEffects/Effects/CauseZombieInfection.cs deleted file mode 100644 index 3f8c58b74c..0000000000 --- a/Content.Shared/EntityEffects/Effects/CauseZombieInfection.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class CauseZombieInfection : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-cause-zombie-infection", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/ChemCleanBloodstream.cs b/Content.Shared/EntityEffects/Effects/ChemCleanBloodstream.cs deleted file mode 100644 index 98181b8667..0000000000 --- a/Content.Shared/EntityEffects/Effects/ChemCleanBloodstream.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Basically smoke and foam reactions. -/// -public sealed partial class ChemCleanBloodstream : EventEntityEffect -{ - [DataField] - public float CleanseRate = 3.0f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-chem-clean-bloodstream", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/ChemHealEyeDamage.cs b/Content.Shared/EntityEffects/Effects/ChemHealEyeDamage.cs deleted file mode 100644 index 83b2aa96e5..0000000000 --- a/Content.Shared/EntityEffects/Effects/ChemHealEyeDamage.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Content.Shared.Eye.Blinding.Systems; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Heal or apply eye damage -/// -public sealed partial class ChemHealEyeDamage : EntityEffect -{ - /// - /// How much eye damage to add. - /// - [DataField] - public int Amount = -1; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-cure-eye-damage", ("chance", Probability), ("deltasign", MathF.Sign(Amount))); - - public override void Effect(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) - if (reagentArgs.Scale != 1f) // huh? - return; - - args.EntityManager.EntitySysManager.GetEntitySystem().AdjustEyeDamage(args.TargetEntity, Amount); - } -} diff --git a/Content.Shared/EntityEffects/Effects/ChemVomit.cs b/Content.Shared/EntityEffects/Effects/ChemVomit.cs deleted file mode 100644 index 1cd6b2552c..0000000000 --- a/Content.Shared/EntityEffects/Effects/ChemVomit.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Forces you to vomit. -/// -public sealed partial class ChemVomit : EventEntityEffect -{ - /// How many units of thirst to add each time we vomit - [DataField] - public float ThirstAmount = -8f; - /// How many units of hunger to add each time we vomit - [DataField] - public float HungerAmount = -8f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-chem-vomit", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/CreateEntityReactionEffect.cs b/Content.Shared/EntityEffects/Effects/CreateEntityReactionEffect.cs deleted file mode 100644 index 33173b1737..0000000000 --- a/Content.Shared/EntityEffects/Effects/CreateEntityReactionEffect.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.EntityEffects.Effects; - -[DataDefinition] -public sealed partial class CreateEntityReactionEffect : EventEntityEffect -{ - /// - /// What entity to create. - /// - [DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string Entity = default!; - - /// - /// How many entities to create per unit reaction. - /// - [DataField] - public uint Number = 1; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-create-entity-reaction-effect", - ("chance", Probability), - ("entname", IoCManager.Resolve().Index(Entity).Name), - ("amount", Number)); -} diff --git a/Content.Shared/EntityEffects/Effects/CreateGas.cs b/Content.Shared/EntityEffects/Effects/CreateGas.cs deleted file mode 100644 index 75d554cdb3..0000000000 --- a/Content.Shared/EntityEffects/Effects/CreateGas.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Content.Shared.Atmos; -using Content.Shared.Atmos.EntitySystems; -using Content.Shared.Database; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class CreateGas : EventEntityEffect -{ - [DataField(required: true)] - public Gas Gas = default!; - - /// - /// For each unit consumed, how many moles of gas should be created? - /// - [DataField] - public float Multiplier = 3f; - - public override bool ShouldLog => true; - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - var atmos = entSys.GetEntitySystem(); - var gasProto = atmos.GetGas(Gas); - - return Loc.GetString("reagent-effect-guidebook-create-gas", - ("chance", Probability), - ("moles", Multiplier), - ("gas", gasProto.Name)); - } - - public override LogImpact LogImpact => LogImpact.High; -} diff --git a/Content.Shared/EntityEffects/Effects/CureZombieInfection.cs b/Content.Shared/EntityEffects/Effects/CureZombieInfection.cs deleted file mode 100644 index dd2d21854c..0000000000 --- a/Content.Shared/EntityEffects/Effects/CureZombieInfection.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class CureZombieInfection : EventEntityEffect -{ - [DataField] - public bool Innoculate; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - if(Innoculate) - return Loc.GetString("reagent-effect-guidebook-innoculate-zombie-infection", ("chance", Probability)); - - return Loc.GetString("reagent-effect-guidebook-cure-zombie-infection", ("chance", Probability)); - } -} - diff --git a/Content.Shared/EntityEffects/Effects/Drunk.cs b/Content.Shared/EntityEffects/Effects/Drunk.cs deleted file mode 100644 index aa15df8f3d..0000000000 --- a/Content.Shared/EntityEffects/Effects/Drunk.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Content.Shared.Drunk; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class Drunk : EntityEffect -{ - /// - /// BoozePower is how long each metabolism cycle will make the drunk effect last for. - /// - [DataField] - public TimeSpan BoozePower = TimeSpan.FromSeconds(3f); - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-drunk", ("chance", Probability)); - - public override void Effect(EntityEffectBaseArgs args) - { - var boozePower = BoozePower; - - if (args is EntityEffectReagentArgs reagentArgs) - boozePower *= reagentArgs.Scale.Float(); - - var drunkSys = args.EntityManager.EntitySysManager.GetEntitySystem(); - drunkSys.TryApplyDrunkenness(args.TargetEntity, boozePower); - } -} diff --git a/Content.Shared/EntityEffects/Effects/Electrocute.cs b/Content.Shared/EntityEffects/Effects/Electrocute.cs deleted file mode 100644 index 32e0ff1172..0000000000 --- a/Content.Shared/EntityEffects/Effects/Electrocute.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Content.Shared.Electrocution; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class Electrocute : EntityEffect -{ - [DataField] public int ElectrocuteTime = 2; - - [DataField] public int ElectrocuteDamageScale = 5; - - /// - /// true - refresh electrocute time, false - accumulate electrocute time - /// - [DataField] public bool Refresh = true; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-electrocute", ("chance", Probability), ("time", ElectrocuteTime)); - - public override bool ShouldLog => true; - - public override void Effect(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) - { - reagentArgs.EntityManager.System().TryDoElectrocution(reagentArgs.TargetEntity, null, - Math.Max((reagentArgs.Quantity * ElectrocuteDamageScale).Int(), 1), TimeSpan.FromSeconds(ElectrocuteTime), Refresh, ignoreInsulation: true); - - if (reagentArgs.Reagent != null) - reagentArgs.Source?.RemoveReagent(reagentArgs.Reagent.ID, reagentArgs.Quantity); - } else - { - args.EntityManager.System().TryDoElectrocution(args.TargetEntity, null, - Math.Max(ElectrocuteDamageScale, 1), TimeSpan.FromSeconds(ElectrocuteTime), Refresh, ignoreInsulation: true); - } - } -} diff --git a/Content.Shared/EntityEffects/Effects/Emote.cs b/Content.Shared/EntityEffects/Effects/Emote.cs deleted file mode 100644 index 494dc502f5..0000000000 --- a/Content.Shared/EntityEffects/Effects/Emote.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Content.Shared.Chat.Prototypes; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Tries to force someone to emote (scream, laugh, etc). Still respects whitelists/blacklists and other limits unless specially forced. -/// -public sealed partial class Emote : EventEntityEffect -{ - /// - /// The emote the entity will preform. - /// - [DataField("emote", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string EmoteId; - - /// - /// If the emote should be recorded in chat. - /// - [DataField] - public bool ShowInChat; - - /// - /// If the forced emote will be listed in the guidebook. - /// - [DataField] - public bool ShowInGuidebook; - - /// - /// If true, the entity will preform the emote even if they normally can't. - /// - [DataField] - public bool Force = false; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - if (!ShowInGuidebook) - return null; // JUSTIFICATION: Emoting is mostly flavor, so same reason popup messages are not in here. - - var emotePrototype = prototype.Index(EmoteId); - return Loc.GetString("reagent-effect-guidebook-emote", ("chance", Probability), ("emote", Loc.GetString(emotePrototype.Name))); - } -} diff --git a/Content.Shared/EntityEffects/Effects/EmoteEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/EmoteEntityEffectSystem.cs new file mode 100644 index 0000000000..d8474e4163 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/EmoteEntityEffectSystem.cs @@ -0,0 +1,58 @@ +using Content.Shared.Chat; +using Content.Shared.Chat.Prototypes; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// Makes this entity emote. +/// +/// +public sealed partial class EmoteEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedChatSystem _chat = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (args.Effect.ShowInChat) + _chat.TryEmoteWithChat(entity, args.Effect.EmoteId, ChatTransmitRange.GhostRangeLimit, forceEmote: args.Effect.Force); + else + _chat.TryEmoteWithoutChat(entity, args.Effect.EmoteId); + } +} + +/// +public sealed partial class Emote : EntityEffectBase +{ + /// + /// The emote the entity will preform. + /// + [DataField("emote", required: true)] + public ProtoId EmoteId; + + /// + /// If the emote should be recorded in chat. + /// + [DataField] + public bool ShowInChat; + + /// + /// If the forced emote will be listed in the guidebook. + /// + [DataField] + public bool ShowInGuidebook; + + /// + /// If true, the entity will preform the emote even if they normally can't. + /// + [DataField] + public bool Force; + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + if (!ShowInGuidebook || !prototype.Resolve(EmoteId, out var emote)) + return null; // JUSTIFICATION: Emoting is mostly flavor, so same reason popup messages are not in here. + + return Loc.GetString("entity-effect-guidebook-emote", ("chance", Probability), ("emote", Loc.GetString(emote.Name))); + } +} diff --git a/Content.Shared/EntityEffects/Effects/EmpReactionEffect.cs b/Content.Shared/EntityEffects/Effects/EmpReactionEffect.cs deleted file mode 100644 index 0f9eacc58d..0000000000 --- a/Content.Shared/EntityEffects/Effects/EmpReactionEffect.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -[DataDefinition] -public sealed partial class EmpReactionEffect : EventEntityEffect -{ - /// - /// Impulse range per unit of quantity - /// - [DataField("rangePerUnit")] - public float EmpRangePerUnit = 0.5f; - - /// - /// Maximum impulse range - /// - [DataField("maxRange")] - public float EmpMaxRange = 10; - - /// - /// How much energy will be drain from sources - /// - [DataField] - public float EnergyConsumption = 12500; - - /// - /// Amount of time entities will be disabled - /// - [DataField("duration")] - public TimeSpan DisableDuration = TimeSpan.FromSeconds(15); - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-emp-reaction-effect", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/EntitySpawning/BaseSpawnEntityEntityEffect.cs b/Content.Shared/EntityEffects/Effects/EntitySpawning/BaseSpawnEntityEntityEffect.cs new file mode 100644 index 0000000000..bd8d68813e --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/EntitySpawning/BaseSpawnEntityEntityEffect.cs @@ -0,0 +1,39 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.EntitySpawning; + +/// +/// A type of for effects that spawn entities by prototype. +/// +/// The entity effect inheriting this BaseEffect +/// +public abstract partial class BaseSpawnEntityEntityEffect : EntityEffectBase where T : BaseSpawnEntityEntityEffect +{ + /// + /// Amount of entities we're spawning + /// + [DataField] + public int Number = 1; + + /// + /// Prototype of the entity we're spawning + /// + [DataField (required: true)] + public EntProtoId Entity; + + /// + /// Whether this spawning is predicted. Set false to not predict the spawn. + /// Entities with animations or that have random elements when spawned should set this to false. + /// + [DataField] + public bool Predicted = true; + + /// + public override bool Scaling => true; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-spawn-entity", + ("chance", Probability), + ("entname", IoCManager.Resolve().Index(Entity).Name), + ("amount", Number)); +} diff --git a/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityEntityEffectSystem.cs new file mode 100644 index 0000000000..9887ecdc60 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityEntityEffectSystem.cs @@ -0,0 +1,37 @@ +using Robust.Shared.Network; + +namespace Content.Shared.EntityEffects.Effects.EntitySpawning; + +/// +/// Spawns a number of entities of a given prototype at the coordinates of this entity. +/// Amount is modified by scale. +/// +/// +public sealed partial class SpawnEntityEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly INetManager _net = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var quantity = args.Effect.Number * (int)Math.Floor(args.Scale); + var proto = args.Effect.Entity; + + if (args.Effect.Predicted) + { + for (var i = 0; i < quantity; i++) + { + PredictedSpawnNextToOrDrop(proto, entity, entity.Comp); + } + } + else if (_net.IsServer) + { + for (var i = 0; i < quantity; i++) + { + SpawnNextToOrDrop(proto, entity, entity.Comp); + } + } + } +} + +/// +public sealed partial class SpawnEntity : BaseSpawnEntityEntityEffect; diff --git a/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityInContainerEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityInContainerEntityEffectSystem.cs new file mode 100644 index 0000000000..6909d503df --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityInContainerEntityEffectSystem.cs @@ -0,0 +1,51 @@ +using Robust.Shared.Containers; +using Robust.Shared.Network; + +namespace Content.Shared.EntityEffects.Effects.EntitySpawning; + +/// +/// Spawns a given number of entities of a given prototype in a specified container owned by this entity. +/// Returns if the prototype cannot spawn in the specified container. +/// Amount is modified by scale. +/// +/// +public sealed partial class SpawnEntityInContainerEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly INetManager _net = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var quantity = args.Effect.Number * (int)Math.Floor(args.Scale); + var proto = args.Effect.Entity; + var container = args.Effect.ContainerName; + + if (args.Effect.Predicted) + { + for (var i = 0; i < quantity; i++) + { + // Stop trying to spawn if it fails + if (!PredictedTrySpawnInContainer(proto, entity, container, out _, entity.Comp)) + return; + } + } + else if (_net.IsServer) + { + for (var i = 0; i < quantity; i++) + { + // Stop trying to spawn if it fails + if (!TrySpawnInContainer(proto, entity, container, out _, entity.Comp)) + return; + } + } + } +} + +/// +public sealed partial class SpawnEntityInContainer : BaseSpawnEntityEntityEffect +{ + /// + /// Name of the container we're trying to spawn into. + /// + [DataField(required: true)] + public string ContainerName; +} diff --git a/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityInContainerOrDropEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityInContainerOrDropEntityEffectSystem.cs new file mode 100644 index 0000000000..e5b1e119c1 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityInContainerOrDropEntityEffectSystem.cs @@ -0,0 +1,49 @@ +using Robust.Shared.Containers; +using Robust.Shared.Network; + +namespace Content.Shared.EntityEffects.Effects.EntitySpawning; + +/// +/// Spawns a given number of entities of a given prototype in a specified container owned by this entity. +/// Acts like if it cannot spawn the prototype in the specified container. +/// Amount is modified by scale. +/// +/// +public sealed partial class SpawnEntityInContainerOrDropEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly INetManager _net = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var quantity = args.Effect.Number * (int)Math.Floor(args.Scale); + var proto = args.Effect.Entity; + var container = args.Effect.ContainerName; + + var xform = Transform(entity); + + if (args.Effect.Predicted) + { + for (var i = 0; i < quantity; i++) + { + PredictedSpawnInContainerOrDrop(proto, entity, container, xform, entity.Comp); + } + } + else if (_net.IsServer) + { + for (var i = 0; i < quantity; i++) + { + SpawnInContainerOrDrop(proto, entity, container, xform, entity.Comp); + } + } + } +} + +/// +public sealed partial class SpawnEntityInContainerOrDrop : BaseSpawnEntityEntityEffect +{ + /// + /// Name of the container we're trying to spawn into. + /// + [DataField(required: true)] + public string ContainerName; +} diff --git a/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityInInventoryEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityInInventoryEntityEffectSystem.cs new file mode 100644 index 0000000000..7514dae4e9 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityInInventoryEntityEffectSystem.cs @@ -0,0 +1,35 @@ +using Content.Shared.Inventory; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.EntitySpawning; + +/// +/// Spawns an entity of a given prototype in a specified inventory slot owned by this entity. +/// Fails if it cannot spawn the entity in the given slot. +/// +/// +public sealed partial class SpawnEntityInInventoryEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly InventorySystem _inventory = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _inventory.SpawnItemInSlot(entity, args.Effect.Slot, args.Effect.Entity); + } +} + +/// +public sealed partial class SpawnEntityInInventory : EntityEffectBase +{ + /// + /// Name of the slot we're spawning the item into. + /// + [DataField(required: true)] + public string Slot = string.Empty; // Rider is drunk and keeps yelling at me to fill this out or make required: true but, it is required true so it's just being an asshole. + + /// + /// Prototype ID of item to spawn. + /// + [DataField(required: true)] + public EntProtoId Entity; +} diff --git a/Content.Shared/EntityEffects/Effects/EvenHealthChange.cs b/Content.Shared/EntityEffects/Effects/EvenHealthChange.cs deleted file mode 100644 index 968d559939..0000000000 --- a/Content.Shared/EntityEffects/Effects/EvenHealthChange.cs +++ /dev/null @@ -1,139 +0,0 @@ -using Content.Shared.Damage; -using Content.Shared.Damage.Prototypes; -using Content.Shared.EntityEffects; -using Content.Shared.FixedPoint; -using Content.Shared.Localizations; -using Robust.Shared.Prototypes; -using Robust.Shared.Utility; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Version of that distributes the healing to groups -/// -public sealed partial class EvenHealthChange : EntityEffect -{ - /// - /// Damage to heal, collected into entire damage groups. - /// - [DataField(required: true)] - public Dictionary, FixedPoint2> Damage = new(); - - /// - /// Should this effect scale the damage by the amount of chemical in the solution? - /// Useful for touch reactions, like styptic powder or acid. - /// Only usable if the EntityEffectBaseArgs is an EntityEffectReagentArgs. - /// - [DataField] - public bool ScaleByQuantity; - - /// - /// Should this effect ignore damage modifiers? - /// - [DataField] - public bool IgnoreResistances = true; - - protected override string ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - var damages = new List(); - var heals = false; - var deals = false; - - var damagableSystem = entSys.GetEntitySystem(); - var universalReagentDamageModifier = damagableSystem.UniversalReagentDamageModifier; - var universalReagentHealModifier = damagableSystem.UniversalReagentHealModifier; - - foreach (var (group, amount) in Damage) - { - var groupProto = prototype.Index(group); - - var sign = FixedPoint2.Sign(amount); - var mod = 1f; - - if (sign < 0) - { - heals = true; - mod = universalReagentHealModifier; - } - else if (sign > 0) - { - deals = true; - mod = universalReagentDamageModifier; - } - - damages.Add( - Loc.GetString("health-change-display", - ("kind", groupProto.LocalizedName), - ("amount", MathF.Abs(amount.Float() * mod)), - ("deltasign", sign) - )); - } - - var healsordeals = heals ? (deals ? "both" : "heals") : (deals ? "deals" : "none"); - return Loc.GetString("reagent-effect-guidebook-even-health-change", - ("chance", Probability), - ("changes", ContentLocalizationManager.FormatList(damages)), - ("healsordeals", healsordeals)); - } - - public override void Effect(EntityEffectBaseArgs args) - { - if (!args.EntityManager.TryGetComponent(args.TargetEntity, out var damageable)) - return; - - var protoMan = IoCManager.Resolve(); - - var scale = FixedPoint2.New(1); - - if (args is EntityEffectReagentArgs reagentArgs) - { - scale = ScaleByQuantity ? reagentArgs.Quantity * reagentArgs.Scale : reagentArgs.Scale; - } - - var damagableSystem = args.EntityManager.System(); - var universalReagentDamageModifier = damagableSystem.UniversalReagentDamageModifier; - var universalReagentHealModifier = damagableSystem.UniversalReagentHealModifier; - - var dspec = new DamageSpecifier(); - - foreach (var (group, amount) in Damage) - { - var groupProto = protoMan.Index(group); - var groupDamage = new Dictionary(); - foreach (var damageId in groupProto.DamageTypes) - { - var damageAmount = damageable.Damage.DamageDict.GetValueOrDefault(damageId); - if (damageAmount != FixedPoint2.Zero) - groupDamage.Add(damageId, damageAmount); - } - - var sum = groupDamage.Values.Sum(); - foreach (var (damageId, damageAmount) in groupDamage) - { - var existing = dspec.DamageDict.GetOrNew(damageId); - dspec.DamageDict[damageId] = existing + damageAmount / sum * amount; - } - } - - if (universalReagentDamageModifier != 1 || universalReagentHealModifier != 1) - { - foreach (var (type, val) in dspec.DamageDict) - { - if (val < 0f) - { - dspec.DamageDict[type] = val * universalReagentHealModifier; - } - if (val > 0f) - { - dspec.DamageDict[type] = val * universalReagentDamageModifier; - } - } - } - - damagableSystem.TryChangeDamage( - args.TargetEntity, - dspec * scale, - IgnoreResistances, - interruptsDoAfters: false); - } -} diff --git a/Content.Shared/EntityEffects/Effects/EvenHealthChangeEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/EvenHealthChangeEntityEffectSystem.cs new file mode 100644 index 0000000000..b26b801264 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/EvenHealthChangeEntityEffectSystem.cs @@ -0,0 +1,115 @@ +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.Localizations; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// Evenly adjust the damage types in a damage group by up to a specified total on this entity. +/// Total adjustment is modified by scale. +/// +/// +public sealed partial class EvenHealthChangeEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var damageSpec = new DamageSpecifier(); + + foreach (var (group, amount) in args.Effect.Damage) + { + var groupProto = _proto.Index(group); + var groupDamage = new Dictionary(); + foreach (var damageId in groupProto.DamageTypes) + { + var damageAmount = entity.Comp.Damage.DamageDict.GetValueOrDefault(damageId); + if (damageAmount != FixedPoint2.Zero) + groupDamage.Add(damageId, damageAmount); + } + + var sum = groupDamage.Values.Sum(); + foreach (var (damageId, damageAmount) in groupDamage) + { + var existing = damageSpec.DamageDict.GetOrNew(damageId); + damageSpec.DamageDict[damageId] = existing + damageAmount / sum * amount; + } + } + + damageSpec *= args.Scale; + + _damageable.TryChangeDamage( + entity.AsNullable(), + damageSpec, + args.Effect.IgnoreResistances, + interruptsDoAfters: false); + } +} + +/// +public sealed partial class EvenHealthChange : EntityEffectBase +{ + /// + /// Damage to heal, collected into entire damage groups. + /// + [DataField(required: true)] + public Dictionary, FixedPoint2> Damage = new(); + + /// + /// Should this effect ignore damage modifiers? + /// + [DataField] + public bool IgnoreResistances = true; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + var damages = new List(); + var heals = false; + var deals = false; + + var damagableSystem = entSys.GetEntitySystem(); + var universalReagentDamageModifier = damagableSystem.UniversalReagentDamageModifier; + var universalReagentHealModifier = damagableSystem.UniversalReagentHealModifier; + + foreach (var (group, amount) in Damage) + { + var groupProto = prototype.Index(group); + + var sign = FixedPoint2.Sign(amount); + float mod; + + switch (sign) + { + case < 0: + heals = true; + mod = universalReagentHealModifier; + break; + case > 0: + deals = true; + mod = universalReagentDamageModifier; + break; + default: + continue; // Don't need to show damage types of 0... + } + + damages.Add( + Loc.GetString("health-change-display", + ("kind", groupProto.LocalizedName), + ("amount", MathF.Abs(amount.Float() * mod)), + ("deltasign", sign) + )); + } + + var healsordeals = heals ? deals ? "both" : "heals" : deals ? "deals" : "none"; + return Loc.GetString("entity-effect-guidebook-even-health-change", + ("chance", Probability), + ("changes", ContentLocalizationManager.FormatList(damages)), + ("healsordeals", healsordeals)); + } +} diff --git a/Content.Shared/EntityEffects/Effects/ExplodeEntityEffect.cs b/Content.Shared/EntityEffects/Effects/ExplodeEntityEffect.cs new file mode 100644 index 0000000000..ecad4e47ad --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/ExplodeEntityEffect.cs @@ -0,0 +1,33 @@ +using Content.Shared.Database; +using Content.Shared.EntityEffects.Effects.Transform; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// +public sealed partial class Explode : EntityEffectBase +{ + /// + /// Optional override for the explosion intensity. + /// + [DataField] + public float? Intensity; + + /// + /// Optional override for the explosion radius. + /// + [DataField] + public float? Radius; + + /// + /// Delete the entity with the explosion? + /// + [DataField] + public bool Delete = true; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-explosion", ("chance", Probability)); + + public override LogImpact? Impact => LogImpact.High; +} diff --git a/Content.Shared/EntityEffects/Effects/ExtinguishReaction.cs b/Content.Shared/EntityEffects/Effects/ExtinguishReaction.cs deleted file mode 100644 index 11e776ac90..0000000000 --- a/Content.Shared/EntityEffects/Effects/ExtinguishReaction.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Content.Shared.Atmos; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects -{ - public sealed partial class ExtinguishReaction : EntityEffect - { - /// - /// Amount of firestacks reduced. - /// - [DataField] - public float FireStacksAdjustment = -1.5f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-extinguish-reaction", ("chance", Probability)); - - public override void Effect(EntityEffectBaseArgs args) - { - var ev = new ExtinguishEvent - { - FireStacksAdjustment = FireStacksAdjustment, - }; - - if (args is EntityEffectReagentArgs reagentArgs) - { - ev.FireStacksAdjustment *= (float)reagentArgs.Quantity; - } - - args.EntityManager.EventBus.RaiseLocalEvent(args.TargetEntity, ref ev); - } - } -} diff --git a/Content.Shared/EntityEffects/Effects/FlammableReaction.cs b/Content.Shared/EntityEffects/Effects/FlammableReaction.cs deleted file mode 100644 index 9f7d504f4e..0000000000 --- a/Content.Shared/EntityEffects/Effects/FlammableReaction.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Content.Shared.Database; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class FlammableReaction : EventEntityEffect -{ - [DataField] - public float Multiplier = 0.05f; - - // The fire stack multiplier if fire stacks already exist on target, only works if 0 or greater - [DataField] - public float MultiplierOnExisting = -1f; - - public override bool ShouldLog => true; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-flammable-reaction", ("chance", Probability)); - - public override LogImpact LogImpact => LogImpact.Medium; -} diff --git a/Content.Shared/EntityEffects/Effects/FlashReactionEffect.cs b/Content.Shared/EntityEffects/Effects/FlashReactionEffect.cs deleted file mode 100644 index c238e94010..0000000000 --- a/Content.Shared/EntityEffects/Effects/FlashReactionEffect.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Robust.Shared.Audio; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -[DataDefinition] -public sealed partial class FlashReactionEffect : EventEntityEffect -{ - /// - /// Flash range per unit of reagent. - /// - [DataField] - public float RangePerUnit = 0.2f; - - /// - /// Maximum flash range. - /// - [DataField] - public float MaxRange = 10f; - - /// - /// How much to entities are slowed down. - /// - [DataField] - public float SlowTo = 0.5f; - - /// - /// The time entities will be flashed. - /// The default is chosen to be better than the hand flash so it is worth using it for grenades etc. - /// - [DataField] - public TimeSpan Duration = TimeSpan.FromSeconds(4); - - /// - /// The prototype ID used for the visual effect. - /// - [DataField] - public EntProtoId? FlashEffectPrototype = "ReactionFlash"; - - /// - /// The sound the flash creates. - /// - [DataField] - public SoundSpecifier? Sound = new SoundPathSpecifier("/Audio/Weapons/flash.ogg"); - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-flash-reaction-effect", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/Glow.cs b/Content.Shared/EntityEffects/Effects/Glow.cs deleted file mode 100644 index 394d406700..0000000000 --- a/Content.Shared/EntityEffects/Effects/Glow.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Content.Shared.EntityEffects; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Makes a mob glow. -/// -public sealed partial class Glow : EntityEffect -{ - [DataField] - public float Radius = 2f; - - [DataField] - public Color Color = Color.Black; - - private static readonly List Colors = new() - { - Color.White, - Color.Red, - Color.Yellow, - Color.Green, - Color.Blue, - Color.Purple, - Color.Pink - }; - - public override void Effect(EntityEffectBaseArgs args) - { - if (Color == Color.Black) - { - var random = IoCManager.Resolve(); - Color = random.Pick(Colors); - } - - var lightSystem = args.EntityManager.System(); - var light = lightSystem.EnsureLight(args.TargetEntity); - lightSystem.SetRadius(args.TargetEntity, Radius, light); - lightSystem.SetColor(args.TargetEntity, Color, light); - lightSystem.SetCastShadows(args.TargetEntity, false, light); // this is expensive, and botanists make lots of plants - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return "TODO"; - } -} diff --git a/Content.Shared/EntityEffects/Effects/GlowEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/GlowEntityEffectSystem.cs new file mode 100644 index 0000000000..05e4fe29ca --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/GlowEntityEffectSystem.cs @@ -0,0 +1,55 @@ +using Robust.Shared.Network; +using Robust.Shared.Random; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// Causes this entity to glow. +/// +/// +public sealed partial class GlowEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedPointLightSystem _lightSystem = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var color = args.Effect.Color; + + if (color == Color.Black) + { + // TODO: When we get proper predicted RNG remove this check... + if (_net.IsClient) + return; + + color = _random.Pick(Colors); + } + + var light = _lightSystem.EnsureLight(entity); + _lightSystem.SetRadius(entity, args.Effect.Radius, light); + _lightSystem.SetColor(entity, color, light); + _lightSystem.SetCastShadows(entity, false, light); // this is expensive, and botanists make lots of plants + } + + public static readonly List Colors = new() + { + Color.White, + Color.Red, + Color.Yellow, + Color.Green, + Color.Blue, + Color.Purple, + Color.Pink + }; +} + +/// +public sealed partial class Glow : EntityEffectBase +{ + [DataField] + public float Radius = 2f; + + [DataField] + public Color Color = Color.Black; +} diff --git a/Content.Shared/EntityEffects/Effects/HealthChange.cs b/Content.Shared/EntityEffects/Effects/HealthChange.cs deleted file mode 100644 index 17c24f6b5a..0000000000 --- a/Content.Shared/EntityEffects/Effects/HealthChange.cs +++ /dev/null @@ -1,126 +0,0 @@ -using Content.Shared.Damage; -using Content.Shared.Damage.Prototypes; -using Content.Shared.EntityEffects; -using Content.Shared.FixedPoint; -using Content.Shared.Localizations; -using Robust.Shared.Prototypes; -using System.Linq; -using System.Text.Json.Serialization; - -namespace Content.Shared.EntityEffects.Effects -{ - /// - /// Default metabolism used for medicine reagents. - /// - public sealed partial class HealthChange : EntityEffect - { - /// - /// Damage to apply every cycle. Damage Ignores resistances. - /// - [DataField(required: true)] - [JsonPropertyName("damage")] - public DamageSpecifier Damage = default!; - - /// - /// Should this effect scale the damage by the amount of chemical in the solution? - /// Useful for touch reactions, like styptic powder or acid. - /// Only usable if the EntityEffectBaseArgs is an EntityEffectReagentArgs. - /// - [DataField] - [JsonPropertyName("scaleByQuantity")] - public bool ScaleByQuantity; - - [DataField] - [JsonPropertyName("ignoreResistances")] - public bool IgnoreResistances = true; - - protected override string ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - var damages = new List(); - var heals = false; - var deals = false; - - var damageSpec = new DamageSpecifier(Damage); - - var universalReagentDamageModifier = entSys.GetEntitySystem().UniversalReagentDamageModifier; - var universalReagentHealModifier = entSys.GetEntitySystem().UniversalReagentHealModifier; - - if (universalReagentDamageModifier != 1 || universalReagentHealModifier != 1) - { - foreach (var (type, val) in damageSpec.DamageDict) - { - if (val < 0f) - { - damageSpec.DamageDict[type] = val * universalReagentHealModifier; - } - if (val > 0f) - { - damageSpec.DamageDict[type] = val * universalReagentDamageModifier; - } - } - } - - damageSpec = entSys.GetEntitySystem().ApplyUniversalAllModifiers(damageSpec); - - foreach (var (kind, amount) in damageSpec.DamageDict) - { - var sign = FixedPoint2.Sign(amount); - - if (sign < 0) - heals = true; - if (sign > 0) - deals = true; - - damages.Add( - Loc.GetString("health-change-display", - ("kind", prototype.Index(kind).LocalizedName), - ("amount", MathF.Abs(amount.Float())), - ("deltasign", sign) - )); - } - - var healsordeals = heals ? (deals ? "both" : "heals") : (deals ? "deals" : "none"); - - return Loc.GetString("reagent-effect-guidebook-health-change", - ("chance", Probability), - ("changes", ContentLocalizationManager.FormatList(damages)), - ("healsordeals", healsordeals)); - } - - public override void Effect(EntityEffectBaseArgs args) - { - var scale = FixedPoint2.New(1); - var damageSpec = new DamageSpecifier(Damage); - - if (args is EntityEffectReagentArgs reagentArgs) - { - scale = ScaleByQuantity ? reagentArgs.Quantity * reagentArgs.Scale : reagentArgs.Scale; - } - - var universalReagentDamageModifier = args.EntityManager.System().UniversalReagentDamageModifier; - var universalReagentHealModifier = args.EntityManager.System().UniversalReagentHealModifier; - - if (universalReagentDamageModifier != 1 || universalReagentHealModifier != 1) - { - foreach (var (type, val) in damageSpec.DamageDict) - { - if (val < 0f) - { - damageSpec.DamageDict[type] = val * universalReagentHealModifier; - } - if (val > 0f) - { - damageSpec.DamageDict[type] = val * universalReagentDamageModifier; - } - } - } - - args.EntityManager.System() - .TryChangeDamage( - args.TargetEntity, - damageSpec * scale, - IgnoreResistances, - interruptsDoAfters: false); - } - } -} diff --git a/Content.Shared/EntityEffects/Effects/HealthChangeEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/HealthChangeEntityEffectSystem.cs new file mode 100644 index 0000000000..595bf15aa5 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/HealthChangeEntityEffectSystem.cs @@ -0,0 +1,92 @@ +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.FixedPoint; +using Content.Shared.Localizations; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// Adjust the damages on this entity by specified amounts. +/// Amounts are modified by scale. +/// +/// +public sealed partial class HealthChangeEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly Damage.Systems.DamageableSystem _damageable = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var damageSpec = new DamageSpecifier(args.Effect.Damage); + + damageSpec *= args.Scale; + + _damageable.TryChangeDamage( + entity.AsNullable(), + damageSpec, + args.Effect.IgnoreResistances, + interruptsDoAfters: false); + } +} + +/// +public sealed partial class HealthChange : EntityEffectBase +{ + /// + /// Damage to apply every cycle. Damage Ignores resistances. + /// + [DataField(required: true)] + public DamageSpecifier Damage = default!; + + [DataField] + public bool IgnoreResistances = true; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + var damages = new List(); + var heals = false; + var deals = false; + + var damageSpec = new DamageSpecifier(Damage); + + var universalReagentDamageModifier = entSys.GetEntitySystem().UniversalReagentDamageModifier; + var universalReagentHealModifier = entSys.GetEntitySystem().UniversalReagentHealModifier; + + damageSpec = entSys.GetEntitySystem().ApplyUniversalAllModifiers(damageSpec); + + foreach (var (kind, amount) in damageSpec.DamageDict) + { + var sign = FixedPoint2.Sign(amount); + float mod; + + switch (sign) + { + case < 0: + heals = true; + mod = universalReagentHealModifier; + break; + case > 0: + deals = true; + mod = universalReagentDamageModifier; + break; + default: + continue; // Don't need to show damage types of 0... + } + + damages.Add( + Loc.GetString("health-change-display", + ("kind", prototype.Index(kind).LocalizedName), + ("amount", MathF.Abs(amount.Float() * mod)), + ("deltasign", sign) + )); + } + + var healsordeals = heals ? (deals ? "both" : "heals") : (deals ? "deals" : "none"); + + return Loc.GetString("entity-effect-guidebook-health-change", + ("chance", Probability), + ("changes", ContentLocalizationManager.FormatList(damages)), + ("healsordeals", healsordeals)); + } +} diff --git a/Content.Shared/EntityEffects/Effects/Ignite.cs b/Content.Shared/EntityEffects/Effects/Ignite.cs deleted file mode 100644 index 707ecc3208..0000000000 --- a/Content.Shared/EntityEffects/Effects/Ignite.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Content.Shared.Database; -using Content.Shared.EntityEffects; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Ignites a mob. -/// -public sealed partial class Ignite : EventEntityEffect -{ - public override bool ShouldLog => true; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-ignite", ("chance", Probability)); - - public override LogImpact LogImpact => LogImpact.Medium; -} diff --git a/Content.Shared/EntityEffects/Effects/MakeSentient.cs b/Content.Shared/EntityEffects/Effects/MakeSentient.cs deleted file mode 100644 index 9c70eb452b..0000000000 --- a/Content.Shared/EntityEffects/Effects/MakeSentient.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Content.Shared.Mind.Components; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class MakeSentient : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-make-sentient", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/MakeSentientEntityEffect.cs b/Content.Shared/EntityEffects/Effects/MakeSentientEntityEffect.cs new file mode 100644 index 0000000000..4beb21454c --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/MakeSentientEntityEffect.cs @@ -0,0 +1,22 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects; + +/// +public sealed partial class MakeSentient : EntityEffectBase +{ + /// + /// Description for the ghost role created by this effect. + /// + [DataField] + public LocId RoleDescription = "ghost-role-information-cognizine-description"; + + /// + /// Whether we give the target the ability to speak coherently. + /// + [DataField] + public bool AllowSpeech = true; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-make-sentient", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/MetaData/DestructibleActEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/MetaData/DestructibleActEntityEffectSystem.cs new file mode 100644 index 0000000000..05fd826f2d --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/MetaData/DestructibleActEntityEffectSystem.cs @@ -0,0 +1,40 @@ +using Content.Shared.Destructible; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.MetaData; + + +/// +public sealed partial class DestructibleActEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedDestructibleSystem _destructible = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if ((args.Effect.Acts & ThresholdActs.Breakage) != 0) + _destructible.BreakEntity(entity); + + if ((args.Effect.Acts & ThresholdActs.Destruction) != 0) + _destructible.DestroyEntity(entity.AsNullable()); + } +} + +/// +/// Destroys or breaks an entity. +/// +public sealed partial class DestructibleAct : EntityEffectBase +{ + /// + /// What acts should be triggered upon activation. + /// + [DataField] + public ThresholdActs Acts; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + if ((Acts & ThresholdActs.Destruction) != 0) + return Loc.GetString("entity-effect-guidebook-destroy", ("chance", Probability)); + + return Loc.GetString("entity-effect-guidebook-break", ("chance", Probability)); + } +} diff --git a/Content.Shared/EntityEffects/Effects/ModifyBleedAmount.cs b/Content.Shared/EntityEffects/Effects/ModifyBleedAmount.cs deleted file mode 100644 index 9f15652095..0000000000 --- a/Content.Shared/EntityEffects/Effects/ModifyBleedAmount.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class ModifyBleedAmount : EventEntityEffect -{ - [DataField] - public bool Scaled = false; - - [DataField] - public float Amount = -1.0f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-modify-bleed-amount", ("chance", Probability), - ("deltasign", MathF.Sign(Amount))); -} diff --git a/Content.Shared/EntityEffects/Effects/ModifyBloodLevel.cs b/Content.Shared/EntityEffects/Effects/ModifyBloodLevel.cs deleted file mode 100644 index 06c026f128..0000000000 --- a/Content.Shared/EntityEffects/Effects/ModifyBloodLevel.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Content.Shared.FixedPoint; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class ModifyBloodLevel : EventEntityEffect -{ - [DataField] - public bool Scaled = false; - - [DataField] - public FixedPoint2 Amount = 1.0f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-modify-blood-level", ("chance", Probability), - ("deltasign", MathF.Sign(Amount.Float()))); -} diff --git a/Content.Shared/EntityEffects/Effects/ModifyLungGas.cs b/Content.Shared/EntityEffects/Effects/ModifyLungGas.cs deleted file mode 100644 index 45dc8c84c6..0000000000 --- a/Content.Shared/EntityEffects/Effects/ModifyLungGas.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Content.Shared.Atmos; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class ModifyLungGas : EventEntityEffect -{ - [DataField("ratios", required: true)] - public Dictionary Ratios = default!; - - // JUSTIFICATION: This is internal magic that players never directly interact with. - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => null; -} diff --git a/Content.Shared/EntityEffects/Effects/MovespeedModifier.cs b/Content.Shared/EntityEffects/Effects/MovespeedModifier.cs deleted file mode 100644 index 5e72746e32..0000000000 --- a/Content.Shared/EntityEffects/Effects/MovespeedModifier.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Content.Shared.Chemistry.Components; -using Content.Shared.Movement.Systems; -using Robust.Shared.Prototypes; -using Robust.Shared.Timing; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Default metabolism for stimulants and tranqs. Attempts to find a MovementSpeedModifier on the target, -/// adding one if not there and to change the movespeed -/// -public sealed partial class MovespeedModifier : EntityEffect -{ - /// - /// How much the entities' walk speed is multiplied by. - /// - [DataField] - public float WalkSpeedModifier { get; set; } = 1; - - /// - /// How much the entities' run speed is multiplied by. - /// - [DataField] - public float SprintSpeedModifier { get; set; } = 1; - - /// - /// How long the modifier applies (in seconds). - /// Is scaled by reagent amount if used with an EntityEffectReagentArgs. - /// - [DataField] - public float StatusLifetime = 2f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return Loc.GetString("reagent-effect-guidebook-movespeed-modifier", - ("chance", Probability), - ("walkspeed", WalkSpeedModifier), - ("time", StatusLifetime)); - } - - /// - /// Remove reagent at set rate, changes the movespeed modifiers and adds a MovespeedModifierMetabolismComponent if not already there. - /// - public override void Effect(EntityEffectBaseArgs args) - { - var status = args.EntityManager.EnsureComponent(args.TargetEntity); - - // Only refresh movement if we need to. - var modified = !status.WalkSpeedModifier.Equals(WalkSpeedModifier) || - !status.SprintSpeedModifier.Equals(SprintSpeedModifier); - - status.WalkSpeedModifier = WalkSpeedModifier; - status.SprintSpeedModifier = SprintSpeedModifier; - - // only going to scale application time - var statusLifetime = StatusLifetime; - - if (args is EntityEffectReagentArgs reagentArgs) - { - statusLifetime *= reagentArgs.Scale.Float(); - } - - IncreaseTimer(status, statusLifetime, args.EntityManager, args.TargetEntity); - - if (modified) - args.EntityManager.System().RefreshMovementSpeedModifiers(args.TargetEntity); - } - private void IncreaseTimer(MovespeedModifierMetabolismComponent status, float time, IEntityManager entityManager, EntityUid uid) - { - var gameTiming = IoCManager.Resolve(); - - var offsetTime = Math.Max(status.ModifierTimer.TotalSeconds, gameTiming.CurTime.TotalSeconds); - - status.ModifierTimer = TimeSpan.FromSeconds(offsetTime + time); - - entityManager.Dirty(uid, status); - } -} diff --git a/Content.Shared/EntityEffects/Effects/Oxygenate.cs b/Content.Shared/EntityEffects/Effects/Oxygenate.cs deleted file mode 100644 index e990a3fec6..0000000000 --- a/Content.Shared/EntityEffects/Effects/Oxygenate.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class Oxygenate : EventEntityEffect -{ - [DataField] - public float Factor = 1f; - - // JUSTIFICATION: This is internal magic that players never directly interact with. - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => null; -} diff --git a/Content.Shared/EntityEffects/Effects/Paralyze.cs b/Content.Shared/EntityEffects/Effects/Paralyze.cs deleted file mode 100644 index 2a2270016b..0000000000 --- a/Content.Shared/EntityEffects/Effects/Paralyze.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Content.Shared.Stunnable; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class Paralyze : EntityEffect -{ - [DataField] public double ParalyzeTime = 2; - - /// - /// true - refresh paralyze time, false - accumulate paralyze time - /// - [DataField] public bool Refresh = true; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString( - "reagent-effect-guidebook-paralyze", - ("chance", Probability), - ("time", ParalyzeTime) - ); - - public override void Effect(EntityEffectBaseArgs args) - { - var paralyzeTime = ParalyzeTime; - - if (args is EntityEffectReagentArgs reagentArgs) - { - paralyzeTime *= (double)reagentArgs.Scale; - } - - var stunSystem = args.EntityManager.System(); - _ = Refresh - ? stunSystem.TryUpdateParalyzeDuration(args.TargetEntity, TimeSpan.FromSeconds(paralyzeTime)) - : stunSystem.TryAddParalyzeDuration(args.TargetEntity, TimeSpan.FromSeconds(paralyzeTime)); - } -} - diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustAttribute.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustAttribute.cs deleted file mode 100644 index 2c8046452b..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustAttribute.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Content.Shared.EntityEffects; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using System.Diagnostics.CodeAnalysis; - -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -[ImplicitDataDefinitionForInheritors] -public abstract partial class PlantAdjustAttribute : EventEntityEffect where T : PlantAdjustAttribute -{ - [DataField] - public float Amount { get; protected set; } = 1; - - /// - /// Localisation key for the name of the adjusted attribute. Used for guidebook descriptions. - /// - [DataField] - public abstract string GuidebookAttributeName { get; set; } - - /// - /// Whether the attribute in question is a good thing. Used for guidebook descriptions to determine the color of the number. - /// - [DataField] - public virtual bool GuidebookIsAttributePositive { get; protected set; } = true; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - string color; - if (GuidebookIsAttributePositive ^ Amount < 0.0) - { - color = "green"; - } - else - { - color = "red"; - } - return Loc.GetString("reagent-effect-guidebook-plant-attribute", ("attribute", Loc.GetString(GuidebookAttributeName)), ("amount", Amount.ToString("0.00")), ("colorName", color), ("chance", Probability)); - } -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustHealth.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustHealth.cs deleted file mode 100644 index c1e71894e5..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustHealth.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantAdjustHealth : PlantAdjustAttribute -{ - public override string GuidebookAttributeName { get; set; } = "plant-attribute-health"; -} - diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustMutationLevel.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustMutationLevel.cs deleted file mode 100644 index 6610adf708..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustMutationLevel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantAdjustMutationLevel : PlantAdjustAttribute -{ - public override string GuidebookAttributeName { get; set; } = "plant-attribute-mutation-level"; -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustMutationMod.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustMutationMod.cs deleted file mode 100644 index 91be222807..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustMutationMod.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantAdjustMutationMod : PlantAdjustAttribute -{ - public override string GuidebookAttributeName { get; set; } = "plant-attribute-mutation-mod"; -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustNutrition.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustNutrition.cs deleted file mode 100644 index db01d5d060..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustNutrition.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantAdjustNutrition : PlantAdjustAttribute -{ - public override string GuidebookAttributeName { get; set; } = "plant-attribute-nutrition"; -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustPotency.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustPotency.cs deleted file mode 100644 index 971f05f32d..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustPotency.cs +++ /dev/null @@ -1,12 +0,0 @@ -// using Content.Server.Botany.Systems; - -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -/// -/// Handles increase or decrease of plant potency. -/// - -public sealed partial class PlantAdjustPotency : PlantAdjustAttribute -{ - public override string GuidebookAttributeName { get; set; } = "plant-attribute-potency"; -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustWater.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustWater.cs deleted file mode 100644 index 610d02231b..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustWater.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantAdjustWater : PlantAdjustAttribute -{ - public override string GuidebookAttributeName { get; set; } = "plant-attribute-water"; -} - diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAffectGrowth.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAffectGrowth.cs deleted file mode 100644 index 36b8f57d08..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAffectGrowth.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantAffectGrowth : PlantAdjustAttribute -{ - public override string GuidebookAttributeName { get; set; } = "plant-attribute-growth"; -} - diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantChangeStat.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantChangeStat.cs deleted file mode 100644 index 66cfb66d4b..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantChangeStat.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantChangeStat : EventEntityEffect -{ - [DataField] - public string TargetValue; - - [DataField] - public float MinValue; - - [DataField] - public float MaxValue; - - [DataField] - public int Steps; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - throw new NotImplementedException(); - } -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantCryoxadone.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantCryoxadone.cs deleted file mode 100644 index 0dabf1f22b..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantCryoxadone.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantCryoxadone : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-plant-cryoxadone", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantDestroySeeds.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantDestroySeeds.cs deleted file mode 100644 index 9f16c35402..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantDestroySeeds.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -/// -/// Handles removal of seeds on a plant. -/// - -public sealed partial class PlantDestroySeeds : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => - Loc.GetString("reagent-effect-guidebook-plant-seeds-remove", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantDiethylamine.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantDiethylamine.cs deleted file mode 100644 index 9feccbf224..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantDiethylamine.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantDiethylamine : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-plant-diethylamine", ("chance", Probability)); -} - diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantPhalanximine.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantPhalanximine.cs deleted file mode 100644 index 9dc5140063..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantPhalanximine.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantPhalanximine : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-plant-phalanximine", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantRestoreSeeds.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantRestoreSeeds.cs deleted file mode 100644 index 51ce353dbb..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantRestoreSeeds.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -/// -/// Handles restoral of seeds on a plant. -/// -public sealed partial class PlantRestoreSeeds : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => - Loc.GetString("reagent-effect-guidebook-plant-seeds-add", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/RobustHarvest.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/RobustHarvest.cs deleted file mode 100644 index 6ba37c9be0..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/RobustHarvest.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class RobustHarvest : EventEntityEffect -{ - [DataField] - public int PotencyLimit = 50; - - [DataField] - public int PotencyIncrease = 3; - - [DataField] - public int PotencySeedlessThreshold = 30; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-plant-robust-harvest", ("seedlesstreshold", PotencySeedlessThreshold), ("limit", PotencyLimit), ("increase", PotencyIncrease), ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMutateChemicals.cs b/Content.Shared/EntityEffects/Effects/PlantMutateChemicals.cs deleted file mode 100644 index 9a3408bb8e..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMutateChemicals.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Content.Shared.Random; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// changes the chemicals available in a plant's produce -/// -public sealed partial class PlantMutateChemicals : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return "TODO"; - } -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMutateGases.cs b/Content.Shared/EntityEffects/Effects/PlantMutateGases.cs deleted file mode 100644 index 5eb5b1d07c..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMutateGases.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using System.Linq; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// changes the gases that a plant or produce create. -/// -public sealed partial class PlantMutateExudeGasses : EventEntityEffect -{ - [DataField] - public float MinValue = 0.01f; - - [DataField] - public float MaxValue = 0.5f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return "TODO"; - } -} - -/// -/// changes the gases that a plant or produce consumes. -/// -public sealed partial class PlantMutateConsumeGasses : EventEntityEffect -{ - [DataField] - public float MinValue = 0.01f; - - [DataField] - public float MaxValue = 0.5f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return "TODO"; - } -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMutateHarvest.cs b/Content.Shared/EntityEffects/Effects/PlantMutateHarvest.cs deleted file mode 100644 index 84d6293185..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMutateHarvest.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Upgrades a plant's harvest type. -/// -public sealed partial class PlantMutateHarvest : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return "TODO"; - } -} diff --git a/Content.Shared/EntityEffects/Effects/PlantSpeciesChange.cs b/Content.Shared/EntityEffects/Effects/PlantSpeciesChange.cs deleted file mode 100644 index e2acc4cb7c..0000000000 --- a/Content.Shared/EntityEffects/Effects/PlantSpeciesChange.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Changes a plant into one of the species its able to mutate into. -/// -public sealed partial class PlantSpeciesChange : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return "TODO"; - } -} diff --git a/Content.Shared/EntityEffects/Effects/Polymorph.cs b/Content.Shared/EntityEffects/Effects/Polymorph.cs deleted file mode 100644 index 65711ff99a..0000000000 --- a/Content.Shared/EntityEffects/Effects/Polymorph.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Content.Shared.Polymorph; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class Polymorph : EventEntityEffect -{ - /// - /// What polymorph prototype is used on effect - /// - [DataField("prototype", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string PolymorphPrototype { get; set; } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-make-polymorph", - ("chance", Probability), ("entityname", - prototype.Index(prototype.Index(PolymorphPrototype).Configuration.Entity).Name)); -} diff --git a/Content.Shared/EntityEffects/Effects/PolymorphEntityEffect.cs b/Content.Shared/EntityEffects/Effects/PolymorphEntityEffect.cs new file mode 100644 index 0000000000..c0d80dffaa --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/PolymorphEntityEffect.cs @@ -0,0 +1,19 @@ +using Content.Shared.Polymorph; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects; + +/// +public sealed partial class Polymorph : EntityEffectBase +{ + /// + /// What polymorph prototype is used on effect + /// + [DataField(required: true)] + public ProtoId Prototype; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-make-polymorph", + ("chance", Probability), + ("entityname", prototype.Index(prototype.Index(Prototype).Configuration.Entity).Name)); +} diff --git a/Content.Shared/EntityEffects/Effects/PopupMessage.cs b/Content.Shared/EntityEffects/Effects/PopupMessage.cs deleted file mode 100644 index a837f816e4..0000000000 --- a/Content.Shared/EntityEffects/Effects/PopupMessage.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Content.Shared.Popups; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Shared.EntityEffects.Effects -{ - public sealed partial class PopupMessage : EntityEffect - { - [DataField(required: true)] - public string[] Messages = default!; - - [DataField] - public PopupRecipients Type = PopupRecipients.Local; - - [DataField] - public PopupType VisualType = PopupType.Small; - - // JUSTIFICATION: This is purely cosmetic. - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => null; - - public override void Effect(EntityEffectBaseArgs args) - { - var popupSys = args.EntityManager.EntitySysManager.GetEntitySystem(); - var random = IoCManager.Resolve(); - - var msg = random.Pick(Messages); - var msgArgs = new (string, object)[] - { - ("entity", args.TargetEntity), - }; - - if (args is EntityEffectReagentArgs reagentArgs) - { - msgArgs = new (string, object)[] - { - ("entity", reagentArgs.TargetEntity), - ("organ", reagentArgs.OrganEntity.GetValueOrDefault()), - }; - } - - if (Type == PopupRecipients.Local) - popupSys.PopupEntity(Loc.GetString(msg, msgArgs), args.TargetEntity, args.TargetEntity, VisualType); - else if (Type == PopupRecipients.Pvs) - popupSys.PopupEntity(Loc.GetString(msg, msgArgs), args.TargetEntity, VisualType); - } - } - - public enum PopupRecipients - { - Pvs, - Local - } -} diff --git a/Content.Shared/EntityEffects/Effects/ReduceRotting.cs b/Content.Shared/EntityEffects/Effects/ReduceRotting.cs deleted file mode 100644 index b5f2a7ae7e..0000000000 --- a/Content.Shared/EntityEffects/Effects/ReduceRotting.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Robust.Shared.Prototypes; -using Content.Shared.Atmos.Rotting; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Reduces the rotting accumulator on the patient, making them revivable. -/// -public sealed partial class ReduceRotting : EntityEffect -{ - [DataField("seconds")] - public double RottingAmount = 10; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-reduce-rotting", - ("chance", Probability), - ("time", RottingAmount)); - - public override void Effect(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) - { - if (reagentArgs.Scale != 1f) - return; - } - - var rottingSys = args.EntityManager.EntitySysManager.GetEntitySystem(); - - rottingSys.ReduceAccumulator(args.TargetEntity, TimeSpan.FromSeconds(RottingAmount)); - } -} diff --git a/Content.Shared/EntityEffects/Effects/ResetNarcolepsy.cs b/Content.Shared/EntityEffects/Effects/ResetNarcolepsy.cs deleted file mode 100644 index 009cf914d5..0000000000 --- a/Content.Shared/EntityEffects/Effects/ResetNarcolepsy.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Reset narcolepsy timer -/// -public sealed partial class ResetNarcolepsy : EventEntityEffect -{ - /// - /// The # of seconds the effect resets the narcolepsy timer to - /// - [DataField("TimerReset")] - public TimeSpan TimerReset = TimeSpan.FromSeconds(600); - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-reset-narcolepsy", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/ResetNarcolepsyEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/ResetNarcolepsyEntityEffectSystem.cs new file mode 100644 index 0000000000..973b139fa4 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/ResetNarcolepsyEntityEffectSystem.cs @@ -0,0 +1,34 @@ +using Content.Shared.Traits.Assorted; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// Resets the narcolepsy timer on a given entity. +/// The new duration of the timer is modified by scale. +/// +/// +public sealed partial class ResetNarcolepsyEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly NarcolepsySystem _narcolepsy = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var timer = args.Effect.TimerReset * args.Scale; + + _narcolepsy.AdjustNarcolepsyTimer(entity.AsNullable(), timer); + } +} + +/// +public sealed partial class ResetNarcolepsy : EntityEffectBase +{ + /// + /// The time we set our narcolepsy timer to. + /// + [DataField("TimerReset")] + public TimeSpan TimerReset = TimeSpan.FromSeconds(600); + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-reset-narcolepsy", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/SatiateHunger.cs b/Content.Shared/EntityEffects/Effects/SatiateHunger.cs deleted file mode 100644 index 3e7af8833c..0000000000 --- a/Content.Shared/EntityEffects/Effects/SatiateHunger.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Nutrition.Components; -using Content.Shared.Nutrition.EntitySystems; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Attempts to find a HungerComponent on the target, -/// and to update it's hunger values. -/// -public sealed partial class SatiateHunger : EntityEffect -{ - private const float DefaultNutritionFactor = 3.0f; - - /// - /// How much hunger is satiated. - /// Is multiplied by quantity if used with EntityEffectReagentArgs. - /// - [DataField("factor")] public float NutritionFactor { get; set; } = DefaultNutritionFactor; - - //Remove reagent at set rate, satiate hunger if a HungerComponent can be found - public override void Effect(EntityEffectBaseArgs args) - { - var entman = args.EntityManager; - if (!entman.TryGetComponent(args.TargetEntity, out HungerComponent? hunger)) - return; - if (args is EntityEffectReagentArgs reagentArgs) - { - entman.System().ModifyHunger(reagentArgs.TargetEntity, NutritionFactor * (float) reagentArgs.Quantity, hunger); - } - else - { - entman.System().ModifyHunger(args.TargetEntity, NutritionFactor, hunger); - } - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-satiate-hunger", ("chance", Probability), ("relative", NutritionFactor / DefaultNutritionFactor)); -} diff --git a/Content.Shared/EntityEffects/Effects/SatiateThirst.cs b/Content.Shared/EntityEffects/Effects/SatiateThirst.cs deleted file mode 100644 index 21a055b528..0000000000 --- a/Content.Shared/EntityEffects/Effects/SatiateThirst.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Nutrition.Components; -using Content.Shared.Nutrition.EntitySystems; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Default metabolism for drink reagents. Attempts to find a ThirstComponent on the target, -/// and to update it's thirst values. -/// -public sealed partial class SatiateThirst : EntityEffect -{ - private const float DefaultHydrationFactor = 3.0f; - - /// How much thirst is satiated each tick. Not currently tied to - /// rate or anything. - [DataField("factor")] - public float HydrationFactor { get; set; } = DefaultHydrationFactor; - - /// Satiate thirst if a ThirstComponent can be found - public override void Effect(EntityEffectBaseArgs args) - { - var uid = args.TargetEntity; - if (args.EntityManager.TryGetComponent(uid, out ThirstComponent? thirst)) - args.EntityManager.System().ModifyThirst(uid, thirst, HydrationFactor); - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-satiate-thirst", ("chance", Probability), ("relative", HydrationFactor / DefaultHydrationFactor)); -} diff --git a/Content.Shared/EntityEffects/Effects/Slipify.cs b/Content.Shared/EntityEffects/Effects/Slipify.cs deleted file mode 100644 index c152b8b010..0000000000 --- a/Content.Shared/EntityEffects/Effects/Slipify.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Content.Shared.Physics; -using Content.Shared.Slippery; -using Content.Shared.StepTrigger.Components; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Components; -using Robust.Shared.Physics.Systems; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Makes a mob slippery. -/// -public sealed partial class Slipify : EntityEffect -{ - public override void Effect(EntityEffectBaseArgs args) - { - var fixtureSystem = args.EntityManager.System(); - var colWakeSystem = args.EntityManager.System(); - var slippery = args.EntityManager.EnsureComponent(args.TargetEntity); - args.EntityManager.Dirty(args.TargetEntity, slippery); - args.EntityManager.EnsureComponent(args.TargetEntity); - // Need a fixture with a slip layer in order to actually do the slipping - var fixtures = args.EntityManager.EnsureComponent(args.TargetEntity); - var body = args.EntityManager.EnsureComponent(args.TargetEntity); - var shape = fixtures.Fixtures["fix1"].Shape; - fixtureSystem.TryCreateFixture(args.TargetEntity, shape, "slips", 1, false, (int)CollisionGroup.SlipLayer, manager: fixtures, body: body); - // Need to disable collision wake so that mobs can collide with and slip on it - var collisionWake = args.EntityManager.EnsureComponent(args.TargetEntity); - colWakeSystem.SetEnabled(args.TargetEntity, false, collisionWake); - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - throw new NotImplementedException(); - } -} diff --git a/Content.Shared/EntityEffects/Effects/SlipifyEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/SlipifyEntityEffectSystem.cs new file mode 100644 index 0000000000..602e5e923a --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/SlipifyEntityEffectSystem.cs @@ -0,0 +1,44 @@ +using System.Linq; +using Content.Shared.Physics; +using Content.Shared.Slippery; +using Content.Shared.StepTrigger.Components; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Systems; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// This effect permanently creates a slippery fixture for this entity and then makes this entity slippery like soap. +/// +/// +public sealed partial class SlipifyEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly CollisionWakeSystem _collisionWake = default!; + [Dependency] private readonly FixtureSystem _fixture = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + EnsureComp(entity, out var slippery); + slippery.SlipData = args.Effect.Slippery; + + Dirty(entity, slippery); + + EnsureComp(entity); + + if (entity.Comp.Fixtures.FirstOrDefault(x => x.Value.Hard).Value.Shape is not { } shape) + return; + + _fixture.TryCreateFixture(entity, shape, "slips", 1, false, (int)CollisionGroup.SlipLayer, manager: entity.Comp); + + // Need to disable collision wake so that mobs can collide with and slip on it + EnsureComp(entity, out var collisionWake); + _collisionWake.SetEnabled(entity, false, collisionWake); + } +} + +/// +public sealed partial class Slipify : EntityEffectBase +{ + [DataField] + public SlipperyEffectEntry Slippery = new(); +} diff --git a/Content.Shared/EntityEffects/Effects/Solution/AddReagentToSolutionEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Solution/AddReagentToSolutionEntityEffectSystem.cs new file mode 100644 index 0000000000..ef96723cff --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Solution/AddReagentToSolutionEntityEffectSystem.cs @@ -0,0 +1,59 @@ +using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Solution; + +// TODO: This should be removed and changed to an "AbsorbentSolutionComponent" +/// +/// Creates a reagent in a specified solution owned by this entity. +/// Quantity is modified by scale. +/// +/// +public sealed class AddReagentToSolutionEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var solution = args.Effect.Solution; + var reagent = args.Effect.Reagent; + + if (!_solutionContainer.TryGetSolution((entity, entity), solution, out var solutionContainer)) + return; + + _solutionContainer.TryAddReagent(solutionContainer.Value, reagent, args.Scale * args.Effect.StrengthModifier); + } +} + +/// +public sealed partial class AddReagentToSolution : EntityEffectBase +{ + /// + /// Prototype of the reagent we're adding. + /// + [DataField(required: true)] + public ProtoId Reagent; + + /// + /// Solution we're looking for + /// + [DataField(required: true)] + public string? Solution = "reagents"; + + /// + /// A modifier for how much reagent we're creating. + /// + [DataField] + public float StrengthModifier = 1.0f; + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + return prototype.Resolve(Reagent, out ReagentPrototype? proto) + ? Loc.GetString("entity-effect-guidebook-add-to-solution-reaction", + ("chance", Probability), + ("reagent", proto.LocalizedName)) + : null; + } +} diff --git a/Content.Shared/EntityEffects/Effects/Solution/AdjustReagentEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Solution/AdjustReagentEntityEffectSystem.cs new file mode 100644 index 0000000000..334a63a95a --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Solution/AdjustReagentEntityEffectSystem.cs @@ -0,0 +1,52 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Solution; + +/// +/// Adjust a reagent in this solution by an amount modified by scale. +/// Quantity is modified by scale. +/// +/// +public sealed partial class AdjustReagentEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var quantity = args.Effect.Amount * args.Scale; + var reagent = args.Effect.Reagent; + + if (quantity > 0) + _solutionContainer.TryAddReagent(entity, reagent, quantity); + else + _solutionContainer.RemoveReagent(entity, reagent, -quantity); + } +} + +/// +public sealed partial class AdjustReagent : EntityEffectBase +{ + /// + /// The reagent ID to add or remove. + /// + [DataField(required: true)] + public ProtoId Reagent; + + [DataField(required: true)] + public FixedPoint2 Amount; + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + return prototype.Resolve(Reagent, out ReagentPrototype? proto) + ? Loc.GetString("entity-effect-guidebook-adjust-reagent-reagent", + ("chance", Probability), + ("deltasign", MathF.Sign(Amount.Float())), + ("reagent", proto.LocalizedName), + ("amount", MathF.Abs(Amount.Float()))) + : null; + } +} diff --git a/Content.Shared/EntityEffects/Effects/Solution/AdjustReagentsByGroupEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Solution/AdjustReagentsByGroupEntityEffectSystem.cs new file mode 100644 index 0000000000..51259956d8 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Solution/AdjustReagentsByGroupEntityEffectSystem.cs @@ -0,0 +1,52 @@ +using Content.Shared.Body.Prototypes; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Solution; + +/// +/// Adjust all reagents in this solution which are metabolized by a given metabolism group. +/// Quantity is modified by scale, quantity is per reagent and not a total. +/// +/// +public sealed partial class AdjustReagentsByGroupEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var quantity = args.Effect.Amount * args.Scale; + var group = args.Effect.Group; + var solution = entity.Comp.Solution; + + foreach (var quant in solution.Contents.ToArray()) + { + var proto = _proto.Index(quant.Reagent.Prototype); + if (proto.Metabolisms == null || !proto.Metabolisms.ContainsKey(group)) + continue; + + if (quantity > 0) + _solutionContainer.TryAddReagent(entity, proto.ID, quantity); + else + _solutionContainer.RemoveReagent(entity, proto.ID, -quantity); + } + } +} + +/// +public sealed partial class AdjustReagentsByGroup : EntityEffectBase +{ + + /// + /// The metabolism group being adjusted. All reagents in an affected solution with this group will be adjusted. + /// + [DataField(required: true)] + public ProtoId Group; + + [DataField(required: true)] + public FixedPoint2 Amount; +} diff --git a/Content.Shared/EntityEffects/Effects/Solution/AreaReactionEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Solution/AreaReactionEntityEffect.cs new file mode 100644 index 0000000000..9d3dc38222 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Solution/AreaReactionEntityEffect.cs @@ -0,0 +1,39 @@ +using Content.Shared.Database; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Solution; + +/// +public sealed partial class AreaReactionEffect : EntityEffectBase +{ + /// + /// How many seconds will the effect stay, counting after fully spreading. + /// + [DataField("duration")] public float Duration = 10; + + /// + /// How big of a reaction scale we need for 1 smoke entity. + /// + [DataField] public float OverflowThreshold = 2.5f; + + /// + /// The entity prototype that is being spread over an area. + /// + [DataField(required: true)] + public EntProtoId PrototypeId; + + /// + /// Sound that will get played when this reaction effect occurs. + /// + [DataField(required: true)] public SoundSpecifier Sound = default!; + + public override bool Scaling => true; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-area-reaction", + ("duration", Duration) + ); + + public override LogImpact? Impact => LogImpact.High; +} diff --git a/Content.Shared/EntityEffects/Effects/Solution/SolutionTemperatureEntityEffectsSystem.cs b/Content.Shared/EntityEffects/Effects/Solution/SolutionTemperatureEntityEffectsSystem.cs new file mode 100644 index 0000000000..c0188fdfb4 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Solution/SolutionTemperatureEntityEffectsSystem.cs @@ -0,0 +1,145 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Solution; + +// TODO: Energy conservation!!! Once HeatContainers are merged nuke this and everything in SolutionContainerSystem to respect energy conservation! +/// +/// Sets the temperature of this solution to a fixed value. +/// +/// +public sealed class SetSolutionTemperatureEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _solutionContainer.SetTemperature(entity, args.Effect.Temperature); + } +} + +/// +public sealed partial class SetSolutionTemperature : EntityEffectBase +{ + /// + /// The temperature to set the solution to. + /// + [DataField(required: true)] + public float Temperature; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-set-solution-temperature-effect", + ("chance", Probability), + ("temperature", Temperature)); +} + +/// +/// Adjusts the temperature of this solution by a given amount. +/// The temperature adjustment is modified by scale. +/// +/// +public sealed class AdjustSolutionTemperatureEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var solution = entity.Comp.Solution; + var temperature = Math.Clamp(solution.Temperature + args.Scale * args.Effect.Delta, args.Effect.MinTemp, args.Effect.MaxTemp); + + _solutionContainer.SetTemperature(entity, temperature); + } +} + +/// +public sealed partial class AdjustSolutionTemperature : EntityEffectBase +{ + /// + /// The change in temperature. + /// + [DataField(required: true)] + public float Delta; + + /// + /// The minimum temperature this effect can reach. + /// + [DataField] + public float MinTemp; + + /// + /// The maximum temperature this effect can reach. + /// + [DataField] + public float MaxTemp = float.PositiveInfinity; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-adjust-solution-temperature-effect", + ("chance", Probability), + ("deltasign", MathF.Sign(Delta)), + ("mintemp", MinTemp), + ("maxtemp", MaxTemp)); +} + +/// +/// Adjusts the thermal energy of this solution by a given amount. +/// The energy adjustment is modified by scale. +/// +/// +public sealed class AdjustSolutionThermalEnergyEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var solution = entity.Comp.Solution; + + var delta = args.Scale * args.Effect.Delta; + + // Don't adjust thermal energy if we're already at or above max temperature. + switch (delta) + { + case > 0: + if (solution.Temperature >= args.Effect.MaxTemp) + return; + break; + case < 0: + if (solution.Temperature <= args.Effect.MinTemp) + return; + break; + default: + return; + } + + _solutionContainer.AddThermalEnergyClamped(entity, delta, args.Effect.MinTemp, args.Effect.MaxTemp); + } +} + +/// +public sealed partial class AdjustSolutionThermalEnergy : EntityEffectBase +{ + /// + /// The change in thermal energy. + /// + [DataField(required: true)] + public float Delta; + + /// + /// The minimum temperature this effect can reach. + /// + [DataField] + public float MinTemp; + + /// + /// The maximum temperature this effect can reach. + /// + [DataField] + public float MaxTemp = float.PositiveInfinity; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-adjust-solution-temperature-effect", + ("chance", Probability), + ("deltasign", MathF.Sign(Delta)), + ("mintemp", MinTemp), + ("maxtemp", MaxTemp)); +} diff --git a/Content.Shared/EntityEffects/Effects/SolutionTemperatureEffects.cs b/Content.Shared/EntityEffects/Effects/SolutionTemperatureEffects.cs deleted file mode 100644 index 30ac6c3d77..0000000000 --- a/Content.Shared/EntityEffects/Effects/SolutionTemperatureEffects.cs +++ /dev/null @@ -1,143 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Sets the temperature of the solution involved with the reaction to a new value. -/// -[DataDefinition] -public sealed partial class SetSolutionTemperatureEffect : EntityEffect -{ - /// - /// The temperature to set the solution to. - /// - [DataField("temperature", required: true)] private float _temperature; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-set-solution-temperature-effect", - ("chance", Probability), ("temperature", _temperature)); - - public override void Effect(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) - { - var solution = reagentArgs.Source; - if (solution == null) - return; - - solution.Temperature = _temperature; - - return; - } - - // TODO: Someone needs to figure out how to do this for non-reagent effects. - throw new NotImplementedException(); - } -} - -/// -/// Adjusts the temperature of the solution involved in the reaction. -/// -[DataDefinition] -public sealed partial class AdjustSolutionTemperatureEffect : EntityEffect -{ - /// - /// The change in temperature. - /// - [DataField("delta", required: true)] private float _delta; - - /// - /// The minimum temperature this effect can reach. - /// - [DataField("minTemp")] private float _minTemp = 0.0f; - - /// - /// The maximum temperature this effect can reach. - /// - [DataField("maxTemp")] private float _maxTemp = float.PositiveInfinity; - - /// - /// If true, then scale ranges by intensity. If not, the ranges are the same regardless of reactant amount. - /// - [DataField("scaled")] private bool _scaled; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-adjust-solution-temperature-effect", - ("chance", Probability), ("deltasign", MathF.Sign(_delta)), ("mintemp", _minTemp), ("maxtemp", _maxTemp)); - - public override void Effect(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) - { - var solution = reagentArgs.Source; - if (solution == null || solution.Volume == 0) - return; - - var deltaT = _scaled ? _delta * (float) reagentArgs.Quantity : _delta; - solution.Temperature = Math.Clamp(solution.Temperature + deltaT, _minTemp, _maxTemp); - - return; - } - - // TODO: Someone needs to figure out how to do this for non-reagent effects. - throw new NotImplementedException(); - } -} - -/// -/// Adjusts the thermal energy of the solution involved in the reaction. -/// -public sealed partial class AdjustSolutionThermalEnergyEffect : EntityEffect -{ - /// - /// The change in energy. - /// - [DataField("delta", required: true)] private float _delta; - - /// - /// The minimum temperature this effect can reach. - /// - [DataField("minTemp")] private float _minTemp = 0.0f; - - /// - /// The maximum temperature this effect can reach. - /// - [DataField("maxTemp")] private float _maxTemp = float.PositiveInfinity; - - /// - /// If true, then scale ranges by intensity. If not, the ranges are the same regardless of reactant amount. - /// - [DataField("scaled")] private bool _scaled; - - public override void Effect(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) - { - var solution = reagentArgs.Source; - if (solution == null || solution.Volume == 0) - return; - - if (_delta > 0 && solution.Temperature >= _maxTemp) - return; - if (_delta < 0 && solution.Temperature <= _minTemp) - return; - - var heatCap = solution.GetHeatCapacity(null); - var deltaT = _scaled - ? _delta / heatCap * (float) reagentArgs.Quantity - : _delta / heatCap; - - solution.Temperature = Math.Clamp(solution.Temperature + deltaT, _minTemp, _maxTemp); - - return; - } - - // TODO: Someone needs to figure out how to do this for non-reagent effects. - throw new NotImplementedException(); - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-adjust-solution-temperature-effect", - ("chance", Probability), ("deltasign", MathF.Sign(_delta)), ("mintemp", _minTemp), ("maxtemp", _maxTemp)); -} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/BaseStatusEffectEntityEffect.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/BaseStatusEffectEntityEffect.cs new file mode 100644 index 0000000000..b028da1595 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/BaseStatusEffectEntityEffect.cs @@ -0,0 +1,35 @@ +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +/// +/// Entity effect that specifically deals with new status effects. +/// +/// The entity effect type, typically for status effects which need systems to pass arguments +public abstract partial class BaseStatusEntityEffect : EntityEffectBase where T : BaseStatusEntityEffect +{ + /// + /// How long the modifier applies (in seconds). + /// Is scaled by reagent amount if used with an EntityEffectReagentArgs. + /// + [DataField] + public TimeSpan? Time = TimeSpan.FromSeconds(2); + + /// + /// Should this effect add the status effect, remove time from it, or set its cooldown? + /// + [DataField] + public StatusEffectMetabolismType Type = StatusEffectMetabolismType.Update; + + /// + /// Delay before the effect starts. If another effect is added with a shorter delay, it takes precedence. + /// + [DataField] + public TimeSpan Delay; +} + +public enum StatusEffectMetabolismType +{ + Update, + Add, + Remove, + Set, +} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/DrunkEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/DrunkEntityEffectSystem.cs new file mode 100644 index 0000000000..553300be72 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/DrunkEntityEffectSystem.cs @@ -0,0 +1,34 @@ +using Content.Shared.Drunk; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +/// +/// Applies the drunk status effect to this entity. +/// The duration of the effect is equal to modified by scale. +/// +/// +public sealed partial class DrunkEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedDrunkSystem _drunk = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var boozePower = args.Effect.BoozePower * args.Scale; + + _drunk.TryApplyDrunkenness(entity, boozePower); + } +} + +/// +public sealed partial class Drunk : EntityEffectBase +{ + /// + /// BoozePower is how long each metabolism cycle will make the drunk effect last for. + /// + [DataField] + public TimeSpan BoozePower = TimeSpan.FromSeconds(3f); + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-drunk", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/ElectrocuteEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/ElectrocuteEntityEffectSystem.cs new file mode 100644 index 0000000000..b5a208f2c7 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/ElectrocuteEntityEffectSystem.cs @@ -0,0 +1,51 @@ +using Content.Shared.Electrocution; +using Content.Shared.StatusEffect; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +// TODO: When Electrocution is moved to new Status, make this use StatusEffectsContainerComponent. +/// +/// Electrocutes this entity for a given amount of damage and time. +/// The shock damage applied by this effect is modified by scale. +/// +/// +public sealed partial class ElectrocuteEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedElectrocutionSystem _electrocution = default!; + + // TODO: When electrocution is new status, change this to new status + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var effect = args.Effect; + + _electrocution.TryDoElectrocution(entity, null, (int)(args.Scale * effect.ShockDamage), effect.ElectrocuteTime, effect.Refresh, ignoreInsulation: effect.BypassInsulation); + } +} + +/// +public sealed partial class Electrocute : EntityEffectBase +{ + /// + /// Time we electrocute this entity + /// + [DataField] public TimeSpan ElectrocuteTime = TimeSpan.FromSeconds(2); + + /// + /// Shock damage we apply to the entity. + /// + [DataField] public int ShockDamage = 5; + + /// + /// Do we refresh the duration? Or add more duration if it already exists. + /// + [DataField] public bool Refresh = true; + + /// + /// Should we by bypassing insulation? + /// + [DataField] public bool BypassInsulation = true; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-electrocute", ("chance", Probability), ("time", ElectrocuteTime.TotalSeconds)); +} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/GenericStatusEffect.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/GenericStatusEffect.cs deleted file mode 100644 index 652c1b90a1..0000000000 --- a/Content.Shared/EntityEffects/Effects/StatusEffects/GenericStatusEffect.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Content.Shared.StatusEffect; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects.StatusEffects; - -/// -/// Adds a generic status effect to the entity, -/// not worrying about things like how to affect the time it lasts for -/// or component fields or anything. Just adds a component to an entity -/// for a given time. Easy. -/// -/// -/// Can be used for things like adding accents or something. I don't know. Go wild. -/// -[Obsolete("Use ModifyStatusEffect with StatusEffectNewSystem instead")] -public sealed partial class GenericStatusEffect : EntityEffect -{ - [DataField(required: true)] - public string Key = default!; - - [DataField] - public string Component = String.Empty; - - [DataField] - public float Time = 2.0f; - - /// - /// true - refresh status effect time, false - accumulate status effect time - /// - [DataField] - public bool Refresh = true; - - /// - /// Should this effect add the status effect, remove time from it, or set its cooldown? - /// - [DataField] - public StatusEffectMetabolismType Type = StatusEffectMetabolismType.Add; - - public override void Effect(EntityEffectBaseArgs args) - { - var statusSys = args.EntityManager.EntitySysManager.GetEntitySystem(); - - var time = Time; - if (args is EntityEffectReagentArgs reagentArgs) - time *= reagentArgs.Scale.Float(); - - if (Type == StatusEffectMetabolismType.Add && Component != String.Empty) - { - statusSys.TryAddStatusEffect(args.TargetEntity, Key, TimeSpan.FromSeconds(time), Refresh, Component); - } - else if (Type == StatusEffectMetabolismType.Remove) - { - statusSys.TryRemoveTime(args.TargetEntity, Key, TimeSpan.FromSeconds(time)); - } - else if (Type == StatusEffectMetabolismType.Set) - { - statusSys.TrySetTime(args.TargetEntity, Key, TimeSpan.FromSeconds(time)); - } - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString( - "reagent-effect-guidebook-status-effect", - ("chance", Probability), - ("type", Type), - ("time", Time), - ("key", $"reagent-effect-status-effect-{Key}")); -} - -public enum StatusEffectMetabolismType -{ - Update, - Add, - Remove, - Set -} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/GenericStatusEffectEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/GenericStatusEffectEntityEffectSystem.cs new file mode 100644 index 0000000000..2d870a433c --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/GenericStatusEffectEntityEffectSystem.cs @@ -0,0 +1,64 @@ +using Content.Shared.StatusEffect; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +/// +/// Applies a Generic Status Effect to this entity, which is a timed Component. +/// The amount of time the Component is applied is modified by scale. +/// +/// +[Obsolete("Use ModifyStatusEffect instead")] +public sealed partial class GenericStatusEffectEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly StatusEffectsSystem _status = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var time = args.Effect.Time * args.Scale; + + switch (args.Effect.Type) + { + case StatusEffectMetabolismType.Update: + if (args.Effect.Component != String.Empty) + _status.TryAddStatusEffect(entity, args.Effect.Key, time, true, args.Effect.Component); + break; + case StatusEffectMetabolismType.Add: + if (args.Effect.Component != String.Empty) + _status.TryAddStatusEffect(entity, args.Effect.Key, time, false, args.Effect.Component); + break; + case StatusEffectMetabolismType.Remove: + _status.TryRemoveTime(entity, args.Effect.Key, time); + break; + case StatusEffectMetabolismType.Set: + _status.TrySetTime(entity, args.Effect.Key, time); + break; + } + } +} + +/// +public sealed partial class GenericStatusEffect : EntityEffectBase +{ + [DataField(required: true)] + public string Key = default!; + + [DataField] + public string Component = String.Empty; + + [DataField] + public TimeSpan Time = TimeSpan.FromSeconds(2f); + + /// + /// Should this effect add the status effect, remove time from it, or set its cooldown? + /// + [DataField] + public StatusEffectMetabolismType Type = StatusEffectMetabolismType.Update; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString( + "entity-effect-guidebook-status-effect-old", + ("chance", Probability), + ("type", Type), + ("time", Time.TotalSeconds), + ("key", $"entity-effect-status-effect-{Key}")); +} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/Jitter.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/Jitter.cs deleted file mode 100644 index 891c791b07..0000000000 --- a/Content.Shared/EntityEffects/Effects/StatusEffects/Jitter.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Jittering; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects.StatusEffects; - -/// -/// Adds the jitter status effect to a mob. -/// This doesn't use generic status effects because it needs to -/// take in some parameters that JitterSystem needs. -/// -public sealed partial class Jitter : EntityEffect -{ - [DataField] - public float Amplitude = 10.0f; - - [DataField] - public float Frequency = 4.0f; - - [DataField] - public float Time = 2.0f; - - /// - /// true - refresh jitter time, false - accumulate jitter time - /// - [DataField] - public bool Refresh = true; - - public override void Effect(EntityEffectBaseArgs args) - { - var time = Time; - if (args is EntityEffectReagentArgs reagentArgs) - time *= reagentArgs.Scale.Float(); - - args.EntityManager.EntitySysManager.GetEntitySystem() - .DoJitter(args.TargetEntity, TimeSpan.FromSeconds(time), Refresh, Amplitude, Frequency); - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => - Loc.GetString("reagent-effect-guidebook-jittering", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/JitterEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/JitterEntityEffectSystem.cs new file mode 100644 index 0000000000..3b445da747 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/JitterEntityEffectSystem.cs @@ -0,0 +1,45 @@ +using Content.Shared.Jittering; +using Content.Shared.StatusEffect; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +// TODO: When Jittering is moved to new Status, make this use StatusEffectsContainerComponent. +/// +/// Applies the Jittering Status Effect to this entity. +/// The amount of time the Jittering is applied is modified by scale. +/// +/// +public sealed partial class JitterEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedJitteringSystem _jittering = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var time = args.Effect.Time * args.Scale; + + _jittering.DoJitter(entity, TimeSpan.FromSeconds(time), args.Effect.Refresh, args.Effect.Amplitude, args.Effect.Frequency); + } +} + +/// +public sealed partial class Jitter : EntityEffectBase +{ + [DataField] + public float Amplitude = 10.0f; + + [DataField] + public float Frequency = 4.0f; + + [DataField] + public float Time = 2.0f; + + /// + /// true - refresh jitter time, false - accumulate jitter time + /// + [DataField] + public bool Refresh = true; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-jittering", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyKnockdown.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyKnockdown.cs deleted file mode 100644 index 59ca5da0f7..0000000000 --- a/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyKnockdown.cs +++ /dev/null @@ -1,99 +0,0 @@ -using Content.Shared.Stunnable; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects.StatusEffects; - -/// -/// Changes the knockdown timer on an entity or causes knockdown. -/// -[UsedImplicitly] -public sealed partial class ModifyKnockdown : EntityEffect -{ - /// - /// Should we only affect those with crawler component? Note if this is false, it will paralyze non-crawler's instead. - /// - [DataField] - public bool Crawling; - - /// - /// Should we drop items when we fall? - /// - [DataField] - public bool Drop; - - /// - /// Time for which knockdown should be applied. Behaviour changes according to . - /// - [DataField] - public TimeSpan Time = TimeSpan.FromSeconds(0.5); - - /// - /// Should this effect add the status effect, remove time from it, or set its cooldown? - /// - [DataField] - public StatusEffectMetabolismType Type = StatusEffectMetabolismType.Add; - - /// - /// Should this effect add knockdown?, remove time from it?, or set its cooldown? - /// - [DataField] - public bool Refresh = true; - - /// - public override void Effect(EntityEffectBaseArgs args) - { - var stunSys = args.EntityManager.EntitySysManager.GetEntitySystem(); - - var time = Time; - if (args is EntityEffectReagentArgs reagentArgs) - time *= reagentArgs.Scale.Float(); - - switch (Type) - { - case StatusEffectMetabolismType.Update: - if (Crawling) - { - stunSys.TryCrawling(args.TargetEntity, time, drop: Drop); - } - else - { - stunSys.TryKnockdown(args.TargetEntity, time, drop: Drop); - } - break; - case StatusEffectMetabolismType.Add: - if (Crawling) - { - stunSys.TryCrawling(args.TargetEntity, time, false, drop: Drop); - } - else - { - stunSys.TryKnockdown(args.TargetEntity, time, false, drop: Drop); - } - break; - case StatusEffectMetabolismType.Remove: - stunSys.AddKnockdownTime(args.TargetEntity, -time); - break; - case StatusEffectMetabolismType.Set: - if (Crawling) - { - stunSys.TryCrawling(args.TargetEntity, time, drop: Drop); - } - else - { - stunSys.TryKnockdown(args.TargetEntity, time, drop: Drop); - } - stunSys.SetKnockdownTime(args.TargetEntity, time); - break; - } - } - - /// - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString( - "reagent-effect-guidebook-knockdown", - ("chance", Probability), - ("type", Type), - ("time", Time.TotalSeconds) - ); -} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyKnockdownEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyKnockdownEntityEffectSystem.cs new file mode 100644 index 0000000000..5e01774ce4 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyKnockdownEntityEffectSystem.cs @@ -0,0 +1,72 @@ +using Content.Shared.Standing; +using Content.Shared.Stunnable; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +/// +/// Applies knockdown to this entity. +/// Duration is modified by scale. +/// +/// +public sealed partial class ModifyKnockdownEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedStunSystem _stun = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var time = args.Effect.Time * args.Scale; + + switch (args.Effect.Type) + { + case StatusEffectMetabolismType.Update: + if (args.Effect.Crawling) + _stun.TryCrawling(entity.Owner, time, drop: args.Effect.Drop); + else + _stun.TryKnockdown(entity.Owner, time, drop: args.Effect.Drop); + break; + case StatusEffectMetabolismType.Add: + if (args.Effect.Crawling) + _stun.TryCrawling(entity.Owner, time, false, drop: args.Effect.Drop); + else + _stun.TryKnockdown(entity.Owner, time, false, drop: args.Effect.Drop); + break; + case StatusEffectMetabolismType.Remove: + _stun.AddKnockdownTime(entity.Owner, - time ?? TimeSpan.Zero); + break; + case StatusEffectMetabolismType.Set: + if (args.Effect.Crawling) + _stun.TryCrawling(entity.Owner, drop: args.Effect.Drop); + else + _stun.TryKnockdown(entity.Owner, time, drop: args.Effect.Drop); + _stun.SetKnockdownTime(entity.Owner, time ?? TimeSpan.Zero); + break; + } + } +} + +/// +public sealed partial class ModifyKnockdown : BaseStatusEntityEffect +{ + /// + /// Should we only affect those with crawler component? Note if this is false, it will paralyze non-crawler's instead. + /// + [DataField] + public bool Crawling; + + /// + /// Should we drop items when we fall? + /// + [DataField] + public bool Drop; + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Time == null + ? null + : Loc.GetString( + "entity-effect-guidebook-knockdown", + ("chance", Probability), + ("type", Type), + ("time", Time.Value.TotalSeconds) + ); +} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyParalysisEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyParalysisEntityEffectSystem.cs new file mode 100644 index 0000000000..c6260eb71d --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyParalysisEntityEffectSystem.cs @@ -0,0 +1,50 @@ +using Content.Shared.StatusEffectNew; +using Content.Shared.Stunnable; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +/// +/// Applies the paralysis status effect to this entity. +/// Duration is modified by scale. +/// +/// +public sealed partial class ModifyParalysisEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly StatusEffectsSystem _status = default!; + [Dependency] private readonly SharedStunSystem _stun = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var time = args.Effect.Time * args.Scale; + + switch (args.Effect.Type) + { + case StatusEffectMetabolismType.Update: + _stun.TryUpdateParalyzeDuration(entity, time); + break; + case StatusEffectMetabolismType.Add: + _stun.TryAddParalyzeDuration(entity, time); + break; + case StatusEffectMetabolismType.Remove: + _status.TryRemoveTime(entity, SharedStunSystem.StunId, time); + break; + case StatusEffectMetabolismType.Set: + _status.TrySetStatusEffectDuration(entity, SharedStunSystem.StunId, time); + break; + } + } +} + +/// +public sealed partial class ModifyParalysis : BaseStatusEntityEffect +{ + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Time == null + ? null // Not gonna make a whole new looc for something that shouldn't ever exist. + : Loc.GetString( + "entity-effect-guidebook-paralyze", + ("chance", Probability), + ("time", Time.Value.TotalSeconds) + ); +} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffect.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffect.cs deleted file mode 100644 index d7bf6482bd..0000000000 --- a/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffect.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Content.Shared.StatusEffectNew; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects.StatusEffects; - -/// -/// Changes status effects on entities: Adds, removes or sets time. -/// -[UsedImplicitly] -public sealed partial class ModifyStatusEffect : EntityEffect -{ - [DataField(required: true)] - public EntProtoId EffectProto; - - /// - /// Time for which status effect should be applied. Behaviour changes according to . - /// - [DataField] - public float Time = 2.0f; - - /// - /// Delay before the effect starts. If another effect is added with a shorter delay, it takes precedence. - /// - [DataField] - public float Delay = 0f; - - /// - /// Should this effect add the status effect, remove time from it, or set its cooldown? - /// - [DataField] - public StatusEffectMetabolismType Type = StatusEffectMetabolismType.Add; - - /// - public override void Effect(EntityEffectBaseArgs args) - { - var statusSys = args.EntityManager.EntitySysManager.GetEntitySystem(); - - var time = Time; - if (args is EntityEffectReagentArgs reagentArgs) - time *= reagentArgs.Scale.Float(); - - var duration = TimeSpan.FromSeconds(time); - switch (Type) - { - case StatusEffectMetabolismType.Update: - statusSys.TryUpdateStatusEffectDuration(args.TargetEntity, EffectProto, duration, Delay > 0 ? TimeSpan.FromSeconds(Delay) : null); - break; - case StatusEffectMetabolismType.Add: - statusSys.TryAddStatusEffectDuration(args.TargetEntity, EffectProto, duration, Delay > 0 ? TimeSpan.FromSeconds(Delay) : null); - break; - case StatusEffectMetabolismType.Remove: - statusSys.TryAddTime(args.TargetEntity, EffectProto, -duration); - break; - case StatusEffectMetabolismType.Set: - statusSys.TrySetStatusEffectDuration(args.TargetEntity, EffectProto, duration, TimeSpan.FromSeconds(Delay)); - break; - } - } - - /// - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => - Delay > 0 - ? Loc.GetString( - "reagent-effect-guidebook-status-effect-delay", - ("chance", Probability), - ("type", Type), - ("time", Time), - ("key", prototype.Index(EffectProto).Name), - ("delay", Delay)) - : Loc.GetString( - "reagent-effect-guidebook-status-effect", - ("chance", Probability), - ("type", Type), - ("time", Time), - ("key", prototype.Index(EffectProto).Name) - ); -} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffectEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffectEntityEffectSystem.cs new file mode 100644 index 0000000000..b42c3f2950 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffectEntityEffectSystem.cs @@ -0,0 +1,65 @@ +using Content.Shared.StatusEffectNew; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +/// +/// Applies a given status effect to this entity. +/// Duration is modified by scale. +/// +/// +public sealed partial class ModifyStatusEffectEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly StatusEffectsSystem _status = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var time = args.Effect.Time * args.Scale; + var delay = args.Effect.Delay; + + switch (args.Effect.Type) + { + case StatusEffectMetabolismType.Update: + _status.TryUpdateStatusEffectDuration(entity, args.Effect.EffectProto, time, delay); + break; + case StatusEffectMetabolismType.Add: + if (time != null) + _status.TryAddStatusEffectDuration(entity, args.Effect.EffectProto, time.Value, delay); + else + _status.TryUpdateStatusEffectDuration(entity, args.Effect.EffectProto, time, delay); + break; + case StatusEffectMetabolismType.Remove: + _status.TryRemoveTime(entity, args.Effect.EffectProto, time); + break; + case StatusEffectMetabolismType.Set: + _status.TrySetStatusEffectDuration(entity, args.Effect.EffectProto, time, delay); + break; + } + } +} + +/// +public sealed partial class ModifyStatusEffect : BaseStatusEntityEffect +{ + /// + /// Prototype of the status effect we're modifying. + /// + [DataField(required: true)] + public EntProtoId EffectProto; + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Time == null + ? Loc.GetString( + "entity-effect-guidebook-status-effect-indef", + ("chance", Probability), + ("type", Type), + ("key", prototype.Index(EffectProto).Name), + ("delay", Delay.TotalSeconds)) + : Loc.GetString( + "entity-effect-guidebook-status-effect", + ("chance", Probability), + ("type", Type), + ("time", Time.Value.TotalSeconds), + ("key", prototype.Index(EffectProto).Name), + ("delay", Delay.TotalSeconds)); +} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/MovementSpeedModifierEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/MovementSpeedModifierEntityEffectSystem.cs new file mode 100644 index 0000000000..ed9923f856 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/MovementSpeedModifierEntityEffectSystem.cs @@ -0,0 +1,92 @@ +using Content.Shared.Movement.Components; +using Content.Shared.Movement.Systems; +using Content.Shared.StatusEffectNew; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +/// +/// Applies a given movement speed modifier status effect to this entity. +/// Duration is modified by scale. +/// +/// +public sealed partial class MovementSpeedModifierEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly StatusEffectsSystem _status = default!; + [Dependency] private readonly MovementModStatusSystem _movementModStatus = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var proto = args.Effect.EffectProto; + var sprintMod = args.Effect.SprintSpeedModifier; + var walkMod = args.Effect.WalkSpeedModifier; + + switch (args.Effect.Type) + { + case StatusEffectMetabolismType.Update: + _movementModStatus.TryUpdateMovementSpeedModDuration( + entity, + proto, + args.Effect.Time * args.Scale, + sprintMod, + walkMod); + break; + case StatusEffectMetabolismType.Add: + if (args.Effect.Time != null) + { + _movementModStatus.TryAddMovementSpeedModDuration( + entity, + proto, + args.Effect.Time.Value * args.Scale, + sprintMod, + walkMod); + } + else + { + _movementModStatus.TryUpdateMovementSpeedModDuration( + entity, + proto, + args.Effect.Time * args.Scale, + sprintMod, + walkMod); + } + break; + case StatusEffectMetabolismType.Remove: + _status.TryRemoveTime(entity, args.Effect.EffectProto, args.Effect.Time * args.Scale); + break; + case StatusEffectMetabolismType.Set: + _status.TrySetStatusEffectDuration(entity, proto, args.Effect.Time * args.Scale); + break; + } + } +} + +/// +public sealed partial class MovementSpeedModifier : BaseStatusEntityEffect +{ + /// + /// How much the entities' walk speed is multiplied by. + /// + [DataField] + public float WalkSpeedModifier = 1f; + + /// + /// How much the entities' run speed is multiplied by. + /// + [DataField] + public float SprintSpeedModifier = 1f; + + /// + /// Movement speed modifier prototype we're adding. Adding in case we ever want more than one prototype that boosts speed. + /// + [DataField] + public EntProtoId EffectProto = MovementModStatusSystem.ReagentSpeed; + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Time == null + ? null // Not gonna make a whole new looc for something that shouldn't ever exist. + : Loc.GetString("entity-effect-guidebook-movespeed-modifier", + ("chance", Probability), + ("sprintspeed", SprintSpeedModifier), + ("time", Time.Value.TotalSeconds)); +} diff --git a/Content.Shared/EntityEffects/Effects/TemplateEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/TemplateEntityEffectSystem.cs new file mode 100644 index 0000000000..ea7c2dc8d4 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/TemplateEntityEffectSystem.cs @@ -0,0 +1,21 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// A brief summary of the effect. +/// +/// +public sealed partial class TemplateEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent