Merge remote-tracking branch 'upstream/master' into upstream-sync

# Conflicts:
#	Content.Client/Audio/ContentAudioSystem.cs
#	Content.Client/Options/UI/Tabs/AudioTab.xaml
#	Content.Client/Options/UI/Tabs/AudioTab.xaml.cs
#	Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs
#	Resources/Prototypes/Datasets/ion_storm.yml
#	Resources/ServerInfo/Guidebook/Security/Forensics.xml
#	Resources/Textures/Clothing/Head/Hats/brownfedora.rsi/equipped-HELMET.png
#	Resources/Textures/Clothing/Head/Hats/brownfedora.rsi/icon.png
#	Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json
#	Resources/Textures/Structures/Doors/Airlocks/Glass/atmospherics.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Glass/basic.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Glass/cargo.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Glass/command.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Glass/engineering.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Glass/maint.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Glass/medical.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Glass/science.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Glass/security.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Glass/virology.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Standard/atmospherics.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Standard/basic.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Standard/cargo.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Standard/command.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Standard/engineering.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Standard/freezer.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Standard/maint.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Standard/medical.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Standard/science.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Standard/security.rsi/assembly.png
#	Resources/Textures/Structures/Doors/Airlocks/Standard/virology.rsi/assembly.png
This commit is contained in:
Morb0 2023-12-29 16:58:52 +03:00
commit 8ba54ec466
660 changed files with 411196 additions and 289714 deletions

View file

@ -4,9 +4,6 @@ using Robust.Client.Console;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
namespace Content.Client.Administration.UI.ManageSolutions

View file

@ -20,7 +20,7 @@ namespace Content.Client.Administration.UI.ManageSolutions
private NetEntity _target = NetEntity.Invalid;
private string? _selectedSolution;
private AddReagentWindow? _addReagentWindow;
private Dictionary<string, Solution>? _solutions;
private Dictionary<string, EntityUid>? _solutions;
public EditSolutionsWindow()
{
@ -60,9 +60,11 @@ namespace Content.Client.Administration.UI.ManageSolutions
if (_selectedSolution == null || _solutions == null)
return;
if (!_solutions.TryGetValue(_selectedSolution, out var solution))
if (!_solutions.TryGetValue(_selectedSolution, out var solutionId) ||
!_entityManager.TryGetComponent(solutionId, out SolutionComponent? solutionComp))
return;
var solution = solutionComp.Solution;
UpdateVolumeBox(solution);
UpdateThermalBox(solution);
@ -198,10 +200,13 @@ namespace Content.Client.Administration.UI.ManageSolutions
/// </summary>
private void SetReagent(FloatSpinBox.FloatSpinBoxEventArgs args, string prototype)
{
if (_solutions == null || _selectedSolution == null)
if (_solutions == null || _selectedSolution == null ||
!_solutions.TryGetValue(_selectedSolution, out var solutionId) ||
!_entityManager.TryGetComponent(solutionId, out SolutionComponent? solutionComp))
return;
var current = _solutions[_selectedSolution].GetTotalPrototypeQuantity(prototype);
var solution = solutionComp.Solution;
var current = solution.GetTotalPrototypeQuantity(prototype);
var delta = args.Value - current.Float();
if (MathF.Abs(delta) < 0.01)
@ -275,22 +280,38 @@ namespace Content.Client.Administration.UI.ManageSolutions
/// <summary>
/// Update the solution options.
/// </summary>
public void UpdateSolutions(Dictionary<string, Solution>? solutions)
public void UpdateSolutions(List<(string, NetEntity)>? solutions)
{
SolutionOption.Clear();
_solutions = solutions;
if (solutions is { Count: > 0 })
{
if (_solutions is { Count: > 0 })
_solutions.Clear();
else
_solutions = new(solutions.Count);
foreach (var (name, netSolution) in solutions)
{
if (_entityManager.TryGetEntity(netSolution, out var solution))
_solutions.Add(name, solution.Value);
}
}
else
_solutions = null;
if (_solutions == null)
return;
int i = 0;
foreach (var solution in _solutions.Keys)
int selectedIndex = 0; // Default to the first solution if none are found.
foreach (var (name, _) in _solutions)
{
SolutionOption.AddItem(solution, i);
SolutionOption.SetItemMetadata(i, solution);
SolutionOption.AddItem(name, i);
SolutionOption.SetItemMetadata(i, name);
if (solution == _selectedSolution)
SolutionOption.Select(i);
if (name == _selectedSolution)
selectedIndex = i;
i++;
}
@ -300,14 +321,11 @@ namespace Content.Client.Administration.UI.ManageSolutions
// No applicable solutions
Close();
Dispose();
return;
}
if (_selectedSolution == null || !_solutions.ContainsKey(_selectedSolution))
{
// the previously selected solution is no longer valid.
SolutionOption.Select(0);
_selectedSolution = (string?) SolutionOption.SelectedMetadata;
}
SolutionOption.Select(selectedIndex);
_selectedSolution = (string?) SolutionOption.SelectedMetadata;
}
}
}

View file

@ -0,0 +1,98 @@
using Content.Shared.CCVar;
using Robust.Client.Audio;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Configuration;
namespace Content.Client.Audio;
public sealed class AudioUIController : UIController
{
[Dependency] private readonly IAudioManager _audioManager = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IResourceCache _cache = default!;
private float _interfaceGain;
private IAudioSource? _clickSource;
private IAudioSource? _hoverSource;
private const float ClickGain = 0.25f;
private const float HoverGain = 0.05f;
public override void Initialize()
{
base.Initialize();
/*
* This exists to load UI sounds outside of the game sim.
*/
// No unsub coz never shuts down until program exit.
_configManager.OnValueChanged(CCVars.UIClickSound, SetClickSound, true);
_configManager.OnValueChanged(CCVars.UIHoverSound, SetHoverSound, true);
_configManager.OnValueChanged(CCVars.InterfaceVolume, SetInterfaceVolume, true);
}
private void SetInterfaceVolume(float obj)
{
_interfaceGain = obj;
if (_clickSource != null)
{
_clickSource.Gain = ClickGain * _interfaceGain;
}
if (_hoverSource != null)
{
_hoverSource.Gain = HoverGain * _interfaceGain;
}
}
private void SetClickSound(string value)
{
if (!string.IsNullOrEmpty(value))
{
var resource = _cache.GetResource<AudioResource>(value);
var source =
_audioManager.CreateAudioSource(resource);
if (source != null)
{
source.Gain = ClickGain * _interfaceGain;
source.Global = true;
}
_clickSource = source;
UIManager.SetClickSound(source);
}
else
{
UIManager.SetClickSound(null);
}
}
private void SetHoverSound(string value)
{
if (!string.IsNullOrEmpty(value))
{
var hoverResource = _cache.GetResource<AudioResource>(value);
var hoverSource =
_audioManager.CreateAudioSource(hoverResource);
if (hoverSource != null)
{
hoverSource.Gain = HoverGain * _interfaceGain;
hoverSource.Global = true;
}
_hoverSource = hoverSource;
UIManager.SetHoverSound(hoverSource);
}
else
{
UIManager.SetHoverSound(null);
}
}
}

View file

@ -1,15 +1,18 @@
using Content.Shared.Audio;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Robust.Client.GameObjects;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Client.Audio;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using AudioComponent = Robust.Shared.Audio.Components.AudioComponent;
namespace Content.Client.Audio;
public sealed partial class ContentAudioSystem : SharedContentAudioSystem
{
[Dependency] private readonly IAudioManager _audioManager = default!;
[Dependency] private readonly IResourceCache _cache = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
// Need how much volume to change per tick and just remove it when it drops below "0"
private readonly Dictionary<EntityUid, float> _fadingOut = new();
@ -32,6 +35,7 @@ public sealed partial class ContentAudioSystem : SharedContentAudioSystem
public const float AmbienceMultiplier = 3f;
public const float AmbientMusicMultiplier = 3f;
public const float LobbyMultiplier = 3f;
public const float InterfaceMultiplier = 2f;
public const float TtsMultiplier = 3f; // Corvax-TTS
public override void Initialize()

View file

@ -1,6 +1,7 @@
using Content.Client.Rotation;
using Content.Shared.Buckle;
using Content.Shared.Buckle.Components;
using Content.Shared.Rotation;
using Content.Shared.Vehicle.Components;
using Robust.Client.GameObjects;
@ -56,17 +57,15 @@ internal sealed class BuckleSystem : SharedBuckleSystem
if (!TryComp<RotationVisualsComponent>(uid, out var rotVisuals))
return;
if (!Appearance.TryGetData<int>(uid, StrapVisuals.RotationAngle, out var angle, args.Component) ||
!Appearance.TryGetData<bool>(uid, BuckleVisuals.Buckled, out var buckled, args.Component) ||
if (!Appearance.TryGetData<bool>(uid, BuckleVisuals.Buckled, out var buckled, args.Component) ||
!buckled ||
args.Sprite == null)
{
_rotationVisualizerSystem.SetHorizontalAngle(uid, rotVisuals.DefaultRotation, rotVisuals);
_rotationVisualizerSystem.SetHorizontalAngle((uid, rotVisuals), rotVisuals.DefaultRotation);
return;
}
// Animate strapping yourself to something at a given angle
_rotationVisualizerSystem.SetHorizontalAngle(uid, Angle.FromDegrees(angle), rotVisuals);
// TODO: Dump this when buckle is better
_rotationVisualizerSystem.AnimateSpriteRotation(uid, args.Sprite, rotVisuals.HorizontalRotation, 0.125f);
}

View file

@ -0,0 +1,28 @@
using Content.Client.UserInterface.Fragments;
using Content.Shared.CartridgeLoader.Cartridges;
using Robust.Client.UserInterface;
namespace Content.Client.CartridgeLoader.Cartridges;
public sealed partial class LogProbeUi : UIFragment
{
private LogProbeUiFragment? _fragment;
public override Control GetUIFragmentRoot()
{
return _fragment!;
}
public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
{
_fragment = new LogProbeUiFragment();
}
public override void UpdateState(BoundUserInterfaceState state)
{
if (state is not LogProbeUiState logProbeUiState)
return;
_fragment?.UpdateState(logProbeUiState.PulledLogs);
}
}

View file

@ -0,0 +1,20 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Margin="4"
Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Name="NumberLabel"
Align="Center"
SetWidth="60"
ClipText="True"/>
<Label Name="TimeLabel"
Align="Center"
SetWidth="280"
ClipText="True"/>
<Label Name="AccessorLabel"
Align="Center"
SetWidth="110"
ClipText="True"/>
</BoxContainer>
<customControls:HSeparator Margin="0 5 0 5"/>
</BoxContainer>

View file

@ -0,0 +1,17 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class LogProbeUiEntry : BoxContainer
{
public LogProbeUiEntry(int numberLabel, string timeText, string accessorText)
{
RobustXamlLoader.Load(this);
NumberLabel.Text = numberLabel.ToString();
TimeLabel.Text = timeText;
AccessorLabel.Text = accessorText;
}
}

View file

@ -0,0 +1,21 @@
 <cartridges:LogProbeUiFragment xmlns="https://spacestation14.io"
xmlns:cartridges="clr-namespace:Content.Client.CartridgeLoader.Cartridges"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Orientation="Vertical"
VerticalExpand="True">
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000FF"
BorderColor="#5a5a5a"
BorderThickness="0 0 0 1"/>
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Horizontal" Align="Center" Margin="8">
<Label HorizontalExpand="True" Text="{Loc 'log-probe-label-number'}"/>
<Label HorizontalExpand="True" Text="{Loc 'log-probe-label-time'}"/>
<Label HorizontalExpand="True" Text="{Loc 'log-probe-label-accessor'}"/>
</BoxContainer>
</PanelContainer>
<ScrollContainer VerticalExpand="True" HScrollEnabled="True">
<BoxContainer Orientation="Vertical" Name="ProbedDeviceContainer"/>
</ScrollContainer>
</cartridges:LogProbeUiFragment>

View file

@ -0,0 +1,39 @@
using Content.Shared.CartridgeLoader.Cartridges;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class LogProbeUiFragment : BoxContainer
{
public LogProbeUiFragment()
{
RobustXamlLoader.Load(this);
}
public void UpdateState(List<PulledAccessLog> logs)
{
ProbedDeviceContainer.RemoveAllChildren();
//Reverse the list so the oldest entries appear at the bottom
logs.Reverse();
var count = 1;
foreach (var log in logs)
{
AddAccessLog(log, count);
count++;
}
}
private void AddAccessLog(PulledAccessLog log, int numberLabelText)
{
var timeLabelText = TimeSpan.FromSeconds(Math.Truncate(log.Time.TotalSeconds)).ToString();
var accessorLabelText = log.Accessor;
var entry = new LogProbeUiEntry(numberLabelText, timeLabelText, accessorLabelText);
ProbedDeviceContainer.AddChild(entry);
}
}

View file

@ -0,0 +1,7 @@
using Content.Shared.Chemistry.EntitySystems;
namespace Content.Client.Chemistry.Containers.EntitySystems;
public sealed partial class SolutionContainerSystem : SharedSolutionContainerSystem
{
}

View file

@ -1,4 +1,5 @@
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Rounding;
using Robust.Client.GameObjects;

View file

@ -394,7 +394,7 @@ namespace Content.Client.Construction.UI
if (IsAtFront)
{
WindowOpen = false;
_uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Pressed = false; // This does not call CraftingButtonToggled
_uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.SetClickPressed(false); // This does not call CraftingButtonToggled
}
else
_constructionView.MoveToFront();
@ -402,7 +402,7 @@ namespace Content.Client.Construction.UI
else
{
WindowOpen = true;
_uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Pressed = true; // This does not call CraftingButtonToggled
_uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.SetClickPressed(true); // This does not call CraftingButtonToggled
}
}

View file

@ -3,6 +3,7 @@ using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameStates;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@ -18,6 +19,7 @@ public sealed class ExplosionOverlaySystem : EntitySystem
[Dependency] private readonly IResourceCache _resCache = default!;
[Dependency] private readonly IOverlayManager _overlayMan = default!;
[Dependency] private readonly SharedPointLightSystem _lights = default!;
[Dependency] private readonly IMapManager _mapMan = default!;
public override void Initialize()
{
@ -65,15 +67,20 @@ public sealed class ExplosionOverlaySystem : EntitySystem
return;
}
// spawn in a client-side light source at the epicenter
var lightEntity = Spawn("ExplosionLight", component.Epicenter);
var light = _lights.EnsureLight(lightEntity);
// Map may have been deleted.
if (_mapMan.MapExists(component.Epicenter.MapId))
{
// spawn in a client-side light source at the epicenter
var lightEntity = Spawn("ExplosionLight", component.Epicenter);
var light = _lights.EnsureLight(lightEntity);
_lights.SetRadius(lightEntity, component.Intensity.Count, light);
_lights.SetEnergy(lightEntity, component.Intensity.Count, light);
_lights.SetColor(lightEntity, type.LightColor, light);
textures.LightEntity = lightEntity;
}
_lights.SetRadius(lightEntity, component.Intensity.Count, light);
_lights.SetEnergy(lightEntity, component.Intensity.Count, light);
_lights.SetColor(lightEntity, type.LightColor, light);
textures.LightEntity = lightEntity;
textures.FireColor = type.FireColor;
textures.IntensityPerState = type.IntensityPerState;

View file

@ -1,9 +1,12 @@
using Content.Client.Movement.Systems;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
using Content.Shared.Eye.Blinding;
using Content.Shared.Eye.Blinding.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
namespace Content.Client.Eye.Blinding
{
@ -11,9 +14,8 @@ namespace Content.Client.Eye.Blinding
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] IEntityManager _entityManager = default!;
[Dependency] ILightManager _lightManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly ILightManager _lightManager = default!;
public override bool RequestScreenTexture => true;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
@ -30,13 +32,13 @@ namespace Content.Client.Eye.Blinding
}
protected override bool BeforeDraw(in OverlayDrawArgs args)
{
if (!_entityManager.TryGetComponent(_playerManager.LocalPlayer?.ControlledEntity, out EyeComponent? eyeComp))
if (!_entityManager.TryGetComponent(_playerManager.LocalSession?.AttachedEntity, out EyeComponent? eyeComp))
return false;
if (args.Viewport.Eye != eyeComp.Eye)
return false;
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
var playerEntity = _playerManager.LocalSession?.AttachedEntity;
if (playerEntity == null)
return false;
@ -64,6 +66,11 @@ namespace Content.Client.Eye.Blinding
if (ScreenTexture == null)
return;
var playerEntity = _playerManager.LocalSession?.AttachedEntity;
if (playerEntity == null)
return;
if (!_blindableComponent.GraceFrame)
{
_blindableComponent.LightSetup = true; // Ok we touched the lights
@ -73,6 +80,10 @@ namespace Content.Client.Eye.Blinding
_blindableComponent.GraceFrame = false;
}
if (_entityManager.TryGetComponent<EyeComponent>(playerEntity, out var content))
{
_circleMaskShader?.SetParameter("ZOOM", content.Zoom.X);
}
_greyscaleShader?.SetParameter("SCREEN_TEXTURE", ScreenTexture);
var worldHandle = args.WorldHandle;

View file

@ -1,11 +1,9 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Kitchen;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
@ -15,7 +13,7 @@ namespace Content.Client.Kitchen.UI
public sealed partial class GrinderMenu : FancyWindow
{
private readonly IEntityManager _entityManager;
private readonly IPrototypeManager _prototypeManager ;
private readonly IPrototypeManager _prototypeManager;
private readonly ReagentGrinderBoundUserInterface _owner;
private readonly Dictionary<int, EntityUid> _chamberVisualContents = new();
@ -122,8 +120,8 @@ namespace Content.Client.Kitchen.UI
{
foreach (var (reagent, quantity) in reagents)
{
var reagentName = _prototypeManager.TryIndex(reagent.Prototype, out ReagentPrototype? proto)
? Loc.GetString($"{quantity} {proto.LocalizedName}")
var reagentName = _prototypeManager.TryIndex(reagent.Prototype, out ReagentPrototype? proto)
? Loc.GetString($"{quantity} {proto.LocalizedName}")
: "???";
BeakerContentBox.BoxContents.AddItem(reagentName);
}

View file

@ -1,11 +1,9 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Kitchen.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Graphics;
namespace Content.Client.Kitchen.UI
{

View file

@ -74,6 +74,19 @@
<Label Name="LobbyVolumeLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="5 0 0 0">
<Label Text="{Loc 'ui-options-interface-volume'}" HorizontalExpand="True" />
<Control MinSize="8 0" />
<Slider Name="InterfaceVolumeSlider"
MinValue="0"
MaxValue="100"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="InterfaceVolumeLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="5 0 0 0">
<Label Text="{Loc 'ui-options-tts-volume'}" HorizontalExpand="True" />
<Control MinSize="8 0" />

View file

@ -3,13 +3,10 @@ using Content.Shared.CCVar;
using Content.Shared.Corvax.CCCVars;
using Robust.Client.Audio;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Range = Robust.Client.UserInterface.Controls.Range;
@ -40,6 +37,7 @@ namespace Content.Client.Options.UI.Tabs
AmbienceVolumeSlider.OnValueChanged += OnAmbienceVolumeSliderChanged;
AmbienceSoundsSlider.OnValueChanged += OnAmbienceSoundsSliderChanged;
LobbyVolumeSlider.OnValueChanged += OnLobbyVolumeSliderChanged;
InterfaceVolumeSlider.OnValueChanged += OnInterfaceVolumeSliderChanged;
TtsVolumeSlider.OnValueChanged += OnTtsVolumeSliderChanged; // Corvax-TTS
LobbyMusicCheckBox.OnToggled += OnLobbyMusicCheckToggled;
RestartSoundsCheckBox.OnToggled += OnRestartSoundsCheckToggled;
@ -61,6 +59,7 @@ namespace Content.Client.Options.UI.Tabs
AmbientMusicVolumeSlider.OnValueChanged -= OnAmbientMusicVolumeSliderChanged;
AmbienceVolumeSlider.OnValueChanged -= OnAmbienceVolumeSliderChanged;
LobbyVolumeSlider.OnValueChanged -= OnLobbyVolumeSliderChanged;
InterfaceVolumeSlider.OnValueChanged -= OnInterfaceVolumeSliderChanged;
TtsVolumeSlider.OnValueChanged -= OnTtsVolumeSliderChanged; // Corvax-TTS
base.Dispose(disposing);
}
@ -70,6 +69,11 @@ namespace Content.Client.Options.UI.Tabs
UpdateChanges();
}
private void OnInterfaceVolumeSliderChanged(Range obj)
{
UpdateChanges();
}
private void OnAmbientMusicVolumeSliderChanged(Range obj)
{
UpdateChanges();
@ -130,6 +134,7 @@ namespace Content.Client.Options.UI.Tabs
_cfg.SetCVar(CCVars.AmbienceVolume, AmbienceVolumeSlider.Value / 100f * ContentAudioSystem.AmbienceMultiplier);
_cfg.SetCVar(CCVars.AmbientMusicVolume, AmbientMusicVolumeSlider.Value / 100f * ContentAudioSystem.AmbientMusicMultiplier);
_cfg.SetCVar(CCVars.LobbyMusicVolume, LobbyVolumeSlider.Value / 100f * ContentAudioSystem.LobbyMultiplier);
_cfg.SetCVar(CCVars.InterfaceVolume, InterfaceVolumeSlider.Value / 100f * ContentAudioSystem.InterfaceMultiplier);
_cfg.SetCVar(CCCVars.TTSVolume, TtsVolumeSlider.Value / 100f * ContentAudioSystem.TtsMultiplier); // Corvax-TTS
_cfg.SetCVar(CCVars.MaxAmbientSources, (int)AmbienceSoundsSlider.Value);
@ -154,6 +159,7 @@ namespace Content.Client.Options.UI.Tabs
AmbienceVolumeSlider.Value = _cfg.GetCVar(CCVars.AmbienceVolume) * 100f / ContentAudioSystem.AmbienceMultiplier;
AmbientMusicVolumeSlider.Value = _cfg.GetCVar(CCVars.AmbientMusicVolume) * 100f / ContentAudioSystem.AmbientMusicMultiplier;
LobbyVolumeSlider.Value = _cfg.GetCVar(CCVars.LobbyMusicVolume) * 100f / ContentAudioSystem.LobbyMultiplier;
InterfaceVolumeSlider.Value = _cfg.GetCVar(CCVars.InterfaceVolume) * 100f / ContentAudioSystem.InterfaceMultiplier;
TtsVolumeSlider.Value = _cfg.GetCVar(CCCVars.TTSVolume) * 100f / ContentAudioSystem.TtsMultiplier; // Corvax-TTS
AmbienceSoundsSlider.Value = _cfg.GetCVar(CCVars.MaxAmbientSources);
@ -178,6 +184,8 @@ namespace Content.Client.Options.UI.Tabs
Math.Abs(AmbientMusicVolumeSlider.Value - _cfg.GetCVar(CCVars.AmbientMusicVolume) * 100f / ContentAudioSystem.AmbientMusicMultiplier) < 0.01f;
var isLobbyVolumeSame =
Math.Abs(LobbyVolumeSlider.Value - _cfg.GetCVar(CCVars.LobbyMusicVolume) * 100f / ContentAudioSystem.LobbyMultiplier) < 0.01f;
var isInterfaceVolumeSame =
Math.Abs(InterfaceVolumeSlider.Value - _cfg.GetCVar(CCVars.InterfaceVolume) * 100f / ContentAudioSystem.InterfaceMultiplier) < 0.01f;
var isTtsVolumeSame =
Math.Abs(LobbyVolumeSlider.Value - _cfg.GetCVar(CCVars.LobbyMusicVolume) * 100f / ContentAudioSystem.TtsMultiplier) < 0.01f;
@ -187,7 +195,7 @@ namespace Content.Client.Options.UI.Tabs
var isEventSame = EventMusicCheckBox.Pressed == _cfg.GetCVar(CCVars.EventMusicEnabled);
var isAdminSoundsSame = AdminSoundsCheckBox.Pressed == _cfg.GetCVar(CCVars.AdminSoundsEnabled);
var isEverythingSame = isMasterVolumeSame && isMidiVolumeSame && isAmbientVolumeSame && isAmbientMusicVolumeSame && isAmbientSoundsSame && isLobbySame && isRestartSoundsSame && isEventSame
&& isAdminSoundsSame && isLobbyVolumeSame;
&& isAdminSoundsSame && isLobbyVolumeSame && isInterfaceVolumeSame;
isEverythingSame = isEverythingSame && isTtsVolumeSame; // Corvax-TTS
ApplyButton.Disabled = isEverythingSame;
ResetButton.Disabled = isEverythingSame;
@ -201,6 +209,8 @@ namespace Content.Client.Options.UI.Tabs
Loc.GetString("ui-options-volume-percent", ("volume", AmbienceVolumeSlider.Value / 100));
LobbyVolumeLabel.Text =
Loc.GetString("ui-options-volume-percent", ("volume", LobbyVolumeSlider.Value / 100));
InterfaceVolumeLabel.Text =
Loc.GetString("ui-options-volume-percent", ("volume", InterfaceVolumeSlider.Value / 100));
TtsVolumeLabel.Text =
Loc.GetString("ui-options-volume-percent", ("volume", TtsVolumeSlider.Value / 100)); // Corvax-TTS
AmbienceSoundsLabel.Text = ((int)AmbienceSoundsSlider.Value).ToString();

View file

@ -0,0 +1,35 @@
using Content.Shared.Pinpointer;
using JetBrains.Annotations;
namespace Content.Client.Pinpointer.UI;
[UsedImplicitly]
public sealed class NavMapBeaconBoundUserInterface : BoundUserInterface
{
[ViewVariables]
private NavMapBeaconWindow? _window;
public NavMapBeaconBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = new NavMapBeaconWindow(Owner);
_window.OpenCentered();
_window.OnClose += Close;
_window.OnApplyButtonPressed += (label, enabled, color) =>
{
SendMessage(new NavMapBeaconConfigureBuiMessage(label, enabled, color));
};
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_window?.Dispose();
}
}

View file

@ -0,0 +1,17 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'nav-beacon-window-title'}"
MinSize="250 230"
SetSize="320 230">
<BoxContainer Orientation="Vertical" Margin="5">
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'nav-beacon-text-label'}" Margin="0 0 5 0"/>
<LineEdit Name="LabelLineEdit" HorizontalExpand="True"/>
<Button Name="VisibleButton" ToggleMode="True" Text="{Loc 'nav-beacon-toggle-visible'}" Margin="10 0 0 0" MinWidth="100"/>
</BoxContainer>
<ColorSelectorSliders Name="ColorSelector" Margin="0 5 0 10"/>
<BoxContainer HorizontalAlignment="Right" VerticalAlignment="Bottom" VerticalExpand="True">
<Button Name="ApplyButton" Text="{Loc 'nav-beacon-button-apply'}" HorizontalAlignment="Right"/>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View file

@ -0,0 +1,78 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Pinpointer;
using Content.Shared.Preferences;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Pinpointer.UI;
[GenerateTypedNameReferences]
public sealed partial class NavMapBeaconWindow : FancyWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
private string? _defaultLabel;
private bool _defaultEnabled;
private Color _defaultColor;
public event Action<string?, bool, Color>? OnApplyButtonPressed;
public NavMapBeaconWindow(EntityUid beaconEntity)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
if (!_entityManager.TryGetComponent<NavMapBeaconComponent>(beaconEntity, out var navMap))
return;
_defaultLabel = navMap.Text;
_defaultEnabled = navMap.Enabled;
_defaultColor = navMap.Color;
UpdateVisibleButton(navMap.Enabled);
VisibleButton.OnPressed += args => UpdateVisibleButton(args.Button.Pressed);
LabelLineEdit.Text = navMap.Text ?? string.Empty;
LabelLineEdit.OnTextChanged += OnTextChanged;
ColorSelector.Color = navMap.Color;
ColorSelector.OnColorChanged += _ => TryEnableApplyButton();
TryEnableApplyButton();
ApplyButton.OnPressed += OnApplyPressed;
}
private void UpdateVisibleButton(bool value)
{
VisibleButton.Pressed = value;
VisibleButton.Text = Loc.GetString(value
? "nav-beacon-toggle-visible"
: "nav-beacon-toggle-invisible");
TryEnableApplyButton();
}
private void OnTextChanged(LineEdit.LineEditEventArgs obj)
{
if (obj.Text.Length > HumanoidCharacterProfile.MaxNameLength)
obj.Control.Text = obj.Text.Substring(0, HumanoidCharacterProfile.MaxNameLength);
TryEnableApplyButton();
}
private void TryEnableApplyButton()
{
ApplyButton.Disabled = LabelLineEdit.Text == (_defaultLabel ?? string.Empty) &&
VisibleButton.Pressed == _defaultEnabled &&
ColorSelector.Color == _defaultColor;
}
private void OnApplyPressed(BaseButton.ButtonEventArgs obj)
{
_defaultLabel = LabelLineEdit.Text == string.Empty ? null : LabelLineEdit.Text;
_defaultEnabled = VisibleButton.Pressed;
_defaultColor = ColorSelector.Color;
OnApplyButtonPressed?.Invoke(_defaultLabel, _defaultEnabled, _defaultColor);
TryEnableApplyButton();
}
}

View file

@ -5,29 +5,26 @@ using Robust.Shared.Animations;
namespace Content.Client.Rotation;
public sealed class RotationVisualizerSystem : VisualizerSystem<RotationVisualsComponent>
public sealed class RotationVisualizerSystem : SharedRotationVisualsSystem
{
public void SetHorizontalAngle(EntityUid uid, Angle angle, RotationVisualsComponent? component = null)
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
public override void Initialize()
{
if (!Resolve(uid, ref component))
return;
base.Initialize();
if (component.HorizontalRotation.Equals(angle))
return;
component.HorizontalRotation = angle;
Dirty(component);
SubscribeLocalEvent<RotationVisualsComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
protected override void OnAppearanceChange(EntityUid uid, RotationVisualsComponent component, ref AppearanceChangeEvent args)
private void OnAppearanceChange(EntityUid uid, RotationVisualsComponent component, ref AppearanceChangeEvent args)
{
base.OnAppearanceChange(uid, component, ref args);
if (args.Sprite == null)
return;
// If not defined, defaults to standing.
AppearanceSystem.TryGetData<RotationState>(uid, RotationVisuals.RotationState, out var state, args.Component);
_appearance.TryGetData<RotationState>(uid, RotationVisuals.RotationState, out var state, args.Component);
switch (state)
{
@ -53,9 +50,9 @@ public sealed class RotationVisualizerSystem : VisualizerSystem<RotationVisualsC
var animationComp = EnsureComp<AnimationPlayerComponent>(uid);
const string animationKey = "rotate";
// Stop the current rotate animation and then start a new one
if (AnimationSystem.HasRunningAnimation(animationComp, animationKey))
if (_animation.HasRunningAnimation(animationComp, animationKey))
{
AnimationSystem.Stop(animationComp, animationKey);
_animation.Stop(animationComp, animationKey);
}
var animation = new Animation
@ -77,6 +74,6 @@ public sealed class RotationVisualizerSystem : VisualizerSystem<RotationVisualsC
}
};
AnimationSystem.Play((uid, animationComp), animation, animationKey);
_animation.Play((uid, animationComp), animation, animationKey);
}
}

View file

@ -1,19 +0,0 @@
namespace Content.Client.Rotation;
[RegisterComponent]
public sealed partial class RotationVisualsComponent : Component
{
[DataField("defaultRotation")]
[ViewVariables(VVAccess.ReadOnly)]
public Angle DefaultRotation = Angle.FromDegrees(90);
[ViewVariables(VVAccess.ReadWrite)]
public Angle VerticalRotation = 0;
[DataField("horizontalRotation")]
[ViewVariables(VVAccess.ReadWrite)]
public Angle HorizontalRotation = Angle.FromDegrees(90);
[ViewVariables(VVAccess.ReadWrite)]
public float AnimationTime = 0.125f;
}

View file

@ -125,19 +125,15 @@ namespace Content.Client.RoundEnd
VerticalExpand = true,
};
var playerUid = _entityManager.GetEntity(playerInfo.PlayerNetEntity);
if (_entityManager.HasComponent<SpriteComponent>(playerUid))
if (playerInfo.PlayerNetEntity != null)
{
var spriteView = new SpriteView
{
OverrideDirection = Direction.South,
VerticalAlignment = VAlignment.Center,
SetSize = new Vector2(32, 32),
VerticalExpand = true,
};
spriteView.SetEntity(playerUid);
hBox.AddChild(spriteView);
hBox.AddChild(new SpriteView(playerInfo.PlayerNetEntity.Value, _entityManager)
{
OverrideDirection = Direction.South,
VerticalAlignment = VAlignment.Center,
SetSize = new Vector2(32, 32),
VerticalExpand = true,
});
}
if (playerInfo.PlayerICName != null)

View file

@ -26,8 +26,10 @@ public sealed class ThiefBackpackBoundUserInterface : BoundUserInterface
if (!disposing)
return;
if (_window != null)
_window.OnClose -= Close;
_window?.Dispose();
_window = null;
}
protected override void UpdateState(BoundUserInterfaceState state)

View file

@ -285,7 +285,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
private void OnWindowOpened()
{
if (ActionButton != null)
ActionButton.Pressed = true;
ActionButton.SetClickPressed(true);
SearchAndDisplay();
}
@ -293,7 +293,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
private void OnWindowClosed()
{
if (ActionButton != null)
ActionButton.Pressed = false;
ActionButton.SetClickPressed(false);
}
public void OnStateExited(GameplayState state)
@ -773,9 +773,10 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
if (_actionsSystem == null)
return;
for (var i = 0; i < assignments.Count; i++)
_actions.Clear();
foreach (var assign in assignments)
{
_actions[i] = assignments[i].ActionId;
_actions.Add(assign.ActionId);
}
_container?.SetActionData(_actionsSystem, _actions.ToArray());

View file

@ -355,6 +355,7 @@ public sealed class ActionButton : Control, IEntityControl
{
base.MouseEntered();
UserInterfaceManager.HoverSound();
_beingHovered = true;
DrawModeChanged();
}

View file

@ -127,14 +127,12 @@ public sealed class AdminUIController : UIController, IOnStateEntered<GameplaySt
private void OnWindowOpen()
{
if (AdminButton != null)
AdminButton.Pressed = true;
AdminButton?.SetClickPressed(true);
}
private void OnWindowClosed()
{
if (AdminButton != null)
AdminButton.Pressed = false;
AdminButton?.SetClickPressed(false);
}
private void OnWindowDisposed()

View file

@ -121,6 +121,7 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSyst
LobbyAHelpButton.Pressed = pressed;
}
UIManager.ClickSound();
UnreadAHelpRead();
}

View file

@ -184,7 +184,7 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
if (CharacterButton != null)
{
CharacterButton.Pressed = !_window.IsOpen;
CharacterButton.SetClickPressed(!_window.IsOpen);
}
if (_window.IsOpen)

View file

@ -51,8 +51,8 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
EscapeButton.OnPressed += EscapeButtonOnOnPressed;
}
private void ActivateButton() => EscapeButton!.Pressed = true;
private void DeactivateButton() => EscapeButton!.Pressed = false;
private void ActivateButton() => EscapeButton!.SetClickPressed(true);
private void DeactivateButton() => EscapeButton!.SetClickPressed(false);
public void OnStateEntered(GameplayState state)
{

View file

@ -139,12 +139,13 @@ public sealed class GuidebookUIController : UIController, IOnStateEntered<LobbyS
if (_guideWindow.IsOpen)
{
UIManager.ClickSound();
_guideWindow.Close();
return;
}
if (GuidebookButton != null)
GuidebookButton.Pressed = !_guideWindow.IsOpen;
GuidebookButton.SetClickPressed(!_guideWindow.IsOpen);
if (guides == null)
{

View file

@ -3,11 +3,13 @@ using System.Numerics;
using Content.Client.Gameplay;
using Content.Client.Hands.Systems;
using Content.Client.Inventory;
using Content.Client.Storage.Systems;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.Gameplay;
using Content.Client.UserInterface.Systems.Inventory.Controls;
using Content.Client.UserInterface.Systems.Inventory.Widgets;
using Content.Client.UserInterface.Systems.Inventory.Windows;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Hands.Components;
using Content.Shared.Input;
using Content.Shared.Storage;
@ -241,7 +243,9 @@ public sealed class InventoryUIController : UIController, IOnStateEntered<Gamepl
}
UpdateInventoryHotbar(_playerInventory);
_inventoryHotbar.Visible = !_inventoryHotbar.Visible;
var shouldBeVisible = !_inventoryHotbar.Visible;
_inventoryHotbar.Visible = shouldBeVisible;
}
// Neuron Activation
@ -336,6 +340,25 @@ public sealed class InventoryUIController : UIController, IOnStateEntered<Gamepl
var fits = _inventorySystem.CanEquip(player.Value, held, control.SlotName, out _, slotDef) &&
_container.CanInsert(held, container);
if (!fits && _entities.TryGetComponent<StorageComponent>(container.ContainedEntity, out var storage))
{
fits = _entities.System<StorageSystem>().CanInsert(container.ContainedEntity.Value, held, out _, storage);
}
else if (!fits && _entities.TryGetComponent<ItemSlotsComponent>(container.ContainedEntity, out var itemSlots))
{
var itemSlotsSys = _entities.System<ItemSlotsSystem>();
foreach (var slot in itemSlots.Slots.Values)
{
if (!slot.InsertOnInteract)
continue;
if (!itemSlotsSys.CanInsert(container.ContainedEntity.Value, held, null, slot))
continue;
fits = true;
break;
}
}
hoverSprite.CopyFrom(sprite);
hoverSprite.Color = fits ? new Color(0, 255, 0, 127) : new Color(255, 0, 0, 127);

View file

@ -172,10 +172,12 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
return;
if (_sandbox.SandboxAllowed && _window.IsOpen != true)
{
UIManager.ClickSound();
_window.OpenCentered();
}
else
{
UIManager.ClickSound();
_window.Close();
}
}

View file

@ -24,7 +24,6 @@ namespace Content.Client.Verbs.UI
public sealed class VerbMenuUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly ContextMenuUIController _context = default!;
[UISystemDependency] private readonly CombatModeSystem _combatMode = default!;
@ -112,7 +111,7 @@ namespace Content.Client.Verbs.UI
// Show the menu at mouse pos
menu.SetPositionLast();
var box = UIBox2.FromDimensions(_userInterfaceManager.MousePositionScaled.Position, new Vector2(1, 1));
var box = UIBox2.FromDimensions(UIManager.MousePositionScaled.Position, new Vector2(1, 1));
menu.Open(box);
}
@ -274,6 +273,7 @@ namespace Content.Client.Verbs.UI
private void ExecuteVerb(Verb verb)
{
UIManager.ClickSound();
_verbSystem.ExecuteVerb(CurrentTarget, verb);
if (verb.CloseMenu ?? verb.CloseMenuDefault)

View file

@ -33,7 +33,7 @@ public sealed partial class GunSystem
var existing = component.Entities[^1];
component.Entities.RemoveAt(component.Entities.Count - 1);
component.Container.Remove(existing);
Containers.Remove(existing, component.Container);
EnsureShootable(existing);
}
else if (component.UnspawnedCount > 0)

View file

@ -28,6 +28,14 @@ public sealed partial class TestPair
public RobustIntegrationTest.ServerIntegrationInstance Server { get; private set; } = default!;
public RobustIntegrationTest.ClientIntegrationInstance Client { get; private set; } = default!;
public void Deconstruct(
out RobustIntegrationTest.ServerIntegrationInstance server,
out RobustIntegrationTest.ClientIntegrationInstance client)
{
server = Server;
client = Client;
}
public ICommonSession? Player => Server.PlayerMan.Sessions.FirstOrDefault();
public ContentPlayerData? PlayerData => Player?.Data.ContentData();
@ -78,6 +86,8 @@ public sealed partial class TestPair
public void Kill()
{
State = PairState.Dead;
ServerLogHandler.ShuttingDown = true;
ClientLogHandler.ShuttingDown = true;
Server.Dispose();
Client.Dispose();
}

View file

@ -36,8 +36,15 @@ public sealed class PoolTestLogHandler : ILogHandler
_prefix = prefix != null ? $"{prefix}: " : "";
}
public bool ShuttingDown;
public void Log(string sawmillName, LogEvent message)
{
var level = message.Level.ToRobust();
if (ShuttingDown && (FailureLevel == null || level < FailureLevel))
return;
if (ActiveContext is not { } testContext)
{
// If this gets hit it means something is logging to this instance while it's "between" tests.
@ -45,7 +52,6 @@ public sealed class PoolTestLogHandler : ILogHandler
throw new InvalidOperationException("Log to pool test log handler without active test context");
}
var level = message.Level.ToRobust();
var name = LogMessage.LogLevelToName(level);
var seconds = _stopwatch.Elapsed.TotalSeconds;
var rendered = message.RenderMessage();

View file

@ -0,0 +1,43 @@
#nullable enable
using Content.Server.Body.Systems;
using Robust.Shared.GameObjects;
namespace Content.IntegrationTests.Tests.Body;
[TestFixture]
public sealed class GibTest
{
[Test]
public async Task TestGib()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true });
var (server, client) = (pair.Server, pair.Client);
var map = await pair.CreateTestMap();
EntityUid target1 = default;
EntityUid target2 = default;
await server.WaitAssertion(() => target1 = server.EntMan.Spawn("MobHuman", map.MapCoords));
await server.WaitAssertion(() => target2 = server.EntMan.Spawn("MobHuman", map.MapCoords));
await pair.WaitCommand($"setoutfit {server.EntMan.GetNetEntity(target1)} CaptainGear");
await pair.WaitCommand($"setoutfit {server.EntMan.GetNetEntity(target2)} CaptainGear");
await pair.RunTicksSync(5);
var nuid1 = pair.ToClientUid(target1);
var nuid2 = pair.ToClientUid(target2);
Assert.That(client.EntMan.EntityExists(nuid1));
Assert.That(client.EntMan.EntityExists(nuid2));
await server.WaitAssertion(() => server.System<BodySystem>().GibBody(target1, gibOrgans: false));
await server.WaitAssertion(() => server.System<BodySystem>().GibBody(target2, gibOrgans: true));
await pair.RunTicksSync(5);
await pair.WaitCommand("dirty");
await pair.RunTicksSync(5);
Assert.That(!client.EntMan.EntityExists(nuid1));
Assert.That(!client.EntMan.EntityExists(nuid2));
await pair.CleanReturnAsync();
}
}

View file

@ -1,5 +1,5 @@
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.FixedPoint;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
@ -51,7 +51,7 @@ public sealed class SolutionSystemTests
var entityManager = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var containerSystem = entityManager.EntitySysManager.GetEntitySystem<SolutionContainerSystem>();
var containerSystem = entityManager.System<SolutionContainerSystem>();
var testMap = await pair.CreateTestMap();
var coordinates = testMap.GridCoords;
@ -67,11 +67,11 @@ public sealed class SolutionSystemTests
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
Assert.That(containerSystem
.TryGetSolution(beaker, "beaker", out var solution));
.TryGetSolution(beaker, "beaker", out var solutionEnt, out var solution));
solution.AddSolution(originalWater, protoMan);
Assert.That(containerSystem
.TryAddSolution(beaker, solution, oilAdded));
.TryAddSolution(solutionEnt.Value, oilAdded));
var water = solution.GetTotalPrototypeQuantity("Water");
var oil = solution.GetTotalPrototypeQuantity("Oil");
@ -97,7 +97,7 @@ public sealed class SolutionSystemTests
var entityManager = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var containerSystem = entityManager.EntitySysManager.GetEntitySystem<SolutionContainerSystem>();
var containerSystem = entityManager.System<SolutionContainerSystem>();
var coordinates = testMap.GridCoords;
EntityUid beaker;
@ -112,11 +112,11 @@ public sealed class SolutionSystemTests
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
Assert.That(containerSystem
.TryGetSolution(beaker, "beaker", out var solution));
.TryGetSolution(beaker, "beaker", out var solutionEnt, out var solution));
solution.AddSolution(originalWater, protoMan);
Assert.That(containerSystem
.TryAddSolution(beaker, solution, oilAdded), Is.False);
.TryAddSolution(solutionEnt.Value, oilAdded), Is.False);
var water = solution.GetTotalPrototypeQuantity("Water");
var oil = solution.GetTotalPrototypeQuantity("Oil");
@ -141,7 +141,7 @@ public sealed class SolutionSystemTests
var entityManager = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var testMap = await pair.CreateTestMap();
var containerSystem = entityManager.EntitySysManager.GetEntitySystem<SolutionContainerSystem>();
var containerSystem = entityManager.System<SolutionContainerSystem>();
var coordinates = testMap.GridCoords;
EntityUid beaker;
@ -158,11 +158,11 @@ public sealed class SolutionSystemTests
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
Assert.That(containerSystem
.TryGetSolution(beaker, "beaker", out var solution));
.TryGetSolution(beaker, "beaker", out var solutionEnt, out var solution));
solution.AddSolution(originalWater, protoMan);
Assert.That(containerSystem
.TryMixAndOverflow(beaker, solution, oilAdded, threshold, out var overflowingSolution));
.TryMixAndOverflow(solutionEnt.Value, oilAdded, threshold, out var overflowingSolution));
Assert.Multiple(() =>
{
@ -194,7 +194,7 @@ public sealed class SolutionSystemTests
var entityManager = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var containerSystem = entityManager.EntitySysManager.GetEntitySystem<SolutionContainerSystem>();
var containerSystem = entityManager.System<SolutionContainerSystem>();
var testMap = await pair.CreateTestMap();
var coordinates = testMap.GridCoords;
@ -212,11 +212,11 @@ public sealed class SolutionSystemTests
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
Assert.That(containerSystem
.TryGetSolution(beaker, "beaker", out var solution));
.TryGetSolution(beaker, "beaker", out var solutionEnt, out var solution));
solution.AddSolution(originalWater, protoMan);
Assert.That(containerSystem
.TryMixAndOverflow(beaker, solution, oilAdded, threshold, out _),
.TryMixAndOverflow(solutionEnt.Value, oilAdded, threshold, out _),
Is.False);
});

View file

@ -1,11 +1,11 @@
using System.Linq;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using System.Linq;
namespace Content.IntegrationTests.Tests.Chemistry
{
@ -34,8 +34,7 @@ namespace Content.IntegrationTests.Tests.Chemistry
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
var testMap = await pair.CreateTestMap();
var coordinates = testMap.GridCoords;
var solutionSystem = server.ResolveDependency<IEntitySystemManager>()
.GetEntitySystem<SolutionContainerSystem>();
var solutionContainerSystem = entityManager.System<SolutionContainerSystem>();
foreach (var reactionPrototype in prototypeManager.EnumeratePrototypes<ReactionPrototype>())
{
@ -43,30 +42,31 @@ namespace Content.IntegrationTests.Tests.Chemistry
Console.WriteLine($"Testing {reactionPrototype.ID}");
EntityUid beaker = default;
Solution component = null;
Entity<SolutionComponent>? solutionEnt = default!;
Solution solution = null;
await server.WaitAssertion(() =>
{
beaker = entityManager.SpawnEntity("TestSolutionContainer", coordinates);
Assert.That(solutionSystem
.TryGetSolution(beaker, "beaker", out component));
Assert.That(solutionContainerSystem
.TryGetSolution(beaker, "beaker", out solutionEnt, out solution));
foreach (var (id, reactant) in reactionPrototype.Reactants)
{
#pragma warning disable NUnit2045
Assert.That(solutionSystem
.TryAddReagent(beaker, component, id, reactant.Amount, out var quantity));
Assert.That(solutionContainerSystem
.TryAddReagent(solutionEnt.Value, id, reactant.Amount, out var quantity));
Assert.That(reactant.Amount, Is.EqualTo(quantity));
#pragma warning restore NUnit2045
}
solutionSystem.SetTemperature(beaker, component, reactionPrototype.MinimumTemperature);
solutionContainerSystem.SetTemperature(solutionEnt.Value, reactionPrototype.MinimumTemperature);
if (reactionPrototype.MixingCategories != null)
{
var dummyEntity = entityManager.SpawnEntity(null, MapCoordinates.Nullspace);
var mixerComponent = entityManager.AddComponent<ReactionMixerComponent>(dummyEntity);
mixerComponent.ReactionTypes = reactionPrototype.MixingCategories;
solutionSystem.UpdateChemicals(beaker, component, true, mixerComponent);
solutionContainerSystem.UpdateChemicals(solutionEnt.Value, true, mixerComponent);
}
});
@ -79,7 +79,7 @@ namespace Content.IntegrationTests.Tests.Chemistry
var foundProductsMap = reactionPrototype.Products
.Concat(reactionPrototype.Reactants.Where(x => x.Value.Catalyst).ToDictionary(x => x.Key, x => x.Value.Amount))
.ToDictionary(x => x, _ => false);
foreach (var (reagent, quantity) in component.Contents)
foreach (var (reagent, quantity) in solution.Contents)
{
Assert.That(foundProductsMap.TryFirstOrNull(x => x.Key.Key == reagent.Prototype && x.Key.Value == quantity, out var foundProduct));
foundProductsMap[foundProduct.Value.Key] = true;

View file

@ -38,10 +38,10 @@ public sealed class DoAfterCancellationTests : InteractionTest
AssertAnchored(false);
// Repeat for screwdriver interaction.
AssertDeleted(false);
AssertExists();
await Interact(Screw, awaitDoAfters: false);
await CancelDoAfters();
AssertDeleted(false);
AssertExists();
await Interact(Screw);
AssertDeleted();
}

View file

@ -80,7 +80,7 @@ public sealed class AbsorbentTest
var entityManager = server.ResolveDependency<IEntityManager>();
var absorbentSystem = entityManager.System<AbsorbentSystem>();
var solutionContainerSystem = entityManager.System<SolutionContainerSystem>();
var solutionContainerSystem = entityManager.System<SharedSolutionContainerSystem>();
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
EntityUid user = default;
@ -94,19 +94,19 @@ public sealed class AbsorbentTest
refillable = entityManager.SpawnEntity(RefillableDummyId, coordinates);
entityManager.TryGetComponent(absorbent, out component);
solutionContainerSystem.TryGetSolution(absorbent, AbsorbentComponent.SolutionName, out var absorbentSolution);
solutionContainerSystem.TryGetRefillableSolution(refillable, out var refillableSolution);
solutionContainerSystem.TryGetSolution(absorbent, AbsorbentComponent.SolutionName, out var absorbentSoln, out var absorbentSolution);
solutionContainerSystem.TryGetRefillableSolution(refillable, out var refillableSoln, out var refillableSolution);
// Arrange
if (testCase.InitialAbsorbentSolution.VolumeOfEvaporable > FixedPoint2.Zero)
solutionContainerSystem.AddSolution(absorbent, absorbentSolution, new Solution(EvaporablePrototypeId, testCase.InitialAbsorbentSolution.VolumeOfEvaporable));
solutionContainerSystem.AddSolution(absorbentSoln.Value, new Solution(EvaporablePrototypeId, testCase.InitialAbsorbentSolution.VolumeOfEvaporable));
if (testCase.InitialAbsorbentSolution.VolumeOfNonEvaporable > FixedPoint2.Zero)
solutionContainerSystem.AddSolution(absorbent, absorbentSolution, new Solution(NonEvaporablePrototypeId, testCase.InitialAbsorbentSolution.VolumeOfNonEvaporable));
solutionContainerSystem.AddSolution(absorbentSoln.Value, new Solution(NonEvaporablePrototypeId, testCase.InitialAbsorbentSolution.VolumeOfNonEvaporable));
if (testCase.InitialRefillableSolution.VolumeOfEvaporable > FixedPoint2.Zero)
solutionContainerSystem.AddSolution(refillable, refillableSolution, new Solution(EvaporablePrototypeId, testCase.InitialRefillableSolution.VolumeOfEvaporable));
solutionContainerSystem.AddSolution(refillableSoln.Value, new Solution(EvaporablePrototypeId, testCase.InitialRefillableSolution.VolumeOfEvaporable));
if (testCase.InitialRefillableSolution.VolumeOfNonEvaporable > FixedPoint2.Zero)
solutionContainerSystem.AddSolution(refillable, refillableSolution, new Solution(NonEvaporablePrototypeId, testCase.InitialRefillableSolution.VolumeOfNonEvaporable));
solutionContainerSystem.AddSolution(refillableSoln.Value, new Solution(NonEvaporablePrototypeId, testCase.InitialRefillableSolution.VolumeOfNonEvaporable));
// Act
absorbentSystem.Mop(user, refillable, absorbent, component);
@ -138,7 +138,7 @@ public sealed class AbsorbentTest
var entityManager = server.ResolveDependency<IEntityManager>();
var absorbentSystem = entityManager.System<AbsorbentSystem>();
var solutionContainerSystem = entityManager.System<SolutionContainerSystem>();
var solutionContainerSystem = entityManager.System<SharedSolutionContainerSystem>();
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
EntityUid user = default;
@ -152,18 +152,18 @@ public sealed class AbsorbentTest
refillable = entityManager.SpawnEntity(SmallRefillableDummyId, coordinates);
entityManager.TryGetComponent(absorbent, out component);
solutionContainerSystem.TryGetSolution(absorbent, AbsorbentComponent.SolutionName, out var absorbentSolution);
solutionContainerSystem.TryGetRefillableSolution(refillable, out var refillableSolution);
solutionContainerSystem.TryGetSolution(absorbent, AbsorbentComponent.SolutionName, out var absorbentSoln, out var absorbentSolution);
solutionContainerSystem.TryGetRefillableSolution(refillable, out var refillableSoln, out var refillableSolution);
// Arrange
solutionContainerSystem.AddSolution(absorbent, absorbentSolution, new Solution(EvaporablePrototypeId, testCase.InitialAbsorbentSolution.VolumeOfEvaporable));
solutionContainerSystem.AddSolution(absorbentSoln.Value, new Solution(EvaporablePrototypeId, testCase.InitialAbsorbentSolution.VolumeOfEvaporable));
if (testCase.InitialAbsorbentSolution.VolumeOfNonEvaporable > FixedPoint2.Zero)
solutionContainerSystem.AddSolution(absorbent, absorbentSolution, new Solution(NonEvaporablePrototypeId, testCase.InitialAbsorbentSolution.VolumeOfNonEvaporable));
solutionContainerSystem.AddSolution(absorbentSoln.Value, new Solution(NonEvaporablePrototypeId, testCase.InitialAbsorbentSolution.VolumeOfNonEvaporable));
if (testCase.InitialRefillableSolution.VolumeOfEvaporable > FixedPoint2.Zero)
solutionContainerSystem.AddSolution(refillable, refillableSolution, new Solution(EvaporablePrototypeId, testCase.InitialRefillableSolution.VolumeOfEvaporable));
solutionContainerSystem.AddSolution(refillableSoln.Value, new Solution(EvaporablePrototypeId, testCase.InitialRefillableSolution.VolumeOfEvaporable));
if (testCase.InitialRefillableSolution.VolumeOfNonEvaporable > FixedPoint2.Zero)
solutionContainerSystem.AddSolution(refillable, refillableSolution, new Solution(NonEvaporablePrototypeId, testCase.InitialRefillableSolution.VolumeOfNonEvaporable));
solutionContainerSystem.AddSolution(refillableSoln.Value, new Solution(NonEvaporablePrototypeId, testCase.InitialRefillableSolution.VolumeOfNonEvaporable));
// Act
absorbentSystem.Mop(user, refillable, absorbent, component);

View file

@ -373,7 +373,7 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
await server.WaitAssertion(() =>
{
#pragma warning disable NUnit2045 // Interdependent assertions.
Assert.That(container.Insert(user));
Assert.That(conSystem.Insert(user, container));
Assert.That(sEntities.GetComponent<TransformComponent>(user).ParentUid, Is.EqualTo(containerEntity));
#pragma warning restore NUnit2045

View file

@ -134,9 +134,9 @@ public abstract partial class InteractionTest
/// <remarks>
/// Automatically enables welders.
/// </remarks>
protected async Task<EntityUid?> PlaceInHands(string? id, int quantity = 1, bool enableWelder = true)
protected async Task<NetEntity> PlaceInHands(string id, int quantity = 1, bool enableWelder = true)
{
return await PlaceInHands(id == null ? null : (id, quantity), enableWelder);
return await PlaceInHands((id, quantity), enableWelder);
}
/// <summary>
@ -145,7 +145,7 @@ public abstract partial class InteractionTest
/// <remarks>
/// Automatically enables welders.
/// </remarks>
protected async Task<EntityUid?> PlaceInHands(EntitySpecifier? entity, bool enableWelder = true)
protected async Task<NetEntity> PlaceInHands(EntitySpecifier entity, bool enableWelder = true)
{
if (Hands.ActiveHand == null)
{
@ -153,15 +153,9 @@ public abstract partial class InteractionTest
return default;
}
Assert.That(!string.IsNullOrWhiteSpace(entity.Prototype));
await DeleteHeldEntity();
if (entity == null || string.IsNullOrWhiteSpace(entity.Prototype))
{
await RunTicks(1);
Assert.That(Hands.ActiveHandEntity, Is.Null);
return null;
}
// spawn and pick up the new item
var item = await SpawnEntity(entity, SEntMan.GetCoordinates(PlayerCoords));
ItemToggleComponent? itemToggle = null;
@ -184,7 +178,7 @@ public abstract partial class InteractionTest
if (enableWelder && itemToggle != null)
Assert.That(itemToggle.Activated);
return item;
return SEntMan.GetNetEntity(item);
}
/// <summary>
@ -330,6 +324,17 @@ public abstract partial class InteractionTest
}
}
/// <summary>
/// Throw the currently held entity. Defaults to targeting the current <see cref="TargetCoords"/>
/// </summary>
protected async Task<bool> ThrowItem(NetCoordinates? target = null, float minDistance = 4)
{
var actualTarget = SEntMan.GetCoordinates(target ?? TargetCoords);
var result = false;
await Server.WaitPost(() => result = HandSys.ThrowHeldItem(SEntMan.GetEntity(Player), actualTarget, minDistance));
return result;
}
#endregion
/// <summary>
@ -483,7 +488,7 @@ public abstract partial class InteractionTest
});
}
protected void AssertDeleted(bool deleted = true, NetEntity? target = null)
protected void AssertDeleted(NetEntity? target = null)
{
target ??= Target;
if (target == null)
@ -494,8 +499,24 @@ public abstract partial class InteractionTest
Assert.Multiple(() =>
{
Assert.That(SEntMan.Deleted(SEntMan.GetEntity(target)), Is.EqualTo(deleted));
Assert.That(CEntMan.Deleted(CEntMan.GetEntity(target)), Is.EqualTo(deleted));
Assert.That(SEntMan.Deleted(SEntMan.GetEntity(target)));
Assert.That(CEntMan.Deleted(CEntMan.GetEntity(target)));
});
}
protected void AssertExists(NetEntity? target = null)
{
target ??= Target;
if (target == null)
{
Assert.Fail("No target specified");
return;
}
Assert.Multiple(() =>
{
Assert.That(SEntMan.EntityExists(SEntMan.GetEntity(target)));
Assert.That(CEntMan.EntityExists(CEntMan.GetEntity(target)));
});
}
@ -733,6 +754,11 @@ public abstract partial class InteractionTest
await RunTicks(5);
}
protected Task Delete(NetEntity nuid)
{
return Delete(SEntMan.GetEntity(nuid));
}
#region Time/Tick managment
protected async Task RunTicks(int ticks)
@ -1064,4 +1090,35 @@ public abstract partial class InteractionTest
}
#endregion
#region Networking
protected EntityUid ToServer(NetEntity nent) => SEntMan.GetEntity(nent);
protected EntityUid ToClient(NetEntity nent) => CEntMan.GetEntity(nent);
protected EntityUid? ToServer(NetEntity? nent) => SEntMan.GetEntity(nent);
protected EntityUid? ToClient(NetEntity? nent) => CEntMan.GetEntity(nent);
protected EntityUid ToServer(EntityUid cuid) => SEntMan.GetEntity(CEntMan.GetNetEntity(cuid));
protected EntityUid ToClient(EntityUid cuid) => CEntMan.GetEntity(SEntMan.GetNetEntity(cuid));
protected EntityUid? ToServer(EntityUid? cuid) => SEntMan.GetEntity(CEntMan.GetNetEntity(cuid));
protected EntityUid? ToClient(EntityUid? cuid) => CEntMan.GetEntity(SEntMan.GetNetEntity(cuid));
protected EntityCoordinates ToServer(NetCoordinates coords) => SEntMan.GetCoordinates(coords);
protected EntityCoordinates ToClient(NetCoordinates coords) => CEntMan.GetCoordinates(coords);
protected EntityCoordinates? ToServer(NetCoordinates? coords) => SEntMan.GetCoordinates(coords);
protected EntityCoordinates? ToClient(NetCoordinates? coords) => CEntMan.GetCoordinates(coords);
#endregion
#region Metadata & Transforms
protected MetaDataComponent Meta(NetEntity uid) => Meta(ToServer(uid));
protected MetaDataComponent Meta(EntityUid uid) => SEntMan.GetComponent<MetaDataComponent>(uid);
protected TransformComponent Xform(NetEntity uid) => Xform(ToServer(uid));
protected TransformComponent Xform(EntityUid uid) => SEntMan.GetComponent<TransformComponent>(uid);
protected EntityCoordinates Position(NetEntity uid) => Position(ToServer(uid));
protected EntityCoordinates Position(EntityUid uid) => Xform(uid).Coordinates;
#endregion
}

View file

@ -5,12 +5,12 @@ using Content.Client.Construction;
using Content.Client.Examine;
using Content.IntegrationTests.Pair;
using Content.Server.Body.Systems;
using Content.Server.Hands.Systems;
using Content.Server.Stack;
using Content.Server.Tools;
using Content.Shared.Body.Part;
using Content.Shared.DoAfter;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Server.Item;
using Content.Shared.Mind;
@ -66,6 +66,9 @@ public abstract partial class InteractionTest
/// </summary>
protected NetEntity Player;
protected EntityUid SPlayer => ToServer(Player);
protected EntityUid CPlayer => ToClient(Player);
protected ICommonSession ClientSession = default!;
protected ICommonSession ServerSession = default!;
@ -81,6 +84,9 @@ public abstract partial class InteractionTest
/// </remarks>
protected NetEntity? Target;
protected EntityUid? STarget => ToServer(Target);
protected EntityUid? CTarget => ToClient(Target);
/// <summary>
/// When attempting to start construction, this is the client-side ID of the construction ghost.
/// </summary>
@ -93,7 +99,7 @@ public abstract partial class InteractionTest
protected IPrototypeManager ProtoMan = default!;
protected IGameTiming STiming = default!;
protected IComponentFactory Factory = default!;
protected SharedHandsSystem HandSys = default!;
protected HandsSystem HandSys = default!;
protected StackSystem Stack = default!;
protected SharedInteractionSystem InteractSys = default!;
protected Content.Server.Construction.ConstructionSystem SConstruction = default!;
@ -152,7 +158,7 @@ public abstract partial class InteractionTest
ProtoMan = Server.ResolveDependency<IPrototypeManager>();
Factory = Server.ResolveDependency<IComponentFactory>();
STiming = Server.ResolveDependency<IGameTiming>();
HandSys = SEntMan.System<SharedHandsSystem>();
HandSys = SEntMan.System<HandsSystem>();
InteractSys = SEntMan.System<SharedInteractionSystem>();
ToolSys = SEntMan.System<ToolSystem>();
ItemToggleSys = SEntMan.System<SharedItemToggleSystem>();

View file

@ -0,0 +1,51 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Networking;
[TestFixture]
public sealed class PvsCommandTest
{
public static EntProtoId TestEnt = "MobHuman";
[Test]
public async Task TestPvsCommands()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true, DummyTicker = false});
var (server, client) = pair;
await pair.RunTicksSync(5);
// Spawn a complex entity.
EntityUid entity = default;
await server.WaitPost(() => entity = server.EntMan.Spawn(TestEnt));
await pair.RunTicksSync(5);
// Check that the client has a variety pf entities.
Assert.That(client.EntMan.EntityCount, Is.GreaterThan(0));
Assert.That(client.EntMan.Count<MapComponent>, Is.GreaterThan(0));
Assert.That(client.EntMan.Count<MapGridComponent>, Is.GreaterThan(0));
var meta = client.MetaData(pair.ToClientUid(entity));
var lastApplied = meta.LastStateApplied;
// Dirty all entities
await server.ExecuteCommand("dirty");
await pair.RunTicksSync(5);
Assert.That(meta.LastStateApplied, Is.GreaterThan(lastApplied));
await pair.RunTicksSync(5);
// Do a client-side full state reset
await client.ExecuteCommand("resetallents");
await pair.RunTicksSync(5);
// Request a full server state.
lastApplied = meta.LastStateApplied;
await client.ExecuteCommand("fullstatereset");
await pair.RunTicksSync(10);
Assert.That(meta.LastStateApplied, Is.GreaterThan(lastApplied));
await server.WaitPost(() => server.EntMan.DeleteEntity(entity));
await pair.CleanReturnAsync();
}
}

View file

@ -67,7 +67,7 @@ public sealed class ModularGrenadeTests : InteractionTest
}
// Grenade has exploded.
await RunTicks(5);
await RunTicks(30);
AssertDeleted();
}
}

View file

@ -0,0 +1,111 @@
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Damage.Components;
using Content.Shared.Throwing;
using Robust.Server.GameObjects;
using Robust.Shared.Physics.Components;
namespace Content.IntegrationTests.Tests.Physics;
public sealed class ItemThrowingTest : InteractionTest
{
/// <summary>
/// Check that an egg breaks when thrown at a wall.
/// </summary>
[Test]
[TestOf(typeof(ThrownItemComponent))]
[TestOf(typeof(DamageOnHighSpeedImpactComponent))]
public async Task TestThrownEggBreaks()
{
// Setup entities
var egg = await PlaceInHands("FoodEgg");
await SpawnTarget("WallSolid");
await RunTicks(5);
AssertExists(egg);
// Currently not a "thrown" item.
AssertComp<ThrownItemComponent>(hasComp: false, egg);
Assert.That(Comp<PhysicsComponent>(egg).BodyStatus, Is.Not.EqualTo(BodyStatus.InAir));
// Throw it.
await ThrowItem();
await RunTicks(1);
AssertExists(egg);
AssertComp<ThrownItemComponent>(hasComp: true, egg);
Assert.That(Comp<PhysicsComponent>(egg).BodyStatus, Is.EqualTo(BodyStatus.InAir));
// Splat
await RunTicks(30);
AssertDeleted(egg);
}
/// <summary>
/// Check that an egg thrown into space continues to be an egg.
/// I.e., verify that the deletions that happen in the other two tests aren't coincidental.
/// </summary>
[Test]
//[TestOf(typeof(Egg))]
public async Task TestEggIsEgg()
{
// Setup entities
var egg = await PlaceInHands("FoodEgg");
await RunTicks(5);
AssertExists(egg);
// Currently not a "thrown" item.
AssertComp<ThrownItemComponent>(hasComp: false, egg);
Assert.That(Comp<PhysicsComponent>(egg).BodyStatus, Is.Not.EqualTo(BodyStatus.InAir));
// Throw it
await ThrowItem();
await RunTicks(5);
AssertExists(egg);
AssertComp<ThrownItemComponent>(hasComp: true, egg);
Assert.That(Comp<PhysicsComponent>(egg).BodyStatus, Is.EqualTo(BodyStatus.InAir));
// Wait a while
await RunTicks(60);
// Egg is egg
AssertExists(egg);
AssertPrototype("FoodEgg", egg);
AssertComp<ThrownItemComponent>(hasComp: false, egg);
Assert.That(Comp<PhysicsComponent>(egg).BodyStatus, Is.Not.EqualTo(BodyStatus.InAir));
}
/// <summary>
/// Check that a physics can handle deleting a thrown entity. As to why this exists, see
/// https://github.com/space-wizards/RobustToolbox/pull/4746
/// </summary>
[Test]
[TestOf(typeof(ThrownItemComponent))]
[TestOf(typeof(PhysicsComponent))]
public async Task TestDeleteThrownItem()
{
// Setup entities
var pen = await PlaceInHands("Pen");
var physics = Comp<PhysicsComponent>(pen);
await RunTicks(5);
AssertExists(pen);
// Currently not a "thrown" item.
AssertComp<ThrownItemComponent>(hasComp: false, pen);
Assert.That(physics.BodyStatus, Is.Not.EqualTo(BodyStatus.InAir));
// Throw it
await ThrowItem();
await RunTicks(5);
AssertExists(pen);
AssertComp<ThrownItemComponent>(hasComp: true, pen);
Assert.That(physics.BodyStatus, Is.EqualTo(BodyStatus.InAir));
Assert.That(physics.CanCollide);
// Attempt to make it sleep mid-air. This happens automatically due to the sleep timer, but we just do it manually.
await Server.WaitPost(() => Server.System<PhysicsSystem>().SetAwake((ToServer(pen), physics), false));
// Then try and delete it
await Delete(pen);
await RunTicks(5);
AssertDeleted(pen);
}
}

View file

@ -2,6 +2,7 @@
using System.Collections.Generic;
using Content.IntegrationTests.Pair;
using Content.Server.Administration.Managers;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Errors;
@ -95,6 +96,7 @@ public abstract class ToolshedTest : IInvocationContext
}
protected ICommonSession? InvocationSession { get; set; }
public NetUserId? User => Session?.UserId;
public ICommonSession? Session
{

View file

@ -0,0 +1,42 @@
using Content.Server.Administration;
using Content.Shared.Access.Components;
using Content.Shared.Administration;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Syntax;
namespace Content.Server.Access;
[ToolshedCommand, AdminCommand(AdminFlags.Mapping)]
public sealed class AddAccessLogCommand : ToolshedCommand
{
[CommandImplementation]
public void AddAccessLog(
[CommandInvocationContext] IInvocationContext ctx,
[CommandArgument] EntityUid input,
[CommandArgument] float seconds,
[CommandArgument] ValueRef<string> accessor)
{
var accessReader = EnsureComp<AccessReaderComponent>(input);
var accessLogCount = accessReader.AccessLog.Count;
if (accessLogCount >= accessReader.AccessLogLimit)
ctx.WriteLine($"WARNING: Surpassing the limit of the log by {accessLogCount - accessReader.AccessLogLimit+1} entries!");
var accessTime = TimeSpan.FromSeconds(seconds);
var accessName = accessor.Evaluate(ctx)!;
accessReader.AccessLog.Enqueue(new AccessRecord(accessTime, accessName));
ctx.WriteLine($"Successfully added access log to {input} with this information inside:\n " +
$"Time of access: {accessTime}\n " +
$"Accessed by: {accessName}");
}
[CommandImplementation]
public void AddAccessLogPiped(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] EntityUid input,
[CommandArgument] float seconds,
[CommandArgument] ValueRef<string> accessor)
{
AddAccessLog(ctx, input, seconds, accessor);
}
}

View file

@ -1,10 +1,11 @@
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Administration;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
using System.Linq;
namespace Content.Server.Administration.Commands
{
@ -41,13 +42,13 @@ namespace Content.Server.Administration.Commands
return;
}
if (!man.Solutions.ContainsKey(args[1]))
var solutionContainerSystem = _entManager.System<SolutionContainerSystem>();
if (!solutionContainerSystem.TryGetSolution((uid.Value, man), args[1], out var solution))
{
var validSolutions = string.Join(", ", man.Solutions.Keys);
var validSolutions = string.Join(", ", solutionContainerSystem.EnumerateSolutions((uid.Value, man)).Select(s => s.Name));
shell.WriteLine($"Entity does not have a \"{args[1]}\" solution. Valid solutions are:\n{validSolutions}");
return;
}
var solution = man.Solutions[args[1]];
if (!_protomanager.HasIndex<ReagentPrototype>(args[2]))
{
@ -63,9 +64,9 @@ namespace Content.Server.Administration.Commands
var quantity = FixedPoint2.New(MathF.Abs(quantityFloat));
if (quantityFloat > 0)
_entManager.System<SolutionContainerSystem>().TryAddReagent(uid.Value, solution, args[2], quantity, out _);
solutionContainerSystem.TryAddReagent(solution.Value, args[2], quantity, out _);
else
_entManager.System<SolutionContainerSystem>().RemoveReagent(uid.Value, solution, args[2], quantity);
solutionContainerSystem.RemoveReagent(solution.Value, args[2], quantity);
}
}
}

View file

@ -38,9 +38,9 @@ public sealed class DirtyCommand : IConsoleCommand
private static void DirtyAll(IEntityManager manager, EntityUid entityUid)
{
foreach (var component in manager.GetComponents(entityUid))
foreach (var component in manager.GetNetComponents(entityUid))
{
manager.Dirty((Component)component);
manager.Dirty(entityUid, component.component);
}
}
}

View file

@ -1,8 +1,9 @@
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Administration;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.FixedPoint;
using Robust.Shared.Console;
using System.Linq;
namespace Content.Server.Administration.Commands
{
@ -35,13 +36,13 @@ namespace Content.Server.Administration.Commands
return;
}
if (!man.Solutions.ContainsKey(args[1]))
var solutionContainerSystem = _entManager.System<SolutionContainerSystem>();
if (!solutionContainerSystem.TryGetSolution((uid.Value, man), args[1], out var solution))
{
var validSolutions = string.Join(", ", man.Solutions.Keys);
var validSolutions = string.Join(", ", solutionContainerSystem.EnumerateSolutions((uid.Value, man)).Select(s => s.Name));
shell.WriteLine($"Entity does not have a \"{args[1]}\" solution. Valid solutions are:\n{validSolutions}");
return;
}
var solution = man.Solutions[args[1]];
if (!float.TryParse(args[2], out var quantityFloat))
{
@ -49,14 +50,14 @@ namespace Content.Server.Administration.Commands
return;
}
if(quantityFloat < 0.0f)
if (quantityFloat < 0.0f)
{
shell.WriteLine($"Cannot set the maximum volume of a solution to a negative number.");
return;
}
var quantity = FixedPoint2.New(quantityFloat);
_entManager.System<SolutionContainerSystem>().SetCapacity(uid.Value, solution, quantity);
solutionContainerSystem.SetCapacity(solution.Value, quantity);
}
}
}

View file

@ -1,7 +1,8 @@
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Administration;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Robust.Shared.Console;
using System.Linq;
namespace Content.Server.Administration.Commands
{
@ -34,13 +35,13 @@ namespace Content.Server.Administration.Commands
return;
}
if (!man.Solutions.ContainsKey(args[1]))
var solutionContainerSystem = _entManager.System<SolutionContainerSystem>();
if (!solutionContainerSystem.TryGetSolution((uid.Value, man), args[1], out var solution))
{
var validSolutions = string.Join(", ", man.Solutions.Keys);
var validSolutions = string.Join(", ", solutionContainerSystem.EnumerateSolutions((uid.Value, man)).Select(s => s.Name));
shell.WriteLine($"Entity does not have a \"{args[1]}\" solution. Valid solutions are:\n{validSolutions}");
return;
}
var solution = man.Solutions[args[1]];
if (!float.TryParse(args[2], out var quantity))
{
@ -54,7 +55,7 @@ namespace Content.Server.Administration.Commands
return;
}
_entManager.System<SolutionContainerSystem>().SetTemperature(uid.Value, solution, quantity);
solutionContainerSystem.SetTemperature(solution.Value, quantity);
}
}
}

View file

@ -1,7 +1,8 @@
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Administration;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Robust.Shared.Console;
using System.Linq;
namespace Content.Server.Administration.Commands
{
@ -34,13 +35,13 @@ namespace Content.Server.Administration.Commands
return;
}
if (!man.Solutions.ContainsKey(args[1]))
var solutionContainerSystem = _entManager.System<SolutionContainerSystem>();
if (!solutionContainerSystem.TryGetSolution((uid.Value, man), args[1], out var solutionEnt, out var solution))
{
var validSolutions = string.Join(", ", man.Solutions.Keys);
var validSolutions = string.Join(", ", solutionContainerSystem.EnumerateSolutions((uid.Value, man)).Select(s => s.Name));
shell.WriteLine($"Entity does not have a \"{args[1]}\" solution. Valid solutions are:\n{validSolutions}");
return;
}
var solution = man.Solutions[args[1]];
if (!float.TryParse(args[2], out var quantity))
{
@ -50,19 +51,19 @@ namespace Content.Server.Administration.Commands
if (solution.GetHeatCapacity(null) <= 0.0f)
{
if(quantity != 0.0f)
if (quantity != 0.0f)
{
shell.WriteLine($"Cannot set the thermal energy of a solution with 0 heat capacity to a non-zero number.");
return;
}
}
else if(quantity <= 0.0f)
else if (quantity <= 0.0f)
{
shell.WriteLine($"Cannot set the thermal energy of a solution with heat capacity to a non-positive number.");
return;
}
_entManager.System<SolutionContainerSystem>().SetThermalEnergy(uid.Value, solution, quantity);
solutionContainerSystem.SetThermalEnergy(solutionEnt.Value, quantity);
}
}
}

View file

@ -132,7 +132,7 @@ public sealed partial class AdminVerbSystem
if (!_minds.TryGetSession(targetMindComp.Mind, out var session))
return;
_thief.MakeThief(session, false); //Midround add pacific is bad
_thief.AdminMakeThief(session, false); //Midround add pacific is bad
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-thief"),

View file

@ -1,4 +1,3 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Server.Administration.UI;
@ -34,6 +33,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Toolshed;
using Robust.Shared.Utility;
using System.Linq;
using static Content.Shared.Configurable.ConfigurationComponent;
namespace Content.Server.Administration.Systems
@ -71,7 +71,7 @@ namespace Content.Server.Administration.Systems
{
SubscribeLocalEvent<GetVerbsEvent<Verb>>(GetVerbs);
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<SolutionContainerManagerComponent, SolutionChangedEvent>(OnSolutionChanged);
SubscribeLocalEvent<SolutionContainerManagerComponent, SolutionContainerChangedEvent>(OnSolutionChanged);
}
private void GetVerbs(GetVerbsEvent<Verb> ev)
@ -470,11 +470,11 @@ namespace Content.Server.Administration.Systems
}
#region SolutionsEui
private void OnSolutionChanged(EntityUid uid, SolutionContainerManagerComponent component, SolutionChangedEvent args)
private void OnSolutionChanged(Entity<SolutionContainerManagerComponent> entity, ref SolutionContainerChangedEvent args)
{
foreach (var eui in _openSolutionUis.Values)
{
if (eui.Target == uid)
if (eui.Target == entity.Owner)
eui.StateDirty();
}
}

View file

@ -1,12 +1,12 @@
using System.Linq;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Administration;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Syntax;
using Robust.Shared.Toolshed.TypeParsers;
using System.Linq;
namespace Content.Server.Administration.Toolshed;
@ -24,10 +24,8 @@ public sealed class SolutionCommand : ToolshedCommand
{
_solutionContainer ??= GetSys<SolutionContainerSystem>();
_solutionContainer.TryGetSolution(input, name.Evaluate(ctx)!, out var solution);
if (solution is not null)
return new SolutionRef(input, solution);
if (_solutionContainer.TryGetSolution(input, name.Evaluate(ctx)!, out var solution))
return new SolutionRef(solution.Value);
return null;
}
@ -55,11 +53,11 @@ public sealed class SolutionCommand : ToolshedCommand
var amount = amountRef.Evaluate(ctx);
if (amount > 0)
{
_solutionContainer.TryAddReagent(input.Owner, input.Solution, name.Value.ID, amount, out _);
_solutionContainer.TryAddReagent(input.Solution, name.Value.ID, amount, out _);
}
else if (amount < 0)
{
_solutionContainer.RemoveReagent(input.Owner, input.Solution, name.Value.ID, -amount);
_solutionContainer.RemoveReagent(input.Solution, name.Value.ID, -amount);
}
return input;
@ -75,10 +73,10 @@ public sealed class SolutionCommand : ToolshedCommand
=> input.Select(x => AdjReagent(ctx, x, name, amountRef));
}
public readonly record struct SolutionRef(EntityUid Owner, Solution Solution)
public readonly record struct SolutionRef(Entity<SolutionComponent> Solution)
{
public override string ToString()
{
return $"{Owner} {Solution}";
return $"{Solution.Owner} {Solution.Comp.Solution}";
}
}

View file

@ -1,4 +1,5 @@
using Content.Server.Administration.Systems;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.EUI;
using Content.Shared.Administration;
using Content.Shared.Chemistry.Components.SolutionManager;
@ -14,11 +15,13 @@ namespace Content.Server.Administration.UI
public sealed class EditSolutionsEui : BaseEui
{
[Dependency] private readonly IEntityManager _entityManager = default!;
private readonly SolutionContainerSystem _solutionContainerSystem = default!;
public readonly EntityUid Target;
public EditSolutionsEui(EntityUid entity)
{
IoCManager.InjectDependencies(this);
_solutionContainerSystem = _entityManager.System<SolutionContainerSystem>();
Target = entity;
}
@ -36,8 +39,23 @@ namespace Content.Server.Administration.UI
public override EuiStateBase GetNewState()
{
var solutions = _entityManager.GetComponentOrNull<SolutionContainerManagerComponent>(Target)?.Solutions;
return new EditSolutionsEuiState(_entityManager.GetNetEntity(Target), solutions);
List<(string Name, NetEntity Solution)>? netSolutions;
if (_entityManager.TryGetComponent(Target, out SolutionContainerManagerComponent? container) && container.Containers.Count > 0)
{
netSolutions = new();
foreach (var (name, solution) in _solutionContainerSystem.EnumerateSolutions((Target, container)))
{
if (name is null || !_entityManager.TryGetNetEntity(solution, out var netSolution))
continue;
netSolutions.Add((name, netSolution.Value));
}
}
else
netSolutions = null;
return new EditSolutionsEuiState(_entityManager.GetNetEntity(Target), netSolutions);
}
}
}

View file

@ -177,7 +177,7 @@ public sealed class AmeControllerSystem : EntitySystem
if (!Exists(jar))
return;
controller.JarSlot.Remove(jar!.Value);
_containerSystem.Remove(jar!.Value, controller.JarSlot);
UpdateUi(uid, controller);
if (Exists(user))
_handsSystem.PickupOrDrop(user, jar!.Value);
@ -303,7 +303,7 @@ public sealed class AmeControllerSystem : EntitySystem
return;
}
comp.JarSlot.Insert(args.Used);
_containerSystem.Insert(args.Used, comp.JarSlot);
_popupSystem.PopupEntity(Loc.GetString("ame-controller-component-interact-using-success"), uid, args.User, PopupType.Medium);
UpdateUi(uid, comp);

View file

@ -1,10 +1,10 @@
using Content.Server.Animals.Systems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Animals.Components
/// <summary>
@ -21,10 +21,16 @@ namespace Content.Server.Animals.Components
public ProtoId<ReagentPrototype> ReagentId = "Milk";
/// <summary>
/// The solution to add reagent to.
/// The name of <see cref="Solution"/>.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public string Solution = "udder";
public string SolutionName = "udder";
/// <summary>
/// The solution to add reagent to.
/// </summary>
[DataField]
public Entity<SolutionComponent>? Solution = null;
/// <summary>
/// The amount of reagent to be generated on update.

View file

@ -1,4 +1,5 @@
using Content.Server.Animals.Systems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
@ -20,10 +21,16 @@ public sealed partial class WoolyComponent : Component
public ProtoId<ReagentPrototype> ReagentId = "Fiber";
/// <summary>
/// The solution to add reagent to.
/// The name of <see cref="Solution"/>.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public string Solution = "wool";
public string SolutionName = "wool";
/// <summary>
/// The solution to add reagent to.
/// </summary>
[DataField]
public Entity<SolutionComponent>? Solution;
/// <summary>
/// The amount of reagent to be generated on update.

View file

@ -1,7 +1,7 @@
using Content.Server.Animals.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Popups;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.DoAfter;
using Content.Shared.IdentityManagement;
using Content.Shared.Mobs.Systems;
@ -61,11 +61,11 @@ internal sealed class UdderSystem : EntitySystem
_hunger.ModifyHunger(uid, -udder.HungerUsage, hunger);
}
if (!_solutionContainerSystem.TryGetSolution(uid, udder.Solution, out var solution))
if (!_solutionContainerSystem.ResolveSolution(uid, udder.SolutionName, ref udder.Solution))
continue;
//TODO: toxins from bloodstream !?
_solutionContainerSystem.TryAddReagent(uid, solution, udder.ReagentId, udder.QuantityPerUpdate, out _);
_solutionContainerSystem.TryAddReagent(udder.Solution.Value, udder.ReagentId, udder.QuantityPerUpdate, out _);
}
}
@ -85,47 +85,50 @@ internal sealed class UdderSystem : EntitySystem
_doAfterSystem.TryStartDoAfter(doargs);
}
private void OnDoAfter(EntityUid uid, UdderComponent component, MilkingDoAfterEvent args)
private void OnDoAfter(Entity<UdderComponent> entity, ref MilkingDoAfterEvent args)
{
if (args.Cancelled || args.Handled || args.Args.Used == null)
return;
if (!_solutionContainerSystem.TryGetSolution(uid, component.Solution, out var solution))
if (!_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.SolutionName, ref entity.Comp.Solution, out var solution))
return;
if (!_solutionContainerSystem.TryGetRefillableSolution(args.Args.Used.Value, out var targetSolution))
if (!_solutionContainerSystem.TryGetRefillableSolution(args.Args.Used.Value, out var targetSoln, out var targetSolution))
return;
args.Handled = true;
var quantity = solution.Volume;
if (quantity == 0)
{
_popupSystem.PopupEntity(Loc.GetString("udder-system-dry"), uid, args.Args.User);
_popupSystem.PopupEntity(Loc.GetString("udder-system-dry"), entity.Owner, args.Args.User);
return;
}
if (quantity > targetSolution.AvailableVolume)
quantity = targetSolution.AvailableVolume;
var split = _solutionContainerSystem.SplitSolution(uid, solution, quantity);
_solutionContainerSystem.TryAddSolution(args.Args.Used.Value, targetSolution, split);
var split = _solutionContainerSystem.SplitSolution(entity.Comp.Solution.Value, quantity);
_solutionContainerSystem.TryAddSolution(targetSoln.Value, split);
_popupSystem.PopupEntity(Loc.GetString("udder-system-success", ("amount", quantity), ("target", Identity.Entity(args.Args.Used.Value, EntityManager))), uid,
_popupSystem.PopupEntity(Loc.GetString("udder-system-success", ("amount", quantity), ("target", Identity.Entity(args.Args.Used.Value, EntityManager))), entity.Owner,
args.Args.User, PopupType.Medium);
}
private void AddMilkVerb(EntityUid uid, UdderComponent component, GetVerbsEvent<AlternativeVerb> args)
private void AddMilkVerb(Entity<UdderComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
{
if (args.Using == null ||
!args.CanInteract ||
!EntityManager.HasComponent<RefillableSolutionComponent>(args.Using.Value))
return;
var uid = entity.Owner;
var user = args.User;
var used = args.Using.Value;
AlternativeVerb verb = new()
{
Act = () =>
{
AttemptMilk(uid, args.User, args.Using.Value);
AttemptMilk(uid, user, used);
},
Text = Loc.GetString("udder-system-verb-milk"),
Priority = 2

View file

@ -1,6 +1,6 @@
using Content.Server.Animals.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Nutrition;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
@ -52,10 +52,10 @@ public sealed class WoolySystem : EntitySystem
_hunger.ModifyHunger(uid, -wooly.HungerUsage, hunger);
}
if (!_solutionContainer.TryGetSolution(uid, wooly.Solution, out var solution))
if (!_solutionContainer.ResolveSolution(uid, wooly.SolutionName, ref wooly.Solution))
continue;
_solutionContainer.TryAddReagent(uid, solution, wooly.ReagentId, wooly.Quantity, out _);
_solutionContainer.TryAddReagent(wooly.Solution.Value, wooly.ReagentId, wooly.Quantity, out _);
}
}

View file

@ -1,4 +1,5 @@
using Content.Server.Anomaly.Effects;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
@ -85,10 +86,17 @@ public sealed partial class ReagentProducerAnomalyComponent : Component
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public ProtoId<ReagentPrototype> ProducingReagent = "Water";
/// <summary>
/// Solution name where the substance is generated
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("solution")]
public string Solution = "default";
public string SolutionName = "default";
/// <summary>
/// Solution where the substance is generated
/// </summary>
[DataField("solutionRef")]
public Entity<SolutionComponent>? Solution = null;
}

View file

@ -1,6 +1,5 @@
using Content.Server.Anomaly.Components;
using Content.Server.Cargo.Systems;
using Content.Shared.Anomaly;
using Content.Shared.Anomaly.Components;
using Robust.Shared.Timing;
namespace Content.Server.Anomaly.Effects;
@ -10,47 +9,19 @@ namespace Content.Server.Anomaly.Effects;
/// </summary>
public sealed class AnomalyCoreSystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
public override void Initialize()
{
SubscribeLocalEvent<AnomalyCoreComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<AnomalyCoreComponent, PriceCalculationEvent>(OnGetPrice);
}
private void OnMapInit(Entity<AnomalyCoreComponent> core, ref MapInitEvent args)
{
core.Comp.DecayMoment = _gameTiming.CurTime + TimeSpan.FromSeconds(core.Comp.TimeToDecay);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<AnomalyCoreComponent>();
while (query.MoveNext(out var uid, out var component))
{
if (component.IsDecayed)
continue;
//When time runs out, we completely decompose
if (component.DecayMoment < _gameTiming.CurTime)
Decay(uid, component);
}
}
private void OnGetPrice(Entity<AnomalyCoreComponent> core, ref PriceCalculationEvent args)
{
var timeLeft = core.Comp.DecayMoment - _gameTiming.CurTime;
var lerp = (double) (timeLeft.TotalSeconds / core.Comp.TimeToDecay);
var lerp = timeLeft.TotalSeconds / core.Comp.TimeToDecay;
lerp = Math.Clamp(lerp, 0, 1);
args.Price = MathHelper.Lerp(core.Comp.EndPrice, core.Comp.StartPrice, lerp);
}
private void Decay(EntityUid uid, AnomalyCoreComponent component)
{
_appearance.SetData(uid, AnomalyCoreVisuals.Decaying, false);
component.IsDecayed = true;
}
}

View file

@ -1,8 +1,8 @@
using System.Linq;
using Content.Server.Anomaly.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Anomaly.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using System.Linq;
namespace Content.Server.Anomaly.Effects;
/// <summary>
@ -27,41 +27,40 @@ public sealed class InjectionAnomalySystem : EntitySystem
_injectableQuery = GetEntityQuery<InjectableSolutionComponent>();
}
private void OnPulse(EntityUid uid, InjectionAnomalyComponent component, ref AnomalyPulseEvent args)
private void OnPulse(Entity<InjectionAnomalyComponent> entity, ref AnomalyPulseEvent args)
{
PulseScalableEffect(uid, component, component.InjectRadius, component.MaxSolutionInjection * args.Severity);
PulseScalableEffect(entity, entity.Comp.InjectRadius, entity.Comp.MaxSolutionInjection * args.Severity);
}
private void OnSupercritical(EntityUid uid, InjectionAnomalyComponent component, ref AnomalySupercriticalEvent args)
private void OnSupercritical(Entity<InjectionAnomalyComponent> entity, ref AnomalySupercriticalEvent args)
{
PulseScalableEffect(uid, component, component.SuperCriticalInjectRadius, component.SuperCriticalSolutionInjection);
PulseScalableEffect(entity, entity.Comp.SuperCriticalInjectRadius, entity.Comp.SuperCriticalSolutionInjection);
}
private void PulseScalableEffect(EntityUid uid, InjectionAnomalyComponent component, float injectRadius, float maxInject)
private void PulseScalableEffect(Entity<InjectionAnomalyComponent> entity, float injectRadius, float maxInject)
{
if (!_solutionContainer.TryGetSolution(uid, component.Solution, out var sol))
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var sol))
return;
//We get all the entity in the radius into which the reagent will be injected.
var xformQuery = GetEntityQuery<TransformComponent>();
var xform = xformQuery.GetComponent(uid);
var xform = xformQuery.GetComponent(entity);
var allEnts = _lookup.GetEntitiesInRange<InjectableSolutionComponent>(xform.MapPosition, injectRadius)
.Select(x => x.Owner).ToList();
//for each matching entity found
foreach (var ent in allEnts)
{
if (!_solutionContainer.TryGetInjectableSolution(ent, out var injectable))
if (!_solutionContainer.TryGetInjectableSolution(ent, out var injectable, out _))
continue;
if (_injectableQuery.TryGetComponent(ent, out var injEnt))
{
var buffer = sol;
_solutionContainer.TryTransferSolution(ent, injectable, buffer, maxInject);
_solutionContainer.TryTransferSolution(injectable.Value, sol, maxInject);
//Spawn Effect
var uidXform = Transform(ent);
Spawn(component.VisualEffectPrototype, uidXform.Coordinates);
Spawn(entity.Comp.VisualEffectPrototype, uidXform.Coordinates);
}
}
}
}

View file

@ -1,7 +1,7 @@
using Content.Server.Anomaly.Components;
using Content.Shared.Anomaly.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Fluids.EntitySystems;
using Content.Shared.Anomaly.Components;
namespace Content.Server.Anomaly.Effects;
@ -19,21 +19,21 @@ public sealed class PuddleCreateAnomalySystem : EntitySystem
SubscribeLocalEvent<PuddleCreateAnomalyComponent, AnomalySupercriticalEvent>(OnSupercritical, before: new[] { typeof(InjectionAnomalySystem) });
}
private void OnPulse(EntityUid uid, PuddleCreateAnomalyComponent component, ref AnomalyPulseEvent args)
private void OnPulse(Entity<PuddleCreateAnomalyComponent> entity, ref AnomalyPulseEvent args)
{
if (!_solutionContainer.TryGetSolution(uid, component.Solution, out var sol))
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var sol, out _))
return;
var xform = Transform(uid);
var puddleSol = _solutionContainer.SplitSolution(uid, sol, component.MaxPuddleSize * args.Severity);
_puddle.TrySplashSpillAt(uid, xform.Coordinates, puddleSol, out _);
var xform = Transform(entity.Owner);
var puddleSol = _solutionContainer.SplitSolution(sol.Value, entity.Comp.MaxPuddleSize * args.Severity);
_puddle.TrySplashSpillAt(entity.Owner, xform.Coordinates, puddleSol, out _);
}
private void OnSupercritical(EntityUid uid, PuddleCreateAnomalyComponent component, ref AnomalySupercriticalEvent args)
private void OnSupercritical(Entity<PuddleCreateAnomalyComponent> entity, ref AnomalySupercriticalEvent args)
{
if (!_solutionContainer.TryGetSolution(uid, component.Solution, out var sol))
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var sol))
return;
var buffer = sol;
var xform = Transform(uid);
_puddle.TrySpillAt(xform.Coordinates, buffer, out _);
var xform = Transform(entity.Owner);
_puddle.TrySpillAt(xform.Coordinates, sol, out _);
}
}

View file

@ -1,12 +1,12 @@
using Content.Server.Anomaly.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Anomaly.Components;
using Robust.Shared.Random;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Robust.Shared.Prototypes;
using Content.Shared.Sprite;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Anomaly.Effects;
@ -42,17 +42,17 @@ public sealed class ReagentProducerAnomalySystem : EntitySystem
SubscribeLocalEvent<ReagentProducerAnomalyComponent, MapInitEvent>(OnMapInit);
}
private void OnPulse(EntityUid uid, ReagentProducerAnomalyComponent component, ref AnomalyPulseEvent args)
private void OnPulse(Entity<ReagentProducerAnomalyComponent> entity, ref AnomalyPulseEvent args)
{
if (_random.NextFloat(0.0f, 1.0f) > args.Stability)
ChangeReagent(uid, component, args.Severity);
ChangeReagent(entity, args.Severity);
}
private void ChangeReagent(EntityUid uid, ReagentProducerAnomalyComponent component, float severity)
private void ChangeReagent(Entity<ReagentProducerAnomalyComponent> entity, float severity)
{
var reagent = GetRandomReagentType(uid, component, severity);
component.ProducingReagent = reagent;
_audio.PlayPvs(component.ChangeSound, uid);
var reagent = GetRandomReagentType(entity, severity);
entity.Comp.ProducingReagent = reagent;
_audio.PlayPvs(entity.Comp.ChangeSound, entity);
}
//reagent realtime generation
@ -68,7 +68,7 @@ public sealed class ReagentProducerAnomalySystem : EntitySystem
if (component.AccumulatedFrametime < component.UpdateInterval)
continue;
if (!_solutionContainer.TryGetSolution(uid, component.Solution, out var producerSol))
if (!_solutionContainer.ResolveSolution(uid, component.SolutionName, ref component.Solution, out var producerSolution))
continue;
Solution newSol = new();
@ -76,7 +76,7 @@ public sealed class ReagentProducerAnomalySystem : EntitySystem
if (anomaly.Severity >= 0.97) reagentProducingAmount *= component.SupercriticalReagentProducingModifier;
newSol.AddReagent(component.ProducingReagent, reagentProducingAmount);
_solutionContainer.TryAddSolution(uid, producerSol, newSol); //TO DO - the container is not fully filled.
_solutionContainer.TryAddSolution(component.Solution.Value, newSol); //TO DO - the container is not fully filled.
component.AccumulatedFrametime = 0;
@ -87,7 +87,7 @@ public sealed class ReagentProducerAnomalySystem : EntitySystem
// and nothing worked out for me. So for now it will be like this.
if (component.NeedRecolor)
{
var color = producerSol.GetColor(_prototypeManager);
var color = producerSolution.GetColor(_prototypeManager);
_light.SetColor(uid, color);
if (TryComp<RandomSpriteComponent>(uid, out var randomSprite))
{
@ -103,9 +103,9 @@ public sealed class ReagentProducerAnomalySystem : EntitySystem
}
}
private void OnMapInit(EntityUid uid, ReagentProducerAnomalyComponent component, MapInitEvent args)
private void OnMapInit(Entity<ReagentProducerAnomalyComponent> entity, ref MapInitEvent args)
{
ChangeReagent(uid, component, 0.1f); //MapInit Reagent 100% change
ChangeReagent(entity, 0.1f); //MapInit Reagent 100% change
}
// returns a random reagent based on a system of random weights.
@ -117,33 +117,33 @@ public sealed class ReagentProducerAnomalySystem : EntitySystem
// After that, a random reagent in the selected category is selected.
//
// Such a system is made to control the danger and interest of the anomaly more.
private string GetRandomReagentType(EntityUid uid, ReagentProducerAnomalyComponent component, float severity)
private string GetRandomReagentType(Entity<ReagentProducerAnomalyComponent> entity, float severity)
{
//Category Weight Randomization
var currentWeightDangerous = MathHelper.Lerp(component.WeightSpreadDangerous.X, component.WeightSpreadDangerous.Y, severity);
var currentWeightFun = MathHelper.Lerp(component.WeightSpreadFun.X, component.WeightSpreadFun.Y, severity);
var currentWeightUseful = MathHelper.Lerp(component.WeightSpreadUseful.X, component.WeightSpreadUseful.Y, severity);
var currentWeightDangerous = MathHelper.Lerp(entity.Comp.WeightSpreadDangerous.X, entity.Comp.WeightSpreadDangerous.Y, severity);
var currentWeightFun = MathHelper.Lerp(entity.Comp.WeightSpreadFun.X, entity.Comp.WeightSpreadFun.Y, severity);
var currentWeightUseful = MathHelper.Lerp(entity.Comp.WeightSpreadUseful.X, entity.Comp.WeightSpreadUseful.Y, severity);
var sumWeight = currentWeightDangerous + currentWeightFun + currentWeightUseful;
var rnd = _random.NextFloat(0f, sumWeight);
//Dangerous
if (rnd <= currentWeightDangerous && component.DangerousChemicals.Count > 0)
if (rnd <= currentWeightDangerous && entity.Comp.DangerousChemicals.Count > 0)
{
var reagent = _random.Pick(component.DangerousChemicals);
var reagent = _random.Pick(entity.Comp.DangerousChemicals);
return reagent;
}
else rnd -= currentWeightDangerous;
//Fun
if (rnd <= currentWeightFun && component.FunChemicals.Count > 0)
if (rnd <= currentWeightFun && entity.Comp.FunChemicals.Count > 0)
{
var reagent = _random.Pick(component.FunChemicals);
var reagent = _random.Pick(entity.Comp.FunChemicals);
return reagent;
}
else rnd -= currentWeightFun;
//Useful
if (rnd <= currentWeightUseful && component.UsefulChemicals.Count > 0)
if (rnd <= currentWeightUseful && entity.Comp.UsefulChemicals.Count > 0)
{
var reagent = _random.Pick(component.UsefulChemicals);
var reagent = _random.Pick(entity.Comp.UsefulChemicals);
return reagent;
}
//We should never end up here.

View file

@ -285,7 +285,7 @@ public sealed class AntagSelectionSystem : GameRuleSystem<GameRuleComponent>
{
if (_containerSystem.CanInsert(itemToSpawn, pocket1Slot))
{
pocket1Slot.Insert(itemToSpawn);
_containerSystem.Insert(itemToSpawn, pocket1Slot);
}
}
else if (_inventory.TryGetSlotContainer(antag, "pocket2", out var pocket2Slot, out _))
@ -294,7 +294,7 @@ public sealed class AntagSelectionSystem : GameRuleSystem<GameRuleComponent>
{
if (_containerSystem.CanInsert(itemToSpawn, pocket2Slot))
{
pocket2Slot.Insert(itemToSpawn);
_containerSystem.Insert(itemToSpawn, pocket2Slot);
}
}
}

View file

@ -1,6 +1,7 @@
using Content.Server.Administration.Logs;
using Content.Server.Atmos.Components;
using Content.Server.Explosion.EntitySystems;
using Content.Server.IgnitionSource;
using Content.Server.Stunnable;
using Content.Server.Temperature.Components;
using Content.Server.Temperature.Systems;
@ -35,6 +36,7 @@ namespace Content.Server.Atmos.EntitySystems
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly StunSystem _stunSystem = default!;
[Dependency] private readonly TemperatureSystem _temperatureSystem = default!;
[Dependency] private readonly IgnitionSourceSystem _ignitionSourceSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
@ -285,6 +287,8 @@ namespace Content.Server.Atmos.EntitySystems
flammable.OnFire = false;
flammable.FireStacks = 0;
_ignitionSourceSystem.SetIgnited(uid, false);
UpdateAppearance(uid, flammable);
}
@ -378,6 +382,18 @@ namespace Content.Server.Atmos.EntitySystems
if (flammable.FireStacks > 0)
{
var air = _atmosphereSystem.GetContainingMixture(uid);
// If we're in an oxygenless environment, put the fire out.
if (air == null || air.GetMoles(Gas.Oxygen) < 1f)
{
Extinguish(uid, flammable);
continue;
}
EnsureComp<IgnitionSourceComponent>(uid);
_ignitionSourceSystem.SetIgnited(uid);
// TODO FLAMMABLE: further balancing
var damageScale = Math.Min((int)flammable.FireStacks, 5);
@ -391,24 +407,6 @@ namespace Content.Server.Atmos.EntitySystems
else
{
Extinguish(uid, flammable);
continue;
}
var air = _atmosphereSystem.GetContainingMixture(uid);
// If we're in an oxygenless environment, put the fire out.
if (air == null || air.GetMoles(Gas.Oxygen) < 1f)
{
Extinguish(uid, flammable);
continue;
}
if(transform.GridUid != null)
{
_atmosphereSystem.HotspotExpose(transform.GridUid.Value,
_transformSystem.GetGridOrMapTilePosition(uid, transform),
700f, 50f, uid, true);
}
}
}

View file

@ -1,4 +1,5 @@
using Content.Server.Atmos.Piping.Unary.EntitySystems;
using Content.Shared.Chemistry.Components;
namespace Content.Server.Atmos.Piping.Unary.Components;
@ -21,6 +22,12 @@ public sealed partial class GasCondenserComponent : Component
[DataField]
public string SolutionId = "tank";
/// <summary>
/// The solution that gases are condensed into.
/// </summary>
[DataField]
public Entity<SolutionComponent>? Solution = null;
/// <summary>
/// For a condenser, how many U of reagents are given per each mole of gas.
/// </summary>

View file

@ -5,10 +5,11 @@ using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Shared.Atmos;
using JetBrains.Annotations;
using Content.Server.Power.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;
namespace Content.Server.Atmos.Piping.Unary.EntitySystems;
@ -18,7 +19,7 @@ public sealed class GasCondenserSystem : EntitySystem
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly PowerReceiverSystem _power = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
[Dependency] private readonly SolutionContainerSystem _solution = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
public override void Initialize()
{
@ -27,12 +28,12 @@ public sealed class GasCondenserSystem : EntitySystem
SubscribeLocalEvent<GasCondenserComponent, AtmosDeviceUpdateEvent>(OnCondenserUpdated);
}
private void OnCondenserUpdated(EntityUid uid, GasCondenserComponent component, ref AtmosDeviceUpdateEvent args)
private void OnCondenserUpdated(Entity<GasCondenserComponent> entity, ref AtmosDeviceUpdateEvent args)
{
if (!(_power.IsPowered(uid) && TryComp<ApcPowerReceiverComponent>(uid, out var receiver))
|| !TryComp<NodeContainerComponent>(uid, out var nodeContainer)
|| !_nodeContainer.TryGetNode(nodeContainer, component.Inlet, out PipeNode? inlet)
|| !_solution.TryGetSolution(uid, component.SolutionId, out var solution))
if (!(_power.IsPowered(entity) && TryComp<ApcPowerReceiverComponent>(entity, out var receiver))
|| !TryComp<NodeContainerComponent>(entity, out var nodeContainer)
|| !_nodeContainer.TryGetNode(nodeContainer, entity.Comp.Inlet, out PipeNode? inlet)
|| !_solution.ResolveSolution(entity.Owner, entity.Comp.SolutionId, ref entity.Comp.Solution, out var solution))
{
return;
}
@ -48,18 +49,21 @@ public sealed class GasCondenserSystem : EntitySystem
if (moles <= 0)
continue;
if (_atmosphereSystem.GetGas(i).Reagent is not {} gasReagent)
if (_atmosphereSystem.GetGas(i).Reagent is not { } gasReagent)
continue;
var moleToReagentMultiplier = component.MolesToReagentMultiplier;
var amount = moles * moleToReagentMultiplier;
if (_solution.TryAddReagent(uid, solution, gasReagent, amount, out var remaining))
var moleToReagentMultiplier = entity.Comp.MolesToReagentMultiplier;
var amount = FixedPoint2.Min(FixedPoint2.New(moles * moleToReagentMultiplier), solution.AvailableVolume);
if (amount <= 0)
continue;
solution.AddReagent(gasReagent, amount);
// if we have leftover reagent, then convert it back to moles and put it back in the mixture.
inlet.Air.AdjustMoles(i, remaining.Float() / moleToReagentMultiplier);
inlet.Air.AdjustMoles(i, moles - (amount.Float() / moleToReagentMultiplier));
}
_solution.UpdateChemicals(entity.Comp.Solution.Value);
}
public float NumberOfMolesToConvert(ApcPowerReceiverComponent comp, GasMixture mix, float dt)

View file

@ -34,52 +34,52 @@ namespace Content.Server.Body.Components
/// <summary>
/// How much should bleeding should be reduced every update interval?
/// </summary>
[DataField("bleedReductionAmount")]
[DataField]
public float BleedReductionAmount = 1.0f;
/// <summary>
/// How high can <see cref="BleedAmount"/> go?
/// </summary>
[DataField("maxBleedAmount")]
[DataField]
public float MaxBleedAmount = 10.0f;
/// <summary>
/// What percentage of current blood is necessary to avoid dealing blood loss damage?
/// </summary>
[DataField("bloodlossThreshold")]
[DataField]
public float BloodlossThreshold = 0.9f;
/// <summary>
/// The base bloodloss damage to be incurred if below <see cref="BloodlossThreshold"/>
/// The default values are defined per mob/species in YML.
/// </summary>
[DataField("bloodlossDamage", required: true)]
[DataField(required: true)]
public DamageSpecifier BloodlossDamage = new();
/// <summary>
/// The base bloodloss damage to be healed if above <see cref="BloodlossThreshold"/>
/// The default values are defined per mob/species in YML.
/// </summary>
[DataField("bloodlossHealDamage", required: true)]
[DataField(required: true)]
public DamageSpecifier BloodlossHealDamage = new();
/// <summary>
/// How frequently should this bloodstream update, in seconds?
/// </summary>
[DataField("updateInterval")]
[DataField]
public float UpdateInterval = 3.0f;
// TODO shouldn't be hardcoded, should just use some organ simulation like bone marrow or smth.
/// <summary>
/// How much reagent of blood should be restored each update interval?
/// </summary>
[DataField("bloodRefreshAmount")]
[DataField]
public float BloodRefreshAmount = 1.0f;
/// <summary>
/// How much blood needs to be in the temporary solution in order to create a puddle?
/// </summary>
[DataField("bleedPuddleThreshold")]
[DataField]
public FixedPoint2 BleedPuddleThreshold = 1.0f;
/// <summary>
@ -89,19 +89,19 @@ namespace Content.Server.Body.Components
/// <remarks>
/// For example, piercing damage is increased while poison damage is nullified entirely.
/// </remarks>
[DataField("damageBleedModifiers", customTypeSerializer:typeof(PrototypeIdSerializer<DamageModifierSetPrototype>))]
[DataField(customTypeSerializer:typeof(PrototypeIdSerializer<DamageModifierSetPrototype>))]
public string DamageBleedModifiers = "BloodlossHuman";
/// <summary>
/// The sound to be played when a weapon instantly deals blood loss damage.
/// </summary>
[DataField("instantBloodSound")]
[DataField]
public SoundSpecifier InstantBloodSound = new SoundCollectionSpecifier("blood");
/// <summary>
/// The sound to be played when some damage actually heals bleeding rather than starting it.
/// </summary>
[DataField("bloodHealedSound")]
[DataField]
public SoundSpecifier BloodHealedSound = new SoundPathSpecifier("/Audio/Effects/lightburn.ogg");
// TODO probably damage bleed thresholds.
@ -109,14 +109,14 @@ namespace Content.Server.Body.Components
/// <summary>
/// Max volume of internal chemical solution storage
/// </summary>
[DataField("chemicalMaxVolume")]
[DataField]
public FixedPoint2 ChemicalMaxVolume = FixedPoint2.New(250);
/// <summary>
/// Max volume of internal blood storage,
/// and starting level of blood.
/// </summary>
[DataField("bloodMaxVolume")]
[DataField]
public FixedPoint2 BloodMaxVolume = FixedPoint2.New(300);
/// <summary>
@ -125,29 +125,40 @@ namespace Content.Server.Body.Components
/// <remarks>
/// Slime-people might use slime as their blood or something like that.
/// </remarks>
[DataField("bloodReagent")]
[DataField]
public string BloodReagent = "Blood";
/// <summary>
/// Internal solution for reagent storage
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[Access(typeof(BloodstreamSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
public Solution ChemicalSolution = default!;
/// <summary>Name/Key that <see cref="BloodSolution"/> is indexed by.</summary>
[DataField]
public string BloodSolutionName = DefaultBloodSolutionName;
/// <summary>Name/Key that <see cref="ChemicalSolution"/> is indexed by.</summary>
[DataField]
public string ChemicalSolutionName = DefaultChemicalsSolutionName;
/// <summary>Name/Key that <see cref="TemporarySolution"/> is indexed by.</summary>
[DataField]
public string BloodTemporarySolutionName = DefaultBloodTemporarySolutionName;
/// <summary>
/// Internal solution for blood storage
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public Solution BloodSolution = default!;
[DataField]
public Entity<SolutionComponent>? BloodSolution = null;
/// <summary>
/// Internal solution for reagent storage
/// </summary>
[DataField]
public Entity<SolutionComponent>? ChemicalSolution = null;
/// <summary>
/// Temporary blood solution.
/// When blood is lost, it goes to this solution, and when this
/// solution hits a certain cap, the blood is actually spilled as a puddle.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public Solution BloodTemporarySolution = default!;
[DataField]
public Entity<SolutionComponent>? TemporarySolution = null;
/// <summary>
/// Variable that stores the amount of status time added by having a low blood level.

View file

@ -8,7 +8,7 @@ namespace Content.Server.Body.Components;
[RegisterComponent, Access(typeof(LungSystem))]
public sealed partial class LungComponent : Component
{
[DataField("air")]
[DataField]
[Access(typeof(LungSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
public GasMixture Air { get; set; } = new()
{
@ -16,7 +16,15 @@ public sealed partial class LungComponent : Component
Temperature = Atmospherics.NormalBodyTemperature
};
[ViewVariables]
[Access(typeof(LungSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
public Solution LungSolution = default!;
/// <summary>
/// The name/key of the solution on this entity which these lungs act on.
/// </summary>
[DataField]
public string SolutionName = LungSystem.LungSolutionName;
/// <summary>
/// The solution on this entity that these lungs act on.
/// </summary>
[DataField]
public Entity<SolutionComponent>? Solution = null;
}

View file

@ -18,7 +18,7 @@ namespace Content.Server.Body.Components
/// How often to metabolize reagents, in seconds.
/// </summary>
/// <returns></returns>
[DataField("updateFrequency")]
[DataField]
public float UpdateFrequency = 1.0f;
/// <summary>
@ -33,13 +33,13 @@ namespace Content.Server.Body.Components
/// <remarks>
/// Most things will use the parent entity (bloodstream).
/// </remarks>
[DataField("solutionOnBody")]
[DataField]
public bool SolutionOnBody = true;
/// <summary>
/// List of metabolizer types that this organ is. ex. Human, Slime, Felinid, w/e.
/// </summary>
[DataField("metabolizerTypes", customTypeSerializer:typeof(PrototypeIdHashSetSerializer<MetabolizerTypePrototype>))]
[DataField(customTypeSerializer:typeof(PrototypeIdHashSetSerializer<MetabolizerTypePrototype>))]
[Access(typeof(MetabolizerSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
public HashSet<string>? MetabolizerTypes = null;
@ -47,7 +47,7 @@ namespace Content.Server.Body.Components
/// Should this metabolizer remove chemicals that have no metabolisms defined?
/// As a stop-gap, basically.
/// </summary>
[DataField("removeEmpty")]
[DataField]
public bool RemoveEmpty = false;
/// <summary>
@ -72,7 +72,7 @@ namespace Content.Server.Body.Components
[DataDefinition]
public sealed partial class MetabolismGroupEntry
{
[DataField("id", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<MetabolismGroupPrototype>))]
[DataField(required: true, customTypeSerializer:typeof(PrototypeIdSerializer<MetabolismGroupPrototype>))]
public string Id = default!;
[DataField("rateModifier")]

View file

@ -2,7 +2,6 @@
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Content.Shared.Whitelist;
namespace Content.Server.Body.Components
@ -15,26 +14,32 @@ namespace Content.Server.Body.Components
/// <summary>
/// How fast should this component update, in seconds?
/// </summary>
[DataField("updateInterval")]
[DataField]
public float UpdateInterval = 1.0f;
/// <summary>
/// The solution inside of this stomach this transfers reagents to the body.
/// </summary>
[DataField]
public Entity<SolutionComponent>? Solution = null;
/// <summary>
/// What solution should this stomach push reagents into, on the body?
/// </summary>
[DataField("bodySolutionName")]
[DataField]
public string BodySolutionName = BloodstreamComponent.DefaultChemicalsSolutionName;
/// <summary>
/// Time in seconds between reagents being ingested and them being
/// transferred to <see cref="BloodstreamComponent"/>
/// </summary>
[DataField("digestionDelay")]
[DataField]
public float DigestionDelay = 20;
/// <summary>
/// A whitelist for what special-digestible-required foods this stomach is capable of eating.
/// </summary>
[DataField("specialDigestible")]
[DataField]
public EntityWhitelist? SpecialDigestible = null;
/// <summary>

View file

@ -1,6 +1,8 @@
using Content.Server.Body.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Chemistry.ReactionEffects;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics;
using Content.Server.HealthExaminable;
using Content.Server.Popups;
using Content.Shared.Alert;
@ -16,13 +18,9 @@ using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Rejuvenate;
using Content.Shared.Speech.EntitySystems;
using Robust.Server.GameObjects;
using Robust.Server.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Content.Shared.Speech.EntitySystems;
using Robust.Server.Audio;
using Robust.Shared.GameObjects;
using Content.Server.Forensics;
namespace Content.Server.Body.Systems;
@ -51,14 +49,13 @@ public sealed class BloodstreamSystem : EntitySystem
SubscribeLocalEvent<BloodstreamComponent, BeingGibbedEvent>(OnBeingGibbed);
SubscribeLocalEvent<BloodstreamComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
SubscribeLocalEvent<BloodstreamComponent, ReactionAttemptEvent>(OnReactionAttempt);
SubscribeLocalEvent<BloodstreamComponent, SolutionRelayEvent<ReactionAttemptEvent>>(OnReactionAttempt);
SubscribeLocalEvent<BloodstreamComponent, RejuvenateEvent>(OnRejuvenate);
}
private void OnReactionAttempt(EntityUid uid, BloodstreamComponent component, ReactionAttemptEvent args)
private void OnReactionAttempt(Entity<BloodstreamComponent> entity, ref ReactionAttemptEvent args)
{
if (args.Solution.Name != BloodstreamComponent.DefaultBloodSolutionName
&& args.Solution.Name != BloodstreamComponent.DefaultChemicalsSolutionName
&& args.Solution.Name != BloodstreamComponent.DefaultBloodTemporarySolutionName)
if (args.Cancelled)
return;
foreach (var effect in args.Reaction.Effects)
@ -67,7 +64,7 @@ public sealed class BloodstreamSystem : EntitySystem
{
case CreateEntityReactionEffect: // Prevent entities from spawning in the bloodstream
case AreaReactionEffect: // No spontaneous smoke or foam leaking out of blood vessels.
args.Cancel();
args.Cancelled = true;
return;
}
}
@ -81,6 +78,16 @@ public sealed class BloodstreamSystem : EntitySystem
// Having cheese-clots form in your veins can't be good for you.
}
private void OnReactionAttempt(Entity<BloodstreamComponent> entity, ref SolutionRelayEvent<ReactionAttemptEvent> args)
{
if (args.Name != entity.Comp.BloodSolutionName
&& args.Name != entity.Comp.ChemicalSolutionName
&& args.Name != entity.Comp.BloodTemporarySolutionName)
return;
OnReactionAttempt(entity, ref args.Event);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
@ -95,8 +102,11 @@ public sealed class BloodstreamSystem : EntitySystem
bloodstream.AccumulatedFrametime -= bloodstream.UpdateInterval;
if (!_solutionContainerSystem.ResolveSolution(uid, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
continue;
// Adds blood to their blood level if it is below the maximum; Blood regeneration. Must be alive.
if (bloodstream.BloodSolution.Volume < bloodstream.BloodSolution.MaxVolume && !_mobStateSystem.IsDead(uid))
if (bloodSolution.Volume < bloodSolution.MaxVolume && !_mobStateSystem.IsDead(uid))
{
TryModifyBloodLevel(uid, bloodstream.BloodRefreshAmount, bloodstream);
}
@ -143,19 +153,18 @@ public sealed class BloodstreamSystem : EntitySystem
}
}
private void OnComponentInit(EntityUid uid, BloodstreamComponent component, ComponentInit args)
private void OnComponentInit(Entity<BloodstreamComponent> entity, ref ComponentInit args)
{
component.ChemicalSolution = _solutionContainerSystem.EnsureSolution(uid, BloodstreamComponent.DefaultChemicalsSolutionName);
component.BloodSolution = _solutionContainerSystem.EnsureSolution(uid, BloodstreamComponent.DefaultBloodSolutionName);
component.BloodTemporarySolution = _solutionContainerSystem.EnsureSolution(uid, BloodstreamComponent.DefaultBloodTemporarySolutionName);
var chemicalSolution = _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.ChemicalSolutionName);
var bloodSolution = _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.BloodSolutionName);
var tempSolution = _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.BloodTemporarySolutionName);
component.ChemicalSolution.MaxVolume = component.ChemicalMaxVolume;
component.BloodSolution.MaxVolume = component.BloodMaxVolume;
component.BloodTemporarySolution.MaxVolume = component.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well
chemicalSolution.MaxVolume = entity.Comp.ChemicalMaxVolume;
bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume;
tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well
// Fill blood solution with BLOOD
_solutionContainerSystem.TryAddReagent(uid, component.BloodSolution, component.BloodReagent,
component.BloodMaxVolume, out _);
bloodSolution.AddReagent(entity.Comp.BloodReagent, entity.Comp.BloodMaxVolume - bloodSolution.Volume);
}
private void OnDamageChanged(EntityUid uid, BloodstreamComponent component, DamageChangedEvent args)
@ -250,34 +259,45 @@ public sealed class BloodstreamSystem : EntitySystem
component.AccumulatedFrametime = component.UpdateInterval;
}
private void OnRejuvenate(EntityUid uid, BloodstreamComponent component, RejuvenateEvent args)
private void OnRejuvenate(Entity<BloodstreamComponent> entity, ref RejuvenateEvent args)
{
TryModifyBleedAmount(uid, -component.BleedAmount, component);
TryModifyBloodLevel(uid, component.BloodSolution.AvailableVolume, component);
_solutionContainerSystem.RemoveAllSolution(uid, component.ChemicalSolution);
TryModifyBleedAmount(entity.Owner, -entity.Comp.BleedAmount, entity.Comp);
if (_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution))
TryModifyBloodLevel(entity.Owner, bloodSolution.AvailableVolume, entity.Comp);
if (_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.ChemicalSolutionName, ref entity.Comp.ChemicalSolution))
_solutionContainerSystem.RemoveAllSolution(entity.Comp.ChemicalSolution.Value);
}
/// <summary>
/// Attempt to transfer provided solution to internal solution.
/// </summary>
public bool TryAddToChemicals(EntityUid uid, Solution solution, BloodstreamComponent? component=null)
public bool TryAddToChemicals(EntityUid uid, Solution solution, BloodstreamComponent? component = null)
{
if (!Resolve(uid, ref component, false))
return false;
return _solutionContainerSystem.TryAddSolution(uid, component.ChemicalSolution, solution);
if (!_solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution))
return false;
return _solutionContainerSystem.TryAddSolution(component.ChemicalSolution.Value, solution);
}
public bool FlushChemicals(EntityUid uid, string excludedReagentID, FixedPoint2 quantity, BloodstreamComponent? component = null) {
public bool FlushChemicals(EntityUid uid, string excludedReagentID, FixedPoint2 quantity, BloodstreamComponent? component = null)
{
if (!Resolve(uid, ref component, false))
return false;
for (var i = component.ChemicalSolution.Contents.Count - 1; i >= 0; i--)
if (!_solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution, out var chemSolution))
return false;
for (var i = chemSolution.Contents.Count - 1; i >= 0; i--)
{
var (reagentId, _) = component.ChemicalSolution.Contents[i];
var (reagentId, _) = chemSolution.Contents[i];
if (reagentId.Prototype != excludedReagentID)
{
_solutionContainerSystem.RemoveReagent(uid, component.ChemicalSolution, reagentId, quantity);
_solutionContainerSystem.RemoveReagent(component.ChemicalSolution.Value, reagentId, quantity);
}
}
@ -289,7 +309,10 @@ public sealed class BloodstreamSystem : EntitySystem
if (!Resolve(uid, ref component))
return 0.0f;
return component.BloodSolution.FillFraction;
if (!_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution, out var bloodSolution))
return 0.0f;
return bloodSolution.FillFraction;
}
public void SetBloodLossThreshold(EntityUid uid, float threshold, BloodstreamComponent? comp = null)
@ -308,28 +331,41 @@ public sealed class BloodstreamSystem : EntitySystem
if (!Resolve(uid, ref component, false))
return false;
if (!_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution))
return false;
if (amount >= 0)
return _solutionContainerSystem.TryAddReagent(uid, component.BloodSolution, component.BloodReagent, amount, out _);
return _solutionContainerSystem.TryAddReagent(component.BloodSolution.Value, component.BloodReagent, amount, out _);
// Removal is more involved,
// since we also wanna handle moving it to the temporary solution
// and then spilling it if necessary.
var newSol = component.BloodSolution.SplitSolution(-amount);
component.BloodTemporarySolution.AddSolution(newSol, _prototypeManager);
var newSol = _solutionContainerSystem.SplitSolution(component.BloodSolution.Value, -amount);
if (component.BloodTemporarySolution.Volume > component.BleedPuddleThreshold)
if (!_solutionContainerSystem.ResolveSolution(uid, component.BloodTemporarySolutionName, ref component.TemporarySolution, out var tempSolution))
return true;
tempSolution.AddSolution(newSol, _prototypeManager);
if (tempSolution.Volume > component.BleedPuddleThreshold)
{
// Pass some of the chemstream into the spilled blood.
var temp = component.ChemicalSolution.SplitSolution(component.BloodTemporarySolution.Volume / 10);
component.BloodTemporarySolution.AddSolution(temp, _prototypeManager);
if (_puddleSystem.TrySpillAt(uid, component.BloodTemporarySolution, out var puddleUid, false))
if (_solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution))
{
var temp = _solutionContainerSystem.SplitSolution(component.ChemicalSolution.Value, tempSolution.Volume / 10);
tempSolution.AddSolution(temp, _prototypeManager);
}
if (_puddleSystem.TrySpillAt(uid, tempSolution, out var puddleUid, false))
{
_forensicsSystem.TransferDna(puddleUid, uid, false);
}
component.BloodTemporarySolution.RemoveAllSolution();
tempSolution.RemoveAllSolution();
}
_solutionContainerSystem.UpdateChemicals(component.TemporarySolution.Value);
return true;
}
@ -363,16 +399,28 @@ public sealed class BloodstreamSystem : EntitySystem
if (!Resolve(uid, ref component))
return;
var max = component.BloodSolution.MaxVolume + component.BloodTemporarySolution.MaxVolume +
component.ChemicalSolution.MaxVolume;
var tempSol = new Solution() { MaxVolume = max };
var tempSol = new Solution();
tempSol.AddSolution(component.BloodSolution, _prototypeManager);
component.BloodSolution.RemoveAllSolution();
tempSol.AddSolution(component.BloodTemporarySolution, _prototypeManager);
component.BloodTemporarySolution.RemoveAllSolution();
tempSol.AddSolution(component.ChemicalSolution, _prototypeManager);
component.ChemicalSolution.RemoveAllSolution();
if (_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution, out var bloodSolution))
{
tempSol.MaxVolume += bloodSolution.MaxVolume;
tempSol.AddSolution(bloodSolution, _prototypeManager);
_solutionContainerSystem.RemoveAllSolution(component.BloodSolution.Value);
}
if (_solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution, out var chemSolution))
{
tempSol.MaxVolume += chemSolution.MaxVolume;
tempSol.AddSolution(chemSolution, _prototypeManager);
_solutionContainerSystem.RemoveAllSolution(component.ChemicalSolution.Value);
}
if (_solutionContainerSystem.ResolveSolution(uid, component.BloodTemporarySolutionName, ref component.TemporarySolution, out var tempSolution))
{
tempSol.MaxVolume += tempSolution.MaxVolume;
tempSol.AddSolution(tempSolution, _prototypeManager);
_solutionContainerSystem.RemoveAllSolution(component.TemporarySolution.Value);
}
if (_puddleSystem.TrySpillAt(uid, tempSol, out var puddleUid))
{
@ -388,13 +436,20 @@ public sealed class BloodstreamSystem : EntitySystem
if (!Resolve(uid, ref component, false))
return;
if(reagent == component.BloodReagent)
if (reagent == component.BloodReagent)
return;
var currentVolume = component.BloodSolution.Volume;
if (!_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution, out var bloodSolution))
{
component.BloodReagent = reagent;
return;
}
var currentVolume = bloodSolution.RemoveReagent(component.BloodReagent, bloodSolution.Volume);
component.BloodReagent = reagent;
component.BloodSolution.RemoveAllSolution();
_solutionContainerSystem.TryAddReagent(uid, component.BloodSolution, component.BloodReagent, currentVolume, out _);
if (currentVolume > 0)
_solutionContainerSystem.TryAddReagent(component.BloodSolution.Value, component.BloodReagent, currentVolume, out _);
}
}

View file

@ -1,8 +1,8 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Clothing;
using Content.Shared.Inventory.Events;
@ -44,11 +44,11 @@ public sealed class LungSystem : EntitySystem
}
}
private void OnComponentInit(EntityUid uid, LungComponent component, ComponentInit args)
private void OnComponentInit(Entity<LungComponent> entity, ref ComponentInit args)
{
component.LungSolution = _solutionContainerSystem.EnsureSolution(uid, LungSolutionName);
component.LungSolution.MaxVolume = 100.0f;
component.LungSolution.CanReact = false; // No dexalin lungs
var solution = _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.SolutionName);
solution.MaxVolume = 100.0f;
solution.CanReact = false; // No dexalin lungs
}
private void OnMaskToggled(Entity<BreathToolComponent> ent, ref ItemMaskToggledEvent args)
@ -71,6 +71,9 @@ public sealed class LungSystem : EntitySystem
public void GasToReagent(EntityUid uid, LungComponent lung)
{
if (!_solutionContainerSystem.ResolveSolution(uid, lung.SolutionName, ref lung.Solution, out var solution))
return;
foreach (var gas in Enum.GetValues<Gas>())
{
var i = (int) gas;
@ -81,11 +84,13 @@ public sealed class LungSystem : EntitySystem
if (reagent == null) continue;
var amount = moles * Atmospherics.BreathMolesToReagentMultiplier;
_solutionContainerSystem.TryAddReagent(uid, lung.LungSolution, reagent, amount, out _);
solution.AddReagent(reagent, amount);
// We don't remove the gas from the lung mix,
// that's the responsibility of whatever gas is being metabolized.
// Most things will just want to exhale again.
}
_solutionContainerSystem.UpdateChemicals(lung.Solution.Value);
}
}

View file

@ -1,9 +1,9 @@
using Content.Server.Body.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Organ;
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.FixedPoint;
@ -37,15 +37,15 @@ namespace Content.Server.Body.Systems
SubscribeLocalEvent<MetabolizerComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
}
private void OnMetabolizerInit(EntityUid uid, MetabolizerComponent component, ComponentInit args)
private void OnMetabolizerInit(Entity<MetabolizerComponent> entity, ref ComponentInit args)
{
if (!component.SolutionOnBody)
if (!entity.Comp.SolutionOnBody)
{
_solutionContainerSystem.EnsureSolution(uid, component.SolutionName);
_solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.SolutionName);
}
else if (_organQuery.CompOrNull(uid)?.Body is { } body)
else if (_organQuery.CompOrNull(entity)?.Body is { } body)
{
_solutionContainerSystem.EnsureSolution(body, component.SolutionName);
_solutionContainerSystem.EnsureSolution(body, entity.Comp.SolutionName);
}
}
@ -95,6 +95,7 @@ namespace Content.Server.Body.Systems
// First step is get the solution we actually care about
Solution? solution = null;
Entity<SolutionComponent>? soln = default!;
EntityUid? solutionEntityUid = null;
SolutionContainerManagerComponent? manager = null;
@ -106,7 +107,7 @@ namespace Content.Server.Body.Systems
if (!_solutionQuery.Resolve(body, ref manager, false))
return;
_solutionContainerSystem.TryGetSolution(body, meta.SolutionName, out solution, manager);
_solutionContainerSystem.TryGetSolution((body, manager), meta.SolutionName, out soln, out solution);
solutionEntityUid = body;
}
}
@ -115,11 +116,11 @@ namespace Content.Server.Body.Systems
if (!_solutionQuery.Resolve(uid, ref manager, false))
return;
_solutionContainerSystem.TryGetSolution(uid, meta.SolutionName, out solution, manager);
_solutionContainerSystem.TryGetSolution((uid, manager), meta.SolutionName, out soln, out solution);
solutionEntityUid = uid;
}
if (solutionEntityUid == null || solution == null || solution.Contents.Count == 0)
if (solutionEntityUid == null || soln is null || solution is null || solution.Contents.Count == 0)
return;
// randomize the reagent list so we don't have any weird quirks
@ -138,8 +139,7 @@ namespace Content.Server.Body.Systems
{
if (meta.RemoveEmpty)
{
_solutionContainerSystem.RemoveReagent(solutionEntityUid.Value, solution, reagent,
FixedPoint2.New(1));
solution.RemoveReagent(reagent, FixedPoint2.New(1));
}
continue;
@ -198,12 +198,14 @@ namespace Content.Server.Body.Systems
// remove a certain amount of reagent
if (mostToRemove > FixedPoint2.Zero)
{
_solutionContainerSystem.RemoveReagent(solutionEntityUid.Value, solution, reagent, mostToRemove);
solution.RemoveReagent(reagent, mostToRemove);
// We have processed a reagant, so count it towards the cap
reagents += 1;
}
}
_solutionContainerSystem.UpdateChemicals(soln.Value);
}
}

View file

@ -2,6 +2,7 @@ using Content.Server.Administration.Logs;
using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Popups;
using Content.Shared.Alert;
using Content.Shared.Atmos;
@ -26,6 +27,7 @@ namespace Content.Server.Body.Systems
[Dependency] private readonly LungSystem _lungSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
public override void Initialize()
{
@ -144,7 +146,9 @@ namespace Content.Server.Body.Systems
{
_atmosSys.Merge(outGas, lung.Air);
lung.Air.Clear();
lung.LungSolution.RemoveAllSolution();
if (_solutionContainerSystem.ResolveSolution(lung.Owner, lung.SolutionName, ref lung.Solution))
_solutionContainerSystem.RemoveAllSolution(lung.Solution.Value);
}
_atmosSys.Merge(ev.Gas, outGas);

View file

@ -1,8 +1,8 @@
using Content.Server.Body.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Body.Organ;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Robust.Shared.Utility;
namespace Content.Server.Body.Systems
@ -31,8 +31,7 @@ namespace Content.Server.Body.Systems
stomach.AccumulatedFrameTime -= stomach.UpdateInterval;
// Get our solutions
if (!_solutionContainerSystem.TryGetSolution(uid, DefaultSolutionName,
out var stomachSolution, sol))
if (!_solutionContainerSystem.ResolveSolution((uid, sol), DefaultSolutionName, ref stomach.Solution, out var stomachSolution))
continue;
if (organ.Body is not { } body || !_solutionContainerSystem.TryGetSolution(body, stomach.BodySolutionName, out var bodySolution))
@ -51,7 +50,7 @@ namespace Content.Server.Body.Systems
if (reagent.Quantity > delta.ReagentQuantity.Quantity)
reagent = new(reagent.Reagent, delta.ReagentQuantity.Quantity);
_solutionContainerSystem.RemoveReagent(uid, stomachSolution, reagent);
stomachSolution.RemoveReagent(reagent);
transferSolution.AddReagent(reagent);
}
@ -64,8 +63,10 @@ namespace Content.Server.Body.Systems
stomach.ReagentDeltas.Remove(item);
}
_solutionContainerSystem.UpdateChemicals(stomach.Solution.Value);
// Transfer everything to the body solution!
_solutionContainerSystem.TryAddSolution(body, bodySolution, transferSolution);
_solutionContainerSystem.TryAddSolution(bodySolution.Value, transferSolution);
}
}
@ -86,12 +87,13 @@ namespace Content.Server.Body.Systems
}
public bool CanTransferSolution(EntityUid uid, Solution solution,
StomachComponent? stomach = null,
SolutionContainerManagerComponent? solutions = null)
{
if (!Resolve(uid, ref solutions, false))
if (!Resolve(uid, ref stomach, ref solutions, false))
return false;
if (!_solutionContainerSystem.TryGetSolution(uid, DefaultSolutionName, out var stomachSolution, solutions))
if (!_solutionContainerSystem.ResolveSolution((uid, solutions), DefaultSolutionName, ref stomach.Solution, out var stomachSolution))
return false;
// TODO: For now no partial transfers. Potentially change by design
@ -108,11 +110,11 @@ namespace Content.Server.Body.Systems
if (!Resolve(uid, ref stomach, ref solutions, false))
return false;
if (!_solutionContainerSystem.TryGetSolution(uid, DefaultSolutionName, out var stomachSolution, solutions)
|| !CanTransferSolution(uid, solution, solutions))
if (!_solutionContainerSystem.ResolveSolution((uid, solutions), DefaultSolutionName, ref stomach.Solution)
|| !CanTransferSolution(uid, solution, stomach, solutions))
return false;
_solutionContainerSystem.TryAddSolution(uid, stomachSolution, solution);
_solutionContainerSystem.TryAddSolution(stomach.Solution.Value, solution);
// Add each reagent to ReagentDeltas. Used to track how long each reagent has been in the stomach
foreach (var reagent in solution.Contents)
{

View file

@ -1,3 +1,4 @@
using Content.Shared.Chemistry.Components;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Botany.Components;
@ -90,4 +91,7 @@ public sealed partial class PlantHolderComponent : Component
[ViewVariables(VVAccess.ReadWrite), DataField("solution")]
public string SoilSolutionName = "soil";
[DataField]
public Entity<SolutionComponent>? SoilSolution = null;
}

View file

@ -1,6 +1,5 @@
using Content.Server.Botany.Components;
using Content.Shared.FixedPoint;
using Robust.Server.GameObjects;
namespace Content.Server.Botany.Systems;
@ -11,7 +10,7 @@ public sealed partial class BotanySystem
if (!TryGetSeed(produce, out var seed))
return;
var solutionContainer = _solutionContainerSystem.EnsureSolution(uid, produce.SolutionName);
var solutionContainer = _solutionContainerSystem.EnsureSolution(uid, produce.SolutionName, FixedPoint2.Zero, out _);
solutionContainer.RemoveAllSolution();
foreach (var (chem, quantity) in seed.Chemicals)

View file

@ -1,10 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Botany.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Kitchen.Components;
using Content.Server.Popups;
using Content.Shared.Botany;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Examine;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Physics;
@ -20,6 +18,8 @@ using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Content.Server.Botany.Systems;

View file

@ -1,12 +1,12 @@
using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Botany.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Fluids.Components;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Kitchen.Components;
using Content.Server.Popups;
using Content.Shared.Botany;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Coordinates.Helpers;
using Content.Shared.Examine;
@ -38,7 +38,7 @@ public sealed class PlantHolderSystem : EntitySystem
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly SharedPointLightSystem _pointLight = default!;
[Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly RandomHelperSystem _randomHelper = default!;
[Dependency] private readonly IRobustRandom _random = default!;
@ -70,11 +70,13 @@ public sealed class PlantHolderSystem : EntitySystem
}
}
private void OnExamine(EntityUid uid, PlantHolderComponent component, ExaminedEvent args)
private void OnExamine(Entity<PlantHolderComponent> entity, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
var (_, component) = entity;
if (component.Seed == null)
{
args.PushMarkup(Loc.GetString("plant-holder-component-nothing-planted-message"));
@ -131,8 +133,10 @@ public sealed class PlantHolderSystem : EntitySystem
}
}
private void OnInteractUsing(EntityUid uid, PlantHolderComponent component, InteractUsingEvent args)
private void OnInteractUsing(Entity<PlantHolderComponent> entity, ref InteractUsingEvent args)
{
var (uid, component) = entity;
if (TryComp(args.Used, out SeedComponent? seeds))
{
if (component.Seed == null)
@ -203,8 +207,8 @@ public sealed class PlantHolderSystem : EntitySystem
return;
}
if (_solutionSystem.TryGetDrainableSolution(args.Used, out var solution)
&& _solutionSystem.TryGetSolution(uid, component.SoilSolutionName, out var targetSolution)
if (_solutionContainerSystem.TryGetDrainableSolution(args.Used, out var solution, out _)
&& _solutionContainerSystem.ResolveSolution(uid, component.SoilSolutionName, ref component.SoilSolution)
&& TryComp(args.Used, out SprayComponent? spray))
{
var amount = FixedPoint2.New(1);
@ -214,7 +218,7 @@ public sealed class PlantHolderSystem : EntitySystem
_audio.PlayPvs(spray.SpraySound, args.Used, AudioParams.Default.WithVariation(0.125f));
var split = _solutionSystem.Drain(solutionEntity, solution, amount);
var split = _solutionContainerSystem.Drain(solutionEntity, solution.Value, amount);
if (split.Volume == 0)
{
@ -227,7 +231,7 @@ public sealed class PlantHolderSystem : EntitySystem
("owner", uid),
("amount", split.Volume)), args.User, PopupType.Medium);
_solutionSystem.TryAddSolution(targetEntity, targetSolution, split);
_solutionContainerSystem.TryAddSolution(component.SoilSolution.Value, split);
ForceUpdateByExternalCause(uid, component);
@ -290,16 +294,15 @@ public sealed class PlantHolderSystem : EntitySystem
("usingItem", args.Used),
("owner", uid)), uid, Filter.PvsExcept(args.User), true);
if (_solutionSystem.TryGetSolution(args.Used, produce.SolutionName, out var solution2))
if (_solutionContainerSystem.TryGetSolution(args.Used, produce.SolutionName, out var soln2, out var solution2))
{
if (_solutionSystem.TryGetSolution(uid, component.SoilSolutionName, out var solution1))
if (_solutionContainerSystem.ResolveSolution(uid, component.SoilSolutionName, ref component.SoilSolution, out var solution1))
{
// We try to fit as much of the composted plant's contained solution into the hydroponics tray as we can,
// since the plant will be consumed anyway.
var fillAmount = FixedPoint2.Min(solution2.Volume, solution1.AvailableVolume);
_solutionSystem.TryAddSolution(uid, solution1,
_solutionSystem.SplitSolution(args.Used, solution2, fillAmount));
_solutionContainerSystem.TryAddSolution(component.SoilSolution.Value, _solutionContainerSystem.SplitSolution(soln2.Value, fillAmount));
ForceUpdateByExternalCause(uid, component);
}
@ -314,9 +317,9 @@ public sealed class PlantHolderSystem : EntitySystem
}
}
private void OnInteractHand(EntityUid uid, PlantHolderComponent component, InteractHandEvent args)
private void OnInteractHand(Entity<PlantHolderComponent> entity, ref InteractHandEvent args)
{
DoHarvest(uid, args.User, component);
DoHarvest(entity, args.User, entity.Comp);
}
public void WeedInvasion()
@ -819,13 +822,13 @@ public sealed class PlantHolderSystem : EntitySystem
if (!Resolve(uid, ref component))
return;
if (!_solutionSystem.TryGetSolution(uid, component.SoilSolutionName, out var solution))
if (!_solutionContainerSystem.ResolveSolution(uid, component.SoilSolutionName, ref component.SoilSolution, out var solution))
return;
if (solution.Volume > 0 && component.MutationLevel < 25)
{
var amt = FixedPoint2.New(1);
foreach (var entry in _solutionSystem.RemoveEachReagent(uid, solution, amt))
foreach (var entry in _solutionContainerSystem.RemoveEachReagent(component.SoilSolution.Value, amt))
{
var reagentProto = _prototype.Index<ReagentPrototype>(entry.Reagent.Prototype);
reagentProto.ReactionPlant(uid, entry, solution);

View file

@ -1,7 +1,7 @@
using System.Linq;
using Content.Server.Administration;
using Content.Server.Administration;
using Content.Server.Body.Systems;
using Content.Server.Cargo.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Administration;
using Content.Shared.Body.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
@ -16,6 +16,7 @@ using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using System.Linq;
namespace Content.Server.Cargo.Systems;
@ -30,6 +31,7 @@ public sealed class PricingSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly BodySystem _bodySystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
/// <inheritdoc/>
public override void Initialize()
@ -106,13 +108,36 @@ public sealed class PricingSystem : EntitySystem
args.Price += (component.Price - partPenalty) * (_mobStateSystem.IsAlive(uid, state) ? 1.0 : component.DeathPenalty);
}
private double GetSolutionPrice(Entity<SolutionContainerManagerComponent> entity)
{
if (Comp<MetaDataComponent>(entity).EntityLifeStage < EntityLifeStage.MapInitialized)
return GetSolutionPrice(entity.Comp);
var price = 0.0;
foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions((entity.Owner, entity.Comp)))
{
var solution = soln.Comp.Solution;
foreach (var (reagent, quantity) in solution.Contents)
{
if (!_prototypeManager.TryIndex<ReagentPrototype>(reagent.Prototype, out var reagentProto))
continue;
// TODO check ReagentData for price information?
price += (float) quantity * reagentProto.PricePerUnit;
}
}
return price;
}
private double GetSolutionPrice(SolutionContainerManagerComponent component)
{
var price = 0.0;
foreach (var solution in component.Solutions.Values)
foreach (var (_, prototype) in _solutionContainerSystem.EnumerateSolutions(component))
{
foreach (var (reagent, quantity) in solution.Contents)
foreach (var (reagent, quantity) in prototype.Contents)
{
if (!_prototypeManager.TryIndex<ReagentPrototype>(reagent.Prototype, out var reagentProto))
continue;
@ -257,7 +282,7 @@ public sealed class PricingSystem : EntitySystem
if (TryComp<SolutionContainerManagerComponent>(uid, out var solComp))
{
price += GetSolutionPrice(solComp);
price += GetSolutionPrice((uid, solComp));
}
return price;

View file

@ -199,7 +199,7 @@ public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
return false;
var installedProgram = Spawn(prototype, new EntityCoordinates(loaderUid, 0, 0));
container.Insert(installedProgram);
_containerSystem.Insert(installedProgram, container);
UpdateCartridgeInstallationStatus(installedProgram, deinstallable ? InstallationStatus.Installed : InstallationStatus.Readonly);

View file

@ -0,0 +1,21 @@
using Content.Shared.CartridgeLoader.Cartridges;
using Robust.Shared.Audio;
namespace Content.Server.CartridgeLoader.Cartridges;
[RegisterComponent]
[Access(typeof(LogProbeCartridgeSystem))]
public sealed partial class LogProbeCartridgeComponent : Component
{
/// <summary>
/// The list of pulled access logs
/// </summary>
[DataField, ViewVariables]
public List<PulledAccessLog> PulledAccessLogs = new();
/// <summary>
/// The sound to make when we scan something with access
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier SoundScan = new SoundPathSpecifier("/Audio/Machines/scan_finish.ogg");
}

View file

@ -0,0 +1,71 @@
using Content.Shared.Access.Components;
using Content.Shared.Audio;
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.Popups;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
namespace Content.Server.CartridgeLoader.Cartridges;
public sealed class LogProbeCartridgeSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly CartridgeLoaderSystem? _cartridgeLoaderSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<LogProbeCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
SubscribeLocalEvent<LogProbeCartridgeComponent, CartridgeAfterInteractEvent>(AfterInteract);
}
/// <summary>
/// The <see cref="CartridgeAfterInteractEvent" /> gets relayed to this system if the cartridge loader is running
/// the LogProbe program and someone clicks on something with it. <br/>
/// <br/>
/// Updates the program's list of logs with those from the device.
/// </summary>
private void AfterInteract(Entity<LogProbeCartridgeComponent> ent, ref CartridgeAfterInteractEvent args)
{
if (args.InteractEvent.Handled || !args.InteractEvent.CanReach || args.InteractEvent.Target is not { } target)
return;
if (!TryComp(target, out AccessReaderComponent? accessReaderComponent))
return;
//Play scanning sound with slightly randomized pitch
_audioSystem.PlayEntity(ent.Comp.SoundScan, args.InteractEvent.User, target, AudioHelpers.WithVariation(0.25f, _random));
_popupSystem.PopupCursor(Loc.GetString("log-probe-scan", ("device", target)), args.InteractEvent.User);
ent.Comp.PulledAccessLogs.Clear();
foreach (var accessRecord in accessReaderComponent.AccessLog)
{
var log = new PulledAccessLog(
accessRecord.AccessTime,
accessRecord.Accessor
);
ent.Comp.PulledAccessLogs.Add(log);
}
UpdateUiState(ent, args.Loader);
}
/// <summary>
/// This gets called when the ui fragment needs to be updated for the first time after activating
/// </summary>
private void OnUiReady(Entity<LogProbeCartridgeComponent> ent, ref CartridgeUiReadyEvent args)
{
UpdateUiState(ent, args.Loader);
}
private void UpdateUiState(Entity<LogProbeCartridgeComponent> ent, EntityUid loaderUid)
{
var state = new LogProbeUiState(ent.Comp.PulledAccessLogs);
_cartridgeLoaderSystem?.UpdateCartridgeUiState(loaderUid, state);
}
}

View file

@ -1,4 +1,3 @@
using Content.Server.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.FixedPoint;
using Robust.Shared.Audio;

View file

@ -1,8 +1,6 @@
using Content.Server.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.FixedPoint;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Chemistry.Components;
@ -17,7 +15,13 @@ public sealed partial class SolutionRegenerationComponent : Component
/// The name of the solution to add to.
/// </summary>
[DataField("solution", required: true), ViewVariables(VVAccess.ReadWrite)]
public string Solution = string.Empty;
public string SolutionName = string.Empty;
/// <summary>
/// The solution to add reagents to.
/// </summary>
[DataField("solutionRef")]
public Entity<SolutionComponent>? Solution = null;
/// <summary>
/// The reagent(s) to be regenerated in the solution.

View file

@ -0,0 +1,186 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.FixedPoint;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Utility;
using System.Numerics;
namespace Content.Server.Chemistry.Containers.EntitySystems;
public sealed partial class SolutionContainerSystem : SharedSolutionContainerSystem
{
[Dependency] private readonly INetManager _netManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SolutionContainerManagerComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<SolutionContainerManagerComponent, ComponentShutdown>(OnComponentShutdown);
SubscribeLocalEvent<ContainedSolutionComponent, ComponentShutdown>(OnComponentShutdown);
}
public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name)
=> EnsureSolution(entity, name, out _);
public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name, out bool existed)
=> EnsureSolution(entity, name, FixedPoint2.Zero, out existed);
public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name, FixedPoint2 minVol, out bool existed)
=> EnsureSolution(entity, name, minVol, null, out existed);
public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name, FixedPoint2 minVol, Solution? prototype, out bool existed)
{
var (uid, meta) = entity;
if (!Resolve(uid, ref meta))
throw new InvalidOperationException("Attempted to ensure solution on invalid entity.");
var manager = EnsureComp<SolutionContainerManagerComponent>(uid);
if (meta.EntityLifeStage >= EntityLifeStage.MapInitialized)
return EnsureSolutionEntity((uid, manager), name, minVol, prototype, out existed).Comp.Solution;
else
return EnsureSolutionPrototype((uid, manager), name, minVol, prototype, out existed);
}
public Entity<SolutionComponent> EnsureSolutionEntity(Entity<SolutionContainerManagerComponent?> entity, string name, FixedPoint2 minVol, Solution? prototype, out bool existed)
{
existed = true;
var (uid, container) = entity;
var solutionSlot = ContainerSystem.EnsureContainer<ContainerSlot>(uid, $"solution@{name}", out existed);
if (!Resolve(uid, ref container, logMissing: false))
{
existed = false;
container = AddComp<SolutionContainerManagerComponent>(uid);
container.Containers.Add(name);
}
else if (!existed)
{
container.Containers.Add(name);
Dirty(uid, container);
}
var needsInit = false;
SolutionComponent solutionComp;
if (solutionSlot.ContainedEntity is not { } solutionId)
{
prototype ??= new() { MaxVolume = minVol };
prototype.Name = name;
(solutionId, solutionComp, _) = SpawnSolutionUninitialized(solutionSlot, name, minVol, prototype);
existed = false;
needsInit = true;
Dirty(uid, container);
}
else
{
solutionComp = Comp<SolutionComponent>(solutionId);
DebugTools.Assert(TryComp(solutionId, out ContainedSolutionComponent? relation) && relation.Container == uid && relation.ContainerName == name);
DebugTools.Assert(solutionComp.Solution.Name == name);
var solution = solutionComp.Solution;
solution.MaxVolume = FixedPoint2.Max(solution.MaxVolume, minVol);
// Depending on MapInitEvent order some systems can ensure solution empty solutions and conflict with the prototype solutions.
// We want the reagents from the prototype to exist even if something else already created the solution.
if (prototype is { Volume.Value: > 0 })
solution.AddSolution(prototype, PrototypeManager);
Dirty(solutionId, solutionComp);
}
if (needsInit)
EntityManager.InitializeAndStartEntity(solutionId, Transform(solutionId).MapID);
return (solutionId, solutionComp);
}
private Solution EnsureSolutionPrototype(Entity<SolutionContainerManagerComponent?> entity, string name, FixedPoint2 minVol, Solution? prototype, out bool existed)
{
existed = true;
var (uid, container) = entity;
if (!Resolve(uid, ref container, logMissing: false))
{
container = AddComp<SolutionContainerManagerComponent>(uid);
existed = false;
}
if (container.Solutions is null)
container.Solutions = new(SolutionContainerManagerComponent.DefaultCapacity);
if (!container.Solutions.TryGetValue(name, out var solution))
{
solution = prototype ?? new() { Name = name, MaxVolume = minVol };
container.Solutions.Add(name, solution);
existed = false;
}
else
solution.MaxVolume = FixedPoint2.Max(solution.MaxVolume, minVol);
Dirty(uid, container);
return solution;
}
private Entity<SolutionComponent, ContainedSolutionComponent> SpawnSolutionUninitialized(ContainerSlot container, string name, FixedPoint2 minVol, Solution prototype)
{
var coords = new EntityCoordinates(container.Owner, Vector2.Zero);
var uid = EntityManager.CreateEntityUninitialized(null, coords, null);
var solution = new SolutionComponent() { Solution = prototype };
AddComp(uid, solution);
var relation = new ContainedSolutionComponent() { Container = container.Owner, ContainerName = name };
AddComp(uid, relation);
ContainerSystem.Insert(uid, container, force: true);
return (uid, solution, relation);
}
#region Event Handlers
private void OnMapInit(Entity<SolutionContainerManagerComponent> entity, ref MapInitEvent args)
{
if (entity.Comp.Solutions is not { } prototypes)
return;
foreach (var (name, prototype) in prototypes)
{
EnsureSolutionEntity((entity.Owner, entity.Comp), name, prototype.MaxVolume, prototype, out _);
}
entity.Comp.Solutions = null;
Dirty(entity);
}
private void OnComponentShutdown(Entity<SolutionContainerManagerComponent> entity, ref ComponentShutdown args)
{
foreach (var name in entity.Comp.Containers)
{
if (ContainerSystem.TryGetContainer(entity, $"solution@{name}", out var solutionContainer))
ContainerSystem.ShutdownContainer(solutionContainer);
}
entity.Comp.Containers.Clear();
}
private void OnComponentShutdown(Entity<ContainedSolutionComponent> entity, ref ComponentShutdown args)
{
if (TryComp(entity.Comp.Container, out SolutionContainerManagerComponent? container))
{
container.Containers.Remove(entity.Comp.ContainerName);
Dirty(entity.Comp.Container, container);
}
if (ContainerSystem.TryGetContainer(entity, $"solution@{entity.Comp.ContainerName}", out var solutionContainer))
ContainerSystem.ShutdownContainer(solutionContainer);
}
#endregion Event Handlers
}

View file

@ -1,6 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Chemistry.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Labels;
using Content.Server.Popups;
using Content.Server.Storage.EntitySystems;
@ -19,6 +18,8 @@ using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Content.Server.Chemistry.EntitySystems
{
@ -47,7 +48,7 @@ namespace Content.Server.Chemistry.EntitySystems
base.Initialize();
SubscribeLocalEvent<ChemMasterComponent, ComponentStartup>(SubscribeUpdateUiState);
SubscribeLocalEvent<ChemMasterComponent, SolutionChangedEvent>(SubscribeUpdateUiState);
SubscribeLocalEvent<ChemMasterComponent, SolutionContainerChangedEvent>(SubscribeUpdateUiState);
SubscribeLocalEvent<ChemMasterComponent, EntInsertedIntoContainerMessage>(SubscribeUpdateUiState);
SubscribeLocalEvent<ChemMasterComponent, EntRemovedFromContainerMessage>(SubscribeUpdateUiState);
SubscribeLocalEvent<ChemMasterComponent, BoundUIOpenedEvent>(SubscribeUpdateUiState);
@ -67,7 +68,7 @@ namespace Content.Server.Chemistry.EntitySystems
private void UpdateUiState(Entity<ChemMasterComponent> ent, bool updateLabel = false)
{
var (owner, chemMaster) = ent;
if (!_solutionContainerSystem.TryGetSolution(owner, SharedChemMaster.BufferSolutionName, out var bufferSolution))
if (!_solutionContainerSystem.TryGetSolution(owner, SharedChemMaster.BufferSolutionName, out _, out var bufferSolution))
return;
var inputContainer = _itemSlotsSystem.GetItemOrNull(owner, SharedChemMaster.InputSlotName);
var outputContainer = _itemSlotsSystem.GetItemOrNull(owner, SharedChemMaster.OutputSlotName);
@ -130,8 +131,8 @@ namespace Content.Server.Chemistry.EntitySystems
{
var container = _itemSlotsSystem.GetItemOrNull(chemMaster, SharedChemMaster.InputSlotName);
if (container is null ||
!_solutionContainerSystem.TryGetFitsInDispenser(container.Value, out var containerSolution) ||
!_solutionContainerSystem.TryGetSolution(chemMaster, SharedChemMaster.BufferSolutionName, out var bufferSolution))
!_solutionContainerSystem.TryGetFitsInDispenser(container.Value, out var containerSoln, out var containerSolution) ||
!_solutionContainerSystem.TryGetSolution(chemMaster.Owner, SharedChemMaster.BufferSolutionName, out _, out var bufferSolution))
{
return;
}
@ -140,12 +141,12 @@ namespace Content.Server.Chemistry.EntitySystems
{
amount = FixedPoint2.Min(amount, containerSolution.AvailableVolume);
amount = bufferSolution.RemoveReagent(id, amount);
_solutionContainerSystem.TryAddReagent(container.Value, containerSolution, id, amount, out var _);
_solutionContainerSystem.TryAddReagent(containerSoln.Value, id, amount, out var _);
}
else // Container to buffer
{
amount = FixedPoint2.Min(amount, containerSolution.GetReagentQuantity(id));
_solutionContainerSystem.RemoveReagent(container.Value, containerSolution, id, amount);
_solutionContainerSystem.RemoveReagent(containerSoln.Value, id, amount);
bufferSolution.AddReagent(id, amount);
}
@ -156,7 +157,7 @@ namespace Content.Server.Chemistry.EntitySystems
{
if (fromBuffer)
{
if (_solutionContainerSystem.TryGetSolution(chemMaster, SharedChemMaster.BufferSolutionName, out var bufferSolution))
if (_solutionContainerSystem.TryGetSolution(chemMaster.Owner, SharedChemMaster.BufferSolutionName, out _, out var bufferSolution))
bufferSolution.RemoveReagent(id, amount);
else
return;
@ -165,9 +166,9 @@ namespace Content.Server.Chemistry.EntitySystems
{
var container = _itemSlotsSystem.GetItemOrNull(chemMaster, SharedChemMaster.InputSlotName);
if (container is not null &&
_solutionContainerSystem.TryGetFitsInDispenser(container.Value, out var containerSolution))
_solutionContainerSystem.TryGetFitsInDispenser(container.Value, out var containerSolution, out _))
{
_solutionContainerSystem.RemoveReagent(container.Value, containerSolution, id, amount);
_solutionContainerSystem.RemoveReagent(containerSolution.Value, id, amount);
}
else
return;
@ -210,10 +211,8 @@ namespace Content.Server.Chemistry.EntitySystems
_storageSystem.Insert(container, item, out _, user: user, storage);
_labelSystem.Label(item, message.Label);
var itemSolution = _solutionContainerSystem.EnsureSolution(item, SharedChemMaster.PillSolutionName);
_solutionContainerSystem.TryAddSolution(
item, itemSolution, withdrawal.SplitSolution(message.Dosage));
var itemSolution = _solutionContainerSystem.EnsureSolutionEntity(item, SharedChemMaster.PillSolutionName, message.Dosage, null, out _);
_solutionContainerSystem.TryAddSolution(itemSolution, withdrawal.SplitSolution(message.Dosage));
var pill = EnsureComp<PillComponent>(item);
pill.PillType = chemMaster.Comp.PillType;
@ -223,13 +222,13 @@ namespace Content.Server.Chemistry.EntitySystems
{
// Log pill creation by a user
_adminLogger.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(user.Value):user} printed {ToPrettyString(item):pill} {SolutionContainerSystem.ToPrettyString(itemSolution)}");
$"{ToPrettyString(user.Value):user} printed {ToPrettyString(item):pill} {SolutionContainerSystem.ToPrettyString(itemSolution.Comp.Solution)}");
}
else
{
// Log pill creation by magic? This should never happen... right?
_adminLogger.Add(LogType.Action, LogImpact.Low,
$"Unknown printed {ToPrettyString(item):pill} {SolutionContainerSystem.ToPrettyString(itemSolution)}");
$"Unknown printed {ToPrettyString(item):pill} {SolutionContainerSystem.ToPrettyString(itemSolution.Comp.Solution)}");
}
}
@ -242,8 +241,7 @@ namespace Content.Server.Chemistry.EntitySystems
var user = message.Session.AttachedEntity;
var maybeContainer = _itemSlotsSystem.GetItemOrNull(chemMaster, SharedChemMaster.OutputSlotName);
if (maybeContainer is not { Valid: true } container
|| !_solutionContainerSystem.TryGetSolution(
container, SharedChemMaster.BottleSolutionName, out var solution))
|| !_solutionContainerSystem.TryGetSolution(container, SharedChemMaster.BottleSolutionName, out var soln, out var solution))
{
return; // output can't fit reagents
}
@ -260,8 +258,7 @@ namespace Content.Server.Chemistry.EntitySystems
return;
_labelSystem.Label(container, message.Label);
_solutionContainerSystem.TryAddSolution(
container, solution, withdrawal);
_solutionContainerSystem.TryAddSolution(soln.Value, withdrawal);
if (user.HasValue)
{
@ -287,8 +284,7 @@ namespace Content.Server.Chemistry.EntitySystems
{
outputSolution = null;
if (!_solutionContainerSystem.TryGetSolution(
chemMaster, SharedChemMaster.BufferSolutionName, out var solution))
if (!_solutionContainerSystem.TryGetSolution(chemMaster.Owner, SharedChemMaster.BufferSolutionName, out _, out var solution))
{
return false;
}
@ -323,7 +319,7 @@ namespace Content.Server.Chemistry.EntitySystems
return null;
if (!TryComp(container, out FitsInDispenserComponent? fits)
|| !_solutionContainerSystem.TryGetSolution(container.Value, fits.Solution, out var solution))
|| !_solutionContainerSystem.TryGetSolution(container.Value, fits.Solution, out _, out var solution))
{
return null;
}
@ -339,7 +335,7 @@ namespace Content.Server.Chemistry.EntitySystems
var name = Name(container.Value);
{
if (_solutionContainerSystem.TryGetSolution(
container.Value, SharedChemMaster.BottleSolutionName, out var solution))
container.Value, SharedChemMaster.BottleSolutionName, out _, out var solution))
{
return BuildContainerInfo(name, solution);
}
@ -350,7 +346,7 @@ namespace Content.Server.Chemistry.EntitySystems
var pills = storage.Container.ContainedEntities.Select((Func<EntityUid, (string, FixedPoint2 quantity)>) (pill =>
{
_solutionContainerSystem.TryGetSolution(pill, SharedChemMaster.PillSolutionName, out var solution);
_solutionContainerSystem.TryGetSolution(pill, SharedChemMaster.PillSolutionName, out _, out var solution);
var quantity = solution?.Volume ?? FixedPoint2.Zero;
return (Name(pill), quantity);
})).ToList();

View file

@ -1,21 +1,22 @@
using Content.Server.Body.Components;
using Content.Server.Chemistry.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
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.DoAfter;
using Content.Shared.FixedPoint;
using Content.Shared.Forensics;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Robust.Shared.GameStates;
using Content.Shared.DoAfter;
using Content.Shared.Mobs.Components;
using Content.Shared.Verbs;
using Content.Shared.Stacks;
using Content.Shared.Verbs;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Content.Shared.Forensics;
namespace Content.Server.Chemistry.EntitySystems;
@ -25,11 +26,11 @@ public sealed partial class ChemistrySystem
/// <summary>
/// Default transfer amounts for the set-transfer verb.
/// </summary>
public static readonly List<int> TransferAmounts = new() {1, 5, 10, 15};
public static readonly List<int> TransferAmounts = new() { 1, 5, 10, 15 };
private void InitializeInjector()
{
SubscribeLocalEvent<InjectorComponent, GetVerbsEvent<AlternativeVerb>>(AddSetTransferVerbs);
SubscribeLocalEvent<InjectorComponent, SolutionChangedEvent>(OnSolutionChange);
SubscribeLocalEvent<InjectorComponent, SolutionContainerChangedEvent>(OnSolutionChange);
SubscribeLocalEvent<InjectorComponent, InjectorDoAfterEvent>(OnInjectDoAfter);
SubscribeLocalEvent<InjectorComponent, ComponentStartup>(OnInjectorStartup);
SubscribeLocalEvent<InjectorComponent, UseInHandEvent>(OnInjectorUse);
@ -37,7 +38,7 @@ public sealed partial class ChemistrySystem
SubscribeLocalEvent<InjectorComponent, ComponentGetState>(OnInjectorGetState);
}
private void AddSetTransferVerbs(EntityUid uid, InjectorComponent component, GetVerbsEvent<AlternativeVerb> args)
private void AddSetTransferVerbs(Entity<InjectorComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanAccess || !args.CanInteract || args.Hands == null)
return;
@ -45,11 +46,14 @@ public sealed partial class ChemistrySystem
if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
return;
var (uid, component) = entity;
// Add specific transfer verbs according to the container's size
var priority = 0;
var user = args.User;
foreach (var amount in TransferAmounts)
{
if ( amount < component.MinimumTransferAmount.Int() || amount > component.MaximumTransferAmount.Int())
if (amount < component.MinimumTransferAmount.Int() || amount > component.MaximumTransferAmount.Int())
continue;
AlternativeVerb verb = new();
@ -58,7 +62,7 @@ public sealed partial class ChemistrySystem
verb.Act = () =>
{
component.TransferAmount = FixedPoint2.New(amount);
_popup.PopupEntity(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), args.User, args.User);
_popup.PopupEntity(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), user, user);
};
// we want to sort by size, not alphabetically by the verb text.
@ -69,22 +73,22 @@ public sealed partial class ChemistrySystem
}
}
private void UseInjector(EntityUid target, EntityUid user, EntityUid injector, InjectorComponent component)
private void UseInjector(Entity<InjectorComponent> injector, EntityUid target, EntityUid user)
{
// Handle injecting/drawing for solutions
if (component.ToggleState == SharedInjectorComponent.InjectorToggleMode.Inject)
if (injector.Comp.ToggleState == SharedInjectorComponent.InjectorToggleMode.Inject)
{
if (_solutions.TryGetInjectableSolution(target, out var injectableSolution))
if (_solutionContainers.TryGetInjectableSolution(target, out var injectableSolution, out _))
{
TryInject(component, injector, target, injectableSolution, user, false);
TryInject(injector, target, injectableSolution.Value, user, false);
}
else if (_solutions.TryGetRefillableSolution(target, out var refillableSolution))
else if (_solutionContainers.TryGetRefillableSolution(target, out var refillableSolution, out _))
{
TryInject(component, injector, target, refillableSolution, user, true);
TryInject(injector, target, refillableSolution.Value, user, true);
}
else if (TryComp<BloodstreamComponent>(target, out var bloodstream))
{
TryInjectIntoBloodstream(component, injector, target, bloodstream, user);
TryInjectIntoBloodstream(injector, (target, bloodstream), user);
}
else
{
@ -92,111 +96,112 @@ public sealed partial class ChemistrySystem
("target", Identity.Entity(target, EntityManager))), injector, user);
}
}
else if (component.ToggleState == SharedInjectorComponent.InjectorToggleMode.Draw)
else if (injector.Comp.ToggleState == SharedInjectorComponent.InjectorToggleMode.Draw)
{
// Draw from a bloodstream, if the target has that
if (TryComp<BloodstreamComponent>(target, out var stream))
if (TryComp<BloodstreamComponent>(target, out var stream) &&
_solutionContainers.ResolveSolution(target, stream.BloodSolutionName, ref stream.BloodSolution))
{
TryDraw(component, injector, target, stream.BloodSolution, user, stream);
TryDraw(injector, (target, stream), stream.BloodSolution.Value, user);
return;
}
// Draw from an object (food, beaker, etc)
if (_solutions.TryGetDrawableSolution(target, out var drawableSolution))
if (_solutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
{
TryDraw(component, injector, target, drawableSolution, user);
TryDraw(injector, target, drawableSolution.Value, user);
}
else
{
_popup.PopupEntity(Loc.GetString("injector-component-cannot-draw-message",
("target", Identity.Entity(target, EntityManager))), injector, user);
("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
}
}
}
private void OnSolutionChange(EntityUid uid, InjectorComponent component, SolutionChangedEvent args)
private void OnSolutionChange(Entity<InjectorComponent> entity, ref SolutionContainerChangedEvent args)
{
Dirty(component);
Dirty(entity);
}
private void OnInjectorGetState(EntityUid uid, InjectorComponent component, ref ComponentGetState args)
private void OnInjectorGetState(Entity<InjectorComponent> entity, ref ComponentGetState args)
{
_solutions.TryGetSolution(uid, InjectorComponent.SolutionName, out var solution);
_solutionContainers.TryGetSolution(entity.Owner, InjectorComponent.SolutionName, out _, out var solution);
var currentVolume = solution?.Volume ?? FixedPoint2.Zero;
var maxVolume = solution?.MaxVolume ?? FixedPoint2.Zero;
args.State = new SharedInjectorComponent.InjectorComponentState(currentVolume, maxVolume, component.ToggleState);
args.State = new SharedInjectorComponent.InjectorComponentState(currentVolume, maxVolume, entity.Comp.ToggleState);
}
private void OnInjectDoAfter(EntityUid uid, InjectorComponent component, DoAfterEvent args)
private void OnInjectDoAfter(Entity<InjectorComponent> entity, ref InjectorDoAfterEvent args)
{
if (args.Cancelled || args.Handled || args.Args.Target == null)
return;
UseInjector(args.Args.Target.Value, args.Args.User, uid, component);
UseInjector(entity, args.Args.Target.Value, args.Args.User);
args.Handled = true;
}
private void OnInjectorAfterInteract(EntityUid uid, InjectorComponent component, AfterInteractEvent args)
private void OnInjectorAfterInteract(Entity<InjectorComponent> entity, ref AfterInteractEvent args)
{
if (args.Handled || !args.CanReach)
return;
//Make sure we have the attacking entity
if (args.Target is not { Valid: true } target || !HasComp<SolutionContainerManagerComponent>(uid))
if (args.Target is not { Valid: true } target || !HasComp<SolutionContainerManagerComponent>(entity))
return;
// Is the target a mob? If yes, use a do-after to give them time to respond.
if (HasComp<MobStateComponent>(target) || HasComp<BloodstreamComponent>(target))
{
// Are use using an injector capible of targeting a mob?
if (component.IgnoreMobs)
if (entity.Comp.IgnoreMobs)
return;
InjectDoAfter(component, args.User, target, uid);
InjectDoAfter(entity, target, args.User);
args.Handled = true;
return;
}
UseInjector(target, args.User, uid, component);
UseInjector(entity, target, args.User);
args.Handled = true;
}
private void OnInjectorStartup(EntityUid uid, InjectorComponent component, ComponentStartup args)
private void OnInjectorStartup(Entity<InjectorComponent> entity, ref ComponentStartup args)
{
// ???? why ?????
Dirty(component);
Dirty(entity);
}
private void OnInjectorUse(EntityUid uid, InjectorComponent component, UseInHandEvent args)
private void OnInjectorUse(Entity<InjectorComponent> entity, ref UseInHandEvent args)
{
if (args.Handled)
return;
Toggle(component, args.User, uid);
Toggle(entity, args.User);
args.Handled = true;
}
/// <summary>
/// Toggle between draw/inject state if applicable
/// </summary>
private void Toggle(InjectorComponent component, EntityUid user, EntityUid injector)
private void Toggle(Entity<InjectorComponent> injector, EntityUid user)
{
if (component.InjectOnly)
if (injector.Comp.InjectOnly)
{
return;
}
string msg;
switch (component.ToggleState)
switch (injector.Comp.ToggleState)
{
case SharedInjectorComponent.InjectorToggleMode.Inject:
component.ToggleState = SharedInjectorComponent.InjectorToggleMode.Draw;
injector.Comp.ToggleState = SharedInjectorComponent.InjectorToggleMode.Draw;
msg = "injector-component-drawing-text";
break;
case SharedInjectorComponent.InjectorToggleMode.Draw:
component.ToggleState = SharedInjectorComponent.InjectorToggleMode.Inject;
injector.Comp.ToggleState = SharedInjectorComponent.InjectorToggleMode.Inject;
msg = "injector-component-injecting-text";
break;
default:
@ -209,18 +214,18 @@ public sealed partial class ChemistrySystem
/// <summary>
/// Send informative pop-up messages and wait for a do-after to complete.
/// </summary>
private void InjectDoAfter(InjectorComponent component, EntityUid user, EntityUid target, EntityUid injector)
private void InjectDoAfter(Entity<InjectorComponent> injector, EntityUid target, EntityUid user)
{
// Create a pop-up for the user
_popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, user);
if (!_solutions.TryGetSolution(injector, InjectorComponent.SolutionName, out var solution))
if (!_solutionContainers.TryGetSolution(injector.Owner, InjectorComponent.SolutionName, out _, out var solution))
return;
var actualDelay = MathF.Max(component.Delay, 1f);
var actualDelay = MathF.Max(injector.Comp.Delay, 1f);
// Injections take 0.5 seconds longer per additional 5u
actualDelay += (float) component.TransferAmount / component.Delay - 0.5f;
actualDelay += (float) injector.Comp.TransferAmount / injector.Comp.Delay - 0.5f;
var isTarget = user != target;
@ -244,7 +249,7 @@ public sealed partial class ChemistrySystem
}
// Add an admin log, using the "force feed" log type. It's not quite feeding, but the effect is the same.
if (component.ToggleState == SharedInjectorComponent.InjectorToggleMode.Inject)
if (injector.Comp.ToggleState == SharedInjectorComponent.InjectorToggleMode.Inject)
{
_adminLogger.Add(LogType.ForceFeed,
$"{EntityManager.ToPrettyString(user):user} is attempting to inject {EntityManager.ToPrettyString(target):target} with a solution {SolutionContainerSystem.ToPrettyString(solution):solution}");
@ -255,11 +260,11 @@ public sealed partial class ChemistrySystem
// Self-injections take half as long.
actualDelay /= 2;
if (component.ToggleState == SharedInjectorComponent.InjectorToggleMode.Inject)
if (injector.Comp.ToggleState == SharedInjectorComponent.InjectorToggleMode.Inject)
_adminLogger.Add(LogType.Ingestion, $"{EntityManager.ToPrettyString(user):user} is attempting to inject themselves with a solution {SolutionContainerSystem.ToPrettyString(solution):solution}.");
}
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, actualDelay, new InjectorDoAfterEvent(), injector, target: target, used: injector)
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, actualDelay, new InjectorDoAfterEvent(), injector.Owner, target: target, used: injector.Owner)
{
BreakOnUserMove = true,
BreakOnDamage = true,
@ -268,77 +273,80 @@ public sealed partial class ChemistrySystem
});
}
private void TryInjectIntoBloodstream(InjectorComponent component, EntityUid injector, EntityUid target, BloodstreamComponent targetBloodstream, EntityUid user)
private void TryInjectIntoBloodstream(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target, EntityUid user)
{
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = FixedPoint2.Min(component.TransferAmount, targetBloodstream.ChemicalSolution.AvailableVolume);
if (!_solutionContainers.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName, ref target.Comp.ChemicalSolution, out var chemSolution))
{
_popup.PopupEntity(Loc.GetString("injector-component-cannot-inject-message", ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
return;
}
var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, chemSolution.AvailableVolume);
if (realTransferAmount <= 0)
{
_popup.PopupEntity(Loc.GetString("injector-component-cannot-inject-message", ("target", Identity.Entity(target, EntityManager))), injector, user);
_popup.PopupEntity(Loc.GetString("injector-component-cannot-inject-message", ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
return;
}
// Move units from attackSolution to targetSolution
var removedSolution = _solutions.SplitSolution(user, targetBloodstream.ChemicalSolution, realTransferAmount);
var removedSolution = _solutionContainers.SplitSolution(target.Comp.ChemicalSolution.Value, realTransferAmount);
_blood.TryAddToChemicals(target, removedSolution, targetBloodstream);
_blood.TryAddToChemicals(target, removedSolution, target.Comp);
_reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection);
_popup.PopupEntity(Loc.GetString("injector-component-inject-success-message",
("amount", removedSolution.Volume),
("target", Identity.Entity(target, EntityManager))), injector, user);
("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
Dirty(component);
AfterInject(component, injector, target);
Dirty(injector);
AfterInject(injector, target);
}
private void TryInject(InjectorComponent component, EntityUid injector, EntityUid targetEntity, Solution targetSolution, EntityUid user, bool asRefill)
private void TryInject(Entity<InjectorComponent> injector, EntityUid targetEntity, Entity<SolutionComponent> targetSolution, EntityUid user, bool asRefill)
{
if (!_solutions.TryGetSolution(injector, InjectorComponent.SolutionName, out var solution)
|| solution.Volume == 0)
if (!_solutionContainers.TryGetSolution(injector.Owner, InjectorComponent.SolutionName, out var soln, out var solution) || solution.Volume == 0)
return;
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = FixedPoint2.Min(component.TransferAmount, targetSolution.AvailableVolume);
var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, targetSolution.Comp.Solution.AvailableVolume);
if (realTransferAmount <= 0)
{
_popup.PopupEntity(Loc.GetString("injector-component-target-already-full-message", ("target", Identity.Entity(targetEntity, EntityManager))),
injector, user);
injector.Owner, user);
return;
}
// Move units from attackSolution to targetSolution
Solution removedSolution;
if (TryComp<StackComponent>(targetEntity, out var stack))
removedSolution = _solutions.SplitStackSolution(injector, solution, realTransferAmount, stack.Count);
removedSolution = _solutionContainers.SplitStackSolution(soln.Value, realTransferAmount, stack.Count);
else
removedSolution = _solutions.SplitSolution(injector, solution, realTransferAmount);
removedSolution = _solutionContainers.SplitSolution(soln.Value, realTransferAmount);
_reactiveSystem.DoEntityReaction(targetEntity, removedSolution, ReactionMethod.Injection);
if (!asRefill)
_solutions.Inject(targetEntity, targetSolution, removedSolution);
_solutionContainers.Inject(targetEntity, targetSolution, removedSolution);
else
_solutions.Refill(targetEntity, targetSolution, removedSolution);
_solutionContainers.Refill(targetEntity, targetSolution, removedSolution);
_popup.PopupEntity(Loc.GetString("injector-component-transfer-success-message",
("amount", removedSolution.Volume),
("target", Identity.Entity(targetEntity, EntityManager))), injector, user);
("target", Identity.Entity(targetEntity, EntityManager))), injector.Owner, user);
Dirty(component);
AfterInject(component, injector, targetEntity);
Dirty(injector);
AfterInject(injector, targetEntity);
}
private void AfterInject(InjectorComponent component, EntityUid injector, EntityUid target)
private void AfterInject(Entity<InjectorComponent> injector, EntityUid target)
{
// Automatically set syringe to draw after completely draining it.
if (_solutions.TryGetSolution(injector, InjectorComponent.SolutionName, out var solution)
&& solution.Volume == 0)
if (_solutionContainers.TryGetSolution(injector.Owner, InjectorComponent.SolutionName, out _, out var solution) && solution.Volume == 0)
{
component.ToggleState = SharedInjectorComponent.InjectorToggleMode.Draw;
injector.Comp.ToggleState = SharedInjectorComponent.InjectorToggleMode.Draw;
}
// Leave some DNA from the injectee on it
@ -346,13 +354,12 @@ public sealed partial class ChemistrySystem
RaiseLocalEvent(target, ref ev);
}
private void AfterDraw(InjectorComponent component, EntityUid injector, EntityUid target)
private void AfterDraw(Entity<InjectorComponent> injector, EntityUid target)
{
// Automatically set syringe to inject after completely filling it.
if (_solutions.TryGetSolution(injector, InjectorComponent.SolutionName, out var solution)
&& solution.AvailableVolume == 0)
if (_solutionContainers.TryGetSolution(injector.Owner, InjectorComponent.SolutionName, out _, out var solution) && solution.AvailableVolume == 0)
{
component.ToggleState = SharedInjectorComponent.InjectorToggleMode.Inject;
injector.Comp.ToggleState = SharedInjectorComponent.InjectorToggleMode.Inject;
}
// Leave some DNA from the drawee on it
@ -360,70 +367,68 @@ public sealed partial class ChemistrySystem
RaiseLocalEvent(target, ref ev);
}
private void TryDraw(InjectorComponent component, EntityUid injector, EntityUid targetEntity, Solution targetSolution, EntityUid user, BloodstreamComponent? stream = null)
private void TryDraw(Entity<InjectorComponent> injector, Entity<BloodstreamComponent?> target, Entity<SolutionComponent> targetSolution, EntityUid user)
{
if (!_solutions.TryGetSolution(injector, InjectorComponent.SolutionName, out var solution)
|| solution.AvailableVolume == 0)
if (!_solutionContainers.TryGetSolution(injector.Owner, InjectorComponent.SolutionName, out var soln, out var solution) || solution.AvailableVolume == 0)
{
return;
}
// 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(component.TransferAmount, targetSolution.Volume, solution.AvailableVolume);
var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, targetSolution.Comp.Solution.Volume, solution.AvailableVolume);
if (realTransferAmount <= 0)
{
_popup.PopupEntity(Loc.GetString("injector-component-target-is-empty-message", ("target", Identity.Entity(targetEntity, EntityManager))),
injector, user);
_popup.PopupEntity(Loc.GetString("injector-component-target-is-empty-message", ("target", Identity.Entity(target, EntityManager))),
injector.Owner, user);
return;
}
// We have some snowflaked behavior for streams.
if (stream != null)
if (target.Comp != null)
{
DrawFromBlood(user, injector, targetEntity, component, solution, stream, realTransferAmount);
DrawFromBlood(injector, (target.Owner, target.Comp), soln.Value, realTransferAmount, user);
return;
}
// Move units from attackSolution to targetSolution
var removedSolution = _solutions.Draw(targetEntity, targetSolution, realTransferAmount);
var removedSolution = _solutionContainers.Draw(target.Owner, targetSolution, realTransferAmount);
if (!_solutions.TryAddSolution(injector, solution, removedSolution))
if (!_solutionContainers.TryAddSolution(soln.Value, removedSolution))
{
return;
}
_popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
("amount", removedSolution.Volume),
("target", Identity.Entity(targetEntity, EntityManager))), injector, user);
("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
Dirty(component);
AfterDraw(component, injector, targetEntity);
Dirty(injector);
AfterDraw(injector, target);
}
private void DrawFromBlood(EntityUid user, EntityUid injector, EntityUid target, InjectorComponent component, Solution injectorSolution, BloodstreamComponent stream, FixedPoint2 transferAmount)
private void DrawFromBlood(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target, Entity<SolutionComponent> injectorSolution, FixedPoint2 transferAmount, EntityUid user)
{
var drawAmount = (float) transferAmount;
var bloodAmount = drawAmount;
var chemAmount = 0f;
if (stream.ChemicalSolution.Volume > 0f) // If they have stuff in their chem stream, we'll draw some of that
if (_solutionContainers.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName, ref target.Comp.ChemicalSolution))
{
bloodAmount = drawAmount * 0.85f;
chemAmount = drawAmount * 0.15f;
var chemTemp = _solutionContainers.SplitSolution(target.Comp.ChemicalSolution.Value, drawAmount * 0.15f);
_solutionContainers.TryAddSolution(injectorSolution, chemTemp);
drawAmount -= (float) chemTemp.Volume;
}
var bloodTemp = stream.BloodSolution.SplitSolution(bloodAmount);
var chemTemp = stream.ChemicalSolution.SplitSolution(chemAmount);
_solutions.TryAddSolution(injector, injectorSolution, bloodTemp);
_solutions.TryAddSolution(injector, injectorSolution, chemTemp);
if (_solutionContainers.ResolveSolution(target.Owner, target.Comp.BloodSolutionName, ref target.Comp.BloodSolution))
{
var bloodTemp = _solutionContainers.SplitSolution(target.Comp.BloodSolution.Value, drawAmount);
_solutionContainers.TryAddSolution(injectorSolution, bloodTemp);
}
_popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
("amount", transferAmount),
("target", Identity.Entity(target, EntityManager))), injector, user);
("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
Dirty(component);
AfterDraw(component, injector, target);
Dirty(injector);
AfterDraw(injector, target);
}
}

View file

@ -1,13 +1,12 @@
using Content.Server.Administration.Logs;
using Content.Server.Body.Systems;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Interaction;
using Content.Server.Popups;
using Content.Shared.CombatMode;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.CombatMode;
using Content.Shared.DoAfter;
using Content.Shared.Mobs.Systems;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Chemistry.EntitySystems;
@ -24,7 +23,7 @@ public sealed partial class ChemistrySystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
[Dependency] private readonly SolutionContainerSystem _solutions = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainers = default!;
public override void Initialize()
{

View file

@ -1,20 +1,21 @@
using System.Linq;
using System.Diagnostics.CodeAnalysis;
using Content.Server.Chemistry.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
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.FixedPoint;
using Content.Shared.Forensics;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Mobs.Components;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Timing;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.GameStates;
using Content.Shared.Forensics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Content.Server.Chemistry.EntitySystems
{
@ -26,33 +27,33 @@ namespace Content.Server.Chemistry.EntitySystems
{
SubscribeLocalEvent<HyposprayComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<HyposprayComponent, MeleeHitEvent>(OnAttack);
SubscribeLocalEvent<HyposprayComponent, SolutionChangedEvent>(OnSolutionChange);
SubscribeLocalEvent<HyposprayComponent, SolutionContainerChangedEvent>(OnSolutionChange);
SubscribeLocalEvent<HyposprayComponent, UseInHandEvent>(OnUseInHand);
SubscribeLocalEvent<HyposprayComponent, ComponentGetState>(OnHypoGetState);
}
private void OnHypoGetState(EntityUid uid, HyposprayComponent component, ref ComponentGetState args)
private void OnHypoGetState(Entity<HyposprayComponent> entity, ref ComponentGetState args)
{
args.State = _solutions.TryGetSolution(uid, component.SolutionName, out var solution)
args.State = _solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out _, out var solution)
? new HyposprayComponentState(solution.Volume, solution.MaxVolume)
: new HyposprayComponentState(FixedPoint2.Zero, FixedPoint2.Zero);
}
private void OnUseInHand(EntityUid uid, HyposprayComponent component, UseInHandEvent args)
private void OnUseInHand(Entity<HyposprayComponent> entity, ref UseInHandEvent args)
{
if (args.Handled)
return;
TryDoInject(uid, args.User, args.User);
TryDoInject(entity, args.User, args.User);
args.Handled = true;
}
private void OnSolutionChange(EntityUid uid, HyposprayComponent component, SolutionChangedEvent args)
private void OnSolutionChange(Entity<HyposprayComponent> entity, ref SolutionContainerChangedEvent args)
{
Dirty(component);
Dirty(entity);
}
public void OnAfterInteract(EntityUid uid, HyposprayComponent component, AfterInteractEvent args)
public void OnAfterInteract(Entity<HyposprayComponent> entity, ref AfterInteractEvent args)
{
if (!args.CanReach)
return;
@ -60,21 +61,20 @@ namespace Content.Server.Chemistry.EntitySystems
var target = args.Target;
var user = args.User;
TryDoInject(uid, target, user);
TryDoInject(entity, target, user);
}
public void OnAttack(EntityUid uid, HyposprayComponent component, MeleeHitEvent args)
public void OnAttack(Entity<HyposprayComponent> entity, ref MeleeHitEvent args)
{
if (!args.HitEntities.Any())
return;
TryDoInject(uid, args.HitEntities.First(), args.User);
TryDoInject(entity, args.HitEntities.First(), args.User);
}
public bool TryDoInject(EntityUid uid, EntityUid? target, EntityUid user, HyposprayComponent? component=null)
public bool TryDoInject(Entity<HyposprayComponent> hypo, EntityUid? target, EntityUid user)
{
if (!Resolve(uid, ref component))
return false;
var (uid, component) = hypo;
if (!EligibleEntity(target, _entMan, component))
return false;
@ -92,15 +92,13 @@ namespace Content.Server.Chemistry.EntitySystems
target = user;
}
_solutions.TryGetSolution(uid, component.SolutionName, out var hypoSpraySolution);
if (hypoSpraySolution == null || hypoSpraySolution.Volume == 0)
if (!_solutionContainers.TryGetSolution(uid, component.SolutionName, out var hypoSpraySoln, out var hypoSpraySolution) || hypoSpraySolution.Volume == 0)
{
_popup.PopupCursor(Loc.GetString("hypospray-component-empty-message"), user);
return true;
}
if (!_solutions.TryGetInjectableSolution(target.Value, out var targetSolution))
if (!_solutionContainers.TryGetInjectableSolution(target.Value, out var targetSoln, out var targetSolution))
{
_popup.PopupCursor(Loc.GetString("hypospray-cant-inject", ("target", Identity.Entity(target.Value, _entMan))), user);
return false;
@ -127,17 +125,17 @@ namespace Content.Server.Chemistry.EntitySystems
if (realTransferAmount <= 0)
{
_popup.PopupCursor(Loc.GetString("hypospray-component-transfer-already-full-message",("owner", target)), user);
_popup.PopupCursor(Loc.GetString("hypospray-component-transfer-already-full-message", ("owner", target)), user);
return true;
}
// Move units from attackSolution to targetSolution
var removedSolution = _solutions.SplitSolution(uid, hypoSpraySolution, realTransferAmount);
var removedSolution = _solutionContainers.SplitSolution(hypoSpraySoln.Value, realTransferAmount);
if (!targetSolution.CanAddSolution(removedSolution))
return true;
_reactiveSystem.DoEntityReaction(target.Value, removedSolution, ReactionMethod.Injection);
_solutions.TryAddSolution(target.Value, targetSolution, removedSolution);
_solutionContainers.TryAddSolution(targetSoln.Value, removedSolution);
var ev = new TransferDnaEvent { Donor = target.Value, Recipient = uid };
RaiseLocalEvent(target.Value, ref ev);

View file

@ -1,10 +1,6 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Robust.Shared.Player;
namespace Content.Server.Chemistry.EntitySystems;
@ -15,27 +11,26 @@ public sealed partial class ChemistrySystem
SubscribeLocalEvent<ReactionMixerComponent, AfterInteractEvent>(OnAfterInteract);
}
private void OnAfterInteract(EntityUid uid, ReactionMixerComponent component, AfterInteractEvent args)
private void OnAfterInteract(Entity<ReactionMixerComponent> entity, ref AfterInteractEvent args)
{
if (!args.Target.HasValue || !args.CanReach)
return;
var mixAttemptEvent = new MixingAttemptEvent(uid);
RaiseLocalEvent(uid, ref mixAttemptEvent);
if(mixAttemptEvent.Cancelled)
var mixAttemptEvent = new MixingAttemptEvent(entity);
RaiseLocalEvent(entity, ref mixAttemptEvent);
if (mixAttemptEvent.Cancelled)
{
return;
}
Solution? solution = null;
if (!_solutions.TryGetMixableSolution(args.Target.Value, out solution))
return;
if (!_solutionContainers.TryGetMixableSolution(args.Target.Value, out var solution))
return;
_popup.PopupEntity(Loc.GetString(component.MixMessage, ("mixed", Identity.Entity(args.Target.Value, EntityManager)), ("mixer", Identity.Entity(uid, EntityManager))), args.User, args.User);
_popup.PopupEntity(Loc.GetString(entity.Comp.MixMessage, ("mixed", Identity.Entity(args.Target.Value, EntityManager)), ("mixer", Identity.Entity(entity.Owner, EntityManager))), args.User, args.User);
_solutions.UpdateChemicals(args.Target.Value, solution, true, component);
_solutionContainers.UpdateChemicals(solution.Value, true, entity.Comp);
var afterMixingEvent = new AfterMixingEvent(uid, args.Target.Value);
RaiseLocalEvent(uid, afterMixingEvent);
var afterMixingEvent = new AfterMixingEvent(entity, args.Target.Value);
RaiseLocalEvent(entity, afterMixingEvent);
}
}

Some files were not shown because too many files have changed in this diff Show more