diff --git a/Content.Client/Clickable/ClickableComponent.cs b/Content.Client/Clickable/ClickableComponent.cs
index 66ecb02e0c..3f4f7b511b 100644
--- a/Content.Client/Clickable/ClickableComponent.cs
+++ b/Content.Client/Clickable/ClickableComponent.cs
@@ -26,7 +26,7 @@ namespace Content.Client.Clickable
/// True if the click worked, false otherwise.
public bool CheckClick(Vector2 worldPos, out int drawDepth, out uint renderOrder)
{
- if (!_entMan.TryGetComponent(Owner, out ISpriteComponent? sprite) || !sprite.Visible)
+ if (!_entMan.TryGetComponent(Owner, out SpriteComponent? sprite) || !sprite.Visible)
{
drawDepth = default;
renderOrder = default;
@@ -54,8 +54,10 @@ namespace Content.Client.Clickable
// This should have been the rotation of the sprite relative to the screen, but this is not the case with no-rot or directional sprites.
var relativeRotation = worldRot + _eyeManager.CurrentEye.Rotation;
+ Angle cardinalSnapping = sprite.SnapCardinals ? relativeRotation.GetCardinalDir().ToAngle() : Angle.Zero;
+
// First we get `localPos`, the clicked location in the sprite-coordinate frame.
- var entityXform = Matrix3.CreateInverseTransform(transform.WorldPosition, sprite.NoRotation ? -_eyeManager.CurrentEye.Rotation : worldRot);
+ var entityXform = Matrix3.CreateInverseTransform(transform.WorldPosition, sprite.NoRotation ? -_eyeManager.CurrentEye.Rotation : worldRot - cardinalSnapping);
var localPos = invSpriteMatrix.Transform(entityXform.Transform(worldPos));
// Check explicitly defined click-able bounds
diff --git a/Content.Client/CloningConsole/UI/CloningConsoleBoundUserInterface.cs b/Content.Client/CloningConsole/UI/CloningConsoleBoundUserInterface.cs
index 8ab2c34627..51ee9d7b4e 100644
--- a/Content.Client/CloningConsole/UI/CloningConsoleBoundUserInterface.cs
+++ b/Content.Client/CloningConsole/UI/CloningConsoleBoundUserInterface.cs
@@ -22,7 +22,6 @@ namespace Content.Client.CloningConsole.UI
};
_window.OnClose += Close;
_window.CloneButton.OnPressed += _ => SendMessage(new UiButtonPressedMessage(UiButton.Clone));
- _window.EjectButton.OnPressed += _ => SendMessage(new UiButtonPressedMessage(UiButton.Eject));
_window.OpenCentered();
}
@@ -43,7 +42,6 @@ namespace Content.Client.CloningConsole.UI
{
_window.OnClose -= Close;
_window.CloneButton.OnPressed -= _ => SendMessage(new UiButtonPressedMessage(UiButton.Clone));
- _window.EjectButton.OnPressed -= _ => SendMessage(new UiButtonPressedMessage(UiButton.Eject));
}
_window?.Dispose();
}
diff --git a/Content.Client/CloningConsole/UI/CloningConsoleWindow.xaml b/Content.Client/CloningConsole/UI/CloningConsoleWindow.xaml
index 7a1886a5af..958bc83d11 100644
--- a/Content.Client/CloningConsole/UI/CloningConsoleWindow.xaml
+++ b/Content.Client/CloningConsole/UI/CloningConsoleWindow.xaml
@@ -46,11 +46,6 @@
-
diff --git a/Content.Client/Decals/DecalOverlay.cs b/Content.Client/Decals/DecalOverlay.cs
index d786885e5c..43f441d8ae 100644
--- a/Content.Client/Decals/DecalOverlay.cs
+++ b/Content.Client/Decals/DecalOverlay.cs
@@ -11,7 +11,6 @@ namespace Content.Client.Decals
public sealed class DecalOverlay : Overlay
{
private readonly DecalSystem _decals;
- private readonly SharedTransformSystem _transform;
private readonly SpriteSystem _sprites;
private readonly IEntityManager _entManager;
private readonly IPrototypeManager _prototypeManager;
@@ -22,13 +21,11 @@ namespace Content.Client.Decals
public DecalOverlay(
DecalSystem decals,
- SharedTransformSystem transforms,
SpriteSystem sprites,
IEntityManager entManager,
IPrototypeManager prototypeManager)
{
_decals = decals;
- _transform = transforms;
_sprites = sprites;
_entManager = entManager;
_prototypeManager = prototypeManager;
@@ -39,10 +36,12 @@ namespace Content.Client.Decals
// Shouldn't need to clear cached textures unless the prototypes get reloaded.
var handle = args.WorldHandle;
var xformQuery = _entManager.GetEntityQuery();
+ var eyeAngle = args.Viewport.Eye?.Rotation ?? Angle.Zero;
foreach (var (gridId, zIndexDictionary) in _decals.DecalRenderIndex)
{
- if (zIndexDictionary.Count == 0) continue;
+ if (zIndexDictionary.Count == 0)
+ continue;
if (!xformQuery.TryGetComponent(gridId, out var xform))
{
@@ -53,7 +52,9 @@ namespace Content.Client.Decals
if (xform.MapID != args.MapId)
continue;
- handle.SetTransform(_transform.GetWorldMatrix(xform, xformQuery));
+ var (_, worldRot, worldMatrix) = xform.GetWorldPositionRotationMatrix(xformQuery);
+
+ handle.SetTransform(worldMatrix);
foreach (var (_, decals) in zIndexDictionary)
{
@@ -66,10 +67,23 @@ namespace Content.Client.Decals
_cachedTextures[decal.Id] = texture;
}
- if (decal.Angle.Equals(Angle.Zero))
+ if (!_prototypeManager.TryIndex(decal.Id, out var decalProto))
+ continue;
+
+ var cardinal = Angle.Zero;
+
+ if (decalProto.SnapCardinals)
+ {
+ var worldAngle = eyeAngle + worldRot;
+ cardinal = worldAngle.GetCardinalDir().ToAngle();
+ }
+
+ var angle = decal.Angle - cardinal;
+
+ if (angle.Equals(Angle.Zero))
handle.DrawTexture(texture, decal.Coordinates, decal.Color);
else
- handle.DrawTexture(texture, decal.Coordinates, decal.Angle, decal.Color);
+ handle.DrawTexture(texture, decal.Coordinates, angle, decal.Color);
}
}
}
@@ -81,11 +95,9 @@ namespace Content.Client.Decals
{
if (_prototypeManager.TryIndex(id, out var proto))
return proto.Sprite;
- else
- {
- Logger.Error($"Unknown decal prototype: {id}");
- return new SpriteSpecifier.Texture(new ResourcePath("/Textures/noSprite.png"));
- }
+
+ Logger.Error($"Unknown decal prototype: {id}");
+ return new SpriteSpecifier.Texture(new ResourcePath("/Textures/noSprite.png"));
}
}
}
diff --git a/Content.Client/Decals/DecalSystem.cs b/Content.Client/Decals/DecalSystem.cs
index 47d5399761..72ffd12179 100644
--- a/Content.Client/Decals/DecalSystem.cs
+++ b/Content.Client/Decals/DecalSystem.cs
@@ -8,18 +8,17 @@ namespace Content.Client.Decals
public sealed class DecalSystem : SharedDecalSystem
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
- [Dependency] private readonly SharedTransformSystem _transforms = default!;
[Dependency] private readonly SpriteSystem _sprites = default!;
private DecalOverlay _overlay = default!;
- public Dictionary>> DecalRenderIndex = new();
- private Dictionary> _decalZIndexIndex = new();
+ public readonly Dictionary>> DecalRenderIndex = new();
+ private readonly Dictionary> _decalZIndexIndex = new();
public override void Initialize()
{
base.Initialize();
- _overlay = new DecalOverlay(this, _transforms, _sprites, EntityManager, PrototypeManager);
+ _overlay = new DecalOverlay(this, _sprites, EntityManager, PrototypeManager);
_overlayManager.AddOverlay(_overlay);
SubscribeNetworkEvent(OnChunkUpdate);
@@ -108,7 +107,7 @@ namespace Content.Client.Decals
chunkCollection[indices] = newChunkData;
foreach (var (uid, decal) in newChunkData)
- {
+ {
if (zIndexIndex.TryGetValue(uid, out var zIndex))
renderIndex[zIndex].Remove(uid);
diff --git a/Content.Client/EscapeMenu/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/EscapeMenu/UI/Tabs/KeyRebindTab.xaml.cs
index d1045b6cce..d983f09024 100644
--- a/Content.Client/EscapeMenu/UI/Tabs/KeyRebindTab.xaml.cs
+++ b/Content.Client/EscapeMenu/UI/Tabs/KeyRebindTab.xaml.cs
@@ -105,6 +105,11 @@ namespace Content.Client.EscapeMenu.UI.Tabs
AddButton(EngineKeyFunctions.MoveRight);
AddButton(EngineKeyFunctions.Walk);
+ AddHeader("ui-options-header-camera");
+ AddButton(EngineKeyFunctions.CameraRotateLeft);
+ AddButton(EngineKeyFunctions.CameraRotateRight);
+ AddButton(EngineKeyFunctions.CameraReset);
+
AddHeader("ui-options-header-interaction-basic");
AddButton(EngineKeyFunctions.Use);
AddButton(ContentKeyFunctions.UseItemInHand);
diff --git a/Content.Client/Eui/EuiManager.cs b/Content.Client/Eui/EuiManager.cs
index 43e57c8bd3..001493c537 100644
--- a/Content.Client/Eui/EuiManager.cs
+++ b/Content.Client/Eui/EuiManager.cs
@@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
using Content.Shared.Eui;
+using Robust.Client.GameStates;
+using Robust.Client.State;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Reflection;
+using Robust.Shared.Utility;
namespace Content.Client.Eui
{
@@ -20,6 +23,16 @@ namespace Content.Client.Eui
_net.RegisterNetMessage(RxMsgCtl);
_net.RegisterNetMessage(RxMsgState);
_net.RegisterNetMessage(RxMsgMessage);
+ _net.Disconnect += NetOnDisconnect;
+ }
+
+ private void NetOnDisconnect(object? sender, NetDisconnectedArgs e)
+ {
+ foreach (var openUi in _openUis)
+ {
+ openUi.Value.Eui.Closed();
+ }
+ _openUis.Clear();
}
private void RxMsgMessage(MsgEuiMessage message)
@@ -36,26 +49,22 @@ namespace Content.Client.Eui
private void RxMsgCtl(MsgEuiCtl message)
{
- switch (message.Type)
+ // Will always close the window first when getting a control message
+ if (_openUis.TryGetValue(message.Id, out var openEui))
{
- case MsgEuiCtl.CtlType.Open:
- var euiType = _refl.LooseGetType(message.OpenType);
- var instance = _dtf.CreateInstance(euiType);
- instance.Initialize(this, message.Id);
- instance.Opened();
- _openUis.Add(message.Id, new EuiData(instance));
- break;
-
- case MsgEuiCtl.CtlType.Close:
- var dat = _openUis[message.Id];
- dat.Eui.Closed();
- _openUis.Remove(message.Id);
-
- break;
-
- default:
- throw new ArgumentOutOfRangeException();
+ openEui.Eui.Closed();
+ _openUis.Remove(message.Id);
}
+
+ if (message.Type != MsgEuiCtl.CtlType.Open)
+ return;
+
+ // Will open/re-open the window if the server wants the eui opened.
+ var euiType = _refl.LooseGetType(message.OpenType);
+ var instance = _dtf.CreateInstance(euiType);
+ instance.Initialize(this, message.Id);
+ instance.Opened();
+ _openUis.Add(message.Id, new EuiData(instance));
}
private sealed class EuiData
diff --git a/Content.Client/Eye/EyeLerpingSystem.cs b/Content.Client/Eye/EyeLerpingSystem.cs
index 7befae2fb6..dbda80ffa3 100644
--- a/Content.Client/Eye/EyeLerpingSystem.cs
+++ b/Content.Client/Eye/EyeLerpingSystem.cs
@@ -1,41 +1,31 @@
-using System;
using Content.Shared.Movement.Components;
+using Content.Shared.Movement.Systems;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Physics;
using Robust.Client.Player;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
+using Robust.Shared.Collections;
using Robust.Shared.Map;
-using Robust.Shared.Maths;
using Robust.Shared.Timing;
+using Robust.Shared.Utility;
namespace Content.Client.Eye;
public sealed class EyeLerpingSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
- [Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
-
- // How fast the camera rotates in radians / s
- private const float CameraRotateSpeed = MathF.PI;
-
- // Safety override
- private const float LerpTimeMax = 1.5f;
-
- // Lerping information for the player's active eye.
- private readonly EyeLerpInformation _playerActiveEye = new();
+ [Dependency] private readonly SharedMoverController _mover = default!;
// Eyes other than the primary eye that are currently active.
private readonly Dictionary _activeEyes = new();
- private readonly List _toRemove = new();
public override void Initialize()
{
base.Initialize();
+ SubscribeLocalEvent(OnEyeStartup);
SubscribeLocalEvent(OnEyeShutdown);
UpdatesAfter.Add(typeof(TransformSystem));
@@ -43,6 +33,27 @@ public sealed class EyeLerpingSystem : EntitySystem
UpdatesBefore.Add(typeof(EyeUpdateSystem));
}
+ private void OnEyeStartup(EntityUid uid, EyeComponent component, ComponentStartup args)
+ {
+ if (component.Eye == null)
+ return;
+
+ // If the eye starts up then don't lerp at all.
+ var xformQuery = GetEntityQuery();
+ TryComp(uid, out var mover);
+ xformQuery.TryGetComponent(uid, out var xform);
+ var lerpInfo = _activeEyes.GetOrNew(uid);
+ lerpInfo.TargetRotation = GetRotation(xformQuery, mover, xform);
+ lerpInfo.LastRotation = lerpInfo.TargetRotation;
+
+ if (xform != null)
+ {
+ lerpInfo.MapId = xform.MapID;
+ }
+
+ component.Eye.Rotation = lerpInfo.TargetRotation;
+ }
+
private void OnEyeShutdown(EntityUid uid, EyeComponent component, ComponentShutdown args)
{
RemoveEye(uid);
@@ -50,145 +61,131 @@ public sealed class EyeLerpingSystem : EntitySystem
public void AddEye(EntityUid uid)
{
- if (!_activeEyes.ContainsKey(uid))
- {
- _activeEyes.Add(uid, new());
- }
+ _activeEyes.TryAdd(uid, new EyeLerpInformation());
}
public void RemoveEye(EntityUid uid)
{
- if (_activeEyes.ContainsKey(uid))
+ _activeEyes.Remove(uid);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ if (!_gameTiming.IsFirstTimePredicted)
+ return;
+
+ var moverQuery = GetEntityQuery();
+ var xformQuery = GetEntityQuery();
+ var foundEyes = new ValueList(1);
+
+ // Set all of our eye rotations to the relevant values.
+ foreach (var (eye, entity) in GetEyes())
{
- _activeEyes.Remove(uid);
+ var lerpInfo = _activeEyes.GetOrNew(entity);
+ foundEyes.Add(entity);
+ moverQuery.TryGetComponent(entity, out var mover);
+ xformQuery.TryGetComponent(entity, out var xform);
+ lerpInfo.LastRotation = eye.Rotation;
+ lerpInfo.TargetRotation = GetRotation(xformQuery, mover, xform);
+
+ if (xform != null)
+ {
+ // If we traverse maps then don't lerp.
+ if (xform.MapID != lerpInfo.MapId)
+ {
+ lerpInfo.LastRotation = lerpInfo.TargetRotation;
+ }
+ }
+ }
+
+ foreach (var eye in foundEyes)
+ {
+ if (!_activeEyes.ContainsKey(eye))
+ {
+ _activeEyes.Remove(eye);
+ }
+ }
+ }
+
+ private Angle GetRotation(EntityQuery xformQuery, InputMoverComponent? mover = null, TransformComponent? xform = null)
+ {
+ // If we can move then tie our eye to our inputs (these also get lerped so it should be fine).
+ if (mover != null)
+ {
+ return -_mover.GetParentGridAngle(mover);
+ }
+
+ // if not tied to a mover then lock it to map / grid
+ if (xform != null)
+ {
+ var relative = xform.GridUid;
+ relative ??= xform.MapUid;
+
+ if (xformQuery.TryGetComponent(relative, out var relativeXform))
+ {
+ return relativeXform.WorldRotation;
+ }
+ }
+
+ return Angle.Zero;
+ }
+
+ private IEnumerable<(IEye Eye, EntityUid Entity)> GetEyes()
+ {
+ if (_playerManager.LocalPlayer?.ControlledEntity is { } player && !Deleted(player))
+ {
+ yield return (_eyeManager.CurrentEye, player);
+ }
+
+ if (_activeEyes.Count == 0)
+ yield break;
+
+ var eyeQuery = GetEntityQuery();
+
+ foreach (var (ent, info) in _activeEyes)
+ {
+ if (!eyeQuery.TryGetComponent(ent, out var eyeComp) ||
+ eyeComp.Eye == null)
+ {
+ continue;
+ }
+
+ yield return (eyeComp.Eye, ent);
}
}
public override void FrameUpdate(float frameTime)
{
- if (!_gameTiming.IsFirstTimePredicted)
- return;
+ var tickFraction = (float) _gameTiming.TickFraction / ushort.MaxValue;
+ var lerpMinimum = 0.01;
- // Always do this one.
- LerpPlayerEye(frameTime);
-
- foreach (var (entity, info) in _activeEyes)
+ foreach (var (eye, entity) in GetEyes())
{
- LerpEntityEye(entity, info, frameTime);
- }
+ if (!_activeEyes.TryGetValue(entity, out var lerpInfo))
+ continue;
- if (_toRemove.Count != 0)
- {
- foreach (var entity in _toRemove)
+ var shortest = Angle.ShortestDistance(lerpInfo.LastRotation, lerpInfo.TargetRotation);
+
+ if (Math.Abs(shortest.Theta) < lerpMinimum)
{
- RemoveEye(entity);
+ eye.Rotation = lerpInfo.TargetRotation;
+ continue;
}
- _toRemove.Clear();
- }
- }
-
- private void LerpPlayerEye(float frameTime)
- {
- if (_playerManager.LocalPlayer?.ControlledEntity is not {} mob || Deleted(mob))
- return;
-
- // We can't lerp if the mob can't move!
- if (!TryComp(mob, out InputMoverComponent? mover))
- return;
-
- LerpEye(_eyeManager.CurrentEye, frameTime, mover.LastGridAngle, _playerActiveEye);
- }
-
- private void LerpEntityEye(EntityUid uid, EyeLerpInformation info, float frameTime)
- {
- if (!TryComp(uid, out TransformComponent? transform)
- || !TryComp(uid, out EyeComponent? eye)
- || eye.Eye == null
- || !_mapManager.TryGetGrid(transform.GridUid, out var grid))
- {
- _toRemove.Add(uid);
- return;
- }
-
- LerpEye(eye.Eye, frameTime, grid.WorldRotation, info);
- }
-
- private void LerpEye(IEye eye, float frameTime, Angle lastAngle, EyeLerpInformation lerpInfo)
- {
-
- // Let's not turn the camera into a washing machine when the game starts.
- if (lerpInfo.LastGridAngle == null)
- {
- lerpInfo.LastGridAngle = lastAngle;
- eye.Rotation = -lastAngle;
- return;
- }
-
- // Check if the last lerp grid angle we have is not the same as the last mover grid angle...
- if (!lerpInfo.LastGridAngle.Value.EqualsApprox(lastAngle))
- {
- // And now, we start lerping.
- lerpInfo.LerpTo = lastAngle;
- lerpInfo.LastGridAngle = lastAngle;
- lerpInfo.LerpStartRotation = eye.Rotation;
- lerpInfo.Accumulator = 0f;
- }
-
- if (lerpInfo.LerpTo != null)
- {
- lerpInfo.Accumulator += frameTime;
-
- var lerpRot = -lerpInfo.LerpTo.Value.FlipPositive().Reduced();
- var startRot = lerpInfo.LerpStartRotation.FlipPositive().Reduced();
-
- var changeNeeded = Angle.ShortestDistance(startRot, lerpRot);
-
- if (changeNeeded.EqualsApprox(Angle.Zero))
- {
- // Nothing to do here!
- lerpInfo.Cleanup(eye);
- return;
- }
-
- // Get how much the camera should have moved by now. Make it faster depending on the change needed.
- var changeRot = (CameraRotateSpeed * Math.Max(1f, Math.Abs(changeNeeded) * 0.75f)) * lerpInfo.Accumulator * Math.Sign(changeNeeded);
-
- // How close is this from reaching the end?
- var percentage = (float)Math.Abs(changeRot / changeNeeded);
-
- eye.Rotation = Angle.Lerp(startRot, lerpRot, percentage);
-
- // Either we have overshot, or we have taken way too long on this, emergency reset time
- if (percentage >= 1.0f || lerpInfo.Accumulator >= LerpTimeMax)
- {
- lerpInfo.Cleanup(eye);
- }
- }
- else
- {
- // This makes it so rotating the camera manually is impossible...
- // However, it is needed. Why? Because of a funny (hilarious, even) race condition involving
- // ghosting, this system listening for attached mob changes, and the eye rotation being reset after our
- // changes back to zero because of an EyeComponent state coming from the server being applied.
- // At some point we'll need to come up with a solution for that. But for now, I just want to fix this.
- eye.Rotation = -lastAngle;
+ eye.Rotation = shortest * tickFraction + lerpInfo.LastRotation;
}
}
private sealed class EyeLerpInformation
{
- public Angle? LastGridAngle { get; set; }
- public Angle? LerpTo { get; set; }
- public Angle LerpStartRotation { get; set; }
- public float Accumulator { get; set; }
+ public Angle LastRotation;
+ public Angle TargetRotation;
- public void Cleanup(IEye eye)
- {
- eye.Rotation = -LerpTo ?? Angle.Zero;
- LerpStartRotation = eye.Rotation;
- LerpTo = null;
- Accumulator = 0;
- }
+ ///
+ /// If we go to a new map then don't lerp and snap instantly.
+ ///
+ public MapId MapId;
}
}
diff --git a/Content.Client/Info/InfoSection.xaml.cs b/Content.Client/Info/InfoSection.xaml.cs
index 5671c833ba..ab9d352d32 100644
--- a/Content.Client/Info/InfoSection.xaml.cs
+++ b/Content.Client/Info/InfoSection.xaml.cs
@@ -11,9 +11,12 @@ public sealed partial class InfoSection : BoxContainer
public InfoSection(string title, string text, bool markup = false)
{
RobustXamlLoader.Load(this);
+ SetText(title, text, markup);
+ }
+ public void SetText(string title, string text, bool markup = false)
+ {
TitleLabel.Text = title;
-
if (markup)
Content.SetMessage(FormattedMessage.FromMarkup(text.Trim()));
else
diff --git a/Content.Client/Info/InfoSystem.cs b/Content.Client/Info/InfoSystem.cs
new file mode 100644
index 0000000000..82dd95ee67
--- /dev/null
+++ b/Content.Client/Info/InfoSystem.cs
@@ -0,0 +1,25 @@
+using Content.Shared.Info;
+using Robust.Shared.Log;
+
+namespace Content.Client.Info;
+
+public sealed class InfoSystem : EntitySystem
+{
+ public RulesMessage Rules = new RulesMessage("Server Rules", "The server did not send any rules.");
+ [Dependency] private readonly RulesManager _rules = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeNetworkEvent(OnRulesReceived);
+ Logger.DebugS("info", "Requested server info.");
+ RaiseNetworkEvent(new RequestRulesMessage());
+ }
+
+ protected void OnRulesReceived(RulesMessage message, EntitySessionEventArgs eventArgs)
+ {
+ Logger.DebugS("info", "Received server rules.");
+ Rules = message;
+ _rules.UpdateRules();
+ }
+}
diff --git a/Content.Client/Info/RulesAndInfoWindow.cs b/Content.Client/Info/RulesAndInfoWindow.cs
index 0e477978b6..acb797828c 100644
--- a/Content.Client/Info/RulesAndInfoWindow.cs
+++ b/Content.Client/Info/RulesAndInfoWindow.cs
@@ -13,6 +13,8 @@ namespace Content.Client.Info
{
[Dependency] private readonly IResourceCache _resourceManager = default!;
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
+ [Dependency] private readonly IEntitySystemManager _sysMan = default!;
+ [Dependency] private readonly RulesManager _rules = default!;
private OptionsMenu optionsMenu;
@@ -35,7 +37,7 @@ namespace Content.Client.Info
TabContainer.SetTabTitle(rulesList, Loc.GetString("ui-info-tab-rules"));
TabContainer.SetTabTitle(tutorialList, Loc.GetString("ui-info-tab-tutorial"));
- PopulateRules(rulesList);
+ AddSection(rulesList, _rules.RulesSection());
PopulateTutorial(tutorialList);
Contents.AddChild(rootContainer);
@@ -43,11 +45,6 @@ namespace Content.Client.Info
SetSize = (650, 650);
}
- private void PopulateRules(Info rulesList)
- {
- AddSection(rulesList, MakeRules(_cfgManager, _resourceManager));
- }
-
private void PopulateTutorial(Info tutorialList)
{
AddSection(tutorialList, Loc.GetString("ui-info-header-intro"), "Intro.txt");
@@ -74,9 +71,5 @@ namespace Content.Client.Info
return new InfoSection(title, res.ContentFileReadAllText($"/Server Info/{path}"), markup);
}
- public static Control MakeRules(IConfigurationManager cfg, IResourceManager res)
- {
- return MakeSection(Loc.GetString(cfg.GetCVar(CCVars.RulesHeader)), cfg.GetCVar(CCVars.RulesFile), true, res);
- }
}
}
diff --git a/Content.Client/Info/RulesControl.xaml.cs b/Content.Client/Info/RulesControl.xaml.cs
index 4470094b92..dda96dbe24 100644
--- a/Content.Client/Info/RulesControl.xaml.cs
+++ b/Content.Client/Info/RulesControl.xaml.cs
@@ -13,12 +13,13 @@ public sealed partial class RulesControl : BoxContainer
{
[Dependency] private readonly IResourceCache _resourceManager = default!;
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
+ [Dependency] private readonly IEntitySystemManager _sysMan = default!;
+ [Dependency] private readonly RulesManager _rules = default!;
public RulesControl()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
-
- AddChild(RulesAndInfoWindow.MakeRules(_cfgManager, _resourceManager));
+ AddChild(_rules.RulesSection());
}
}
diff --git a/Content.Client/Info/RulesManager.cs b/Content.Client/Info/RulesManager.cs
index ccb742befa..1ee5de77b4 100644
--- a/Content.Client/Info/RulesManager.cs
+++ b/Content.Client/Info/RulesManager.cs
@@ -17,7 +17,9 @@ public sealed class RulesManager : SharedRulesManager
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly INetManager _netManager = default!;
+ [Dependency] private readonly IEntitySystemManager _sysMan = default!;
+ private InfoSection rulesSection = new InfoSection("", "", false);
private bool _shouldShowRules;
public void Initialize()
@@ -72,4 +74,17 @@ public sealed class RulesManager : SharedRulesManager
var message = _netManager.CreateNetMessage();
_netManager.ClientSendMessage(message);
}
+
+ public void UpdateRules()
+ {
+ var rules = _sysMan.GetEntitySystem().Rules;
+ rulesSection.SetText(rules.Title, rules.Text, true);
+ }
+
+ public Control RulesSection()
+ {
+ rulesSection = new InfoSection("", "", false);
+ UpdateRules();
+ return rulesSection;
+ }
}
diff --git a/Content.Client/Kitchen/Components/MicrowaveComponent.cs b/Content.Client/Kitchen/Components/MicrowaveComponent.cs
deleted file mode 100644
index 7b666f9182..0000000000
--- a/Content.Client/Kitchen/Components/MicrowaveComponent.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using Content.Shared.Kitchen.Components;
-using Robust.Shared.Audio;
-
-namespace Content.Client.Kitchen.Components
-{
- [RegisterComponent]
- public sealed class MicrowaveComponent : SharedMicrowaveComponent
- {
- public IPlayingAudioStream? PlayingStream { get; set; }
-
- [DataField("loopingSound")]
- public SoundSpecifier LoopingSound = new SoundPathSpecifier("/Audio/Machines/microwave_loop.ogg");
- }
-}
diff --git a/Content.Client/Kitchen/EntitySystems/MicrowaveSystem.cs b/Content.Client/Kitchen/EntitySystems/MicrowaveSystem.cs
deleted file mode 100644
index 16e8579840..0000000000
--- a/Content.Client/Kitchen/EntitySystems/MicrowaveSystem.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System;
-using Content.Client.Kitchen.Components;
-using Robust.Shared.Audio;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Player;
-
-namespace Content.Client.Kitchen.EntitySystems
-{
- public sealed class MicrowaveSystem : EntitySystem
- {
- public void StartSoundLoop(MicrowaveComponent microwave)
- {
- StopSoundLoop(microwave);
-
- microwave.PlayingStream = SoundSystem.Play(microwave.LoopingSound.GetSound(), Filter.Local(),
- microwave.Owner, AudioParams.Default.WithMaxDistance(5).WithLoop(true));
- }
-
- public void StopSoundLoop(MicrowaveComponent microwave)
- {
- microwave.PlayingStream?.Stop();
- }
- }
-}
diff --git a/Content.Client/Kitchen/UI/MicrowaveBoundUserInterface.cs b/Content.Client/Kitchen/UI/MicrowaveBoundUserInterface.cs
index 5397bbcb87..be2f8440c7 100644
--- a/Content.Client/Kitchen/UI/MicrowaveBoundUserInterface.cs
+++ b/Content.Client/Kitchen/UI/MicrowaveBoundUserInterface.cs
@@ -10,7 +10,6 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
-using static Content.Shared.Kitchen.Components.SharedMicrowaveComponent;
namespace Content.Client.Kitchen.UI
{
diff --git a/Content.Client/Kitchen/Visualizers/MicrowaveVisualizer.cs b/Content.Client/Kitchen/Visualizers/MicrowaveVisualizer.cs
deleted file mode 100644
index 93d66e0d3d..0000000000
--- a/Content.Client/Kitchen/Visualizers/MicrowaveVisualizer.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-using Content.Client.Kitchen.Components;
-using Content.Client.Kitchen.EntitySystems;
-using Content.Shared.Kitchen.Components;
-using Content.Shared.Power;
-using JetBrains.Annotations;
-using Robust.Client.GameObjects;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Log;
-
-namespace Content.Client.Kitchen.Visualizers
-{
- [UsedImplicitly]
- public sealed class MicrowaveVisualizer : AppearanceVisualizer
- {
- [Obsolete("Subscribe to AppearanceChangeEvent instead.")]
- public override void OnChangeData(AppearanceComponent component)
- {
- base.OnChangeData(component);
- var entMan = IoCManager.Resolve();
- var sprite = entMan.GetComponent(component.Owner);
-
- var microwaveComponent = entMan.GetComponentOrNull(component.Owner);
-
- if (!component.TryGetData(PowerDeviceVisuals.VisualState, out MicrowaveVisualState state))
- {
- state = MicrowaveVisualState.Idle;
- }
- // The only reason we get the entity system so late is so that tests don't fail... Amazing, huh?
- switch (state)
- {
- case MicrowaveVisualState.Broken:
- sprite.LayerSetState(MicrowaveVisualizerLayers.BaseUnlit, "mwb");
- if(microwaveComponent != null)
- EntitySystem.Get().StopSoundLoop(microwaveComponent);
- break;
-
- case MicrowaveVisualState.Idle:
- sprite.LayerSetState(MicrowaveVisualizerLayers.Base, "mw");
- sprite.LayerSetState(MicrowaveVisualizerLayers.BaseUnlit, "mw_unlit");
- if(microwaveComponent != null)
- EntitySystem.Get().StopSoundLoop(microwaveComponent);
- break;
-
- case MicrowaveVisualState.Cooking:
- sprite.LayerSetState(MicrowaveVisualizerLayers.Base, "mw");
- sprite.LayerSetState(MicrowaveVisualizerLayers.BaseUnlit, "mw_running_unlit");
- if(microwaveComponent != null)
- EntitySystem.Get().StartSoundLoop(microwaveComponent);
- break;
-
- default:
- Logger.Debug($"Something terrible happened in {this}");
- break;
-
- }
-
- var glowingPartsVisible = !(component.TryGetData(PowerDeviceVisuals.Powered, out bool powered) && !powered);
- sprite.LayerSetVisible(MicrowaveVisualizerLayers.BaseUnlit, glowingPartsVisible);
- }
-
- private enum MicrowaveVisualizerLayers : byte
- {
- Base,
- BaseUnlit
- }
- }
-
-
-}
diff --git a/Content.Client/Kitchen/Visualizers/MicrowaveVisuals.cs b/Content.Client/Kitchen/Visualizers/MicrowaveVisuals.cs
new file mode 100644
index 0000000000..539cd0560c
--- /dev/null
+++ b/Content.Client/Kitchen/Visualizers/MicrowaveVisuals.cs
@@ -0,0 +1,8 @@
+namespace Content.Client.Kitchen.Visualizers;
+
+public enum MicrowaveVisualizerLayers : byte
+{
+ Base,
+ BaseUnlit
+}
+
diff --git a/Content.Client/MobState/Overlays/DamageOverlay.cs b/Content.Client/MobState/Overlays/DamageOverlay.cs
index 70fc218407..cd1d1df237 100644
--- a/Content.Client/MobState/Overlays/DamageOverlay.cs
+++ b/Content.Client/MobState/Overlays/DamageOverlay.cs
@@ -73,29 +73,45 @@ public sealed class DamageOverlay : Overlay
{
DeadLevel = 1f;
}
- else if (!DeadLevel.Equals(0f))
+ else if (!MathHelper.CloseTo(0f, DeadLevel, 0.001f))
{
var diff = -DeadLevel;
DeadLevel += GetDiff(diff, lastFrameTime);
}
+ else
+ {
+ DeadLevel = 0f;
+ }
if (!MathHelper.CloseTo(_oldBruteLevel, BruteLevel, 0.001f))
{
var diff = BruteLevel - _oldBruteLevel;
_oldBruteLevel += GetDiff(diff, lastFrameTime);
}
+ else
+ {
+ _oldBruteLevel = BruteLevel;
+ }
if (!MathHelper.CloseTo(_oldOxygenLevel, OxygenLevel, 0.001f))
{
var diff = OxygenLevel - _oldOxygenLevel;
_oldOxygenLevel += GetDiff(diff, lastFrameTime);
}
+ else
+ {
+ _oldOxygenLevel = OxygenLevel;
+ }
if (!MathHelper.CloseTo(_oldCritLevel, CritLevel, 0.001f))
{
var diff = CritLevel - _oldCritLevel;
_oldCritLevel += GetDiff(diff, lastFrameTime);
}
+ else
+ {
+ _oldCritLevel = CritLevel;
+ }
/*
* darknessAlphaOuter is the maximum alpha for anything outside of the larger circle
diff --git a/Content.Client/Physics/Controllers/MoverController.cs b/Content.Client/Physics/Controllers/MoverController.cs
index d9697ddea0..b023b835c6 100644
--- a/Content.Client/Physics/Controllers/MoverController.cs
+++ b/Content.Client/Physics/Controllers/MoverController.cs
@@ -3,7 +3,6 @@ using Content.Shared.Movement.Systems;
using Content.Shared.Pulling.Components;
using Robust.Client.Player;
using Robust.Shared.Physics;
-using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Client.Physics.Controllers
@@ -23,7 +22,17 @@ namespace Content.Client.Physics.Controllers
if (TryComp(player, out var relayMover))
{
if (relayMover.RelayEntity != null)
+ {
+ if (TryComp(player, out var mover) &&
+ TryComp(relayMover.RelayEntity, out var relayed))
+ {
+ relayed.RelativeEntity = mover.RelativeEntity;
+ relayed.RelativeRotation = mover.RelativeRotation;
+ relayed.TargetRelativeRotation = mover.RelativeRotation;
+ }
+
HandleClientsideMovement(relayMover.RelayEntity.Value, frameTime);
+ }
}
HandleClientsideMovement(player, frameTime);
@@ -31,8 +40,10 @@ namespace Content.Client.Physics.Controllers
private void HandleClientsideMovement(EntityUid player, float frameTime)
{
+ var xformQuery = GetEntityQuery();
+
if (!TryComp(player, out InputMoverComponent? mover) ||
- !TryComp(player, out TransformComponent? xform))
+ !xformQuery.TryGetComponent(player, out var xform))
{
return;
}
@@ -47,20 +58,12 @@ namespace Content.Client.Physics.Controllers
{
return;
}
-
- if (TryComp(xform.ParentUid, out var parentMover))
- {
- mover.LastGridAngle = parentMover.LastGridAngle;
- }
}
else if (!TryComp(player, out body))
{
return;
}
- if (xform.GridUid != null)
- mover.LastGridAngle = GetParentGridAngle(xform, mover);
-
// Essentially we only want to set our mob to predicted so every other entity we just interpolate
// (i.e. only see what the server has sent us).
// The exception to this is joints.
@@ -98,7 +101,7 @@ namespace Content.Client.Physics.Controllers
}
// Server-side should just be handled on its own so we'll just do this shizznit
- HandleMobMovement(mover, body, xformMover, frameTime);
+ HandleMobMovement(mover, body, xformMover, frameTime, xformQuery);
}
protected override bool CanSound()
diff --git a/Content.Client/Radiation/RadiationPulseComponent.cs b/Content.Client/Radiation/RadiationPulseComponent.cs
deleted file mode 100644
index b260c41147..0000000000
--- a/Content.Client/Radiation/RadiationPulseComponent.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using Content.Shared.Radiation;
-
-namespace Content.Client.Radiation
-{
- [RegisterComponent]
- [ComponentReference(typeof(SharedRadiationPulseComponent))]
- public sealed class RadiationPulseComponent : SharedRadiationPulseComponent
- {
- private bool _draw;
- private bool _decay;
- private float _range;
- private TimeSpan _startTime;
- private TimeSpan _endTime;
-
- public override float Range => _range;
- public override TimeSpan StartTime => _startTime;
- public override TimeSpan EndTime => _endTime;
- public override bool Draw => _draw;
- public override bool Decay => _decay;
-
- public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
- {
- base.HandleComponentState(curState, nextState);
-
- if (curState is not RadiationPulseState state)
- {
- return;
- }
-
- _range = state.Range;
- _draw = state.Draw;
- _decay = state.Decay;
- _startTime = state.StartTime;
- _endTime = state.EndTime;
- }
- }
-}
diff --git a/Content.Client/Radiation/RadiationPulseOverlay.cs b/Content.Client/Radiation/RadiationPulseOverlay.cs
index 475816ce7a..258696ec5d 100644
--- a/Content.Client/Radiation/RadiationPulseOverlay.cs
+++ b/Content.Client/Radiation/RadiationPulseOverlay.cs
@@ -1,4 +1,5 @@
using System.Linq;
+using Content.Shared.Radiation.Components;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Map;
@@ -18,8 +19,6 @@ namespace Content.Client.Radiation
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public override bool RequestScreenTexture => true;
- private TimeSpan _lastTick = default;
-
private readonly ShaderInstance _baseShader;
private readonly Dictionary _pulses = new();
@@ -52,7 +51,7 @@ namespace Content.Client.Radiation
shd?.SetParameter("renderScale", viewport.RenderScale);
shd?.SetParameter("positionInput", tempCoords);
shd?.SetParameter("range", instance.Range);
- var life = (_lastTick - instance.Start) / (instance.End - instance.Start);
+ var life = (_gameTiming.RealTime - instance.Start).TotalSeconds / instance.Duration;
shd?.SetParameter("life", (float) life);
// There's probably a very good reason not to do this.
@@ -75,8 +74,6 @@ namespace Content.Client.Radiation
return;
}
- _lastTick = _gameTiming.CurTime;
-
var currentEyeLoc = currentEye.Position;
var pulses = _entityManager.EntityQuery();
@@ -92,9 +89,9 @@ namespace Content.Client.Radiation
_baseShader.Duplicate(),
new RadiationShaderInstance(
_entityManager.GetComponent(pulseEntity).MapPosition.Position,
- pulse.Range,
+ pulse.VisualRange,
pulse.StartTime,
- pulse.EndTime
+ pulse.VisualDuration
)
)
);
@@ -110,7 +107,7 @@ namespace Content.Client.Radiation
{
var shaderInstance = _pulses[pulseEntity];
shaderInstance.instance.CurrentMapCoords = _entityManager.GetComponent(pulseEntity).MapPosition.Position;
- shaderInstance.instance.Range = pulse.Range;
+ shaderInstance.instance.Range = pulse.VisualRange;
} else {
_pulses[pulseEntity].shd.Dispose();
_pulses.Remove(pulseEntity);
@@ -124,12 +121,12 @@ namespace Content.Client.Radiation
return _entityManager.GetComponent(pulseEntity).MapID == currentEyeLoc.MapId && _entityManager.GetComponent(pulseEntity).Coordinates.InRange(_entityManager, EntityCoordinates.FromMap(_entityManager, _entityManager.GetComponent(pulseEntity).ParentUid, currentEyeLoc), MaxDist);
}
- private sealed record RadiationShaderInstance(Vector2 CurrentMapCoords, float Range, TimeSpan Start, TimeSpan End)
+ private sealed record RadiationShaderInstance(Vector2 CurrentMapCoords, float Range, TimeSpan Start, float Duration)
{
public Vector2 CurrentMapCoords = CurrentMapCoords;
public float Range = Range;
public TimeSpan Start = Start;
- public TimeSpan End = End;
+ public float Duration = Duration;
};
}
}
diff --git a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs
index 47a0061d73..61817f767b 100644
--- a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs
+++ b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs
@@ -1,62 +1,87 @@
-using System;
-using System.Collections.Generic;
+using Content.Shared.VendingMachines;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
-using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
-using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
-using static Content.Shared.VendingMachines.SharedVendingMachineComponent;
namespace Content.Client.VendingMachines.UI
{
[GenerateTypedNameReferences]
public sealed partial class VendingMachineMenu : DefaultWindow
{
- [Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- private VendingMachineBoundUserInterface Owner { get; }
+ public event Action? OnItemSelected;
- private List _cachedInventory = new();
-
- public VendingMachineMenu(VendingMachineBoundUserInterface owner)
+ public VendingMachineMenu()
{
- IoCManager.InjectDependencies(this);
+ MinSize = SetSize = (250, 150);
RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
- Owner = owner;
- VendingContents.OnItemSelected += ItemSelected;
+ VendingContents.OnItemSelected += args =>
+ {
+ OnItemSelected?.Invoke(args);
+ };
}
+ ///
+ /// Populates the list of available items on the vending machine interface
+ /// and sets icons based on their prototypes
+ ///
public void Populate(List inventory)
{
- VendingContents.Clear();
- _cachedInventory = inventory;
- var longestEntry = "";
- foreach (VendingMachineInventoryEntry entry in inventory)
+ if (inventory.Count == 0)
{
- var itemName = _prototypeManager.Index(entry.ID).Name;
+ VendingContents.Clear();
+ var outOfStockText = Loc.GetString("vending-machine-component-try-eject-out-of-stock");
+ VendingContents.AddItem(outOfStockText);
+ SetSizeAfterUpdate(outOfStockText.Length);
+ return;
+ }
+
+ while (inventory.Count != VendingContents.Count)
+ {
+ if (inventory.Count > VendingContents.Count)
+ VendingContents.AddItem(string.Empty);
+ else
+ VendingContents.RemoveAt(VendingContents.Count - 1);
+ }
+
+ var longestEntry = string.Empty;
+ var spriteSystem = EntitySystem.Get();
+ for (var i = 0; i < inventory.Count; i++)
+ {
+ var entry = inventory[i];
+ var vendingItem = VendingContents[i];
+ vendingItem.Text = string.Empty;
+ vendingItem.Icon = null;
+
+ var itemName = entry.ID;
+ Texture? icon = null;
+ if (_prototypeManager.TryIndex(entry.ID, out var prototype))
+ {
+ itemName = prototype.Name;
+ icon = spriteSystem.GetPrototypeIcon(prototype).Default;
+ }
+
if (itemName.Length > longestEntry.Length)
longestEntry = itemName;
- Texture? icon = null;
- if(_prototypeManager.TryIndex(entry.ID, out EntityPrototype? prototype))
- icon = SpriteComponent.GetPrototypeIcon(prototype, _resourceCache).Default;
-
- VendingContents.AddItem($"{itemName} [{entry.Amount}]", icon);
+ vendingItem.Text = $"{itemName} [{entry.Amount}]";
+ vendingItem.Icon = icon;
}
- SetSize = (Math.Clamp((longestEntry.Length + 2) * 12, 250, 300),
- Math.Clamp(VendingContents.Count * 50, 150, 350));
+ SetSizeAfterUpdate(longestEntry.Length);
}
- public void ItemSelected(ItemList.ItemListSelectedEventArgs args)
+ private void SetSizeAfterUpdate(int longestEntryLength)
{
- Owner.Eject(_cachedInventory[args.ItemIndex].Type, _cachedInventory[args.ItemIndex].ID);
+ SetSize = (Math.Clamp((longestEntryLength + 2) * 12, 250, 300),
+ Math.Clamp(VendingContents.Count * 50, 150, 350));
}
}
}
diff --git a/Content.Client/VendingMachines/UI/VendingMachineVisualizer.cs b/Content.Client/VendingMachines/UI/VendingMachineVisualizer.cs
deleted file mode 100644
index ca679a4a95..0000000000
--- a/Content.Client/VendingMachines/UI/VendingMachineVisualizer.cs
+++ /dev/null
@@ -1,241 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Content.Shared.VendingMachines;
-using JetBrains.Annotations;
-using Robust.Client.Animations;
-using Robust.Client.GameObjects;
-using Robust.Client.ResourceManagement;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
-using Robust.Shared.Serialization.Manager.Attributes;
-using static Content.Shared.VendingMachines.SharedVendingMachineComponent;
-
-namespace Content.Client.VendingMachines.UI
-{
- [UsedImplicitly]
- public sealed class VendingMachineVisualizer : AppearanceVisualizer, ISerializationHooks
- {
- // TODO: Should default to off or broken if damaged
- //
-
- // TODO: The length of these animations is supposed to be dictated
- // by the vending machine's pack prototype's `AnimationDuration`
- // but we have no good way of passing that data from the server
- // to the client at the moment. Rework Visualizers?
-
- private Dictionary _baseStates = new();
-
- private static readonly Dictionary LayerMap =
- new()
- {
- {"off", VendingMachineVisualLayers.Unlit},
- {"screen", VendingMachineVisualLayers.Screen},
- {"normal", VendingMachineVisualLayers.Base},
- {"normal-unshaded", VendingMachineVisualLayers.BaseUnshaded},
- {"eject", VendingMachineVisualLayers.Base},
- {"eject-unshaded", VendingMachineVisualLayers.BaseUnshaded},
- {"deny", VendingMachineVisualLayers.Base},
- {"deny-unshaded", VendingMachineVisualLayers.BaseUnshaded},
- {"broken", VendingMachineVisualLayers.Unlit},
- };
-
- [DataField("screen")]
- private bool _screen;
-
- [DataField("normal")]
- private bool _normal;
-
- [DataField("normalUnshaded")]
- private bool _normalUnshaded;
-
- [DataField("eject")]
- private bool _eject;
-
- [DataField("ejectUnshaded")]
- private bool _ejectUnshaded;
-
- [DataField("deny")]
- private bool _deny;
-
- [DataField("denyUnshaded")]
- private bool _denyUnshaded;
-
- [DataField("broken")]
- private bool _broken;
-
- [DataField("brokenUnshaded")]
- private bool _brokenUnshaded;
-
- private readonly Dictionary _animations = new();
-
- void ISerializationHooks.AfterDeserialization()
- {
- // Used a dictionary so the yaml can adhere to the style-guide and the texture states can be clear
- var states = new Dictionary
- {
- {"off", true},
- {"screen", _screen},
- {"normal", _normal},
- {"normal-unshaded", _normalUnshaded},
- {"eject", _eject},
- {"eject-unshaded", _ejectUnshaded},
- {"deny", _deny},
- {"deny-unshaded", _denyUnshaded},
- {"broken", _broken},
- {"broken-unshaded", _brokenUnshaded},
- };
-
- _baseStates = states;
-
- if (_baseStates["deny"])
- {
- InitializeAnimation("deny");
- }
-
- if (_baseStates["deny-unshaded"])
- {
- InitializeAnimation("deny-unshaded", true);
- }
-
- if (_baseStates["eject"])
- {
- InitializeAnimation("eject");
- }
-
- if (_baseStates["eject-unshaded"])
- {
- InitializeAnimation("eject-unshaded", true);
- }
- }
-
- private void InitializeAnimation(string key, bool unshaded = false)
- {
- _animations.Add(key, new Animation {Length = TimeSpan.FromSeconds(1.2f)});
-
- var flick = new AnimationTrackSpriteFlick();
- _animations[key].AnimationTracks.Add(flick);
- flick.LayerKey = unshaded ? VendingMachineVisualLayers.BaseUnshaded : VendingMachineVisualLayers.Base;
- flick.KeyFrames.Add(new AnimationTrackSpriteFlick.KeyFrame(key, 0f));
- }
-
- [Obsolete("Subscribe to your component being initialised instead.")]
- public override void InitializeEntity(EntityUid entity)
- {
- base.InitializeEntity(entity);
-
- IoCManager.Resolve().EnsureComponent(entity);
- }
-
- private void HideLayers(ISpriteComponent spriteComponent)
- {
- foreach (var layer in spriteComponent.AllLayers)
- {
- layer.Visible = false;
- }
-
- spriteComponent.LayerSetVisible(VendingMachineVisualLayers.Unlit, true);
- }
-
- [Obsolete("Subscribe to AppearanceChangeEvent instead.")]
- public override void OnChangeData(AppearanceComponent component)
- {
- base.OnChangeData(component);
-
- var entMan = IoCManager.Resolve();
- var sprite = entMan.GetComponent(component.Owner);
-
- // TODO when moving to a system visualizer, re work how this is done
- // Currently this only gets called during init, so unless it NEEEDS to be configurable, just make this party of the entity prototype.
- if (component.TryGetData(VendingMachineVisuals.Inventory, out string? invId) &&
- IoCManager.Resolve().TryIndex(invId, out VendingMachineInventoryPrototype? prototype) &&
- IoCManager.Resolve().TryGetResource(
- SharedSpriteComponent.TextureRoot / $"Structures/Machines/VendingMachines/{prototype.SpriteName}.rsi", out var res))
- {
- sprite.BaseRSI = res.RSI;
- }
-
- var animPlayer = entMan.GetComponent(component.Owner);
- if (!component.TryGetData(VendingMachineVisuals.VisualState, out VendingMachineVisualState state))
- {
- state = VendingMachineVisualState.Normal;
- }
-
- // Hide last state
- HideLayers(sprite);
- ActivateState(sprite, "off");
-
- switch (state)
- {
- case VendingMachineVisualState.Normal:
- ActivateState(sprite, "screen");
- ActivateState(sprite, "normal-unshaded");
- ActivateState(sprite, "normal");
- break;
-
- case VendingMachineVisualState.Off:
- break;
-
- case VendingMachineVisualState.Broken:
- ActivateState(sprite, "broken-unshaded");
- ActivateState(sprite, "broken");
-
- break;
- case VendingMachineVisualState.Deny:
- ActivateState(sprite, "screen");
- ActivateAnimation(sprite, animPlayer, "deny-unshaded");
- ActivateAnimation(sprite, animPlayer, "deny");
-
- break;
- case VendingMachineVisualState.Eject:
- ActivateState(sprite, "screen");
- ActivateAnimation(sprite, animPlayer, "eject-unshaded");
- ActivateAnimation(sprite, animPlayer, "eject");
-
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
- }
-
- // Helper methods just to avoid all of that hard-to-read-indented code
- private void ActivateState(ISpriteComponent spriteComponent, string stateId)
- {
- // No state for it on the rsi :(
- if (!_baseStates[stateId])
- {
- return;
- }
-
- var stateLayer = LayerMap[stateId];
- spriteComponent.LayerSetVisible(stateLayer, true);
- spriteComponent.LayerSetState(stateLayer, stateId);
- }
-
- private void ActivateAnimation(ISpriteComponent spriteComponent, AnimationPlayerComponent animationPlayer, string key)
- {
- if (!_animations.TryGetValue(key, out var animation))
- {
- return;
- }
-
- if (!animationPlayer.HasRunningAnimation(key))
- {
- spriteComponent.LayerSetVisible(LayerMap[key], true);
- animationPlayer.Play(animation, key);
- }
- }
-
- public enum VendingMachineVisualLayers : byte
- {
- // Off / Broken. The other layers will overlay this if the machine is on.
- Unlit,
- // Normal / Deny / Eject
- Base,
- BaseUnshaded,
- // Screens that are persistent (where the machine is not off or broken)
- Screen,
- }
- }
-}
diff --git a/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs b/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs
index 688a54f50d..fe4aaa062d 100644
--- a/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs
+++ b/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs
@@ -1,22 +1,20 @@
using Content.Client.VendingMachines.UI;
using Content.Shared.VendingMachines;
using Robust.Client.GameObjects;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.ViewVariables;
-using static Content.Shared.VendingMachines.SharedVendingMachineComponent;
+using Robust.Client.UserInterface.Controls;
+using System.Linq;
namespace Content.Client.VendingMachines
{
public sealed class VendingMachineBoundUserInterface : BoundUserInterface
{
- [ViewVariables] private VendingMachineMenu? _menu;
+ [ViewVariables]
+ private VendingMachineMenu? _menu;
- public SharedVendingMachineComponent? VendingMachine { get; private set; }
+ private List _cachedInventory = new();
public VendingMachineBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
- SendMessage(new InventorySyncRequestMessage());
}
protected override void Open()
@@ -24,33 +22,43 @@ namespace Content.Client.VendingMachines
base.Open();
var entMan = IoCManager.Resolve();
- if (!entMan.TryGetComponent(Owner.Owner, out SharedVendingMachineComponent? vendingMachine))
- {
- return;
- }
+ var vendingMachineSys = EntitySystem.Get();
- VendingMachine = vendingMachine;
+ _cachedInventory = vendingMachineSys.GetAllInventory(Owner.Owner);
- _menu = new VendingMachineMenu(this) {Title = entMan.GetComponent(Owner.Owner).EntityName};
- _menu.Populate(VendingMachine.AllInventory);
+ _menu = new VendingMachineMenu {Title = entMan.GetComponent(Owner.Owner).EntityName};
_menu.OnClose += Close;
+ _menu.OnItemSelected += OnItemSelected;
+
+ _menu.Populate(_cachedInventory);
+
_menu.OpenCentered();
}
- public void Eject(InventoryType type, string id)
+ protected override void UpdateState(BoundUserInterfaceState state)
{
- SendMessage(new VendingMachineEjectMessage(type, id));
+ base.UpdateState(state);
+
+ if (state is not VendingMachineInterfaceState newState)
+ return;
+
+ _cachedInventory = newState.Inventory;
+
+ _menu?.Populate(_cachedInventory);
}
- protected override void ReceiveMessage(BoundUserInterfaceMessage message)
+ private void OnItemSelected(ItemList.ItemListSelectedEventArgs args)
{
- switch (message)
- {
- case VendingMachineInventoryMessage msg:
- _menu?.Populate(msg.Inventory);
- break;
- }
+ if (_cachedInventory == null || _cachedInventory.Count == 0)
+ return;
+
+ var selectedItem = _cachedInventory.ElementAtOrDefault(args.ItemIndex);
+
+ if (selectedItem == null)
+ return;
+
+ SendMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
}
protected override void Dispose(bool disposing)
@@ -59,7 +67,12 @@ namespace Content.Client.VendingMachines
if (!disposing)
return;
- _menu?.Dispose();
+ if (_menu == null)
+ return;
+
+ _menu.OnItemSelected -= OnItemSelected;
+ _menu.OnClose -= Close;
+ _menu.Dispose();
}
}
}
diff --git a/Content.Client/VendingMachines/VendingMachineComponent.cs b/Content.Client/VendingMachines/VendingMachineComponent.cs
index f7c16584a3..b392a49c80 100644
--- a/Content.Client/VendingMachines/VendingMachineComponent.cs
+++ b/Content.Client/VendingMachines/VendingMachineComponent.cs
@@ -4,7 +4,57 @@ namespace Content.Client.VendingMachines;
[RegisterComponent]
[ComponentReference(typeof(SharedVendingMachineComponent))]
+[Access(typeof(VendingMachineSystem))]
public sealed class VendingMachineComponent : SharedVendingMachineComponent
{
+ ///
+ /// RSI state for when the vending machine is unpowered.
+ /// Will be displayed on the layer
+ ///
+ [DataField("offState")]
+ public string? OffState;
+ ///
+ /// RSI state for the screen of the vending machine
+ /// Will be displayed on the layer
+ ///
+ [DataField("screenState")]
+ public string? ScreenState;
+
+ ///
+ /// RSI state for the vending machine's normal state. Usually a looping animation.
+ /// Will be displayed on the layer
+ ///
+ [DataField("normalState")]
+ public string? NormalState;
+
+ ///
+ /// RSI state for the vending machine's eject animation.
+ /// Will be displayed on the layer
+ ///
+ [DataField("ejectState")]
+ public string? EjectState;
+
+ ///
+ /// RSI state for the vending machine's deny animation. Will either be played once as sprite flick
+ /// or looped depending on how is set.
+ /// Will be displayed on the layer
+ ///
+ [DataField("denyState")]
+ public string? DenyState;
+
+ ///
+ /// RSI state for when the vending machine is unpowered.
+ /// Will be displayed on the layer
+ ///
+ [DataField("brokenState")]
+ public string? BrokenState;
+
+ ///
+ /// If set to true (default) will loop the animation of the for the duration
+ /// of . If set to false will play a sprite
+ /// flick animation for the state and then linger on the final frame until the end of the delay.
+ ///
+ [DataField("loopDeny")]
+ public bool LoopDenyAnimation = true;
}
diff --git a/Content.Client/VendingMachines/VendingMachineSystem.cs b/Content.Client/VendingMachines/VendingMachineSystem.cs
index c08f5e10d2..cb9878b119 100644
--- a/Content.Client/VendingMachines/VendingMachineSystem.cs
+++ b/Content.Client/VendingMachines/VendingMachineSystem.cs
@@ -1,7 +1,148 @@
using Content.Shared.VendingMachines;
+using Robust.Client.Animations;
+using Robust.Client.GameObjects;
namespace Content.Client.VendingMachines;
public sealed class VendingMachineSystem : SharedVendingMachineSystem
{
+ [Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnAppearanceChange);
+ SubscribeLocalEvent(OnAnimationCompleted);
+ }
+
+ private void OnAnimationCompleted(EntityUid uid, VendingMachineComponent component, AnimationCompletedEvent args)
+ {
+ if (!TryComp(uid, out var sprite))
+ return;
+
+ UpdateAppearance(uid, VendingMachineVisualState.Normal, component, sprite);
+ }
+
+ private void OnAppearanceChange(EntityUid uid, VendingMachineComponent component, ref AppearanceChangeEvent args)
+ {
+ if (args.Sprite == null)
+ return;
+
+ if (!args.AppearanceData.TryGetValue(VendingMachineVisuals.VisualState, out var visualStateObject) ||
+ visualStateObject is not VendingMachineVisualState visualState)
+ {
+ visualState = VendingMachineVisualState.Normal;
+ }
+
+ UpdateAppearance(uid, visualState, component, args.Sprite);
+ }
+
+ private void UpdateAppearance(EntityUid uid, VendingMachineVisualState visualState, VendingMachineComponent component, SpriteComponent sprite)
+ {
+ SetLayerState(VendingMachineVisualLayers.Base, component.OffState, sprite);
+
+ switch (visualState)
+ {
+ case VendingMachineVisualState.Normal:
+ SetLayerState(VendingMachineVisualLayers.BaseUnshaded, component.NormalState, sprite);
+ SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
+ break;
+
+ case VendingMachineVisualState.Deny:
+ if (component.LoopDenyAnimation)
+ SetLayerState(VendingMachineVisualLayers.BaseUnshaded, component.DenyState, sprite);
+ else
+ PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.DenyState, component.DenyDelay, sprite);
+
+ SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
+ break;
+
+ case VendingMachineVisualState.Eject:
+ PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.EjectState, component.EjectDelay, sprite);
+ SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
+ break;
+
+ case VendingMachineVisualState.Broken:
+ HideLayers(sprite);
+ SetLayerState(VendingMachineVisualLayers.Base, component.BrokenState, sprite);
+ break;
+
+ case VendingMachineVisualState.Off:
+ HideLayers(sprite);
+ break;
+ }
+ }
+
+ private static void SetLayerState(VendingMachineVisualLayers layer, string? state, SpriteComponent sprite)
+ {
+ if (string.IsNullOrEmpty(state))
+ return;
+
+ sprite.LayerSetVisible(layer, true);
+ sprite.LayerSetAutoAnimated(layer, true);
+ sprite.LayerSetState(layer, state);
+ }
+
+ private void PlayAnimation(EntityUid uid, VendingMachineVisualLayers layer, string? state, float animationTime, SpriteComponent sprite)
+ {
+ if (string.IsNullOrEmpty(state))
+ return;
+
+ if (!_animationPlayer.HasRunningAnimation(uid, state))
+ {
+ var animation = GetAnimation(layer, state, animationTime);
+ sprite.LayerSetVisible(layer, true);
+ _animationPlayer.Play(uid, animation, state);
+ }
+ }
+
+ private static Animation GetAnimation(VendingMachineVisualLayers layer, string state, float animationTime)
+ {
+ return new Animation
+ {
+ Length = TimeSpan.FromSeconds(animationTime),
+ AnimationTracks =
+ {
+ new AnimationTrackSpriteFlick
+ {
+ LayerKey = layer,
+ KeyFrames =
+ {
+ new AnimationTrackSpriteFlick.KeyFrame(state, 0f)
+ }
+ }
+ }
+ };
+ }
+
+ private static void HideLayers(SpriteComponent sprite)
+ {
+ HideLayer(VendingMachineVisualLayers.BaseUnshaded, sprite);
+ HideLayer(VendingMachineVisualLayers.Screen, sprite);
+ }
+
+ private static void HideLayer(VendingMachineVisualLayers layer, SpriteComponent sprite)
+ {
+ if (!sprite.LayerMapTryGet(layer, out var actualLayer))
+ return;
+
+ sprite.LayerSetVisible(actualLayer, false);
+ }
+}
+
+public enum VendingMachineVisualLayers : byte
+{
+ ///
+ /// Off / Broken. The other layers will overlay this if the machine is on.
+ ///
+ Base,
+ ///
+ /// Normal / Deny / Eject
+ ///
+ BaseUnshaded,
+ ///
+ /// Screens that are persistent (where the machine is not off or broken)
+ ///
+ Screen
}
diff --git a/Content.IntegrationTests/PoolManager.cs b/Content.IntegrationTests/PoolManager.cs
index 03b457dd46..3bb55ef6fb 100644
--- a/Content.IntegrationTests/PoolManager.cs
+++ b/Content.IntegrationTests/PoolManager.cs
@@ -219,68 +219,84 @@ public static class PoolManager
///
/// See
///
- public static async Task GetServerClient(PoolSettings poolSettings = null,
- [System.Runtime.CompilerServices.CallerFilePath] string testMethodFilePath = "",
- [System.Runtime.CompilerServices.CallerMemberName] string testMethodName = "") =>
- await GetServerClientPair(poolSettings ?? new PoolSettings(), $"{testMethodFilePath}, {testMethodName}");
+ public static async Task GetServerClient(PoolSettings poolSettings = null) =>
+ await GetServerClientPair(poolSettings ?? new PoolSettings());
- private static async Task GetServerClientPair(PoolSettings poolSettings, string testMethodName)
+ private static string GetDefaultTestName()
{
+ return TestContext.CurrentContext.Test.FullName
+ .Replace("Content.IntegrationTests.Tests.", "");
+ }
+
+ private static async Task GetServerClientPair(PoolSettings poolSettings)
+ {
+ DieIfPoolFailure();
+ var currentTestName = poolSettings.TestName ?? GetDefaultTestName();
+ var poolRetrieveTimeWatch = new Stopwatch();
+ await TestContext.Out.WriteLineAsync($"{nameof(GetServerClientPair)}: Called by test {currentTestName}");
Pair pair = null;
try
{
- var poolRetrieveTimeWatch = new Stopwatch();
poolRetrieveTimeWatch.Start();
- await TestContext.Out.WriteLineAsync("Getting server/client");
if (poolSettings.MustBeNew)
{
- await TestContext.Out.WriteLineAsync($"Creating, because must be new pair");
+ await TestContext.Out.WriteLineAsync(
+ $"{nameof(GetServerClientPair)}: Creating pair, because settings of pool settings");
pair = await CreateServerClientPair(poolSettings);
}
else
{
+ await TestContext.Out.WriteLineAsync($"{nameof(GetServerClientPair)}: Looking in pool for a suitable pair");
pair = GrabOptimalPair(poolSettings);
if (pair != null)
{
+ await TestContext.Out.WriteLineAsync($"{nameof(GetServerClientPair)}: Suitable pair found");
var canSkip = pair.Settings.CanFastRecycle(poolSettings);
- if (!canSkip)
+ if (canSkip)
{
- await TestContext.Out.WriteLineAsync($"Cleaning existing pair");
- await CleanPooledPair(poolSettings, pair);
+ await TestContext.Out.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleanup not needed, Skipping cleanup of pair");
}
else
{
- await TestContext.Out.WriteLineAsync($"Skip cleanup pair");
+ await TestContext.Out.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleaning existing pair");
+ await CleanPooledPair(poolSettings, pair);
}
}
else
{
- await TestContext.Out.WriteLineAsync($"Creating, because pool empty");
+ await TestContext.Out.WriteLineAsync($"{nameof(GetServerClientPair)}: Creating a new pair, no suitable pair found in pool");
pair = await CreateServerClientPair(poolSettings);
}
}
- var poolRetrieveTime = poolRetrieveTimeWatch.Elapsed;
- await TestContext.Out.WriteLineAsync(
- $"Got server/client (id:{pair.PairId},uses:{pair.TestHistory.Count}) in {poolRetrieveTime.TotalMilliseconds} ms");
- pair.Settings = poolSettings;
- pair.TestHistory.Add(testMethodName);
- var usageWatch = new Stopwatch();
- usageWatch.Start();
- return new PairTracker()
- {
- Pair = pair,
- UsageWatch = usageWatch
- };
}
finally
{
- if (pair != null)
+ if (pair != null && pair.TestHistory.Count > 1)
{
- TestContext.Out.WriteLine($"Test History|\n{string.Join('\n', pair.TestHistory)}\n|Test History End");
+ await TestContext.Out.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.PairId} Test History Start");
+ for (int i = 0; i < pair.TestHistory.Count - 1; i++)
+ {
+ await TestContext.Out.WriteLineAsync($"- Pair {pair.PairId} Test #{i}: {pair.TestHistory[i]}");
+ }
+ await TestContext.Out.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.PairId} Test History End");
}
}
+ var poolRetrieveTime = poolRetrieveTimeWatch.Elapsed;
+ await TestContext.Out.WriteLineAsync(
+ $"{nameof(GetServerClientPair)}: Retrieving pair {pair.PairId} from pool took {poolRetrieveTime.TotalMilliseconds} ms");
+ await TestContext.Out.WriteLineAsync(
+ $"{nameof(GetServerClientPair)}: Returning pair {pair.PairId}");
+ pair.Settings = poolSettings;
+ pair.TestHistory.Add(currentTestName);
+ var usageWatch = new Stopwatch();
+ usageWatch.Start();
+ return new PairTracker
+ {
+ Pair = pair,
+ UsageWatch = usageWatch
+ };
}
private static Pair GrabOptimalPair(PoolSettings poolSettings)
@@ -409,9 +425,8 @@ public static class PoolManager
await TestContext.Out.WriteLineAsync($"Recycling: {methodWatch.Elapsed.TotalMilliseconds} ms: Done recycling");
}
- private static async Task CreateServerClientPair(PoolSettings poolSettings)
+ private static void DieIfPoolFailure()
{
- Pair pair;
if (PoolFailureReason != null)
{
Assert.Inconclusive(@"
@@ -419,7 +434,10 @@ In a different test, the pool manager had an exception when trying to create a s
Instead of risking that the pool manager will fail at creating a server/client pairs for every single test,
we are just going to end this here to save a lot of time. This is the exception that started this:\n {0}", PoolFailureReason);
}
-
+ }
+ private static async Task CreateServerClientPair(PoolSettings poolSettings)
+ {
+ Pair pair;
try
{
var client = await GenerateClient(poolSettings);
@@ -654,6 +672,11 @@ public sealed class PoolSettings
///
public bool NoServer { get; init; }
+ ///
+ /// Overrides the test name detection, and uses this in the test history instead
+ ///
+ public string TestName { get; set; }
+
///
/// Tries to guess if we can skip recycling the server/client pair.
///
@@ -709,19 +732,19 @@ public sealed class PairTracker : IAsyncDisposable
private async Task OnDirtyDispose()
{
var usageTime = UsageWatch.Elapsed;
- await TestContext.Out.WriteLineAsync($"Dirty: Test returned in {usageTime.TotalMilliseconds} ms");
+ await TestContext.Out.WriteLineAsync($"{nameof(DisposeAsync)}: Test gave back pair {Pair.PairId} in {usageTime.TotalMilliseconds} ms");
var dirtyWatch = new Stopwatch();
dirtyWatch.Start();
Pair.Client.Dispose();
Pair.Server.Dispose();
var disposeTime = dirtyWatch.Elapsed;
- await TestContext.Out.WriteLineAsync($"Dirty: Disposed in {disposeTime.TotalMilliseconds} ms");
+ await TestContext.Out.WriteLineAsync($"{nameof(DisposeAsync)}: Disposed pair {Pair.PairId} in {disposeTime.TotalMilliseconds} ms");
}
private async Task OnCleanDispose()
{
var usageTime = UsageWatch.Elapsed;
- await TestContext.Out.WriteLineAsync($"Clean: Test returned in {usageTime.TotalMilliseconds} ms");
+ await TestContext.Out.WriteLineAsync($"{nameof(CleanReturnAsync)}: Test borrowed pair {Pair.PairId} for {usageTime.TotalMilliseconds} ms");
var cleanWatch = new Stopwatch();
cleanWatch.Start();
// Let any last minute failures the test cause happen.
@@ -730,12 +753,12 @@ public sealed class PairTracker : IAsyncDisposable
{
if (Pair.Client.IsAlive == false)
{
- throw new Exception("Test killed the client", Pair.Client.UnhandledException);
+ throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the client in pair {Pair.PairId}:", Pair.Client.UnhandledException);
}
if (Pair.Server.IsAlive == false)
{
- throw new Exception("Test killed the server", Pair.Server.UnhandledException);
+ throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the server in pair {Pair.PairId}:", Pair.Server.UnhandledException);
}
}
@@ -743,18 +766,19 @@ public sealed class PairTracker : IAsyncDisposable
{
Pair.Client.Dispose();
Pair.Server.Dispose();
+ await PoolManager.ReallyBeIdle(Pair);
var returnTime2 = cleanWatch.Elapsed;
- await TestContext.Out.WriteLineAsync($"Clean: Clean disposed in {returnTime2.TotalMilliseconds} ms");
+ await TestContext.Out.WriteLineAsync($"{nameof(CleanReturnAsync)}: Clean disposed in {returnTime2.TotalMilliseconds} ms");
return;
}
var sRuntimeLog = Pair.Server.ResolveDependency();
- if (sRuntimeLog.ExceptionCount > 0) throw new Exception("Server logged exceptions");
+ if (sRuntimeLog.ExceptionCount > 0) throw new Exception($"{nameof(CleanReturnAsync)}: Server logged exceptions");
var cRuntimeLog = Pair.Client.ResolveDependency();
- if (cRuntimeLog.ExceptionCount > 0) throw new Exception("Client logged exceptions");
+ if (cRuntimeLog.ExceptionCount > 0) throw new Exception($"{nameof(CleanReturnAsync)}: Client logged exceptions");
PoolManager.NoCheckReturn(Pair);
var returnTime = cleanWatch.Elapsed;
- await TestContext.Out.WriteLineAsync($"Clean: Clean returned to pool in {returnTime.TotalMilliseconds} ms");
+ await TestContext.Out.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Pair.PairId} back into the pool");
}
public Stopwatch UsageWatch { get; set; }
public Pair Pair { get; init; }
@@ -765,18 +789,17 @@ public sealed class PairTracker : IAsyncDisposable
switch (disposed)
{
case 0:
- await TestContext.Out.WriteLineAsync("Clean Return Start");
+ await TestContext.Out.WriteLineAsync($"{nameof(CleanReturnAsync)}: Return of pair {Pair.PairId} started");
break;
case 1:
- throw new Exception("Already called clean return before");
+ throw new Exception($"{nameof(CleanReturnAsync)}: Already clean returned");
case 2:
- throw new Exception("Already dirty disposed");
+ throw new Exception($"{nameof(CleanReturnAsync)}: Already dirty disposed");
default:
- throw new Exception("Unexpected disposed value");
+ throw new Exception($"{nameof(CleanReturnAsync)}: Unexpected disposed value");
}
await OnCleanDispose();
- await TestContext.Out.WriteLineAsync($"Clean Return Exiting");
}
public async ValueTask DisposeAsync()
@@ -785,18 +808,16 @@ public sealed class PairTracker : IAsyncDisposable
switch (disposed)
{
case 0:
- await TestContext.Out.WriteLineAsync("Dirty Return Start");
+ await TestContext.Out.WriteLineAsync($"{nameof(DisposeAsync)}: Dirty return of pair {Pair.PairId} started");
break;
case 1:
- await TestContext.Out.WriteLineAsync("Dirty Return - Already Clean Disposed");
+ await TestContext.Out.WriteLineAsync($"{nameof(DisposeAsync)}: Pair {Pair.PairId} was properly clean disposed");
return;
case 2:
- throw new Exception("Already called dirty return before");
+ throw new Exception($"{nameof(DisposeAsync)}: Already dirty disposed pair {Pair.PairId}");
default:
- throw new Exception("Unexpected disposed value");
+ throw new Exception($"{nameof(DisposeAsync)}: Unexpected disposed value");
}
-
await OnDirtyDispose();
- await TestContext.Out.WriteLineAsync($"Dirty Return Exiting");
}
}
diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs
index 749ac7ef93..e193c77fb4 100644
--- a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs
+++ b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs
@@ -56,6 +56,10 @@ namespace Content.IntegrationTests.Tests.Buckle
var testMap = await PoolManager.CreateTestMap(pairTracker);
var coordinates = testMap.GridCoords;
+ var entityManager = server.ResolveDependency();
+ var actionBlocker = entityManager.EntitySysManager.GetEntitySystem();
+ var standingState = entityManager.EntitySysManager.GetEntitySystem();
+
EntityUid human = default;
EntityUid chair = default;
@@ -64,12 +68,6 @@ namespace Content.IntegrationTests.Tests.Buckle
await server.WaitAssertion(() =>
{
- var mapManager = IoCManager.Resolve();
- var entityManager = IoCManager.Resolve();
-
- var actionBlocker = EntitySystem.Get();
- var standingState = EntitySystem.Get();
-
human = entityManager.SpawnEntity(BuckleDummyId, coordinates);
chair = entityManager.SpawnEntity(StrapDummyId, coordinates);
@@ -119,9 +117,6 @@ namespace Content.IntegrationTests.Tests.Buckle
await server.WaitAssertion(() =>
{
- var actionBlocker = EntitySystem.Get();
- var standingState = EntitySystem.Get();
-
// Still buckled
Assert.True(buckle.Buckled);
@@ -155,10 +150,6 @@ namespace Content.IntegrationTests.Tests.Buckle
await server.WaitAssertion(() =>
{
- var entityManager = IoCManager.Resolve();
- var actionBlocker = EntitySystem.Get();
- var standingState = EntitySystem.Get();
-
// Still buckled
Assert.True(buckle.Buckled);
@@ -228,10 +219,11 @@ namespace Content.IntegrationTests.Tests.Buckle
await server.WaitIdleAsync();
+ var entityManager = server.ResolveDependency();
+ var handsSys = entityManager.EntitySysManager.GetEntitySystem();
+
await server.WaitAssertion(() =>
{
- var entityManager = IoCManager.Resolve();
-
human = entityManager.SpawnEntity(BuckleDummyId, coordinates);
var chair = entityManager.SpawnEntity(StrapDummyId, coordinates);
@@ -251,7 +243,7 @@ namespace Content.IntegrationTests.Tests.Buckle
{
var akms = entityManager.SpawnEntity(ItemDummyId, coordinates);
- Assert.True(EntitySystem.Get().TryPickupAnyHand(human, akms));
+ Assert.True(handsSys.TryPickupAnyHand(human, akms));
}
});
@@ -265,7 +257,7 @@ namespace Content.IntegrationTests.Tests.Buckle
// With items in all hands
foreach (var hand in hands.Hands.Values)
{
- Assert.NotNull(hands.ActiveHandEntity);
+ Assert.NotNull(hand.HeldEntity);
}
var legs = body.GetPartsOfType(BodyPartType.Leg);
@@ -287,7 +279,7 @@ namespace Content.IntegrationTests.Tests.Buckle
// Now with no item in any hand
foreach (var hand in hands.Hands.Values)
{
- Assert.Null(hands.ActiveHandEntity);
+ Assert.Null(hand.HeldEntity);
}
buckle.TryUnbuckle(human, true);
@@ -304,6 +296,7 @@ namespace Content.IntegrationTests.Tests.Buckle
var testMap = await PoolManager.CreateTestMap(pairTracker);
var coordinates = testMap.GridCoords;
+ var entityManager = server.ResolveDependency();
EntityUid human = default;
EntityUid chair = default;
@@ -311,9 +304,6 @@ namespace Content.IntegrationTests.Tests.Buckle
await server.WaitAssertion(() =>
{
- var mapManager = IoCManager.Resolve();
- var entityManager = IoCManager.Resolve();
-
human = entityManager.SpawnEntity(BuckleDummyId, coordinates);
chair = entityManager.SpawnEntity(StrapDummyId, coordinates);
@@ -336,8 +326,6 @@ namespace Content.IntegrationTests.Tests.Buckle
await server.WaitAssertion(() =>
{
- var entityManager = IoCManager.Resolve();
-
// Move the now unbuckled entity back onto the chair
entityManager.GetComponent(human).WorldPosition -= (100, 0);
diff --git a/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs b/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs
index d4eacbfb4d..f9cbe9e9dd 100644
--- a/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs
+++ b/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs
@@ -32,6 +32,7 @@ public sealed class SolutionSystemTests
var server = pairTracker.Pair.Server;
var entityManager = server.ResolveDependency();
+ var containerSystem = entityManager.EntitySysManager.GetEntitySystem();
var testMap = await PoolManager.CreateTestMap(pairTracker);
var coordinates = testMap.GridCoords;
@@ -46,11 +47,11 @@ public sealed class SolutionSystemTests
var originalWater = new Solution("Water", waterQuantity);
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
- Assert.That(EntitySystem.Get()
+ Assert.That(containerSystem
.TryGetSolution(beaker, "beaker", out var solution));
solution.AddSolution(originalWater);
- Assert.That(EntitySystem.Get()
+ Assert.That(containerSystem
.TryAddSolution(beaker, solution, oilAdded));
solution.ContainsReagent("Water", out var water);
@@ -73,6 +74,7 @@ public sealed class SolutionSystemTests
var testMap = await PoolManager.CreateTestMap(pairTracker);
var entityManager = server.ResolveDependency();
+ var containerSystem = entityManager.EntitySysManager.GetEntitySystem();
var coordinates = testMap.GridCoords;
EntityUid beaker;
@@ -86,11 +88,11 @@ public sealed class SolutionSystemTests
var originalWater = new Solution("Water", waterQuantity);
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
- Assert.That(EntitySystem.Get()
+ Assert.That(containerSystem
.TryGetSolution(beaker, "beaker", out var solution));
solution.AddSolution(originalWater);
- Assert.That(EntitySystem.Get()
+ Assert.That(containerSystem
.TryAddSolution(beaker, solution, oilAdded), Is.False);
solution.ContainsReagent("Water", out var water);
@@ -112,6 +114,7 @@ public sealed class SolutionSystemTests
var entityManager = server.ResolveDependency();
var testMap = await PoolManager.CreateTestMap(pairTracker);
+ var containerSystem = entityManager.EntitySysManager.GetEntitySystem();
var coordinates = testMap.GridCoords;
EntityUid beaker;
@@ -127,11 +130,11 @@ public sealed class SolutionSystemTests
var originalWater = new Solution("Water", waterQuantity);
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
- Assert.That(EntitySystem.Get()
+ Assert.That(containerSystem
.TryGetSolution(beaker, "beaker", out var solution));
solution.AddSolution(originalWater);
- Assert.That(EntitySystem.Get()
+ Assert.That(containerSystem
.TryMixAndOverflow(beaker, solution, oilAdded, threshold, out var overflowingSolution));
Assert.That(solution.CurrentVolume, Is.EqualTo(FixedPoint2.New(threshold)));
@@ -158,6 +161,7 @@ public sealed class SolutionSystemTests
var server = pairTracker.Pair.Server;
var entityManager = server.ResolveDependency();
+ var containerSystem = entityManager.EntitySysManager.GetEntitySystem();
var testMap = await PoolManager.CreateTestMap(pairTracker);
var coordinates = testMap.GridCoords;
@@ -174,11 +178,11 @@ public sealed class SolutionSystemTests
var originalWater = new Solution("Water", waterQuantity);
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
- Assert.That(EntitySystem.Get()
+ Assert.That(containerSystem
.TryGetSolution(beaker, "beaker", out var solution));
solution.AddSolution(originalWater);
- Assert.That(EntitySystem.Get()
+ Assert.That(containerSystem
.TryMixAndOverflow(beaker, solution, oilAdded, threshold, out _),
Is.False);
});
diff --git a/Content.IntegrationTests/Tests/Cleanup/EuiManagerTest.cs b/Content.IntegrationTests/Tests/Cleanup/EuiManagerTest.cs
new file mode 100644
index 0000000000..e3c392450e
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Cleanup/EuiManagerTest.cs
@@ -0,0 +1,34 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Content.Server.Administration.UI;
+using Content.Server.EUI;
+using NUnit.Framework;
+using Robust.Server.Player;
+using Robust.Shared.IoC;
+
+namespace Content.IntegrationTests.Tests.Cleanup;
+
+public sealed class EuiManagerTest
+{
+ [Test]
+ public async Task EuiManagerRecycleWithOpenWindowTest()
+ {
+ // Even though we are using the server EUI here, we actually want to see if the client EUIManager crashes
+ for (int i = 0; i < 2; i++)
+ {
+ await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{Dirty = true});
+ var server = pairTracker.Pair.Server;
+
+ var sPlayerManager = server.ResolveDependency();
+
+ await server.WaitAssertion(async () =>
+ {
+ var clientSession = sPlayerManager.ServerSessions.Single();
+ var eui = IoCManager.Resolve();
+ var ui = new AdminAnnounceEui();
+ eui.OpenEui(ui, clientSession);
+ });
+ await pairTracker.CleanReturnAsync();
+ }
+ }
+}
diff --git a/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs b/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs
index 3fe35dc9a7..96c7b3db74 100644
--- a/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs
+++ b/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs
@@ -27,18 +27,21 @@ namespace Content.IntegrationTests.Tests.DoAfter
Task task = null;
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes});
var server = pairTracker.Pair.Server;
+ await server.WaitIdleAsync();
+
+ var mapManager = server.ResolveDependency();
+ var entityManager = server.ResolveDependency();
+ var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem();
// That it finishes successfully
await server.WaitPost(() =>
{
var tickTime = 1.0f / IoCManager.Resolve().TickRate;
- var mapManager = IoCManager.Resolve();
mapManager.CreateNewMapEntity(MapId.Nullspace);
- var entityManager = IoCManager.Resolve();
var mob = entityManager.SpawnEntity("Dummy", MapCoordinates.Nullspace);
var cancelToken = new CancellationTokenSource();
var args = new DoAfterEventArgs(mob, tickTime / 2, cancelToken.Token);
- task = EntitySystem.Get().WaitDoAfter(args);
+ task = doAfterSystem.WaitDoAfter(args);
});
await server.WaitRunTicks(1);
@@ -57,17 +60,19 @@ namespace Content.IntegrationTests.Tests.DoAfter
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes});
var server = pairTracker.Pair.Server;
+ var entityManager = server.ResolveDependency();
+ var mapManager = server.ResolveDependency();
+ var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem();
await server.WaitPost(() =>
{
var tickTime = 1.0f / IoCManager.Resolve().TickRate;
- var mapManager = IoCManager.Resolve();
mapManager.CreateNewMapEntity(MapId.Nullspace);
- var entityManager = IoCManager.Resolve();
+
var mob = entityManager.SpawnEntity("Dummy", MapCoordinates.Nullspace);
var cancelToken = new CancellationTokenSource();
var args = new DoAfterEventArgs(mob, tickTime * 2, cancelToken.Token);
- task = EntitySystem.Get().WaitDoAfter(args);
+ task = doAfterSystem.WaitDoAfter(args);
cancelToken.Cancel();
});
diff --git a/Content.IntegrationTests/Tests/Doors/AirlockTest.cs b/Content.IntegrationTests/Tests/Doors/AirlockTest.cs
index e7c81b7333..719b50a809 100644
--- a/Content.IntegrationTests/Tests/Doors/AirlockTest.cs
+++ b/Content.IntegrationTests/Tests/Doors/AirlockTest.cs
@@ -53,6 +53,7 @@ namespace Content.IntegrationTests.Tests.Doors
var mapManager = server.ResolveDependency();
var entityManager = server.ResolveDependency();
+ var doors = entityManager.EntitySysManager.GetEntitySystem();
EntityUid airlock = default;
DoorComponent doorComponent = null;
@@ -71,7 +72,7 @@ namespace Content.IntegrationTests.Tests.Doors
await server.WaitAssertion(() =>
{
- EntitySystem.Get().StartOpening(airlock);
+ doors.StartOpening(airlock);
Assert.That(doorComponent.State, Is.EqualTo(DoorState.Opening));
});
@@ -83,7 +84,7 @@ namespace Content.IntegrationTests.Tests.Doors
await server.WaitAssertion(() =>
{
- EntitySystem.Get().TryClose((EntityUid) airlock);
+ doors.TryClose(airlock);
Assert.That(doorComponent.State, Is.EqualTo(DoorState.Closing));
});
diff --git a/Content.IntegrationTests/Tests/EntityTest.cs b/Content.IntegrationTests/Tests/EntityTest.cs
index 5966446dd4..9f967b33df 100644
--- a/Content.IntegrationTests/Tests/EntityTest.cs
+++ b/Content.IntegrationTests/Tests/EntityTest.cs
@@ -6,6 +6,7 @@ using Content.Shared.CCVar;
using Content.Shared.Coordinates;
using NUnit.Framework;
using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
@@ -18,79 +19,89 @@ namespace Content.IntegrationTests.Tests
public sealed class EntityTest
{
[Test]
- public async Task SpawnTest()
+ public async Task SpawnAndDeleteAllEntitiesOnDifferentMaps()
{
- //TODO: Run this test in a for loop, and figure out why garbage is ending up in the Entities list on cleanup.
- //If this gets fixed, see also UninitializedSaveTest.
- await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, Dirty = true, Destructive = true});
+ await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, Destructive = true});
var server = pairTracker.Pair.Server;
- var mapManager = server.ResolveDependency();
- var entityMan = server.ResolveDependency();
- var prototypeMan = server.ResolveDependency();
- var tileDefinitionManager = server.ResolveDependency();
+ IEntityManager entityMan = null;
- var prototypes = new List();
- IMapGrid grid = default;
- EntityUid testEntity;
-
- //Build up test environment
await server.WaitPost(() =>
{
- // Create a one tile grid to stave off the grid 0 monsters
- var mapId = mapManager.CreateMap();
-
- mapManager.AddUninitializedMap(mapId);
-
- grid = mapManager.CreateGrid(mapId);
-
- var tileDefinition = tileDefinitionManager["UnderPlating"];
- var tile = new Tile(tileDefinition.TileId);
- var coordinates = grid.ToCoordinates();
-
- grid.SetTile(coordinates, tile);
-
- mapManager.DoMapInitialize(mapId);
+ entityMan = IoCManager.Resolve();
+ var mapManager = IoCManager.Resolve();
+ var prototypeMan = IoCManager.Resolve();
+ var protoIds = prototypeMan
+ .EnumeratePrototypes()
+ .Where(p=>!p.Abstract)
+ .Select(p => p.ID)
+ .ToList();
+ foreach (var protoId in protoIds)
+ {
+ var mapId = mapManager.CreateMap();
+ var grid = mapManager.CreateGrid(mapId);
+ var coord = new EntityCoordinates(grid.GridEntityId, 0, 0);
+ entityMan.SpawnEntity(protoId, coord);
+ }
});
await server.WaitRunTicks(5);
- await server.WaitAssertion(() =>
+ await server.WaitPost(() =>
{
- var testLocation = grid.ToCoordinates();
-
- //Generate list of non-abstract prototypes to test
- foreach (var prototype in prototypeMan.EnumeratePrototypes())
+ var entityMetas = entityMan.EntityQuery().ToList();
+ foreach (var meta in entityMetas)
{
- if (prototype.Abstract)
- {
- continue;
- }
-
- prototypes.Add(prototype);
+ if(!entityMan.Deleted(meta.Owner))
+ entityMan.DeleteEntity(meta.Owner);
}
- Assert.Multiple(() =>
- {
- //Iterate list of prototypes to spawn
- foreach (var prototype in prototypes)
- {
- Assert.DoesNotThrow(() =>
- {
- Logger.LogS(LogLevel.Debug, "EntityTest", $"Testing: {prototype.ID}");
- testEntity = entityMan.SpawnEntity(prototype.ID, testLocation);
- server.RunTicks(1);
- if(!entityMan.Deleted(testEntity))
- entityMan.DeleteEntity(testEntity);
- }, "Entity '{0}' threw an exception.",
- prototype.ID);
- }
- });
+ Assert.That(entityMan.EntityCount, Is.Zero);
});
-
await pairTracker.CleanReturnAsync();
}
+ [Test]
+ public async Task SpawnAndDeleteAllEntitiesInTheSameSpot()
+ {
+ await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, Destructive = true});
+ var server = pairTracker.Pair.Server;
+
+ IEntityManager entityMan = null;
+
+ await server.WaitPost(() =>
+ {
+ entityMan = IoCManager.Resolve();
+ var mapManager = IoCManager.Resolve();
+
+ var prototypeMan = IoCManager.Resolve();
+ var protoIds = prototypeMan
+ .EnumeratePrototypes()
+ .Where(p=>!p.Abstract)
+ .Select(p => p.ID)
+ .ToList();
+ var mapId = mapManager.CreateMap();
+ var grid = mapManager.CreateGrid(mapId);
+ var coord = new EntityCoordinates(grid.GridEntityId, 0, 0);
+ foreach (var protoId in protoIds)
+ {
+ entityMan.SpawnEntity(protoId, coord);
+ }
+ });
+ await server.WaitRunTicks(5);
+ await server.WaitPost(() =>
+ {
+ var entityMetas = entityMan.EntityQuery().ToList();
+ foreach (var meta in entityMetas)
+ {
+ if(!entityMan.Deleted(meta.Owner))
+ entityMan.DeleteEntity(meta.Owner);
+ }
+
+ Assert.That(entityMan.EntityCount, Is.Zero);
+ });
+ await pairTracker.CleanReturnAsync();
+ }
[Test]
public async Task AllComponentsOneToOneDeleteTest()
{
diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs
index 1d20929b2d..6c8a9d1f04 100644
--- a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs
+++ b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs
@@ -39,7 +39,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
alertsSystem.ShowAlert(playerUid, AlertType.Debug1);
alertsSystem.ShowAlert(playerUid, AlertType.Debug2);
- Assert.AreEqual(alertCount + 2, alerts.Count);
+ Assert.That(alerts, Has.Count.EqualTo(alertCount + 2));
});
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
diff --git a/Content.IntegrationTests/Tests/Lobby/ServerReloginTest.cs b/Content.IntegrationTests/Tests/Lobby/ServerReloginTest.cs
index 60021cade4..f9e6df00b3 100644
--- a/Content.IntegrationTests/Tests/Lobby/ServerReloginTest.cs
+++ b/Content.IntegrationTests/Tests/Lobby/ServerReloginTest.cs
@@ -8,7 +8,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Network;
namespace Content.IntegrationTests.Tests.Lobby;
-public class ServerReloginTest
+public sealed class ServerReloginTest
{
[Test]
public async Task Relogin()
diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs
index b0cdd1e3ec..50ce95d023 100644
--- a/Content.IntegrationTests/Tests/PostMapInitTest.cs
+++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs
@@ -69,7 +69,13 @@ namespace Content.IntegrationTests.Tests
task = Task.Run(static async () =>
{
await Task.Yield();
- await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{Disconnected = true});
+ await using var pairTracker = await PoolManager.GetServerClient(
+ new PoolSettings
+ {
+ Disconnected = true,
+ TestName = $"{nameof(PostMapInitTest)}.{nameof(GetMapNames)}"
+ }
+ );
var server = pairTracker.Pair.Server;
var resourceManager = server.ResolveDependency();
var mapFolder = new ResourcePath("/Maps");
diff --git a/Content.IntegrationTests/Tests/PrototypeSaveTest.cs b/Content.IntegrationTests/Tests/PrototypeSaveTest.cs
index 617d55069d..8522b72a91 100644
--- a/Content.IntegrationTests/Tests/PrototypeSaveTest.cs
+++ b/Content.IntegrationTests/Tests/PrototypeSaveTest.cs
@@ -129,7 +129,6 @@ public sealed class PrototypeSaveTest
"UniformPrinter",
"OreProcessor",
"MedicalScanner",
- "KitchenMicrowave",
"MagazinePistolSubMachineGunTopMounted",
"Recycler",
"EpinephrineChemistryBottle",
@@ -235,7 +234,7 @@ public sealed class PrototypeSaveTest
{
uid = entityMan.SpawnEntity(prototype.ID, testLocation);
server.RunTicks(1);
-
+
// get default prototype data
Dictionary protoData = new();
try
diff --git a/Content.IntegrationTests/Tests/RoundEndTest.cs b/Content.IntegrationTests/Tests/RoundEndTest.cs
index 01a95bf2a7..6a5a0da8c7 100644
--- a/Content.IntegrationTests/Tests/RoundEndTest.cs
+++ b/Content.IntegrationTests/Tests/RoundEndTest.cs
@@ -122,7 +122,7 @@ namespace Content.IntegrationTests.Tests
roundEndSystem.DefaultCooldownDuration = TimeSpan.FromSeconds(30);
roundEndSystem.DefaultCountdownDuration = TimeSpan.FromMinutes(4);
roundEndSystem.DefaultRestartRoundDuration = TimeSpan.FromMinutes(1);
- EntitySystem.Get().RestartRound();
+ ticker.RestartRound();
});
await PoolManager.ReallyBeIdle(pairTracker.Pair, 10);
diff --git a/Content.IntegrationTests/Tests/StorageFillTest.cs b/Content.IntegrationTests/Tests/StorageTest.cs
similarity index 51%
rename from Content.IntegrationTests/Tests/StorageFillTest.cs
rename to Content.IntegrationTests/Tests/StorageTest.cs
index 1f2f4c4c60..21b5e7c40a 100644
--- a/Content.IntegrationTests/Tests/StorageFillTest.cs
+++ b/Content.IntegrationTests/Tests/StorageTest.cs
@@ -1,14 +1,42 @@
#nullable enable
using System.Threading.Tasks;
using Content.Server.Storage.Components;
+using Content.Shared.Item;
+using Content.Shared.Storage;
using NUnit.Framework;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests
{
[TestFixture]
- public sealed class StorageFillTest
+ public sealed class StorageTest
{
+ ///
+ /// Can an item store more than itself weighs.
+ /// In an ideal world this test wouldn't need to exist because sizes would be recursive.
+ ///
+ [Test]
+ public async Task StorageSizeArbitrageTest()
+ {
+ await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true});
+ var server = pairTracker.Pair.Server;
+
+ var protoManager = server.ResolveDependency();
+
+ await server.WaitAssertion(() =>
+ {
+ foreach (var proto in protoManager.EnumeratePrototypes())
+ {
+ if (!proto.TryGetComponent("Storage", out var storage) ||
+ storage.Whitelist != null ||
+ !proto.TryGetComponent("Item", out var item)) continue;
+
+ Assert.That(storage.StorageCapacityMax, Is.LessThanOrEqualTo(item.Size), $"Found storage arbitrage on {proto.ID}");
+ }
+ });
+ await pairTracker.CleanReturnAsync();
+ }
+
[Test]
public async Task TestStorageFillPrototypes()
{
diff --git a/Content.IntegrationTests/Tests/Utility/SandboxTest.cs b/Content.IntegrationTests/Tests/Utility/SandboxTest.cs
index 030d9e27c1..45b681c221 100644
--- a/Content.IntegrationTests/Tests/Utility/SandboxTest.cs
+++ b/Content.IntegrationTests/Tests/Utility/SandboxTest.cs
@@ -8,7 +8,7 @@ public sealed class SandboxTest
[Test]
public async Task Test()
{
- await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoServer = true});
+ await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoServer = true, Destructive = true});
var client = pairTracker.Pair.Client;
await client.CheckSandboxed(typeof(Client.Entry.EntryPoint).Assembly);
await pairTracker.CleanReturnAsync();
diff --git a/Content.IntegrationTests/Tests/VendingMachineTest.cs b/Content.IntegrationTests/Tests/VendingMachineTest.cs
index 04f2e1be97..3d3b263305 100644
--- a/Content.IntegrationTests/Tests/VendingMachineTest.cs
+++ b/Content.IntegrationTests/Tests/VendingMachineTest.cs
@@ -29,7 +29,7 @@ namespace Content.IntegrationTests.Tests
}
catch (UnknownPrototypeException)
{
- throw new UnknownPrototypeException($"Unknown prototype {item} on vending inventory {vendorProto.Name}");
+ throw new UnknownPrototypeException($"Unknown prototype {item} on vending inventory {vendorProto.ID}");
}
}
}
diff --git a/Content.Server/AI/Pathfinding/Accessible/AiReachableSystem.cs b/Content.Server/AI/Pathfinding/Accessible/AiReachableSystem.cs
index 86a08855d6..a9bd15991d 100644
--- a/Content.Server/AI/Pathfinding/Accessible/AiReachableSystem.cs
+++ b/Content.Server/AI/Pathfinding/Accessible/AiReachableSystem.cs
@@ -445,10 +445,11 @@ namespace Content.Server.AI.Pathfinding.Accessible
var parentChunk = node.ParentChunk;
// No guarantee the node even has a region yet (if we're doing neighbor lookups)
- if (!_regions[parentChunk.GridId].TryGetValue(parentChunk, out var regions))
- {
+ if (!_regions.TryGetValue(parentChunk.GridId, out var chunk))
+ return null;
+
+ if (!chunk.TryGetValue(parentChunk, out var regions))
return null;
- }
foreach (var region in regions)
{
diff --git a/Content.Server/Access/Systems/IdCardSystem.cs b/Content.Server/Access/Systems/IdCardSystem.cs
index d823376b2b..716f16c3dd 100644
--- a/Content.Server/Access/Systems/IdCardSystem.cs
+++ b/Content.Server/Access/Systems/IdCardSystem.cs
@@ -29,9 +29,6 @@ namespace Content.Server.Access.Systems
private void OnMapInit(EntityUid uid, IdCardComponent id, MapInitEvent args)
{
- // On one hand, these prototypes should default to having the correct name. On the other hand, id cards are
- // rarely ever spawned in on their own without an owner, so this is fine.
- id.OriginalEntityName ??= EntityManager.GetComponent(id.Owner).EntityName;
UpdateEntityName(uid, id);
}
@@ -80,21 +77,32 @@ namespace Content.Server.Access.Systems
///
/// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs.
///
- public bool TryChangeJobTitle(EntityUid uid, string jobTitle, IdCardComponent? id = null, EntityUid? player = null)
+ public bool TryChangeJobTitle(EntityUid uid, string? jobTitle, IdCardComponent? id = null, EntityUid? player = null)
{
if (!Resolve(uid, ref id))
return false;
- if (jobTitle.Length > SharedIdCardConsoleComponent.MaxJobTitleLength)
- jobTitle = jobTitle[..SharedIdCardConsoleComponent.MaxJobTitleLength];
+ if (!string.IsNullOrWhiteSpace(jobTitle))
+ {
+ jobTitle = jobTitle.Trim();
+
+ if (jobTitle.Length > SharedIdCardConsoleComponent.MaxJobTitleLength)
+ jobTitle = jobTitle[..SharedIdCardConsoleComponent.MaxJobTitleLength];
+ }
+ else
+ {
+ jobTitle = null;
+ }
id.JobTitle = jobTitle;
Dirty(id);
UpdateEntityName(uid, id);
if (player != null)
+ {
_adminLogger.Add(LogType.Identity, LogImpact.Low,
- $"{ToPrettyString(player.Value):player} has changed the job title of {ToPrettyString(id.Owner):entity} to {jobTitle} ");
+ $"{ToPrettyString(player.Value):player} has changed the job title of {ToPrettyString(id.Owner):entity} to {jobTitle} ");
+ }
return true;
}
@@ -105,21 +113,31 @@ namespace Content.Server.Access.Systems
///
/// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs.
///
- public bool TryChangeFullName(EntityUid uid, string fullName, IdCardComponent? id = null, EntityUid? player = null)
+ public bool TryChangeFullName(EntityUid uid, string? fullName, IdCardComponent? id = null, EntityUid? player = null)
{
if (!Resolve(uid, ref id))
return false;
- if (fullName.Length > SharedIdCardConsoleComponent.MaxFullNameLength)
- fullName = fullName[..SharedIdCardConsoleComponent.MaxFullNameLength];
+ if (!string.IsNullOrWhiteSpace(fullName))
+ {
+ fullName = fullName.Trim();
+ if (fullName.Length > SharedIdCardConsoleComponent.MaxFullNameLength)
+ fullName = fullName[..SharedIdCardConsoleComponent.MaxFullNameLength];
+ }
+ else
+ {
+ fullName = null;
+ }
id.FullName = fullName;
Dirty(id);
UpdateEntityName(uid, id);
if (player != null)
+ {
_adminLogger.Add(LogType.Identity, LogImpact.Low,
- $"{ToPrettyString(player.Value):player} has changed the name of {ToPrettyString(id.Owner):entity} to {fullName} ");
+ $"{ToPrettyString(player.Value):player} has changed the name of {ToPrettyString(id.Owner):entity} to {fullName} ");
+ }
return true;
}
@@ -135,17 +153,10 @@ namespace Content.Server.Access.Systems
if (!Resolve(uid, ref id))
return;
- if (string.IsNullOrWhiteSpace(id.FullName) && string.IsNullOrWhiteSpace(id.JobTitle))
- {
- EntityManager.GetComponent(id.Owner).EntityName = id.OriginalEntityName;
- return;
- }
-
var jobSuffix = string.IsNullOrWhiteSpace(id.JobTitle) ? string.Empty : $" ({id.JobTitle})";
var val = string.IsNullOrWhiteSpace(id.FullName)
? Loc.GetString("access-id-card-component-owner-name-job-title-text",
- ("originalOwnerName", id.OriginalEntityName),
("jobSuffix", jobSuffix))
: Loc.GetString("access-id-card-component-owner-full-name-job-title-text",
("fullName", id.FullName),
diff --git a/Content.Server/Access/Systems/IdExaminableSystem.cs b/Content.Server/Access/Systems/IdExaminableSystem.cs
index 2966624368..2e02b33cec 100644
--- a/Content.Server/Access/Systems/IdExaminableSystem.cs
+++ b/Content.Server/Access/Systems/IdExaminableSystem.cs
@@ -66,7 +66,6 @@ public sealed class IdExaminableSystem : EntitySystem
var val = string.IsNullOrWhiteSpace(id.FullName)
? Loc.GetString("access-id-card-component-owner-name-job-title-text",
- ("originalOwnerName", id.OriginalEntityName),
("jobSuffix", jobSuffix))
: Loc.GetString("access-id-card-component-owner-full-name-job-title-text",
("fullName", id.FullName),
diff --git a/Content.Server/Administration/Commands/AGhost.cs b/Content.Server/Administration/Commands/AGhost.cs
index 76a85bfa0c..dce4d9d4a2 100644
--- a/Content.Server/Administration/Commands/AGhost.cs
+++ b/Content.Server/Administration/Commands/AGhost.cs
@@ -44,7 +44,7 @@ namespace Content.Server.Administration.Commands
var coordinates = player.AttachedEntity != null
? _entities.GetComponent(player.AttachedEntity.Value).Coordinates
: EntitySystem.Get().GetObserverSpawnPoint();
- var ghost = _entities.SpawnEntity("AdminObserver", coordinates.ToMap(_entities));
+ var ghost = _entities.SpawnEntity("AdminObserver", coordinates);
if (canReturn)
{
diff --git a/Content.Server/Atmos/Miasma/MiasmaSystem.cs b/Content.Server/Atmos/Miasma/MiasmaSystem.cs
index df066048c0..3e5ba01e02 100644
--- a/Content.Server/Atmos/Miasma/MiasmaSystem.cs
+++ b/Content.Server/Atmos/Miasma/MiasmaSystem.cs
@@ -17,7 +17,6 @@ namespace Content.Server.Atmos.Miasma
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
-
[Dependency] private readonly IRobustRandom _random = default!;
/// System Variables
diff --git a/Content.Server/Chat/Systems/ChatSystem.Radio.cs b/Content.Server/Chat/Systems/ChatSystem.Radio.cs
index e764b9a387..a317d07afc 100644
--- a/Content.Server/Chat/Systems/ChatSystem.Radio.cs
+++ b/Content.Server/Chat/Systems/ChatSystem.Radio.cs
@@ -74,7 +74,7 @@ public sealed partial class ChatSystem
}
// Re-capitalize message since we removed the prefix.
- message = SanitizeMessageCapital(source, message);
+ message = SanitizeMessageCapital(message);
if (_inventory.TryGetSlotEntity(source, "ears", out var entityUid) &&
TryComp(entityUid, out HeadsetComponent? headset))
diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs
index 586b2e78d8..e6bb375f22 100644
--- a/Content.Server/Chat/Systems/ChatSystem.cs
+++ b/Content.Server/Chat/Systems/ChatSystem.cs
@@ -127,8 +127,9 @@ public sealed partial class ChatSystem : SharedChatSystem
return;
bool shouldCapitalize = (desiredType != InGameICChatType.Emote);
+ bool shouldPunctuate = _configurationManager.GetCVar(CCVars.ChatPunctuation);
- message = SanitizeInGameICMessage(source, message, out var emoteStr, shouldCapitalize);
+ message = SanitizeInGameICMessage(source, message, out var emoteStr, shouldCapitalize, shouldPunctuate);
// Was there an emote in the message? If so, send it.
if (player != null && emoteStr != message && emoteStr != null)
@@ -423,11 +424,13 @@ public sealed partial class ChatSystem : SharedChatSystem
}
// ReSharper disable once InconsistentNaming
- private string SanitizeInGameICMessage(EntityUid source, string message, out string? emoteStr, bool capitalize = true)
+ private string SanitizeInGameICMessage(EntityUid source, string message, out string? emoteStr, bool capitalize = true, bool punctuate = false)
{
var newMessage = message.Trim();
if (capitalize)
- newMessage = SanitizeMessageCapital(source, newMessage);
+ newMessage = SanitizeMessageCapital(newMessage);
+ if (punctuate)
+ newMessage = SanitizeMessagePeriod(newMessage);
newMessage = FormattedMessage.EscapeText(newMessage);
_sanitizer.TrySanitizeOutSmilies(newMessage, source, out newMessage, out emoteStr);
@@ -460,7 +463,7 @@ public sealed partial class ChatSystem : SharedChatSystem
.Select(p => p.ConnectedClient);
}
- private string SanitizeMessageCapital(EntityUid source, string message)
+ private string SanitizeMessageCapital(string message)
{
if (string.IsNullOrEmpty(message))
return message;
@@ -469,6 +472,16 @@ public sealed partial class ChatSystem : SharedChatSystem
return message;
}
+ private string SanitizeMessagePeriod(string message)
+ {
+ if (string.IsNullOrEmpty(message))
+ return message;
+ // Adds a period if the last character is a letter.
+ if (char.IsLetter(message[^1]))
+ message += ".";
+ return message;
+ }
+
private void ClientDistanceToList(EntityUid source, int voiceRange, List playerSessions)
{
var ghosts = GetEntityQuery();
diff --git a/Content.Server/Chemistry/TileReactions/SpillTileReaction.cs b/Content.Server/Chemistry/TileReactions/SpillTileReaction.cs
index 7744746ee1..5f8113185f 100644
--- a/Content.Server/Chemistry/TileReactions/SpillTileReaction.cs
+++ b/Content.Server/Chemistry/TileReactions/SpillTileReaction.cs
@@ -1,10 +1,11 @@
-using Content.Server.Fluids.EntitySystems;
+using Content.Server.Fluids.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Content.Shared.Slippery;
using Content.Shared.StepTrigger;
+using Content.Shared.StepTrigger.Components;
using Content.Shared.StepTrigger.Systems;
using JetBrains.Annotations;
using Robust.Shared.Map;
@@ -24,18 +25,21 @@ namespace Content.Server.Chemistry.TileReactions
{
if (reactVolume < 5) return FixedPoint2.Zero;
+ var entityManager = IoCManager.Resolve();
+
// TODO Make this not puddle smear.
- var puddle = EntitySystem.Get()
+ var puddle = entityManager.EntitySysManager.GetEntitySystem()
.SpillAt(tile, new Solution(reagent.ID, reactVolume), "PuddleSmear", _overflow, false, true);
if (puddle != null)
{
- var entityManager = IoCManager.Resolve();
- var slippery = entityManager.GetComponent(puddle.Owner);
+ var slippery = entityManager.EnsureComponent(puddle.Owner);
slippery.LaunchForwardsMultiplier = _launchForwardsMultiplier;
- EntitySystem.Get().SetRequiredTriggerSpeed(puddle.Owner, _requiredSlipSpeed);
slippery.ParalyzeTime = _paralyzeTime;
+ var step = entityManager.EnsureComponent(puddle.Owner);
+ entityManager.EntitySysManager.GetEntitySystem().SetRequiredTriggerSpeed(puddle.Owner, _requiredSlipSpeed, step);
+
return reactVolume;
}
diff --git a/Content.Server/Climbing/ClimbSystem.cs b/Content.Server/Climbing/ClimbSystem.cs
index c43e26b4a9..d9f8e87cb0 100644
--- a/Content.Server/Climbing/ClimbSystem.cs
+++ b/Content.Server/Climbing/ClimbSystem.cs
@@ -116,13 +116,13 @@ public sealed class ClimbSystem : SharedClimbSystem
if (TryBonk(component, user))
return;
- _doAfterSystem.DoAfter(new DoAfterEventArgs(entityToMove, component.ClimbDelay, default, climbable)
+ _doAfterSystem.DoAfter(new DoAfterEventArgs(entityToMove, component.ClimbDelay, default, climbable, user)
{
BreakOnTargetMove = true,
BreakOnUserMove = true,
BreakOnDamage = true,
BreakOnStun = true,
- UserFinishedEvent = new ClimbFinishedEvent(user, climbable)
+ UserFinishedEvent = new ClimbFinishedEvent(user, climbable, entityToMove)
});
}
@@ -155,10 +155,10 @@ public sealed class ClimbSystem : SharedClimbSystem
private void OnClimbFinished(EntityUid uid, ClimbingComponent climbing, ClimbFinishedEvent args)
{
- Climb(uid, args.User, args.Climbable, climbing: climbing);
+ Climb(uid, args.User, args.Instigator, args.Climbable, climbing: climbing);
}
- private void Climb(EntityUid uid, EntityUid user, EntityUid climbable, bool silent = false, ClimbingComponent? climbing = null,
+ private void Climb(EntityUid uid, EntityUid user, EntityUid instigator, EntityUid climbable, bool silent = false, ClimbingComponent? climbing = null,
PhysicsComponent? physics = null, FixturesComponent? fixtures = null)
{
if (!Resolve(uid, ref climbing, ref physics, ref fixtures, false))
@@ -174,7 +174,7 @@ public sealed class ClimbSystem : SharedClimbSystem
// there's also the cases where the user might collide with the person they are forcing onto the climbable that i haven't accounted for
RaiseLocalEvent(uid, new StartClimbEvent(climbable), false);
- RaiseLocalEvent(climbable, new ClimbedOnEvent(uid), false);
+ RaiseLocalEvent(climbable, new ClimbedOnEvent(uid, user), false);
if (silent)
return;
@@ -343,7 +343,7 @@ public sealed class ClimbSystem : SharedClimbSystem
public void ForciblySetClimbing(EntityUid uid, EntityUid climbable, ClimbingComponent? component = null)
{
- Climb(uid, uid, climbable, true, component);
+ Climb(uid, uid, uid, climbable, true, component);
}
private void OnBuckleChange(EntityUid uid, ClimbingComponent component, BuckleChangeEvent args)
@@ -436,14 +436,16 @@ public sealed class ClimbSystem : SharedClimbSystem
internal sealed class ClimbFinishedEvent : EntityEventArgs
{
- public ClimbFinishedEvent(EntityUid user, EntityUid climbable)
+ public ClimbFinishedEvent(EntityUid user, EntityUid climbable, EntityUid instigator)
{
User = user;
Climbable = climbable;
+ Instigator = instigator;
}
public EntityUid User { get; }
public EntityUid Climbable { get; }
+ public EntityUid Instigator { get; }
}
///
@@ -452,10 +454,12 @@ internal sealed class ClimbFinishedEvent : EntityEventArgs
public sealed class ClimbedOnEvent : EntityEventArgs
{
public EntityUid Climber;
+ public EntityUid Instigator;
- public ClimbedOnEvent(EntityUid climber)
+ public ClimbedOnEvent(EntityUid climber, EntityUid instigator)
{
Climber = climber;
+ Instigator = instigator;
}
}
diff --git a/Content.Server/Cloning/CloningConsoleSystem.cs b/Content.Server/Cloning/CloningConsoleSystem.cs
index 8026c5ea46..b57b4db96c 100644
--- a/Content.Server/Cloning/CloningConsoleSystem.cs
+++ b/Content.Server/Cloning/CloningConsoleSystem.cs
@@ -7,8 +7,8 @@ using Content.Server.Mind.Components;
using Content.Server.MachineLinking.System;
using Content.Server.MachineLinking.Events;
using Content.Server.UserInterface;
-using Content.Shared.MobState.Components;
using Content.Server.MobState;
+using Content.Shared.MobState.Components;
using Content.Server.Power.EntitySystems;
using Robust.Server.GameObjects;
using Robust.Server.Player;
@@ -55,10 +55,6 @@ namespace Content.Server.Cloning.Systems
if (consoleComponent.GeneticScanner != null && consoleComponent.CloningPod != null)
TryClone(uid, consoleComponent.CloningPod.Value, consoleComponent.GeneticScanner.Value, consoleComponent: consoleComponent);
break;
- case UiButton.Eject:
- if (consoleComponent.CloningPod != null)
- TryEject(uid, consoleComponent.CloningPod.Value, consoleComponent: consoleComponent);
- break;
}
UpdateUserInterface(consoleComponent);
}
@@ -123,14 +119,6 @@ namespace Content.Server.Cloning.Systems
_uiSystem.GetUiOrNull(consoleComponent.Owner, CloningConsoleUiKey.Key)?.SetState(newState);
}
- public void TryEject(EntityUid uid, EntityUid clonePodUid, CloningPodComponent? cloningPod = null, CloningConsoleComponent? consoleComponent = null)
- {
- if (!Resolve(uid, ref consoleComponent) || !Resolve(clonePodUid, ref cloningPod))
- return;
-
- _cloningSystem.Eject(clonePodUid, cloningPod);
- }
-
public void TryClone(EntityUid uid, EntityUid cloningPodUid, EntityUid scannerUid, CloningPodComponent? cloningPod = null, MedicalScannerComponent? scannerComp = null, CloningConsoleComponent? consoleComponent = null)
{
if (!Resolve(uid, ref consoleComponent) || !Resolve(cloningPodUid, ref cloningPod) || !Resolve(scannerUid, ref scannerComp))
@@ -222,9 +210,10 @@ namespace Content.Server.Cloning.Systems
EntityUid? cloneBody = clonePod.BodyContainer.ContainedEntity;
clonerMindPresent = clonePod.Status == CloningPodStatus.Cloning;
- if (cloneBody != null)
+ if (HasComp(consoleComponent.CloningPod))
{
- cloneBodyInfo = Identity.Name(cloneBody.Value, EntityManager);
+ if (cloneBody != null)
+ cloneBodyInfo = Identity.Name(cloneBody.Value, EntityManager);
clonerStatus = ClonerStatus.ClonerOccupied;
}
}
@@ -244,5 +233,6 @@ namespace Content.Server.Cloning.Systems
clonerInRange
);
}
+
}
}
diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs
index be67623a00..689f504b29 100644
--- a/Content.Server/Cloning/CloningSystem.cs
+++ b/Content.Server/Cloning/CloningSystem.cs
@@ -1,19 +1,35 @@
-using Content.Server.Cloning.Components;
-using Content.Server.Mind.Components;
-using Content.Server.Power.EntitySystems;
using Content.Shared.GameTicking;
using Content.Shared.CharacterAppearance.Systems;
using Content.Shared.CharacterAppearance.Components;
using Content.Shared.Species;
-using Robust.Server.Player;
-using Robust.Shared.Prototypes;
-using Content.Server.EUI;
-using Robust.Shared.Containers;
-using Robust.Server.Containers;
+using Content.Shared.Damage;
+using Content.Shared.Stacks;
+using Content.Shared.Examine;
using Content.Shared.Cloning;
+using Content.Shared.Atmos;
+using Content.Shared.CCVar;
+using Content.Server.Cloning.Components;
+using Content.Server.Mind.Components;
+using Content.Server.Power.EntitySystems;
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.EUI;
using Content.Server.MachineLinking.System;
using Content.Server.MachineLinking.Events;
using Content.Server.MobState;
+using Content.Server.Lathe.Components;
+using Content.Shared.Chemistry.Components;
+using Content.Server.Fluids.EntitySystems;
+using Content.Server.Chat.Systems;
+using Content.Server.Construction.Components;
+using Content.Server.Stack;
+using Robust.Server.GameObjects;
+using Robust.Server.Containers;
+using Robust.Server.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Configuration;
+using Robust.Shared.Containers;
+
namespace Content.Server.Cloning.Systems
{
@@ -28,17 +44,29 @@ namespace Content.Server.Cloning.Systems
[Dependency] private readonly ContainerSystem _containerSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
+ [Dependency] private readonly IRobustRandom _robustRandom = default!;
+ [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
+ [Dependency] private readonly TransformSystem _transformSystem = default!;
+ [Dependency] private readonly SharedStackSystem _stackSystem = default!;
+ [Dependency] private readonly StackSystem _serverStackSystem = default!;
+ [Dependency] private readonly SpillableSystem _spillableSystem = default!;
+ [Dependency] private readonly ChatSystem _chatSystem = default!;
+ [Dependency] private readonly IConfigurationManager _configManager = default!;
+
public readonly Dictionary ClonesWaitingForMind = new();
+ public const float EasyModeCloningCost = 0.7f;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnComponentInit);
+ SubscribeLocalEvent(OnDeconstruct);
SubscribeLocalEvent(Reset);
SubscribeLocalEvent(HandleMindAdded);
SubscribeLocalEvent(OnPortDisconnected);
SubscribeLocalEvent(OnAnchor);
+ SubscribeLocalEvent(OnExamined);
}
private void OnComponentInit(EntityUid uid, CloningPodComponent clonePod, ComponentInit args)
@@ -47,6 +75,14 @@ namespace Content.Server.Cloning.Systems
_signalSystem.EnsureReceiverPorts(uid, CloningPodComponent.PodPort);
}
+ private void OnDeconstruct(EntityUid uid, CloningPodComponent component, MachineDeconstructedEvent args)
+ {
+ if (!TryComp(uid, out var storage))
+ return;
+
+ _serverStackSystem.SpawnMultiple(storage.GetMaterialAmount("Biomass"), 100, "Biomass", Transform(uid).Coordinates);
+ }
+
private void UpdateAppearance(CloningPodComponent clonePod)
{
if (TryComp(clonePod.Owner, out var appearance))
@@ -97,11 +133,23 @@ namespace Content.Server.Cloning.Systems
_cloningConsoleSystem.UpdateUserInterface(console);
}
+ private void OnExamined(EntityUid uid, CloningPodComponent component, ExaminedEvent args)
+ {
+ if (!args.IsInDetailsRange || !_powerReceiverSystem.IsPowered(uid))
+ return;
+
+ if (TryComp(uid, out var storage))
+ args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", storage.GetMaterialAmount("Biomass"))));
+ }
+
public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Mind.Mind mind, CloningPodComponent? clonePod)
{
if (!Resolve(uid, ref clonePod) || bodyToClone == null)
return false;
+ if (HasComp(uid))
+ return false;
+
if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
{
if (EntityManager.EntityExists(clone) &&
@@ -120,11 +168,56 @@ namespace Content.Server.Cloning.Systems
if (mind.UserId == null || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client))
return false; // If we can't track down the client, we can't offer transfer. That'd be quite bad.
+ if (!TryComp(clonePod.Owner, out var podStorage))
+ return false;
+
if (!TryComp(bodyToClone, out var humanoid))
return false; // whatever body was to be cloned, was not a humanoid
- var speciesProto = _prototype.Index(humanoid.Species).Prototype;
- var mob = Spawn(speciesProto, Transform(clonePod.Owner).MapPosition);
+ if (!_prototype.TryIndex(humanoid.Species, out var speciesPrototype))
+ return false;
+
+ if (!TryComp(bodyToClone, out var physics))
+ return false;
+
+ int cloningCost = (int) physics.FixturesMass;
+
+ if (_configManager.GetCVar(CCVars.BiomassEasyMode))
+ cloningCost = (int) Math.Round(cloningCost * EasyModeCloningCost);
+
+ // biomass checks
+ var biomassAmount = podStorage.GetMaterialAmount("Biomass");
+
+ if (biomassAmount < cloningCost)
+ {
+ if (clonePod.ConnectedConsole != null)
+ _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-chat-error", ("units", cloningCost)), InGameICChatType.Speak, false);
+ return false;
+ }
+
+ podStorage.RemoveMaterial("Biomass", cloningCost);
+ clonePod.UsedBiomass = cloningCost;
+ // end of biomass checks
+
+ // genetic damage checks
+ if (TryComp(bodyToClone, out var damageable) &&
+ damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg))
+ {
+ var chance = Math.Clamp((float) (cellularDmg / 100), 0, 1);
+ if (cellularDmg > 0 && clonePod.ConnectedConsole != null)
+ _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - (chance * 100)))), InGameICChatType.Speak, false);
+
+ if (_robustRandom.Prob(chance))
+ {
+ UpdateStatus(CloningPodStatus.Gore, clonePod);
+ clonePod.FailedClone = true;
+ AddComp(uid);
+ return true;
+ }
+ }
+ // end of genetic damage checks
+
+ var mob = Spawn(speciesPrototype.Prototype, Transform(clonePod.Owner).MapPosition);
_appearanceSystem.UpdateAppearance(mob, humanoid.Appearance);
_appearanceSystem.UpdateSexGender(mob, humanoid.Sex, humanoid.Gender);
@@ -156,16 +249,17 @@ namespace Content.Server.Cloning.Systems
if (!_powerReceiverSystem.IsPowered(cloning.Owner))
continue;
- if (cloning.BodyContainer.ContainedEntity != null)
- {
- cloning.CloningProgress += frameTime;
- cloning.CloningProgress = MathHelper.Clamp(cloning.CloningProgress, 0f, cloning.CloningTime);
- }
+ if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
+ continue;
- if (cloning.CapturedMind?.Session?.AttachedEntity == cloning.BodyContainer.ContainedEntity)
- {
+ cloning.CloningProgress += frameTime;
+ if (cloning.CloningProgress < cloning.CloningTime)
+ continue;
+
+ if (cloning.FailedClone)
+ EndFailedCloning(cloning.Owner, cloning);
+ else
Eject(cloning.Owner, cloning);
- }
}
}
@@ -181,10 +275,40 @@ namespace Content.Server.Cloning.Systems
clonePod.BodyContainer.Remove(entity);
clonePod.CapturedMind = null;
clonePod.CloningProgress = 0f;
+ clonePod.UsedBiomass = 0;
UpdateStatus(CloningPodStatus.Idle, clonePod);
RemCompDeferred(uid);
}
+ private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
+ {
+ clonePod.FailedClone = false;
+ clonePod.CloningProgress = 0f;
+ UpdateStatus(CloningPodStatus.Idle, clonePod);
+ var transform = Transform(uid);
+ var indices = _transformSystem.GetGridOrMapTilePosition(uid);
+
+ var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
+
+ Solution bloodSolution = new();
+
+ int i = 0;
+ while (i < 1)
+ {
+ tileMix?.AdjustMoles(Gas.Miasma, 6f);
+ bloodSolution.AddReagent("Blood", 50);
+ if (_robustRandom.Prob(0.2f))
+ i++;
+ }
+ _spillableSystem.SpillAt(uid, bloodSolution, "PuddleBlood");
+
+ var biomassStack = Spawn("MaterialBiomass", transform.Coordinates);
+ _stackSystem.SetCount(biomassStack, _robustRandom.Next(1, (int) (clonePod.UsedBiomass / 2.5)));
+
+ clonePod.UsedBiomass = 0;
+ RemCompDeferred(uid);
+ }
+
public void Reset(RoundRestartCleanupEvent ev)
{
ClonesWaitingForMind.Clear();
diff --git a/Content.Server/Cloning/Components/CloningPodComponent.cs b/Content.Server/Cloning/Components/CloningPodComponent.cs
index b76f31f3ed..8c8fe37ef2 100644
--- a/Content.Server/Cloning/Components/CloningPodComponent.cs
+++ b/Content.Server/Cloning/Components/CloningPodComponent.cs
@@ -10,6 +10,8 @@ namespace Content.Server.Cloning.Components
[ViewVariables] public ContainerSlot BodyContainer = default!;
[ViewVariables] public Mind.Mind? CapturedMind;
[ViewVariables] public float CloningProgress = 0;
+ [ViewVariables] public int UsedBiomass = 70;
+ [ViewVariables] public bool FailedClone = false;
[DataField("cloningTime")]
[ViewVariables] public float CloningTime = 30f;
[ViewVariables] public CloningPodStatus Status;
diff --git a/Content.Server/Construction/MachineFrameSystem.cs b/Content.Server/Construction/MachineFrameSystem.cs
index b62925f12e..5aa6175e0e 100644
--- a/Content.Server/Construction/MachineFrameSystem.cs
+++ b/Content.Server/Construction/MachineFrameSystem.cs
@@ -1,6 +1,7 @@
-using Content.Server.Construction.Components;
+using Content.Server.Construction.Components;
using Content.Server.Stack;
using Content.Shared.Construction;
+using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Tag;
using Robust.Shared.Containers;
@@ -22,6 +23,7 @@ public sealed class MachineFrameSystem : EntitySystem
SubscribeLocalEvent(OnInit);
SubscribeLocalEvent(OnStartup);
SubscribeLocalEvent(OnInteractUsing);
+ SubscribeLocalEvent(OnMachineFrameExamined);
}
private void OnInit(EntityUid uid, MachineFrameComponent component, ComponentInit args)
@@ -295,4 +297,11 @@ public sealed class MachineFrameSystem : EntitySystem
}
}
}
+ private void OnMachineFrameExamined(EntityUid uid, MachineFrameComponent component, ExaminedEvent args)
+ {
+ if (!args.IsInDetailsRange)
+ return;
+ if (component.HasBoard)
+ args.PushMarkup(Loc.GetString("machine-frame-component-on-examine-label", ("board", EntityManager.GetComponent(component.BoardContainer.ContainedEntities[0]).EntityName)));
+ }
}
diff --git a/Content.Server/CrewManifest/CrewManifestEui.cs b/Content.Server/CrewManifest/CrewManifestEui.cs
index 82a64987ee..0f9394f53a 100644
--- a/Content.Server/CrewManifest/CrewManifestEui.cs
+++ b/Content.Server/CrewManifest/CrewManifestEui.cs
@@ -40,8 +40,15 @@ public sealed class CrewManifestEui : BaseEui
switch (msg)
{
case CrewManifestEuiClosed:
- _crewManifest.CloseEui(_station, Player, Owner);
+ Closed();
break;
}
}
+
+ public override void Closed()
+ {
+ base.Closed();
+
+ _crewManifest.CloseEui(_station, Player, Owner);
+ }
}
diff --git a/Content.Server/CrewManifest/CrewManifestSystem.cs b/Content.Server/CrewManifest/CrewManifestSystem.cs
index 9ce621ae76..1064379a17 100644
--- a/Content.Server/CrewManifest/CrewManifestSystem.cs
+++ b/Content.Server/CrewManifest/CrewManifestSystem.cs
@@ -186,8 +186,8 @@ public sealed class CrewManifestSystem : EntitySystem
if (eui.Owner == owner)
{
- eui.Close();
euis.Remove(session);
+ eui.Close();
}
if (euis.Count == 0)
diff --git a/Content.Server/Destructible/Thresholds/Behaviors/EjectVendorItems.cs b/Content.Server/Destructible/Thresholds/Behaviors/EjectVendorItems.cs
index c0d267f0d2..f4dcc3ebce 100644
--- a/Content.Server/Destructible/Thresholds/Behaviors/EjectVendorItems.cs
+++ b/Content.Server/Destructible/Thresholds/Behaviors/EjectVendorItems.cs
@@ -1,6 +1,4 @@
using Content.Server.VendingMachines;
-using Content.Shared.Throwing;
-using Robust.Shared.Random;
namespace Content.Server.Destructible.Thresholds.Behaviors
{
@@ -30,17 +28,15 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
!system.EntityManager.TryGetComponent(owner, out var xform))
return;
- var throwingsys = system.EntityManager.EntitySysManager.GetEntitySystem();
- var totalItems = vendingcomp.AllInventory.Count;
+ var vendingMachineSystem = EntitySystem.Get();
+ var inventory = vendingMachineSystem.GetAvailableInventory(owner, vendingcomp);
+ if (inventory.Count <= 0)
+ return;
- var toEject = Math.Min(totalItems * Percent, Max);
+ var toEject = Math.Min(inventory.Count * Percent, Max);
for (var i = 0; i < toEject; i++)
{
- var entity = system.EntityManager.SpawnEntity(system.Random.PickAndTake(vendingcomp.AllInventory).ID, xform.Coordinates);
-
- float range = vendingcomp.NonLimitedEjectRange;
- Vector2 direction = new Vector2(system.Random.NextFloat(-range, range), system.Random.NextFloat(-range, range));
- throwingsys.TryThrow(entity, direction, vendingcomp.NonLimitedEjectForce);
+ vendingMachineSystem.EjectRandom(owner, throwItem: true, forceEject: true, vendingcomp);
}
}
}
diff --git a/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs b/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs
index 999e08180a..2bd21ca207 100644
--- a/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs
+++ b/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs
@@ -53,7 +53,6 @@ public sealed partial class EnsnareableSystem
///
/// Used where you want to try to ensnare an entity with the
///
- /// The entity that will be used to ensnare
/// The entity that will be ensnared
/// The ensnaring component
public void TryEnsnare(EntityUid target, EnsnaringComponent component)
@@ -68,7 +67,7 @@ public sealed partial class EnsnareableSystem
UpdateAlert(ensnareable);
var ev = new EnsnareEvent(component.WalkSpeed, component.SprintSpeed);
- RaiseLocalEvent(target, ev, false);
+ RaiseLocalEvent(target, ev);
}
///
@@ -79,7 +78,7 @@ public sealed partial class EnsnareableSystem
public void TryFree(EntityUid target, EnsnaringComponent component, EntityUid? user = null)
{
//Don't do anything if they don't have the ensnareable component.
- if (!TryComp(target, out var ensnareable))
+ if (!HasComp(target))
return;
if (component.CancelToken != null)
@@ -132,7 +131,7 @@ public sealed partial class EnsnareableSystem
UpdateAlert(ensnareable);
var ev = new EnsnareRemoveEvent();
- RaiseLocalEvent(component.Owner, ev, false);
+ RaiseLocalEvent(component.Owner, ev);
}
public void UpdateAlert(EnsnareableComponent component)
diff --git a/Content.Server/Ensnaring/EnsnareableSystem.cs b/Content.Server/Ensnaring/EnsnareableSystem.cs
index fb5ab949b7..e9ff866da8 100644
--- a/Content.Server/Ensnaring/EnsnareableSystem.cs
+++ b/Content.Server/Ensnaring/EnsnareableSystem.cs
@@ -44,7 +44,7 @@ public sealed partial class EnsnareableSystem : SharedEnsnareableSystem
UpdateAlert(component);
var ev = new EnsnareRemoveEvent();
- RaiseLocalEvent(uid, ev, false);
+ RaiseLocalEvent(uid, ev);
ensnaring.CancelToken = null;
}
diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs
index 87513df99d..6ebcf59f2e 100644
--- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs
+++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs
@@ -158,7 +158,9 @@ public sealed partial class ExplosionSystem : EntitySystem
damagePerIntensity += value * Math.Max(0, ev.DamageCoefficient);
}
- explosionTolerance[index] = (float) ((totalDamageTarget - damageable.TotalDamage) / damagePerIntensity);
+ explosionTolerance[index] = damagePerIntensity > 0
+ ? (float) ((totalDamageTarget - damageable.TotalDamage) / damagePerIntensity)
+ : float.MaxValue;
}
return explosionTolerance;
diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.TimedCollide.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.TimedCollide.cs
index 54b13f76d1..3251702469 100644
--- a/Content.Server/Explosion/EntitySystems/TriggerSystem.TimedCollide.cs
+++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.TimedCollide.cs
@@ -19,6 +19,8 @@ public sealed partial class TriggerSystem
//Ensures the entity trigger will have an active component
EnsureComp(uid);
var otherUID = args.OtherFixture.Body.Owner;
+ if (component.Colliding.ContainsKey(otherUID))
+ return;
component.Colliding.Add(otherUID, 0);
}
diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs
index 3bdf14336d..57541b289a 100644
--- a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs
+++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs
@@ -126,7 +126,8 @@ namespace Content.Server.Explosion.EntitySystems
{
// Check if entity is bomb/mod. grenade/etc
if (_container.TryGetContainer(uid, "payload", out IContainer? container) &&
- TryComp(container.ContainedEntities.First(), out ChemicalPayloadComponent? chemicalPayloadComponent))
+ container.ContainedEntities.Count > 0 &&
+ TryComp(container.ContainedEntities[0], out ChemicalPayloadComponent? chemicalPayloadComponent))
{
// If a beaker is missing, the entity won't explode, so no reason to log it
if (!TryComp(chemicalPayloadComponent?.BeakerSlotA.Item, out SolutionContainerManagerComponent? beakerA) ||
diff --git a/Content.Server/Flash/FlashSystem.cs b/Content.Server/Flash/FlashSystem.cs
index 4a1803edbc..c5784f2b94 100644
--- a/Content.Server/Flash/FlashSystem.cs
+++ b/Content.Server/Flash/FlashSystem.cs
@@ -165,7 +165,7 @@ namespace Content.Server.Flash
foreach (var entity in flashableEntities)
{
// Check for unobstructed entities while ignoring the mobs with flashable components.
- if (!_interactionSystem.InRangeUnobstructed(entity, mapPosition, range, CollisionGroup.Opaque, (e) => flashableEntities.Contains(e)))
+ if (!_interactionSystem.InRangeUnobstructed(entity, mapPosition, range, CollisionGroup.Opaque, (e) => flashableEntities.Contains(e) || e == source))
continue;
// They shouldn't have flash removed in between right?
diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs
index 967971ad85..1626d9f8e5 100644
--- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs
+++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs
@@ -121,10 +121,9 @@ namespace Content.Server.GameTicking
var (entities, gridIds) = _mapLoader.LoadMap(targetMapId, ev.GameMap.MapPath.ToString(), ev.Options);
- var gridUids = gridIds.Select(g => (EntityUid)g).ToList();
+ var gridUids = gridIds.Select(g => g).ToList();
RaiseLocalEvent(new PostGameMapLoad(map, targetMapId, entities, gridUids, stationName));
- _spawnPoint = _mapManager.GetGrid(gridIds[0]).ToCoordinates();
return (entities, gridUids);
}
diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs
index 55f3fd8040..bdc37c6d29 100644
--- a/Content.Server/GameTicking/GameTicker.Spawning.cs
+++ b/Content.Server/GameTicking/GameTicker.Spawning.cs
@@ -25,9 +25,6 @@ namespace Content.Server.GameTicking
{
private const string ObserverPrototypeName = "MobObserver";
- [ViewVariables(VVAccess.ReadWrite), Obsolete("Due for removal when observer spawning is refactored.")] // See also: MindComponent's OnShutdown shitcode
- private EntityCoordinates _spawnPoint;
-
///
/// How many players have joined the round through normal methods.
/// Useful for game rules to look at. Doesn't count observers, people in lobby, etc.
@@ -281,24 +278,72 @@ namespace Content.Server.GameTicking
#region Spawn Points
public EntityCoordinates GetObserverSpawnPoint()
{
- // TODO rename this to TryGetObserverSpawnPoint to make it clear that the result might be invalid. Or at
- // least try try more fallback values, like randomly spawning them in any available map or just creating a
- // "we fucked up" map. Its better than dumping them into the void.
-
- var location = _spawnPoint.IsValid(EntityManager) ? _spawnPoint : EntityCoordinates.Invalid;
-
_possiblePositions.Clear();
foreach (var (point, transform) in EntityManager.EntityQuery(true))
{
- if (point.SpawnType == SpawnPointType.Observer)
- _possiblePositions.Add(transform.Coordinates);
+ if (point.SpawnType != SpawnPointType.Observer)
+ continue;
+
+ _possiblePositions.Add(transform.Coordinates);
+ }
+
+ var metaQuery = GetEntityQuery();
+
+ // Fallback to a random grid.
+ if (_possiblePositions.Count == 0)
+ {
+ foreach (var grid in _mapManager.GetAllGrids())
+ {
+ if (!metaQuery.TryGetComponent(grid.GridEntityId, out var meta) ||
+ meta.EntityPaused)
+ {
+ continue;
+ }
+
+ _possiblePositions.Add(new EntityCoordinates(grid.GridEntityId, Vector2.Zero));
+ }
}
if (_possiblePositions.Count != 0)
- location = _robustRandom.Pick(_possiblePositions);
+ {
+ // TODO: This is just here for the eye lerping.
+ // Ideally engine would just spawn them on grid directly I guess? Right now grid traversal is handling it during
+ // update which means we need to add a hack somewhere around it.
+ var spawn = _robustRandom.Pick(_possiblePositions);
+ var toMap = spawn.ToMap(EntityManager);
- return location;
+ if (_mapManager.TryFindGridAt(toMap, out var foundGrid))
+ {
+ return new EntityCoordinates(foundGrid.GridEntityId,
+ foundGrid.InvWorldMatrix.Transform(toMap.Position));
+ }
+
+ return spawn;
+ }
+
+ if (_mapManager.MapExists(DefaultMap))
+ {
+ return new EntityCoordinates(_mapManager.GetMapEntityId(DefaultMap), Vector2.Zero);
+ }
+
+ // Just pick a point at this point I guess.
+ foreach (var map in _mapManager.GetAllMapIds())
+ {
+ var mapUid = _mapManager.GetMapEntityId(map);
+
+ if (!metaQuery.TryGetComponent(mapUid, out var meta) ||
+ meta.EntityPaused)
+ {
+ continue;
+ }
+
+ return new EntityCoordinates(mapUid, Vector2.Zero);
+ }
+
+ // AAAAAAAAAAAAA
+ _sawmill.Error("Found no observer spawn points!");
+ return EntityCoordinates.Invalid;
}
#endregion
}
diff --git a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs
index d42effa334..ecb826cdca 100644
--- a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs
@@ -2,8 +2,8 @@ using System.Linq;
using Content.Server.Actions;
using Content.Server.Chat.Managers;
using Content.Server.Disease;
-using Content.Server.GameTicking.Rules.Configurations;
using Content.Server.Mind.Components;
+using Content.Server.MobState;
using Content.Server.Players;
using Content.Server.Popups;
using Content.Server.Preferences.Managers;
@@ -13,7 +13,6 @@ using Content.Server.Zombies;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.CCVar;
using Content.Shared.CharacterAppearance.Components;
-using Content.Shared.FixedPoint;
using Content.Shared.MobState;
using Content.Shared.MobState.Components;
using Content.Shared.Preferences;
@@ -40,6 +39,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem
[Dependency] private readonly DiseaseSystem _diseaseSystem = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly ActionsSystem _action = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly ZombifyOnDeathSystem _zombify = default!;
private Dictionary _initialInfectedNames = new();
@@ -90,8 +90,8 @@ public sealed class ZombieRuleSystem : GameRuleSystem
("username", player.Value)));
}
- ///Gets a bunch of the living players and displays them if they're under a threshold.
- ///InitialInfected is used for the threshold because it scales with the player count well.
+ //Gets a bunch of the living players and displays them if they're under a threshold.
+ //InitialInfected is used for the threshold because it scales with the player count well.
if (livingHumans.Count > 0 && livingHumans.Count <= _initialInfectedNames.Count)
{
ev.AddLine("");
@@ -203,16 +203,16 @@ public sealed class ZombieRuleSystem : GameRuleSystem
livingHumans = new();
- foreach (var ent in allPlayers)
+ foreach (var (_, mob) in allPlayers)
{
- if (ent.Item2.IsAlive())
+ if (_mobState.IsAlive(mob.Owner, mob))
{
- totalPlayers.Add(ent.Item2.Owner);
+ totalPlayers.Add(mob.Owner);
- if (allZombers.HasComponent(ent.Item1.Owner))
- livingZombies.Add(ent.Item2.Owner);
+ if (allZombers.HasComponent(mob.Owner))
+ livingZombies.Add(mob.Owner);
else
- livingHumans.Add(ent.Item2.Owner);
+ livingHumans.Add(mob.Owner);
}
}
return ((float) livingZombies.Count) / (float) totalPlayers.Count;
@@ -300,8 +300,8 @@ public sealed class ZombieRuleSystem : GameRuleSystem
var messageWrapper = Loc.GetString("chat-manager-server-wrap-message");
//gets the names now in case the players leave.
- if (inCharacterName != null)
- _initialInfectedNames.Add(inCharacterName, mind.Session.Name);
+ //this gets unhappy if people with the same name get chose. Probably shouldn't happen.
+ _initialInfectedNames.Add(inCharacterName, mind.Session.Name);
// I went all the way to ChatManager.cs and all i got was this lousy T-shirt
_chatManager.ChatMessageToOne(Shared.Chat.ChatChannel.Server, Loc.GetString("zombie-patientzero-role-greeting"),
diff --git a/Content.Server/Info/InfoSystem.cs b/Content.Server/Info/InfoSystem.cs
new file mode 100644
index 0000000000..78b1adfa4a
--- /dev/null
+++ b/Content.Server/Info/InfoSystem.cs
@@ -0,0 +1,36 @@
+using Content.Shared.CCVar;
+using Content.Shared.Info;
+using Robust.Shared.Configuration;
+using Robust.Shared.ContentPack;
+using Robust.Shared.Log;
+
+namespace Content.Server.Info;
+
+public class InfoSystem : EntitySystem
+{
+ [Dependency] private readonly IResourceManager _res = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeNetworkEvent(OnRequestRules);
+ }
+
+ protected void OnRequestRules(RequestRulesMessage message, EntitySessionEventArgs eventArgs)
+ {
+ Logger.DebugS("info", "Client requested rules.");
+ var title = Loc.GetString(_cfg.GetCVar(CCVars.RulesHeader));
+ var path = _cfg.GetCVar(CCVars.RulesFile);
+ var rules = "Server could not read its rules.";
+ try
+ {
+ rules = _res.ContentFileReadAllText($"/Server Info/{path}");
+ }
+ catch (System.Exception)
+ {
+ Logger.ErrorS("info", "Could not read server rules file.");
+ }
+ var response = new RulesMessage(title, rules);
+ RaiseNetworkEvent(response, eventArgs.SenderSession.ConnectedClient);
+ }
+}
diff --git a/Content.Server/Kitchen/Components/ActiveMicrowaveComponent.cs b/Content.Server/Kitchen/Components/ActiveMicrowaveComponent.cs
new file mode 100644
index 0000000000..e8cfe86ab3
--- /dev/null
+++ b/Content.Server/Kitchen/Components/ActiveMicrowaveComponent.cs
@@ -0,0 +1,19 @@
+using Content.Shared.Kitchen;
+
+namespace Content.Server.Kitchen.Components;
+
+///
+/// Attached to a microwave that is currently in the process of cooking
+///
+[RegisterComponent]
+public sealed class ActiveMicrowaveComponent : Component
+{
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float CookTimeRemaining;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float TotalTime;
+
+ [ViewVariables]
+ public (FoodRecipePrototype?, int) PortionedRecipe;
+}
diff --git a/Content.Server/Kitchen/Components/MicrowaveComponent.cs b/Content.Server/Kitchen/Components/MicrowaveComponent.cs
index 60e2facc2c..1a613aad15 100644
--- a/Content.Server/Kitchen/Components/MicrowaveComponent.cs
+++ b/Content.Server/Kitchen/Components/MicrowaveComponent.cs
@@ -1,49 +1,34 @@
-using System.Linq;
-using Content.Server.Chemistry.Components.SolutionManager;
-using Content.Server.Chemistry.EntitySystems;
-using Content.Server.Power.Components;
-using Content.Server.Temperature.Components;
-using Content.Server.Temperature.Systems;
-using Content.Server.UserInterface;
-using Content.Shared.FixedPoint;
-using Content.Shared.Kitchen;
-using Content.Shared.Kitchen.Components;
-using Content.Shared.Power;
-using Content.Shared.Tag;
-using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
-using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Kitchen.Components
{
[RegisterComponent]
- public sealed class MicrowaveComponent : SharedMicrowaveComponent
+ public sealed class MicrowaveComponent : Component
{
- [Dependency] private readonly IEntityManager _entities = default!;
-
- [Dependency] private readonly RecipeManager _recipeManager = default!;
-
- #region YAMLSERIALIZE
-
- [DataField("cookTime")] private uint _cookTimeDefault = 5;
- [DataField("cookTimeMultiplier")] private int _cookTimeMultiplier = 1000; //For upgrades and stuff I guess?
- [DataField("failureResult")] private string _badRecipeName = "FoodBadRecipe";
-
- [DataField("beginCookingSound")] private SoundSpecifier _startCookingSound =
- new SoundPathSpecifier("/Audio/Machines/microwave_start_beep.ogg");
-
- [DataField("foodDoneSound")] private SoundSpecifier _cookingCompleteSound =
- new SoundPathSpecifier("/Audio/Machines/microwave_done_beep.ogg");
+ [DataField("cookTimeMultiplier")]
+ public int CookTimeMultiplier = 1; //For upgrades and stuff I guess? don't ask me.
+ [DataField("failureResult", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string BadRecipeEntityId = "FoodBadRecipe";
+ #region audio
+ [DataField("beginCookingSound")]
+ public SoundSpecifier StartCookingSound = new SoundPathSpecifier("/Audio/Machines/microwave_start_beep.ogg");
+ [DataField("foodDoneSound")]
+ public SoundSpecifier FoodDoneSound = new SoundPathSpecifier("/Audio/Machines/microwave_done_beep.ogg");
[DataField("clickSound")]
- private SoundSpecifier _clickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
-
+ public SoundSpecifier ClickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
+ [DataField("ItemBreakSound")]
public SoundSpecifier ItemBreakSound = new SoundPathSpecifier("/Audio/Effects/clang.ogg");
- #endregion YAMLSERIALIZE
+ public IPlayingAudioStream? PlayingStream { get; set; }
+ [DataField("loopingSound")]
+ public SoundSpecifier LoopingSound = new SoundPathSpecifier("/Audio/Machines/microwave_loop.ogg");
+ #endregion
- [ViewVariables] private bool _busy = false;
+ [ViewVariables]
public bool Broken;
///
@@ -51,7 +36,8 @@ namespace Content.Server.Kitchen.Components
/// The cook times for all recipes should be divisible by 5,with a minimum of 1 second.
/// For right now, I don't think any recipe cook time should be greater than 60 seconds.
///
- [ViewVariables] private uint _currentCookTimerTime = 1;
+ [DataField("currentCookTimerTime"), ViewVariables(VVAccess.ReadWrite)]
+ public uint CurrentCookTimerTime = 5;
///
/// The max temperature that this microwave can heat objects to.
@@ -59,403 +45,9 @@ namespace Content.Server.Kitchen.Components
[DataField("temperatureUpperThreshold")]
public float TemperatureUpperThreshold = 373.15f;
- public bool Powered => !_entities.TryGetComponent(Owner, out ApcPowerReceiverComponent? receiver) || receiver.Powered;
-
- private bool HasContents => Storage.ContainedEntities.Count > 0;
-
- public bool UIDirty = true;
- private bool _lostPower;
- private int _currentCookTimeButtonIndex;
-
- public void DirtyUi()
- {
- UIDirty = true;
- }
+ public int CurrentCookTimeButtonIndex;
public Container Storage = default!;
-
- [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(MicrowaveUiKey.Key);
-
- protected override void Initialize()
- {
- base.Initialize();
-
- _currentCookTimerTime = _cookTimeDefault;
-
- Storage = ContainerHelpers.EnsureContainer(Owner, "microwave_entity_container",
- out _);
-
- if (UserInterface != null)
- {
- UserInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage;
- }
- }
-
- public void SetCookTime(uint cookTime)
- {
- _currentCookTimerTime = cookTime;
- UIDirty = true;
- }
-
- private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage message)
- {
- if (!Powered || _busy)
- {
- return;
- }
-
- switch (message.Message)
- {
- case MicrowaveStartCookMessage:
- Wzhzhzh();
- break;
- case MicrowaveEjectMessage:
- if (HasContents)
- {
- EjectSolids();
- ClickSound();
- UIDirty = true;
- }
-
- break;
- case MicrowaveEjectSolidIndexedMessage msg:
- if (HasContents)
- {
- EjectSolid(msg.EntityID);
- ClickSound();
- UIDirty = true;
- }
- break;
-
- case MicrowaveSelectCookTimeMessage msg:
- _currentCookTimeButtonIndex = msg.ButtonIndex;
- _currentCookTimerTime = msg.NewCookTime;
- ClickSound();
- UIDirty = true;
- break;
- }
- }
-
- public void OnUpdate()
- {
- if (!Powered)
- {
- //TODO:If someone cuts power currently, microwave magically keeps going. FIX IT!
- SetAppearance(MicrowaveVisualState.Idle);
- }
-
- if (_busy && !Powered)
- {
- //we lost power while we were cooking/busy!
- _lostPower = true;
- EjectSolids();
- _busy = false;
- UIDirty = true;
- }
-
- if (_busy && Broken)
- {
- SetAppearance(MicrowaveVisualState.Broken);
- //we broke while we were cooking/busy!
- _lostPower = true;
- EjectSolids();
- _busy = false;
- UIDirty = true;
- }
-
- if (UIDirty)
- {
- UserInterface?.SetState(new MicrowaveUpdateUserInterfaceState
- (
- Storage.ContainedEntities.Select(item => item).ToArray(),
- _busy,
- _currentCookTimeButtonIndex,
- _currentCookTimerTime
- ));
- UIDirty = false;
- }
- }
-
- public void SetAppearance(MicrowaveVisualState state)
- {
- var finalState = state;
- if (Broken)
- {
- finalState = MicrowaveVisualState.Broken;
- }
-
- if (_entities.TryGetComponent(Owner, out AppearanceComponent? appearance))
- {
- appearance.SetData(PowerDeviceVisuals.VisualState, finalState);
- }
- }
-
- // ReSharper disable once InconsistentNaming
- // ReSharper disable once IdentifierTypo
- public void Wzhzhzh()
- {
- if (!HasContents)
- {
- return;
- }
-
- _busy = true;
- // Convert storage into Dictionary of ingredients
- var solidsDict = new Dictionary();
- var reagentDict = new Dictionary();
- foreach (var item in Storage.ContainedEntities)
- {
- // special behavior when being microwaved ;)
- var ev = new BeingMicrowavedEvent(Owner);
- _entities.EventBus.RaiseLocalEvent(item, ev, false);
-
- if (ev.Handled)
- {
- _busy = false;
- UIDirty = true;
- return;
- }
-
- var tagSys = EntitySystem.Get();
-
- if (tagSys.HasTag(item, "MicrowaveMachineUnsafe")
- || tagSys.HasTag(item, "Metal"))
- {
- // destroy microwave
- Broken = true;
- SetAppearance(MicrowaveVisualState.Broken);
- SoundSystem.Play(ItemBreakSound.GetSound(), Filter.Pvs(Owner), Owner);
- return;
- }
-
- if (tagSys.HasTag(item, "MicrowaveSelfUnsafe")
- || tagSys.HasTag(item, "Plastic"))
- {
- _entities.SpawnEntity(_badRecipeName,
- _entities.GetComponent(Owner).Coordinates);
- _entities.QueueDeleteEntity(item);
- }
-
- var metaData = _entities.GetComponent(item);
- if (metaData.EntityPrototype == null)
- {
- continue;
- }
-
- if (solidsDict.ContainsKey(metaData.EntityPrototype.ID))
- {
- solidsDict[metaData.EntityPrototype.ID]++;
- }
- else
- {
- solidsDict.Add(metaData.EntityPrototype.ID, 1);
- }
-
- if (!_entities.TryGetComponent(item, out var solMan))
- continue;
-
- foreach (var (_, solution) in solMan.Solutions)
- {
- foreach (var reagent in solution.Contents)
- {
- if (reagentDict.ContainsKey(reagent.ReagentId))
- reagentDict[reagent.ReagentId] += reagent.Quantity;
- else
- reagentDict.Add(reagent.ReagentId, reagent.Quantity);
- }
- }
- }
-
- // Check recipes
- (FoodRecipePrototype, int) portionedRecipe = _recipeManager.Recipes.Select(
- r => CanSatisfyRecipe(r, solidsDict, reagentDict)).Where(r => r.Item2 > 0).FirstOrDefault();
- FoodRecipePrototype? recipeToCook = portionedRecipe.Item1;
-
- SetAppearance(MicrowaveVisualState.Cooking);
- var time = _currentCookTimerTime * _cookTimeMultiplier;
- SoundSystem.Play(_startCookingSound.GetSound(), Filter.Pvs(Owner), Owner, AudioParams.Default);
- Owner.SpawnTimer((int) (_currentCookTimerTime * _cookTimeMultiplier), () =>
- {
- if (_lostPower)
- {
- return;
- }
-
- AddTemperature(time);
-
- if (recipeToCook != null)
- {
- for (int i = 0; i < portionedRecipe.Item2; i++)
- {
- SubtractContents(recipeToCook);
- _entities.SpawnEntity(recipeToCook.Result,
- _entities.GetComponent(Owner).Coordinates);
- }
- }
-
- EjectSolids();
-
- SoundSystem.Play(_cookingCompleteSound.GetSound(), Filter.Pvs(Owner),
- Owner, AudioParams.Default.WithVolume(-1f));
-
- SetAppearance(MicrowaveVisualState.Idle);
- _busy = false;
-
- UIDirty = true;
- });
- _lostPower = false;
- UIDirty = true;
- }
-
- ///
- /// Adds temperature to every item in the microwave,
- /// based on the time it took to microwave.
- ///
- /// The time on the microwave, in seconds.
- public void AddTemperature(float time)
- {
- var solutionContainerSystem = EntitySystem.Get();
- foreach (var entity in Storage.ContainedEntities)
- {
- if (_entities.TryGetComponent(entity, out TemperatureComponent? temp))
- {
- EntitySystem.Get().ChangeHeat(entity, time / 10, false, temp);
- }
-
- if (_entities.TryGetComponent(entity, out SolutionContainerManagerComponent? solutions))
- {
- foreach (var (_, solution) in solutions.Solutions)
- {
- if (solution.Temperature > TemperatureUpperThreshold)
- continue;
-
- solutionContainerSystem.AddThermalEnergy(entity, solution, time / 10);
- }
- }
- }
- }
-
- private void EjectSolids()
- {
- for (var i = Storage.ContainedEntities.Count - 1; i >= 0; i--)
- {
- Storage.Remove(Storage.ContainedEntities.ElementAt(i));
- }
- }
-
- private void EjectSolid(EntityUid entityId)
- {
- if (_entities.EntityExists(entityId))
- {
- Storage.Remove(entityId);
- }
- }
-
- private void SubtractContents(FoodRecipePrototype recipe)
- {
- var totalReagentsToRemove = new Dictionary(recipe.IngredientsReagents);
- var solutionContainerSystem = EntitySystem.Get();
-
- // this is spaghetti ngl
- foreach (var item in Storage.ContainedEntities)
- {
- if (!_entities.TryGetComponent(item, out var solMan))
- continue;
-
- // go over every solution
- foreach (var (_, solution) in solMan.Solutions)
- {
- foreach (var (reagent, _) in recipe.IngredientsReagents)
- {
- // removed everything
- if (!totalReagentsToRemove.ContainsKey(reagent))
- continue;
-
- if (!solution.ContainsReagent(reagent))
- continue;
-
- var quant = solution.GetReagentQuantity(reagent);
-
- if (quant >= totalReagentsToRemove[reagent])
- {
- quant = totalReagentsToRemove[reagent];
- totalReagentsToRemove.Remove(reagent);
- }
- else
- {
- totalReagentsToRemove[reagent] -= quant;
- }
-
- solutionContainerSystem.TryRemoveReagent(item, solution, reagent, quant);
- }
- }
- }
-
- foreach (var recipeSolid in recipe.IngredientsSolids)
- {
- for (var i = 0; i < recipeSolid.Value; i++)
- {
- foreach (var item in Storage.ContainedEntities)
- {
- var metaData = _entities.GetComponent(item);
- if (metaData.EntityPrototype == null)
- {
- continue;
- }
-
- if (metaData.EntityPrototype.ID == recipeSolid.Key)
- {
- Storage.Remove(item);
- _entities.DeleteEntity(item);
- break;
- }
- }
- }
- }
- }
-
- private (FoodRecipePrototype, int) CanSatisfyRecipe(FoodRecipePrototype recipe, Dictionary solids, Dictionary reagents)
- {
- int portions = 0;
-
- if(_currentCookTimerTime % recipe.CookTime != 0)
- {
- //can't be a multiple of this recipe
- return (recipe, 0);
- }
-
- foreach (var solid in recipe.IngredientsSolids)
- {
- if (!solids.ContainsKey(solid.Key))
- return (recipe, 0);
-
- if (solids[solid.Key] < solid.Value)
- return (recipe, 0);
- else
- portions = portions == 0 ? solids[solid.Key] / solid.Value.Int()
- : Math.Min(portions, solids[solid.Key] / solid.Value.Int());
- }
-
- foreach (var reagent in recipe.IngredientsReagents)
- {
- if (!reagents.ContainsKey(reagent.Key))
- return (recipe, 0);
-
- if (reagents[reagent.Key] < reagent.Value)
- return (recipe, 0);
- else
- portions = portions == 0 ? reagents[reagent.Key].Int() / reagent.Value.Int()
- : Math.Min(portions, reagents[reagent.Key].Int() / reagent.Value.Int());
- }
-
- //cook only as many of those portions as time allows
- return (recipe, (int)Math.Min(portions, _currentCookTimerTime / recipe.CookTime));
- }
-
- public void ClickSound()
- {
- SoundSystem.Play(_clickSound.GetSound(), Filter.Pvs(Owner), Owner, AudioParams.Default.WithVolume(-2f));
- }
}
public sealed class BeingMicrowavedEvent : HandledEntityEventArgs
diff --git a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs
index 78d762d98f..97e0fac8ca 100644
--- a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs
+++ b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs
@@ -1,45 +1,187 @@
+using System.Linq;
+using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Construction;
using Content.Server.Kitchen.Components;
-using Content.Server.Popups;
using Content.Shared.Destructible;
using Content.Shared.Interaction;
using Content.Shared.Item;
using Content.Shared.Kitchen.Components;
using Robust.Shared.Player;
-using JetBrains.Annotations;
using Content.Shared.Interaction.Events;
using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
-using Content.Shared.Popups;
using Content.Server.Hands.Systems;
+using Content.Server.Power.Components;
+using Content.Server.Temperature.Components;
+using Content.Server.Temperature.Systems;
+using Content.Shared.FixedPoint;
+using Content.Shared.Kitchen;
+using Content.Shared.Popups;
+using Content.Shared.Power;
+using Content.Shared.Tag;
+using Robust.Server.Containers;
+using Robust.Server.GameObjects;
+using Robust.Shared.Audio;
+using Robust.Shared.Containers;
namespace Content.Server.Kitchen.EntitySystems
{
- [UsedImplicitly]
- internal sealed class MicrowaveSystem : EntitySystem
+ public sealed class MicrowaveSystem : EntitySystem
{
- [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly ContainerSystem _container = default!;
+ [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+ [Dependency] private readonly RecipeManager _recipeManager = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedContainerSystem _sharedContainer = default!;
+ [Dependency] private readonly SolutionContainerSystem _solutionContainer = default!;
+ [Dependency] private readonly TagSystem _tag = default!;
+ [Dependency] private readonly TemperatureSystem _temperature = default!;
+ [Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly HandsSystem _handsSystem = default!;
public override void Initialize()
{
base.Initialize();
+ SubscribeLocalEvent(OnInit);
SubscribeLocalEvent(OnSolutionChange);
SubscribeLocalEvent(OnInteractUsing, after: new[]{typeof(AnchorableSystem)});
SubscribeLocalEvent(OnBreak);
+ SubscribeLocalEvent(OnPowerChanged);
SubscribeLocalEvent(OnSuicide);
+
+ SubscribeLocalEvent((u,c,_) => Wzhzhzh(u,c));
+ SubscribeLocalEvent(OnEjectMessage);
+ SubscribeLocalEvent(OnEjectIndex);
+ SubscribeLocalEvent(OnSelectTime);
+
+ SubscribeLocalEvent(OnCookStart);
+ SubscribeLocalEvent(OnCookStop);
+ }
+
+ private void OnCookStart(EntityUid uid, ActiveMicrowaveComponent component, ComponentStartup args)
+ {
+ if (!TryComp(uid, out var microwaveComponent))
+ return;
+ SetAppearance(microwaveComponent, MicrowaveVisualState.Cooking);
+
+ microwaveComponent.PlayingStream =
+ _audio.Play(microwaveComponent.LoopingSound, Filter.Pvs(uid), uid, AudioParams.Default.WithLoop(true).WithMaxDistance(5));
+ }
+
+ private void OnCookStop(EntityUid uid, ActiveMicrowaveComponent component, ComponentShutdown args)
+ {
+ if (!TryComp(uid, out var microwaveComponent))
+ return;
+ SetAppearance(microwaveComponent, MicrowaveVisualState.Idle);
+
+ microwaveComponent.PlayingStream?.Stop();
+ }
+
+ ///
+ /// Adds temperature to every item in the microwave,
+ /// based on the time it took to microwave.
+ ///
+ /// The microwave that is heating up.
+ /// The time on the microwave, in seconds.
+ private void AddTemperature(MicrowaveComponent component, float time)
+ {
+ foreach (var entity in component.Storage.ContainedEntities)
+ {
+ if (TryComp(entity, out var tempComp))
+ _temperature.ChangeHeat(entity, time / 10, false, tempComp);
+
+ if (!TryComp(entity, out var solutions))
+ continue;
+ foreach (var (_, solution) in solutions.Solutions)
+ {
+ if (solution.Temperature > component.TemperatureUpperThreshold)
+ continue;
+
+ _solutionContainer.AddThermalEnergy(entity, solution, time / 10);
+ }
+ }
+ }
+
+ private void SubtractContents(MicrowaveComponent component, FoodRecipePrototype recipe)
+ {
+ var totalReagentsToRemove = new Dictionary(recipe.IngredientsReagents);
+
+ // this is spaghetti ngl
+ foreach (var item in component.Storage.ContainedEntities)
+ {
+ if (!TryComp(item, out var solMan))
+ continue;
+
+ // go over every solution
+ foreach (var (_, solution) in solMan.Solutions)
+ {
+ foreach (var (reagent, _) in recipe.IngredientsReagents)
+ {
+ // removed everything
+ if (!totalReagentsToRemove.ContainsKey(reagent))
+ continue;
+
+ if (!solution.ContainsReagent(reagent))
+ continue;
+
+ var quant = solution.GetReagentQuantity(reagent);
+
+ if (quant >= totalReagentsToRemove[reagent])
+ {
+ quant = totalReagentsToRemove[reagent];
+ totalReagentsToRemove.Remove(reagent);
+ }
+ else
+ {
+ totalReagentsToRemove[reagent] -= quant;
+ }
+
+ _solutionContainer.TryRemoveReagent(item, solution, reagent, quant);
+ }
+ }
+ }
+
+ foreach (var recipeSolid in recipe.IngredientsSolids)
+ {
+ for (var i = 0; i < recipeSolid.Value; i++)
+ {
+ foreach (var item in component.Storage.ContainedEntities)
+ {
+ var metaData = MetaData(item);
+ if (metaData.EntityPrototype == null)
+ {
+ continue;
+ }
+
+ if (metaData.EntityPrototype.ID == recipeSolid.Key)
+ {
+ component.Storage.Remove(item);
+ EntityManager.DeleteEntity(item);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private void OnInit(EntityUid uid, MicrowaveComponent component, ComponentInit ags)
+ {
+ component.Storage = _container.EnsureContainer(uid,"microwave_entity_container");
}
private void OnSuicide(EntityUid uid, MicrowaveComponent component, SuicideEvent args)
{
- if (args.Handled) return;
+ if (args.Handled)
+ return;
+
args.SetHandled(SuicideKind.Heat);
var victim = args.Victim;
var headCount = 0;
- if (TryComp