diff --git a/Content.Client/Administration/UI/AdminAnnounceWindow.xaml b/Content.Client/Administration/UI/AdminAnnounceWindow.xaml
index bb5c29bc78..a703a7f5e6 100644
--- a/Content.Client/Administration/UI/AdminAnnounceWindow.xaml
+++ b/Content.Client/Administration/UI/AdminAnnounceWindow.xaml
@@ -1,15 +1,15 @@
-
+
-
+
-
+
diff --git a/Content.Client/Administration/UI/AdminAnnounceWindow.xaml.cs b/Content.Client/Administration/UI/AdminAnnounceWindow.xaml.cs
index 0cc0a8f61b..653a9c2a98 100644
--- a/Content.Client/Administration/UI/AdminAnnounceWindow.xaml.cs
+++ b/Content.Client/Administration/UI/AdminAnnounceWindow.xaml.cs
@@ -19,9 +19,9 @@ namespace Content.Client.Administration.UI
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
- AnnounceMethod.AddItem(_localization.GetString("announce-type-station"));
+ AnnounceMethod.AddItem(_localization.GetString("admin-announce-type-station"));
AnnounceMethod.SetItemMetadata(0, AdminAnnounceType.Station);
- AnnounceMethod.AddItem(_localization.GetString("announce-type-server"));
+ AnnounceMethod.AddItem(_localization.GetString("admin-announce-type-server"));
AnnounceMethod.SetItemMetadata(1, AdminAnnounceType.Server);
AnnounceMethod.OnItemSelected += AnnounceMethodOnOnItemSelected;
Announcement.OnTextChanged += AnnouncementOnOnTextChanged;
diff --git a/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml b/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml
index 21acf47204..2d643c9f7c 100644
--- a/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml
+++ b/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml
@@ -8,14 +8,14 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs b/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs
index b45836a437..dc37dca7a3 100644
--- a/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs
+++ b/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs
@@ -1,264 +1,70 @@
using Content.Client.Atmos.Overlays;
-using Content.Shared.Atmos;
using Content.Shared.Atmos.EntitySystems;
+using Content.Shared.GameTicking;
using JetBrains.Annotations;
+using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
-using Robust.Client.Utility;
using Robust.Shared.Utility;
namespace Content.Client.Atmos.EntitySystems
{
[UsedImplicitly]
- internal sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem
+ public sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
- [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
+ [Dependency] private readonly IOverlayManager _overlayMan = default!;
+ [Dependency] private readonly SpriteSystem _spriteSys = default!;
- // Gas overlays
- public readonly float[] Timer = new float[Atmospherics.TotalNumberOfGases];
- public readonly float[][] FrameDelays = new float[Atmospherics.TotalNumberOfGases][];
- public readonly int[] FrameCounter = new int[Atmospherics.TotalNumberOfGases];
- public readonly Texture[][] Frames = new Texture[Atmospherics.TotalNumberOfGases][];
-
- // Fire overlays
- public const int FireStates = 3;
- public const string FireRsiPath = "/Textures/Effects/fire.rsi";
-
- public readonly float[] FireTimer = new float[FireStates];
- public readonly float[][] FireFrameDelays = new float[FireStates][];
- public readonly int[] FireFrameCounter = new int[FireStates];
- public readonly Texture[][] FireFrames = new Texture[FireStates][];
-
- private readonly Dictionary> _tileData =
- new();
-
- public const int GasOverlayZIndex = 1;
+ private GasTileOverlay _overlay = default!;
public override void Initialize()
{
base.Initialize();
- SubscribeNetworkEvent(HandleGasOverlayMessage);
+ SubscribeNetworkEvent(HandleGasOverlayUpdate);
SubscribeLocalEvent(OnGridRemoved);
- for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
- {
- var overlay = _atmosphereSystem.GetOverlay(i);
- switch (overlay)
- {
- case SpriteSpecifier.Rsi animated:
- var rsi = _resourceCache.GetResource(animated.RsiPath).RSI;
- var stateId = animated.RsiState;
-
- if(!rsi.TryGetState(stateId, out var state)) continue;
-
- Frames[i] = state.GetFrames(RSI.State.Direction.South);
- FrameDelays[i] = state.GetDelays();
- FrameCounter[i] = 0;
- break;
- case SpriteSpecifier.Texture texture:
- Frames[i] = new[] {texture.Frame0()};
- FrameDelays[i] = Array.Empty();
- break;
- case null:
- Frames[i] = Array.Empty();
- FrameDelays[i] = Array.Empty();
- break;
- }
- }
-
- var fire = _resourceCache.GetResource(FireRsiPath).RSI;
-
- for (var i = 0; i < FireStates; i++)
- {
- if (!fire.TryGetState((i+1).ToString(), out var state))
- throw new ArgumentOutOfRangeException($"Fire RSI doesn't have state \"{i}\"!");
-
- FireFrames[i] = state.GetFrames(RSI.State.Direction.South);
- FireFrameDelays[i] = state.GetDelays();
- FireFrameCounter[i] = 0;
- }
-
- var overlayManager = IoCManager.Resolve();
- overlayManager.AddOverlay(new GasTileOverlay());
- overlayManager.AddOverlay(new FireTileOverlay());
+ _overlay = new GasTileOverlay(this, _resourceCache, ProtoMan, _spriteSys);
+ _overlayMan.AddOverlay(_overlay);
}
- private void HandleGasOverlayMessage(GasOverlayMessage message)
+ public override void Reset(RoundRestartCleanupEvent ev)
{
- foreach (var (indices, data) in message.OverlayData)
- {
- var chunk = GetOrCreateChunk(message.GridId, indices);
- chunk.Update(data, indices);
- }
- }
-
- // Slightly different to the server-side system version
- private GasOverlayChunk GetOrCreateChunk(EntityUid gridId, Vector2i indices)
- {
- if (!_tileData.TryGetValue(gridId, out var chunks))
- {
- chunks = new Dictionary();
- _tileData[gridId] = chunks;
- }
-
- var chunkIndices = GetGasChunkIndices(indices);
-
- if (!chunks.TryGetValue(chunkIndices, out var chunk))
- {
- chunk = new GasOverlayChunk(gridId, chunkIndices);
- chunks[chunkIndices] = chunk;
- }
-
- return chunk;
+ _overlay.TileData.Clear();
}
public override void Shutdown()
{
base.Shutdown();
- var overlayManager = IoCManager.Resolve();
- overlayManager.RemoveOverlay();
- overlayManager.RemoveOverlay();
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+
+ private void HandleGasOverlayUpdate(GasOverlayUpdateEvent ev)
+ {
+ foreach (var (grid, removedIndicies) in ev.RemovedChunks)
+ {
+ if (!_overlay.TileData.TryGetValue(grid, out var chunks))
+ continue;
+
+ foreach (var index in removedIndicies)
+ {
+ chunks.Remove(index);
+ }
+ }
+
+ foreach (var (grid, gridData) in ev.UpdatedChunks)
+ {
+ var chunks = _overlay.TileData.GetOrNew(grid);
+ foreach (var chunkData in gridData)
+ {
+ chunks[chunkData.Index] = chunkData;
+ }
+ }
}
private void OnGridRemoved(GridRemovalEvent ev)
{
- _tileData.Remove(ev.EntityUid);
- }
-
- public bool HasData(EntityUid gridId)
- {
- return _tileData.ContainsKey(gridId);
- }
-
- public GasOverlayEnumerator GetOverlays(EntityUid gridIndex, Vector2i indices)
- {
- if (!_tileData.TryGetValue(gridIndex, out var chunks))
- return default;
-
- var chunkIndex = GetGasChunkIndices(indices);
- if (!chunks.TryGetValue(chunkIndex, out var chunk))
- return default;
-
- var overlays = chunk.GetData(indices);
-
- return new GasOverlayEnumerator(overlays, this);
- }
-
- public FireOverlayEnumerator GetFireOverlays(EntityUid gridIndex, Vector2i indices)
- {
- if (!_tileData.TryGetValue(gridIndex, out var chunks))
- return default;
-
- var chunkIndex = GetGasChunkIndices(indices);
- if (!chunks.TryGetValue(chunkIndex, out var chunk))
- return default;
-
- var overlays = chunk.GetData(indices);
-
- return new FireOverlayEnumerator(overlays, this);
- }
-
- public override void FrameUpdate(float frameTime)
- {
- base.FrameUpdate(frameTime);
-
- for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
- {
- var delays = FrameDelays[i];
- if (delays.Length == 0) continue;
-
- var frameCount = FrameCounter[i];
- Timer[i] += frameTime;
- var time = delays[frameCount];
-
- if (Timer[i] < time)
- continue;
-
- Timer[i] -= time;
- FrameCounter[i] = (frameCount + 1) % Frames[i].Length;
- }
-
- for (var i = 0; i < FireStates; i++)
- {
- var delays = FireFrameDelays[i];
- if (delays.Length == 0) continue;
-
- var frameCount = FireFrameCounter[i];
- FireTimer[i] += frameTime;
- var time = delays[frameCount];
-
- if (FireTimer[i] < time) continue;
- FireTimer[i] -= time;
- FireFrameCounter[i] = (frameCount + 1) % FireFrames[i].Length;
- }
- }
-
- public struct GasOverlayEnumerator
- {
- private readonly GasTileOverlaySystem _system;
- private readonly GasData[]? _data;
- // TODO: Take Fire Temperature into account, when we code fire color
-
- private readonly int _length; // We cache the length so we can avoid a pointer dereference, for speed. Brrr.
- private int _current;
-
- public GasOverlayEnumerator(in GasOverlayData data, GasTileOverlaySystem system)
- {
- // Gas can't be null, as the caller to this constructor already ensured it wasn't.
- _data = data.Gas;
-
- _system = system;
-
- _length = _data?.Length ?? 0;
- _current = 0;
- }
-
- public bool MoveNext(out (Texture Texture, Color Color) overlay)
- {
- if (_current < _length)
- {
- // Data can't be null here unless length/current are incorrect
- var gas = _data![_current++];
- var frames = _system.Frames[gas.Index];
- overlay = (frames[_system.FrameCounter[gas.Index]], Color.White.WithAlpha(gas.Opacity));
- return true;
- }
-
- overlay = default;
- return false;
- }
- }
-
- public struct FireOverlayEnumerator
- {
- private readonly GasTileOverlaySystem _system;
- private byte _fireState;
- // TODO: Take Fire Temperature into account, when we code fire color
-
- public FireOverlayEnumerator(in GasOverlayData data, GasTileOverlaySystem system)
- {
- _fireState = data.FireState;
- _system = system;
- }
- public bool MoveNext(out (Texture Texture, Color Color) overlay)
- {
-
- if (_fireState != 0)
- {
- var state = _fireState - 1;
- var frames = _system.FireFrames[state];
- // TODO ATMOS Set color depending on temperature
- overlay = (frames[_system.FireFrameCounter[state]], Color.White);
-
- // Setting this to zero so we don't get stuck in an infinite loop.
- _fireState = 0;
- return true;
- }
-
- overlay = default;
- return false;
- }
+ _overlay.TileData.Remove(ev.EntityUid);
}
}
}
diff --git a/Content.Client/Atmos/Overlays/FireTileOverlay.cs b/Content.Client/Atmos/Overlays/FireTileOverlay.cs
deleted file mode 100644
index b32f8699fc..0000000000
--- a/Content.Client/Atmos/Overlays/FireTileOverlay.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-using Content.Client.Atmos.EntitySystems;
-using Robust.Client.Graphics;
-using Robust.Shared.Enums;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Map;
-
-namespace Content.Client.Atmos.Overlays
-{
- public sealed class FireTileOverlay : Overlay
- {
- private readonly GasTileOverlaySystem _gasTileOverlaySystem;
-
- [Dependency] private readonly IMapManager _mapManager = default!;
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
-
- public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
- private readonly ShaderInstance _shader;
-
- public FireTileOverlay()
- {
- IoCManager.InjectDependencies(this);
-
- _gasTileOverlaySystem = EntitySystem.Get();
- _shader = _prototypeManager.Index("unshaded").Instance().Duplicate();
- ZIndex = GasTileOverlaySystem.GasOverlayZIndex + 1;
- }
-
- protected override void Draw(in OverlayDrawArgs args)
- {
- var drawHandle = args.WorldHandle;
-
- var mapId = args.Viewport.Eye!.Position.MapId;
- var worldBounds = args.WorldBounds;
-
- drawHandle.UseShader(_shader);
-
- foreach (var mapGrid in _mapManager.FindGridsIntersecting(mapId, worldBounds))
- {
- if (!_gasTileOverlaySystem.HasData(mapGrid.GridEntityId))
- continue;
-
- drawHandle.SetTransform(mapGrid.WorldMatrix);
-
- foreach (var tile in mapGrid.GetTilesIntersecting(worldBounds))
- {
- var enumerator = _gasTileOverlaySystem.GetFireOverlays(mapGrid.GridEntityId, tile.GridIndices);
- while (enumerator.MoveNext(out var tuple))
- {
- drawHandle.DrawTexture(tuple.Texture, new Vector2(tile.X, tile.Y), tuple.Color);
- }
- }
- }
-
- drawHandle.SetTransform(Matrix3.Identity);
- }
- }
-}
diff --git a/Content.Client/Atmos/Overlays/GasTileOverlay.cs b/Content.Client/Atmos/Overlays/GasTileOverlay.cs
index b97eaa15dc..ed98936e91 100644
--- a/Content.Client/Atmos/Overlays/GasTileOverlay.cs
+++ b/Content.Client/Atmos/Overlays/GasTileOverlay.cs
@@ -1,54 +1,208 @@
using Content.Client.Atmos.EntitySystems;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Prototypes;
+using Robust.Client.GameObjects;
using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
using Robust.Shared.Map;
-using Robust.Shared.Maths;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
namespace Content.Client.Atmos.Overlays
{
public sealed class GasTileOverlay : Overlay
{
- private readonly GasTileOverlaySystem _gasTileOverlaySystem;
+ private readonly GasTileOverlaySystem _system;
+ private readonly IMapManager _mapManager;
+
+ public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities;
+ private readonly ShaderInstance _shader;
- [Dependency] private readonly IMapManager _mapManager = default!;
+ public readonly Dictionary> TileData = new();
- public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
+ // Gas overlays
+ private readonly float[] _timer;
+ private readonly float[][] _frameDelays;
+ private readonly int[] _frameCounter;
- public GasTileOverlay()
+ // TODO combine textures into a single texture atlas.
+ private readonly Texture[][] _frames;
+
+ // Fire overlays
+ private const int FireStates = 3;
+ private const string FireRsiPath = "/Textures/Effects/fire.rsi";
+
+ private readonly float[] _fireTimer = new float[FireStates];
+ private readonly float[][] _fireFrameDelays = new float[FireStates][];
+ private readonly int[] _fireFrameCounter = new int[FireStates];
+ private readonly Texture[][] _fireFrames = new Texture[FireStates][];
+
+ private int _gasCount;
+
+ public const int GasOverlayZIndex = (int) Content.Shared.DrawDepth.DrawDepth.Effects; // Under ghosts, above mostly everything else
+
+ public GasTileOverlay(GasTileOverlaySystem system, IResourceCache resourceCache, IPrototypeManager protoMan, SpriteSystem spriteSys)
{
- IoCManager.InjectDependencies(this);
+ _system = system;
+ _mapManager = IoCManager.Resolve();
+ _shader = protoMan.Index("unshaded").Instance();
+ ZIndex = GasOverlayZIndex;
- _gasTileOverlaySystem = EntitySystem.Get();
- ZIndex = GasTileOverlaySystem.GasOverlayZIndex;
+ _gasCount = _system.VisibleGasId.Length;
+ _timer = new float[_gasCount];
+ _frameDelays = new float[_gasCount][];
+ _frameCounter = new int[_gasCount];
+ _frames = new Texture[_gasCount][];
+
+ for (var i = 0; i < _gasCount; i++)
+ {
+ var gasPrototype = protoMan.Index(_system.VisibleGasId[i].ToString());
+
+ SpriteSpecifier overlay;
+
+ if (!string.IsNullOrEmpty(gasPrototype.GasOverlaySprite) && !string.IsNullOrEmpty(gasPrototype.GasOverlayState))
+ overlay = new SpriteSpecifier.Rsi(new ResourcePath(gasPrototype.GasOverlaySprite), gasPrototype.GasOverlayState);
+ else if (!string.IsNullOrEmpty(gasPrototype.GasOverlayTexture))
+ overlay = new SpriteSpecifier.Texture(new ResourcePath(gasPrototype.GasOverlayTexture));
+ else
+ continue;
+
+ switch (overlay)
+ {
+ case SpriteSpecifier.Rsi animated:
+ var rsi = resourceCache.GetResource(animated.RsiPath).RSI;
+ var stateId = animated.RsiState;
+
+ if (!rsi.TryGetState(stateId, out var state)) continue;
+
+ _frames[i] = state.GetFrames(RSI.State.Direction.South);
+ _frameDelays[i] = state.GetDelays();
+ _frameCounter[i] = 0;
+ break;
+ case SpriteSpecifier.Texture texture:
+ _frames[i] = new[] { spriteSys.Frame0(texture) };
+ _frameDelays[i] = Array.Empty();
+ break;
+ }
+ }
+
+ var fire = resourceCache.GetResource(FireRsiPath).RSI;
+
+ for (var i = 0; i < FireStates; i++)
+ {
+ if (!fire.TryGetState((i + 1).ToString(), out var state))
+ throw new ArgumentOutOfRangeException($"Fire RSI doesn't have state \"{i}\"!");
+
+ _fireFrames[i] = state.GetFrames(RSI.State.Direction.South);
+ _fireFrameDelays[i] = state.GetDelays();
+ _fireFrameCounter[i] = 0;
+ }
+ }
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ for (var i = 0; i < _gasCount; i++)
+ {
+ var delays = _frameDelays[i];
+ if (delays.Length == 0) continue;
+
+ var frameCount = _frameCounter[i];
+ _timer[i] += args.DeltaSeconds;
+ var time = delays[frameCount];
+
+ if (_timer[i] < time)
+ continue;
+
+ _timer[i] -= time;
+ _frameCounter[i] = (frameCount + 1) % _frames[i].Length;
+ }
+
+ for (var i = 0; i < FireStates; i++)
+ {
+ var delays = _fireFrameDelays[i];
+ if (delays.Length == 0) continue;
+
+ var frameCount = _fireFrameCounter[i];
+ _fireTimer[i] += args.DeltaSeconds;
+ var time = delays[frameCount];
+
+ if (_fireTimer[i] < time) continue;
+ _fireTimer[i] -= time;
+ _fireFrameCounter[i] = (frameCount + 1) % _fireFrames[i].Length;
+ }
}
protected override void Draw(in OverlayDrawArgs args)
{
var drawHandle = args.WorldHandle;
- var mapId = args.Viewport.Eye!.Position.MapId;
- var worldBounds = args.WorldBounds;
-
- drawHandle.UseShader(null);
-
- foreach (var mapGrid in _mapManager.FindGridsIntersecting(mapId, worldBounds))
+ foreach (var mapGrid in _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds))
{
- if (!_gasTileOverlaySystem.HasData(mapGrid.GridEntityId))
+ if (!TileData.TryGetValue(mapGrid.GridEntityId, out var gridData))
continue;
drawHandle.SetTransform(mapGrid.WorldMatrix);
+ var floatBounds = mapGrid.InvWorldMatrix.TransformBox(in args.WorldBounds);
+ var localBounds = new Box2i(
+ (int) MathF.Floor(floatBounds.Left),
+ (int) MathF.Floor(floatBounds.Bottom),
+ (int) MathF.Ceiling(floatBounds.Right),
+ (int) MathF.Ceiling(floatBounds.Top));
- foreach (var tile in mapGrid.GetTilesIntersecting(worldBounds))
+ // Currently it would be faster to group drawing by gas rather than by chunk, but if the textures are
+ // ever moved to a single atlas, that should no longer be the case. So this is just grouping draw calls
+ // by chunk, even though its currently slower.
+
+ drawHandle.UseShader(null);
+ foreach (var chunk in gridData.Values)
{
- var enumerator = _gasTileOverlaySystem.GetOverlays(mapGrid.GridEntityId, tile.GridIndices);
- while (enumerator.MoveNext(out var tuple))
+ var enumerator = new GasChunkEnumerator(chunk);
+
+ while (enumerator.MoveNext(out var gas))
{
- drawHandle.DrawTexture(tuple.Texture, new Vector2(tile.X, tile.Y), tuple.Color);
+ if (gas.Value.Opacity == null)
+ continue;
+
+ var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y);
+ if (!localBounds.Contains(tilePosition))
+ continue;
+
+ for (var i = 0; i < _gasCount; i++)
+ {
+ var opacity = gas.Value.Opacity[i];
+ if (opacity > 0)
+ drawHandle.DrawTexture(_frames[i][_frameCounter[i]], tilePosition, Color.White.WithAlpha(opacity));
+ }
+ }
+ }
+
+ // And again for fire, with the unshaded shader
+ drawHandle.UseShader(_shader);
+ foreach (var chunk in gridData.Values)
+ {
+ var enumerator = new GasChunkEnumerator(chunk);
+
+ while (enumerator.MoveNext(out var gas))
+ {
+ if (gas.Value.FireState == 0)
+ continue;
+
+ var index = chunk.Origin + (enumerator.X, enumerator.Y);
+ if (!localBounds.Contains(index))
+ continue;
+
+ var state = gas.Value.FireState - 1;
+ var texture = _fireFrames[state][_fireFrameCounter[state]];
+ drawHandle.DrawTexture(texture, index);
}
}
}
+
+ drawHandle.UseShader(null);
+ drawHandle.SetTransform(Matrix3.Identity);
}
}
}
diff --git a/Content.Client/Audio/BackgroundAudioSystem.cs b/Content.Client/Audio/BackgroundAudioSystem.cs
index b66d11b68e..1215560d7d 100644
--- a/Content.Client/Audio/BackgroundAudioSystem.cs
+++ b/Content.Client/Audio/BackgroundAudioSystem.cs
@@ -1,24 +1,19 @@
-using System.Threading;
using Content.Client.GameTicking.Managers;
using Content.Client.Lobby;
using Content.Client.Viewport;
-using Content.Shared;
-using Content.Shared.Audio;
using Content.Shared.CCVar;
-using Content.Shared.Maps;
using JetBrains.Annotations;
using Robust.Client;
using Robust.Client.Player;
using Robust.Client.State;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
+using System.Threading;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Client.Audio
diff --git a/Content.Client/Buckle/BuckleComponent.cs b/Content.Client/Buckle/BuckleComponent.cs
index be9370f43c..1bc43c3ce9 100644
--- a/Content.Client/Buckle/BuckleComponent.cs
+++ b/Content.Client/Buckle/BuckleComponent.cs
@@ -46,14 +46,18 @@ namespace Content.Client.Buckle
return;
}
- if (_buckled && buckle.DrawDepth.HasValue)
+ // TODO when ECSing, make this a visualizer
+ if (_buckled &&
+ LastEntityBuckledTo != null &&
+ EntMan.GetComponent(LastEntityBuckledTo.Value).LocalRotation.GetCardinalDir() == Direction.North &&
+ EntMan.TryGetComponent(LastEntityBuckledTo, out var buckledSprite))
{
_originalDrawDepth ??= ownerSprite.DrawDepth;
- ownerSprite.DrawDepth = buckle.DrawDepth.Value;
+ ownerSprite.DrawDepth = buckledSprite.DrawDepth - 1;
return;
}
- if (_originalDrawDepth.HasValue && !buckle.DrawDepth.HasValue)
+ if (_originalDrawDepth.HasValue && !_buckled)
{
ownerSprite.DrawDepth = _originalDrawDepth.Value;
_originalDrawDepth = null;
diff --git a/Content.Client/Clothing/ClothingComponent.cs b/Content.Client/Clothing/ClothingComponent.cs
index cb7edaac69..28dfcf38a2 100644
--- a/Content.Client/Clothing/ClothingComponent.cs
+++ b/Content.Client/Clothing/ClothingComponent.cs
@@ -1,18 +1,15 @@
-using Content.Client.Items.Components;
-using Content.Shared.Item;
+using Content.Shared.Clothing.Components;
using Robust.Shared.GameStates;
namespace Content.Client.Clothing
{
[RegisterComponent]
- [ComponentReference(typeof(SharedItemComponent))]
- [ComponentReference(typeof(ItemComponent))]
- [NetworkedComponent()]
- public sealed class ClothingComponent : ItemComponent
+ [ComponentReference(typeof(SharedClothingComponent))]
+ public sealed class ClothingComponent : SharedClothingComponent
{
[ViewVariables(VVAccess.ReadWrite)]
[DataField("femaleMask")]
- public FemaleClothingMask FemaleMask { get; } = FemaleClothingMask.UniformFull;
+ public FemaleClothingMask FemaleMask = FemaleClothingMask.UniformFull;
public string? InSlot;
}
diff --git a/Content.Client/Clothing/ClothingSystem.cs b/Content.Client/Clothing/ClothingVisualsSystem.cs
similarity index 94%
rename from Content.Client/Clothing/ClothingSystem.cs
rename to Content.Client/Clothing/ClothingVisualsSystem.cs
index f640ef028f..967a96d34d 100644
--- a/Content.Client/Clothing/ClothingSystem.cs
+++ b/Content.Client/Clothing/ClothingVisualsSystem.cs
@@ -4,6 +4,7 @@ using System.Linq;
using Content.Client.Inventory;
using Content.Shared.CharacterAppearance;
using Content.Shared.Clothing;
+using Content.Shared.Clothing.Components;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
using Content.Shared.Item;
@@ -16,7 +17,7 @@ using static Robust.Shared.GameObjects.SharedSpriteComponent;
namespace Content.Client.Clothing;
-public sealed class ClothingSystem : EntitySystem
+public sealed class ClothingVisualsSystem : EntitySystem
{
///
/// This is a shitty hotfix written by me (Paul) to save me from renaming all files.
@@ -52,13 +53,13 @@ public sealed class ClothingSystem : EntitySystem
SubscribeLocalEvent(OnGotEquipped);
SubscribeLocalEvent(OnGotUnequipped);
- SubscribeLocalEvent(OnGetVisuals);
+ SubscribeLocalEvent(OnGetVisuals);
SubscribeLocalEvent(OnVisualsChanged);
SubscribeLocalEvent(OnDidUnequip);
}
- private void OnGetVisuals(EntityUid uid, SharedItemComponent item, GetEquipmentVisualsEvent args)
+ private void OnGetVisuals(EntityUid uid, ClothingComponent item, GetEquipmentVisualsEvent args)
{
if (!TryComp(args.Equipee, out ClientInventoryComponent? inventory))
return;
@@ -99,15 +100,15 @@ public sealed class ClothingSystem : EntitySystem
///
/// Useful for lazily adding clothing sprites without modifying yaml. And for backwards compatibility.
///
- private bool TryGetDefaultVisuals(EntityUid uid, SharedItemComponent item, string slot, string? speciesId,
+ private bool TryGetDefaultVisuals(EntityUid uid, ClothingComponent clothing, string slot, string? speciesId,
[NotNullWhen(true)] out List? layers)
{
layers = null;
RSI? rsi = null;
- if (item.RsiPath != null)
- rsi = _cache.GetResource(TextureRoot / item.RsiPath).RSI;
+ if (clothing.RsiPath != null)
+ rsi = _cache.GetResource(TextureRoot / clothing.RsiPath).RSI;
else if (TryComp(uid, out SpriteComponent? sprite))
rsi = sprite.BaseRSI;
@@ -117,9 +118,9 @@ public sealed class ClothingSystem : EntitySystem
var correctedSlot = slot;
TemporarySlotMap.TryGetValue(correctedSlot, out correctedSlot);
- var state = (item.EquippedPrefix == null)
+ var state = (clothing.EquippedPrefix == null)
? $"equipped-{correctedSlot}"
- : $"{item.EquippedPrefix}-equipped-{correctedSlot}";
+ : $"{clothing.EquippedPrefix}-equipped-{correctedSlot}";
// species specific
if (speciesId != null && rsi.TryGetState($"{state}-{speciesId}", out _))
diff --git a/Content.Client/Communications/UI/CommunicationsConsoleMenu.cs b/Content.Client/Communications/UI/CommunicationsConsoleMenu.cs
index c0781f2fd8..293ab3ba5e 100644
--- a/Content.Client/Communications/UI/CommunicationsConsoleMenu.cs
+++ b/Content.Client/Communications/UI/CommunicationsConsoleMenu.cs
@@ -35,7 +35,7 @@ namespace Content.Client.Communications.UI
SizeFlagsStretchRatio = 1
};
AnnounceButton = new Button();
- AnnounceButton.Text = "Announce";
+ AnnounceButton.Text = Loc.GetString("comms-console-menu-announcement-button");
AnnounceButton.OnPressed += (_) => Owner.AnnounceButtonPressed(_messageInput.Text.Trim());
AnnounceButton.Disabled = !owner.CanAnnounce;
diff --git a/Content.Client/ContextMenu/UI/EntityMenuPresenterGrouping.cs b/Content.Client/ContextMenu/UI/EntityMenuPresenterGrouping.cs
index 2609527a03..56f5f321e6 100644
--- a/Content.Client/ContextMenu/UI/EntityMenuPresenterGrouping.cs
+++ b/Content.Client/ContextMenu/UI/EntityMenuPresenterGrouping.cs
@@ -1,10 +1,6 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
using Content.Shared.IdentityManagement;
using Robust.Client.GameObjects;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
+using System.Linq;
namespace Content.Client.ContextMenu.UI
{
diff --git a/Content.Client/Disposal/Visualizers/DisposalUnitVisualizer.cs b/Content.Client/Disposal/Visualizers/DisposalUnitVisualizer.cs
index 8a358275cc..e2d258cd85 100644
--- a/Content.Client/Disposal/Visualizers/DisposalUnitVisualizer.cs
+++ b/Content.Client/Disposal/Visualizers/DisposalUnitVisualizer.cs
@@ -1,12 +1,8 @@
-using System;
-using Content.Shared.Sound;
using JetBrains.Annotations;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
+using Robust.Shared.Audio;
using Robust.Shared.Serialization;
-using Robust.Shared.Serialization.Manager.Attributes;
using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent;
namespace Content.Client.Disposal.Visualizers
diff --git a/Content.Client/DoAfter/DoAfterSystem.cs b/Content.Client/DoAfter/DoAfterSystem.cs
index bfdd6f7dea..af457e3da9 100644
--- a/Content.Client/DoAfter/DoAfterSystem.cs
+++ b/Content.Client/DoAfter/DoAfterSystem.cs
@@ -6,6 +6,7 @@ using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
+using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameStates;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
diff --git a/Content.Client/DoAfter/UI/DoAfterBar.cs b/Content.Client/DoAfter/UI/DoAfterBar.cs
index fb47a17048..06a92daf14 100644
--- a/Content.Client/DoAfter/UI/DoAfterBar.cs
+++ b/Content.Client/DoAfter/UI/DoAfterBar.cs
@@ -72,6 +72,7 @@ namespace Content.Client.DoAfter.UI
{
IoCManager.InjectDependencies(this);
_shader = IoCManager.Resolve().Index("unshaded").Instance();
+ VerticalAlignment = VAlignment.Center;
}
protected override void FrameUpdate(FrameEventArgs args)
diff --git a/Content.Client/DoAfter/UI/DoAfterControl.cs b/Content.Client/DoAfter/UI/DoAfterControl.cs
new file mode 100644
index 0000000000..1f7d4dbcdf
--- /dev/null
+++ b/Content.Client/DoAfter/UI/DoAfterControl.cs
@@ -0,0 +1,45 @@
+using Content.Client.Resources;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Map;
+using Robust.Shared.Timing;
+
+namespace Content.Client.DoAfter.UI;
+
+public sealed class DoAfterControl : PanelContainer
+{
+ public float Ratio
+ {
+ get => _bar.Ratio;
+ set => _bar.Ratio = value;
+ }
+
+ public bool Cancelled
+ {
+ get => _bar.Cancelled;
+ set => _bar.Cancelled = value;
+ }
+
+ private DoAfterBar _bar;
+
+ public DoAfterControl()
+ {
+ IoCManager.InjectDependencies(this);
+
+ var cache = IoCManager.Resolve();
+
+ AddChild(new TextureRect
+ {
+ Texture = cache.GetTexture("/Textures/Interface/Misc/progress_bar.rsi/icon.png"),
+ TextureScale = Vector2.One * DoAfterBar.DoAfterBarScale,
+ VerticalAlignment = VAlignment.Center,
+ });
+
+ _bar = new DoAfterBar();
+ AddChild(_bar);
+ VerticalAlignment = VAlignment.Bottom;
+ _bar.Measure(Vector2.Infinity);
+ Measure(Vector2.Infinity);
+ }
+}
diff --git a/Content.Client/DoAfter/UI/DoAfterGui.cs b/Content.Client/DoAfter/UI/DoAfterGui.cs
index a7973adc61..7b40b20bf2 100644
--- a/Content.Client/DoAfter/UI/DoAfterGui.cs
+++ b/Content.Client/DoAfter/UI/DoAfterGui.cs
@@ -1,17 +1,8 @@
-using System;
-using System.Collections.Generic;
-using Content.Client.IoC;
-using Content.Client.Resources;
using Content.Shared.DoAfter;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Map;
-using Robust.Shared.Maths;
using Robust.Shared.Timing;
-using Robust.Shared.Utility;
namespace Content.Client.DoAfter.UI
{
@@ -21,14 +12,12 @@ namespace Content.Client.DoAfter.UI
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
- private readonly Dictionary _doAfterControls = new();
- private readonly Dictionary _doAfterBars = new();
+ private readonly Dictionary _doAfterControls = new();
// We'll store cancellations for a little bit just so we can flash the graphic to indicate it's cancelled
private readonly Dictionary _cancelledDoAfters = new();
- public EntityUid AttachedEntity { get; set; }
- private ScreenCoordinates _playerPosition;
+ public EntityUid? AttachedEntity { get; set; }
public DoAfterGui()
{
@@ -37,8 +26,6 @@ namespace Content.Client.DoAfter.UI
IoCManager.InjectDependencies(this);
IoCManager.Resolve().StateRoot.AddChild(this);
SeparationOverride = 0;
-
- LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.Begin);
}
protected override void Dispose(bool disposing)
@@ -53,43 +40,21 @@ namespace Content.Client.DoAfter.UI
}
_doAfterControls.Clear();
- _doAfterBars.Clear();
_cancelledDoAfters.Clear();
}
///
/// Add the necessary control for a DoAfter progress bar.
///
- ///
public void AddDoAfter(ClientDoAfter message)
{
if (_doAfterControls.ContainsKey(message.ID))
return;
- var doAfterBar = new DoAfterBar
- {
- VerticalAlignment = VAlignment.Center
- };
-
- _doAfterBars[message.ID] = doAfterBar;
-
- var control = new PanelContainer
- {
- Children =
- {
- new TextureRect
- {
- Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/Misc/progress_bar.rsi/icon.png"),
- TextureScale = Vector2.One * DoAfterBar.DoAfterBarScale,
- VerticalAlignment = VAlignment.Center,
- },
-
- doAfterBar
- }
- };
-
- AddChild(control);
- _doAfterControls.Add(message.ID, control);
+ var doAfterBar = new DoAfterControl();
+ AddChild(doAfterBar);
+ _doAfterControls.Add(message.ID, doAfterBar);
+ Measure(Vector2.Infinity);
}
// NOTE THAT THE BELOW ONLY HANDLES THE UI SIDE
@@ -107,8 +72,6 @@ namespace Content.Client.DoAfter.UI
RemoveChild(control);
control.DisposeAllChildren();
_doAfterControls.Remove(id);
- _doAfterBars.Remove(id);
-
_cancelledDoAfters.Remove(id);
}
@@ -122,23 +85,13 @@ namespace Content.Client.DoAfter.UI
if (_cancelledDoAfters.ContainsKey(id))
return;
- DoAfterBar doAfterBar;
-
if (!_doAfterControls.TryGetValue(id, out var doAfterControl))
{
- doAfterControl = new PanelContainer();
+ doAfterControl = new DoAfterControl();
AddChild(doAfterControl);
- DebugTools.Assert(!_doAfterBars.ContainsKey(id));
- doAfterBar = new DoAfterBar();
- doAfterControl.AddChild(doAfterBar);
- _doAfterBars[id] = doAfterBar;
- }
- else
- {
- doAfterBar = _doAfterBars[id];
}
- doAfterBar.Cancelled = true;
+ doAfterControl.Cancelled = true;
_cancelledDoAfters.Add(id, _gameTiming.CurTime);
}
@@ -146,32 +99,29 @@ namespace Content.Client.DoAfter.UI
{
base.FrameUpdate(args);
- if (!AttachedEntity.IsValid() ||
- !_entityManager.TryGetComponent(AttachedEntity, out DoAfterComponent? doAfterComponent))
+ if (!_entityManager.TryGetComponent(AttachedEntity, out DoAfterComponent? doAfterComponent))
{
Visible = false;
return;
}
var doAfters = doAfterComponent.DoAfters;
+
if (doAfters.Count == 0)
{
Visible = false;
return;
}
- var transform = _entityManager.GetComponent(AttachedEntity);
+ var transform = _entityManager.GetComponent(AttachedEntity.Value);
if (_eyeManager.CurrentMap != transform.MapID || !transform.Coordinates.IsValid(_entityManager))
{
Visible = false;
return;
}
- else
- {
- Visible = true;
- }
+ Visible = true;
var currentTime = _gameTiming.CurTime;
var toRemove = new List();
@@ -195,15 +145,16 @@ namespace Content.Client.DoAfter.UI
if (_cancelledDoAfters.ContainsKey(id) || !_doAfterControls.ContainsKey(id))
continue;
- var doAfterBar = _doAfterBars[id];
+ var control = _doAfterControls[id];
var ratio = (currentTime - message.StartTime).TotalSeconds;
- doAfterBar.Ratio = MathF.Min(1.0f,
+ control.Ratio = MathF.Min(1.0f,
(float) ratio / message.Delay);
// Just in case it doesn't get cleaned up by the system for whatever reason.
if (ratio > message.Delay + DoAfterSystem.ExcessTime)
{
toRemove.Add(id);
+ continue;
}
}
@@ -212,9 +163,14 @@ namespace Content.Client.DoAfter.UI
RemoveDoAfter(id);
}
- var screenCoordinates = _eyeManager.CoordinatesToScreen(transform.Coordinates);
- _playerPosition = new ScreenCoordinates(screenCoordinates.Position / UIScale, screenCoordinates.Window);
- LayoutContainer.SetPosition(this, new Vector2(_playerPosition.X - Width / 2, _playerPosition.Y - Height - 30.0f));
+ UpdatePosition(transform);
+ }
+
+ public void UpdatePosition(TransformComponent xform)
+ {
+ var screenCoordinates = _eyeManager.CoordinatesToScreen(xform.Coordinates);
+ var position = screenCoordinates.Position / UIScale - DesiredSize / 2f;
+ LayoutContainer.SetPosition(this, position - new Vector2(0, 40f));
}
}
}
diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs
index ee70797110..96eee75fc5 100644
--- a/Content.Client/Entry/EntryPoint.cs
+++ b/Content.Client/Entry/EntryPoint.cs
@@ -73,7 +73,6 @@ namespace Content.Client.Entry
// Do not add to these, they are legacy.
factory.RegisterClass();
factory.RegisterClass();
- factory.RegisterClass();
factory.RegisterClass();
factory.RegisterClass();
factory.RegisterClass();
@@ -183,10 +182,10 @@ namespace Content.Client.Entry
ContentContexts.SetupContexts(inputMan.Contexts);
IoCManager.Resolve().Initialize();
- IoCManager.Resolve().LoadParallax(); // Have to do this later because prototypes are needed.
+ IoCManager.Resolve().LoadDefaultParallax(); // Have to do this later because prototypes are needed.
var overlayMgr = IoCManager.Resolve();
- overlayMgr.AddOverlay(new ParallaxOverlay());
+
overlayMgr.AddOverlay(new SingularityOverlay());
overlayMgr.AddOverlay(new FlashOverlay());
overlayMgr.AddOverlay(new RadiationPulseOverlay());
diff --git a/Content.Client/Examine/ExamineSystem.cs b/Content.Client/Examine/ExamineSystem.cs
index 1ffa008559..37483e135f 100644
--- a/Content.Client/Examine/ExamineSystem.cs
+++ b/Content.Client/Examine/ExamineSystem.cs
@@ -1,5 +1,3 @@
-using System.Linq;
-using System.Threading;
using Content.Client.Verbs;
using Content.Shared.Examine;
using Content.Shared.IdentityManagement;
@@ -14,6 +12,8 @@ using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input.Binding;
using Robust.Shared.Map;
using Robust.Shared.Utility;
+using System.Linq;
+using System.Threading;
using static Content.Shared.Interaction.SharedInteractionSystem;
using static Robust.Client.UserInterface.Controls.BoxContainer;
diff --git a/Content.Client/Extinguisher/FireExtinguisherVisualizer.cs b/Content.Client/Extinguisher/FireExtinguisherVisualizer.cs
deleted file mode 100644
index 1f69afbaa5..0000000000
--- a/Content.Client/Extinguisher/FireExtinguisherVisualizer.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using Content.Shared.Extinguisher;
-using JetBrains.Annotations;
-using Robust.Client.GameObjects;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Serialization.Manager.Attributes;
-
-namespace Content.Client.Extinguisher
-{
- [UsedImplicitly]
- public sealed class FireExtinguisherVisualizer : AppearanceVisualizer
- {
- [DataField("safety_on_state")]
- private string? _safetyOnState;
- [DataField("safety_off_state")]
- private string? _safetyOffState;
-
- public override void OnChangeData(AppearanceComponent component)
- {
- base.OnChangeData(component);
-
- if (component.TryGetData(FireExtinguisherVisuals.Safety, out var safety))
- {
- SetSafety(component, safety);
- }
- }
-
- private void SetSafety(AppearanceComponent component, bool safety)
- {
- var sprite = IoCManager.Resolve().GetComponent(component.Owner);
-
- sprite.LayerSetState(FireExtinguisherVisualLayers.Base, safety ? _safetyOnState : _safetyOffState);
- }
- }
-
- public enum FireExtinguisherVisualLayers : byte
- {
- Base
- }
-}
diff --git a/Content.Client/Gravity/GravitySystem.cs b/Content.Client/Gravity/GravitySystem.cs
index 2bcf53d146..3efaa6ff96 100644
--- a/Content.Client/Gravity/GravitySystem.cs
+++ b/Content.Client/Gravity/GravitySystem.cs
@@ -2,7 +2,7 @@ using Content.Shared.Gravity;
namespace Content.Client.Gravity
{
- internal sealed class GravitySystem : SharedGravitySystem
+ public sealed class GravitySystem : SharedGravitySystem
{
}
diff --git a/Content.Client/Hands/Systems/HandsSystem.cs b/Content.Client/Hands/Systems/HandsSystem.cs
index cf12b6527e..ffaf2814a3 100644
--- a/Content.Client/Hands/Systems/HandsSystem.cs
+++ b/Content.Client/Hands/Systems/HandsSystem.cs
@@ -99,6 +99,14 @@ namespace Content.Client.Hands
}
#endregion
+ public override void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, SharedHandsComponent? hands = null)
+ {
+ base.DoDrop(uid, hand, doDropInteraction, hands);
+
+ if (TryComp(hand.HeldEntity, out SpriteComponent? sprite))
+ sprite.RenderOrder = EntityManager.CurrentTick.Value;
+ }
+
public EntityUid? GetActiveHandEntity()
{
return TryGetPlayerHands(out var hands) ? hands.ActiveHandEntity : null;
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
index ca2e25b94d..35c6aa511d 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
@@ -1,13 +1,13 @@
-using System.Text;
-using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface.CustomControls;
-using Robust.Client.UserInterface.XAML;
+using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Disease.Components;
using Content.Shared.FixedPoint;
-using Robust.Shared.Prototypes;
-using Content.Shared.Damage;
using Content.Shared.IdentityManagement;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using System.Text;
using static Content.Shared.MedicalScanner.SharedHealthAnalyzerComponent;
namespace Content.Client.HealthAnalyzer.UI
diff --git a/Content.Client/IdentityManagement/IdentitySystem.cs b/Content.Client/IdentityManagement/IdentitySystem.cs
index 7a7c96010d..a43fc72084 100644
--- a/Content.Client/IdentityManagement/IdentitySystem.cs
+++ b/Content.Client/IdentityManagement/IdentitySystem.cs
@@ -1,4 +1,4 @@
-using Content.Shared.IdentityManagement;
+using Content.Shared.IdentityManagement;
namespace Content.Client.IdentityManagement;
diff --git a/Content.Client/Inventory/ClientInventorySystem.cs b/Content.Client/Inventory/ClientInventorySystem.cs
index ce9aae6ca4..1207556b54 100644
--- a/Content.Client/Inventory/ClientInventorySystem.cs
+++ b/Content.Client/Inventory/ClientInventorySystem.cs
@@ -36,7 +36,7 @@ namespace Content.Client.Inventory
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IItemSlotManager _itemSlotManager = default!;
- [Dependency] private readonly ClothingSystem _clothingSystem = default!;
+ [Dependency] private readonly ClothingVisualsSystem _clothingVisualsSystem = default!;
public const int ButtonSize = 64;
private const int ButtonSeparation = 4;
@@ -165,7 +165,7 @@ namespace Content.Client.Inventory
private void OnInit(EntityUid uid, ClientInventoryComponent component, ComponentInit args)
{
- _clothingSystem.InitClothing(uid, component);
+ _clothingVisualsSystem.InitClothing(uid, component);
if (!TryGetUIElements(uid, out var window, out var bottomLeft, out var bottomRight, out var topQuick,
component))
diff --git a/Content.Client/Inventory/StrippableBoundUserInterface.cs b/Content.Client/Inventory/StrippableBoundUserInterface.cs
index a2009d661e..d7eb9952e2 100644
--- a/Content.Client/Inventory/StrippableBoundUserInterface.cs
+++ b/Content.Client/Inventory/StrippableBoundUserInterface.cs
@@ -1,13 +1,8 @@
-using System.Collections.Generic;
using Content.Client.Strip;
using Content.Shared.IdentityManagement;
using Content.Shared.Strip.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Localization;
-using Robust.Shared.ViewVariables;
namespace Content.Client.Inventory
{
@@ -33,7 +28,7 @@ namespace Content.Client.Inventory
_strippingMenu = new StrippingMenu($"{Loc.GetString("strippable-bound-user-interface-stripping-menu-title", ("ownerName", Identity.Name(Owner.Owner, entMan)))}");
_strippingMenu.OnClose += Close;
- _strippingMenu.OpenCentered();
+ _strippingMenu.OpenCenteredLeft();
UpdateMenu();
}
diff --git a/Content.Client/Items/Components/ItemComponent.cs b/Content.Client/Items/Components/ItemComponent.cs
deleted file mode 100644
index fce89fb009..0000000000
--- a/Content.Client/Items/Components/ItemComponent.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Content.Shared.Item;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-
-namespace Content.Client.Items.Components
-{
- [RegisterComponent]
- [ComponentReference(typeof(SharedItemComponent))]
- [Virtual]
- public class ItemComponent : SharedItemComponent { }
-}
diff --git a/Content.Client/Items/Systems/ItemSystem.cs b/Content.Client/Items/Systems/ItemSystem.cs
index 333be889db..2277712235 100644
--- a/Content.Client/Items/Systems/ItemSystem.cs
+++ b/Content.Client/Items/Systems/ItemSystem.cs
@@ -1,11 +1,11 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
using Content.Shared.Hands;
using Content.Shared.Item;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Containers;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
using static Robust.Shared.GameObjects.SharedSpriteComponent;
namespace Content.Client.Items.Systems;
@@ -19,18 +19,16 @@ public sealed class ItemSystem : SharedItemSystem
{
base.Initialize();
- SubscribeLocalEvent(OnGetVisuals);
+ SubscribeLocalEvent(OnGetVisuals);
}
#region InhandVisuals
+
///
/// When an items visual state changes, notify and entities that are holding this item that their sprite may need updating.
///
- public override void VisualsChanged(EntityUid uid, SharedItemComponent? item = null)
+ public override void VisualsChanged(EntityUid uid)
{
- if (!Resolve(uid, ref item, false))
- return;
-
// if the item is in a container, it might be equipped to hands or inventory slots --> update visuals.
if (_containerSystem.TryGetContainingContainer(uid, out var container))
RaiseLocalEvent(container.Owner, new VisualsChangedEvent(uid, container.ID), true);
@@ -39,12 +37,12 @@ public sealed class ItemSystem : SharedItemSystem
///
/// An entity holding this item is requesting visual information for in-hand sprites.
///
- private void OnGetVisuals(EntityUid uid, SharedItemComponent item, GetInhandVisualsEvent args)
+ private void OnGetVisuals(EntityUid uid, ItemComponent item, GetInhandVisualsEvent args)
{
var defaultKey = $"inhand-{args.Location.ToString().ToLowerInvariant()}";
// try get explicit visuals
- if (item.InhandVisuals == null || !item.InhandVisuals.TryGetValue(args.Location, out var layers))
+ if (!item.InhandVisuals.TryGetValue(args.Location, out var layers))
{
// get defaults
if (!TryGetDefaultVisuals(uid, item, defaultKey, out layers))
@@ -71,7 +69,7 @@ public sealed class ItemSystem : SharedItemSystem
///
/// Useful for lazily adding in-hand sprites without modifying yaml. And backwards compatibility.
///
- private bool TryGetDefaultVisuals(EntityUid uid, SharedItemComponent item, string defaultKey, [NotNullWhen(true)] out List? result)
+ private bool TryGetDefaultVisuals(EntityUid uid, ItemComponent item, string defaultKey, [NotNullWhen(true)] out List? result)
{
result = null;
@@ -85,9 +83,9 @@ public sealed class ItemSystem : SharedItemSystem
if (rsi == null || rsi.Path == null)
return false;
- var state = (item.EquippedPrefix == null)
+ var state = (item.HeldPrefix == null)
? defaultKey
- : $"{item.EquippedPrefix}-{defaultKey}";
+ : $"{item.HeldPrefix}-{defaultKey}";
if (!rsi.TryGetState(state, out var _))
return false;
diff --git a/Content.Client/Items/UI/ItemStatusPanel.cs b/Content.Client/Items/UI/ItemStatusPanel.cs
index 3cd1e01110..99d55445b4 100644
--- a/Content.Client/Items/UI/ItemStatusPanel.cs
+++ b/Content.Client/Items/UI/ItemStatusPanel.cs
@@ -1,5 +1,3 @@
-using System;
-using System.Collections.Generic;
using Content.Client.Items.Components;
using Content.Client.Resources;
using Content.Client.Stylesheets;
@@ -8,12 +6,8 @@ using Content.Shared.IdentityManagement;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
-using Robust.Shared.ViewVariables;
using static Content.Client.IoC.StaticIoC;
using static Robust.Client.UserInterface.Controls.BoxContainer;
diff --git a/Content.Client/Kitchen/Components/MicrowaveComponent.cs b/Content.Client/Kitchen/Components/MicrowaveComponent.cs
index 50050a5235..7b666f9182 100644
--- a/Content.Client/Kitchen/Components/MicrowaveComponent.cs
+++ b/Content.Client/Kitchen/Components/MicrowaveComponent.cs
@@ -1,8 +1,5 @@
using Content.Shared.Kitchen.Components;
-using Content.Shared.Sound;
using Robust.Shared.Audio;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Client.Kitchen.Components
{
diff --git a/Content.Client/Light/Components/HandheldLightComponent.cs b/Content.Client/Light/Components/HandheldLightComponent.cs
index 0a5f43ea93..e777f14a63 100644
--- a/Content.Client/Light/Components/HandheldLightComponent.cs
+++ b/Content.Client/Light/Components/HandheldLightComponent.cs
@@ -1,4 +1,4 @@
-using Content.Client.Items.Components;
+using Content.Shared.Light;
using Content.Shared.Light.Component;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
@@ -6,103 +6,79 @@ using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
using static Robust.Client.UserInterface.Controls.BoxContainer;
-namespace Content.Client.Light.Components
+namespace Content.Client.Light.Components;
+
+public sealed class HandheldLightStatus : Control
{
- [RegisterComponent]
- [Access(typeof(HandheldLightSystem))]
- public sealed class HandheldLightComponent : SharedHandheldLightComponent, IItemStatus
+ private const float TimerCycle = 1;
+
+ private readonly HandheldLightComponent _parent;
+ private readonly PanelContainer[] _sections = new PanelContainer[HandheldLightComponent.StatusLevels - 1];
+
+ private float _timer;
+
+ private static readonly StyleBoxFlat StyleBoxLit = new()
{
- public byte? Level;
- public bool Activated;
+ BackgroundColor = Color.LimeGreen
+ };
- ///
- /// Whether to automatically set item-prefixes when toggling the flashlight.
- ///
- ///
- /// Flashlights should probably be using explicit unshaded sprite, in-hand and clothing layers, this is
- /// mostly here for backwards compatibility.
- ///
- [DataField("addPrefix")]
- public bool AddPrefix = false;
+ private static readonly StyleBoxFlat StyleBoxUnlit = new()
+ {
+ BackgroundColor = Color.Black
+ };
- public Control MakeControl()
+ public HandheldLightStatus(HandheldLightComponent parent)
+ {
+ _parent = parent;
+
+ var wrapper = new BoxContainer
{
- return new StatusControl(this);
+ Orientation = LayoutOrientation.Horizontal,
+ SeparationOverride = 4,
+ HorizontalAlignment = HAlignment.Center
+ };
+
+ AddChild(wrapper);
+
+ for (var i = 0; i < _sections.Length; i++)
+ {
+ var panel = new PanelContainer {MinSize = (20, 20)};
+ wrapper.AddChild(panel);
+ _sections[i] = panel;
}
+ }
- private sealed class StatusControl : Control
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ _timer += args.DeltaSeconds;
+ _timer %= TimerCycle;
+
+ var level = _parent.Level;
+
+ for (var i = 0; i < _sections.Length; i++)
{
- private const float TimerCycle = 1;
-
- private readonly HandheldLightComponent _parent;
- private readonly PanelContainer[] _sections = new PanelContainer[StatusLevels - 1];
-
- private float _timer;
-
- private static readonly StyleBoxFlat StyleBoxLit = new()
+ if (i == 0)
{
- BackgroundColor = Color.LimeGreen
- };
-
- private static readonly StyleBoxFlat StyleBoxUnlit = new()
- {
- BackgroundColor = Color.Black
- };
-
- public StatusControl(HandheldLightComponent parent)
- {
- _parent = parent;
-
- var wrapper = new BoxContainer
+ if (level == 0 || level == null)
{
- Orientation = LayoutOrientation.Horizontal,
- SeparationOverride = 4,
- HorizontalAlignment = HAlignment.Center
- };
-
- AddChild(wrapper);
-
- for (var i = 0; i < _sections.Length; i++)
- {
- var panel = new PanelContainer {MinSize = (20, 20)};
- wrapper.AddChild(panel);
- _sections[i] = panel;
+ _sections[0].PanelOverride = StyleBoxUnlit;
}
+ else if (level == 1)
+ {
+ // Flash the last light.
+ _sections[0].PanelOverride = _timer > TimerCycle / 2 ? StyleBoxLit : StyleBoxUnlit;
+ }
+ else
+ {
+ _sections[0].PanelOverride = StyleBoxLit;
+ }
+
+ continue;
}
- protected override void FrameUpdate(FrameEventArgs args)
- {
- base.FrameUpdate(args);
-
- _timer += args.DeltaSeconds;
- _timer %= TimerCycle;
-
- var level = _parent.Level;
-
- for (var i = 0; i < _sections.Length; i++)
- {
- if (i == 0)
- {
- if (level == 0 || level == null)
- {
- _sections[0].PanelOverride = StyleBoxUnlit;
- }
- else if (level == 1)
- {
- // Flash the last light.
- _sections[0].PanelOverride = _timer > TimerCycle / 2 ? StyleBoxLit : StyleBoxUnlit;
- }
- else
- {
- _sections[0].PanelOverride = StyleBoxLit;
- }
-
- continue;
- }
-
- _sections[i].PanelOverride = level >= i + 2 ? StyleBoxLit : StyleBoxUnlit;
- }
- }
+ _sections[i].PanelOverride = level >= i + 2 ? StyleBoxLit : StyleBoxUnlit;
}
}
}
diff --git a/Content.Client/Light/HandheldLightSystem.cs b/Content.Client/Light/HandheldLightSystem.cs
index 68f4438b20..a78464abb9 100644
--- a/Content.Client/Light/HandheldLightSystem.cs
+++ b/Content.Client/Light/HandheldLightSystem.cs
@@ -1,39 +1,21 @@
-using Content.Client.Items.Systems;
+using Content.Client.Items;
using Content.Client.Light.Components;
-using Content.Shared.Item;
+using Content.Shared.Light;
using Content.Shared.Light.Component;
-using Robust.Shared.GameStates;
namespace Content.Client.Light;
-public sealed class HandheldLightSystem : EntitySystem
+public sealed class HandheldLightSystem : SharedHandheldLightSystem
{
- [Dependency] private readonly ItemSystem _itemSys = default!;
-
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent(OnHandleState);
+
+ SubscribeLocalEvent(OnGetStatusControl);
}
-
- private void OnHandleState(EntityUid uid, HandheldLightComponent component, ref ComponentHandleState args)
+
+ private static void OnGetStatusControl(EntityUid uid, HandheldLightComponent component, ItemStatusCollectMessage args)
{
- if (args.Current is not SharedHandheldLightComponent.HandheldLightComponentState state)
- return;
-
- component.Level = state.Charge;
-
- if (state.Activated == component.Activated)
- return;
-
- component.Activated = state.Activated;
-
- // really hand-held lights should be using a separate unshaded layer. (see FlashlightVisualizer)
- // this prefix stuff is largely for backwards compatibility with RSIs/yamls that have not been updated.
- if (component.AddPrefix && TryComp(uid, out SharedItemComponent? item))
- {
- item.EquippedPrefix = state.Activated ? "on" : "off";
- _itemSys.VisualsChanged(uid);
- }
+ args.Controls.Add(new HandheldLightStatus(component));
}
}
diff --git a/Content.Client/Light/Visualizers/FlashLightVisualizer.cs b/Content.Client/Light/Visualizers/FlashLightVisualizer.cs
index faa8b457bd..663676a237 100644
--- a/Content.Client/Light/Visualizers/FlashLightVisualizer.cs
+++ b/Content.Client/Light/Visualizers/FlashLightVisualizer.cs
@@ -1,4 +1,5 @@
using System;
+using Content.Shared.Light;
using Content.Shared.Light.Component;
using JetBrains.Annotations;
using Robust.Client.Animations;
diff --git a/Content.Client/Light/Visualizers/PoweredLightVisualizer.cs b/Content.Client/Light/Visualizers/PoweredLightVisualizer.cs
index 136114c312..ec19bd619e 100644
--- a/Content.Client/Light/Visualizers/PoweredLightVisualizer.cs
+++ b/Content.Client/Light/Visualizers/PoweredLightVisualizer.cs
@@ -1,14 +1,10 @@
-using System;
using Content.Shared.Light;
-using Content.Shared.Sound;
using JetBrains.Annotations;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.Animations;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
+using Robust.Shared.Audio;
using Robust.Shared.Random;
-using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Client.Light.Visualizers
{
diff --git a/Content.Client/MachineLinking/UI/SignalPortSelectorMenu.xaml b/Content.Client/MachineLinking/UI/SignalPortSelectorMenu.xaml
index 125f360cbb..d01a01cadf 100644
--- a/Content.Client/MachineLinking/UI/SignalPortSelectorMenu.xaml
+++ b/Content.Client/MachineLinking/UI/SignalPortSelectorMenu.xaml
@@ -1,4 +1,4 @@
-
+
@@ -12,8 +12,8 @@
-
-
+
+
diff --git a/Content.Client/Movement/Systems/JetpackSystem.cs b/Content.Client/Movement/Systems/JetpackSystem.cs
index 727c3cefa3..8c48b55694 100644
--- a/Content.Client/Movement/Systems/JetpackSystem.cs
+++ b/Content.Client/Movement/Systems/JetpackSystem.cs
@@ -1,4 +1,5 @@
using Content.Client.Clothing;
+using Content.Shared.Clothing.EntitySystems;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.Weapons.Ranged.Systems;
@@ -14,6 +15,7 @@ public sealed class JetpackSystem : SharedJetpackSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly ClothingSystem _clothing = default!;
public override void Initialize()
{
@@ -35,7 +37,7 @@ public sealed class JetpackSystem : SharedJetpackSystem
args.Sprite?.LayerSetState(0, state);
if (TryComp(uid, out var clothing))
- clothing.EquippedPrefix = enabled ? "on" : null;
+ _clothing.SetEquippedPrefix(uid, enabled ? "on" : null, clothing);
}
public override void Update(float frameTime)
diff --git a/Content.Client/PDA/PDABoundUserInterface.cs b/Content.Client/PDA/PDABoundUserInterface.cs
index 48edfecf2c..841902c58c 100644
--- a/Content.Client/PDA/PDABoundUserInterface.cs
+++ b/Content.Client/PDA/PDABoundUserInterface.cs
@@ -20,7 +20,7 @@ namespace Content.Client.PDA
base.Open();
SendMessage(new PDARequestUpdateInterfaceMessage());
_menu = new PDAMenu();
- _menu.OpenToLeft();
+ _menu.OpenCenteredLeft();
_menu.OnClose += Close;
_menu.FlashLightToggleButton.OnToggled += _ =>
{
@@ -79,14 +79,16 @@ namespace Content.Client.PDA
if (msg.PDAOwnerInfo.IdOwner != null || msg.PDAOwnerInfo.JobTitle != null)
{
_menu.IdInfoLabel.SetMarkup(Loc.GetString("comp-pda-ui",
- ("Owner",msg.PDAOwnerInfo.IdOwner ?? "Unknown"),
- ("JobTitle",msg.PDAOwnerInfo.JobTitle ?? "Unassigned")));
+ ("Owner",msg.PDAOwnerInfo.IdOwner ?? Loc.GetString("comp-pda-ui-unknown")),
+ ("JobTitle",msg.PDAOwnerInfo.JobTitle ?? Loc.GetString("comp-pda-ui-unassigned"))));
}
else
{
_menu.IdInfoLabel.SetMarkup(Loc.GetString("comp-pda-ui-blank"));
}
+ _menu.StationNameLabel.SetMarkup(Loc.GetString("comp-pda-ui-station", ("Station",msg.StationName ?? Loc.GetString("comp-pda-ui-unknown"))));
+
_menu.EjectIdButton.Visible = msg.PDAOwnerInfo.IdOwner != null || msg.PDAOwnerInfo.JobTitle != null;
_menu.EjectPenButton.Visible = msg.HasPen;
_menu.ActivateUplinkButton.Visible = msg.HasUplink;
diff --git a/Content.Client/PDA/PDAMenu.xaml b/Content.Client/PDA/PDAMenu.xaml
index 40c31b9b2d..e17c2416c8 100644
--- a/Content.Client/PDA/PDAMenu.xaml
+++ b/Content.Client/PDA/PDAMenu.xaml
@@ -8,9 +8,12 @@
HorizontalExpand="True"
MinSize="50 50">
+
-
public sealed class ParallaxControl : Control
{
+ [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IParallaxManager _parallaxManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
- [ViewVariables(VVAccess.ReadWrite)] public Vector2i Offset { get; set; }
+ [ViewVariables(VVAccess.ReadWrite)] public Vector2 Offset { get; set; }
public ParallaxControl()
{
@@ -24,20 +24,24 @@ public sealed class ParallaxControl : Control
Offset = (_random.Next(0, 1000), _random.Next(0, 1000));
RectClipContent = true;
+ _parallaxManager.LoadParallaxByName("FastSpace");
}
protected override void Draw(DrawingHandleScreen handle)
{
- foreach (var layer in _parallaxManager.ParallaxLayers)
+ foreach (var layer in _parallaxManager.GetParallaxLayers("FastSpace"))
{
var tex = layer.Texture;
- var texSize = tex.Size * layer.Config.Scale.Floored();
+ var texSize = (tex.Size.X * (int) Size.X, tex.Size.Y * (int) Size.X) * layer.Config.Scale.Floored() / 540;
var ourSize = PixelSize;
+ var currentTime = (float) _timing.RealTime.TotalSeconds;
+ var offset = Offset + new Vector2(currentTime * 100f, currentTime * 0f);
+
if (layer.Config.Tiled)
{
// Multiply offset by slowness to match normal parallax
- var scaledOffset = (Offset * layer.Config.Slowness).Floored();
+ var scaledOffset = (offset * layer.Config.Slowness).Floored();
// Then modulo the scaled offset by the size to prevent drawing a bunch of offscreen tiles for really small images.
scaledOffset.X %= texSize.X;
diff --git a/Content.Client/Parallax/ParallaxOverlay.cs b/Content.Client/Parallax/ParallaxOverlay.cs
index 89b2719426..1f401cde91 100644
--- a/Content.Client/Parallax/ParallaxOverlay.cs
+++ b/Content.Client/Parallax/ParallaxOverlay.cs
@@ -1,47 +1,55 @@
-using System;
using Content.Client.Parallax.Managers;
using Content.Shared.CCVar;
using Robust.Client.Graphics;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
-using Robust.Shared.IoC;
-using Robust.Shared.Maths;
+using Robust.Shared.Map;
using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
namespace Content.Client.Parallax;
public sealed class ParallaxOverlay : Overlay
{
- [Dependency] private readonly IParallaxManager _parallaxManager = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
+ [Dependency] private readonly IParallaxManager _manager = default!;
+ private readonly ParallaxSystem _parallax;
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowWorld;
- private readonly ShaderInstance _shader;
public ParallaxOverlay()
{
IoCManager.InjectDependencies(this);
- _shader = _prototypeManager.Index("unshaded").Instance();
+ _parallax = IoCManager.Resolve().GetEntitySystem();
+
}
protected override void Draw(in OverlayDrawArgs args)
{
- if (args.Viewport.Eye == null)
- {
+ if (args.MapId == MapId.Nullspace)
return;
- }
if (!_configurationManager.GetCVar(CCVars.ParallaxEnabled))
- {
return;
- }
+ var position = args.Viewport.Eye?.Position.Position ?? Vector2.Zero;
var screenHandle = args.WorldHandle;
- screenHandle.UseShader(_shader);
- foreach (var layer in _parallaxManager.ParallaxLayers)
+ var layers = _parallax.GetParallaxLayers(args.MapId);
+ var realTime = (float) _timing.RealTime.TotalSeconds;
+
+ foreach (var layer in layers)
{
+ ShaderInstance? shader;
+
+ if (!string.IsNullOrEmpty(layer.Config.Shader))
+ shader = _prototypeManager.Index(layer.Config.Shader).Instance();
+ else
+ shader = null;
+
+ screenHandle.UseShader(shader);
var tex = layer.Texture;
// Size of the texture in world units.
@@ -52,10 +60,11 @@ public sealed class ParallaxOverlay : Overlay
// The effects of this are such that a slowness of 1 anchors the layer to the centre of the screen, while a slowness of 0 anchors the layer to the world.
// (For values 0.0 to 1.0 this is in effect a lerp, but it's deliberately unclamped.)
// The ParallaxAnchor adapts the parallax for station positioning and possibly map-specific tweaks.
- var home = layer.Config.WorldHomePosition + _parallaxManager.ParallaxAnchor;
+ var home = layer.Config.WorldHomePosition + _manager.ParallaxAnchor;
+ var scrolled = layer.Config.Scrolling * realTime;
// Origin - start with the parallax shift itself.
- var originBL = (args.Viewport.Eye.Position.Position - home) * layer.Config.Slowness;
+ var originBL = (position - home) * layer.Config.Slowness + scrolled;
// Place at the home.
originBL += home;
@@ -90,6 +99,8 @@ public sealed class ParallaxOverlay : Overlay
screenHandle.DrawTextureRect(tex, Box2.FromDimensions(originBL, size));
}
}
+
+ screenHandle.UseShader(null);
}
}
diff --git a/Content.Client/Parallax/ParallaxSystem.cs b/Content.Client/Parallax/ParallaxSystem.cs
new file mode 100644
index 0000000000..d3320ce1f9
--- /dev/null
+++ b/Content.Client/Parallax/ParallaxSystem.cs
@@ -0,0 +1,71 @@
+using Content.Client.Parallax.Managers;
+using Content.Shared.Parallax;
+using Robust.Client.Graphics;
+using Robust.Shared.GameStates;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Parallax;
+
+public sealed class ParallaxSystem : SharedParallaxSystem
+{
+ [Dependency] private readonly IMapManager _map = default!;
+ [Dependency] private readonly IOverlayManager _overlay = default!;
+ [Dependency] private readonly IParallaxManager _parallax = default!;
+ [Dependency] private readonly IPrototypeManager _protoManager = default!;
+
+ private const string Fallback = "Default";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ _overlay.AddOverlay(new ParallaxOverlay());
+ SubscribeLocalEvent(OnParallaxHandleState);
+ _protoManager.PrototypesReloaded += OnReload;
+ }
+
+ private void OnReload(PrototypesReloadedEventArgs obj)
+ {
+ _parallax.UnloadParallax(Fallback);
+ _parallax.LoadDefaultParallax();
+
+ foreach (var comp in EntityQuery(true))
+ {
+ _parallax.UnloadParallax(comp.Parallax);
+ _parallax.LoadParallaxByName(comp.Parallax);
+ }
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ _overlay.RemoveOverlay();
+ _protoManager.PrototypesReloaded -= OnReload;
+ }
+
+ private void OnParallaxHandleState(EntityUid uid, ParallaxComponent component, ref ComponentHandleState args)
+ {
+ if (args.Current is not ParallaxComponentState state) return;
+ component.Parallax = state.Parallax;
+
+ if (!_parallax.IsLoaded(component.Parallax))
+ {
+ _parallax.LoadParallaxByName(component.Parallax);
+ }
+ }
+
+ public ParallaxLayerPrepared[] GetParallaxLayers(MapId mapId)
+ {
+ return _parallax.GetParallaxLayers(GetParallax(_map.GetMapEntityId(mapId)));
+ }
+
+ public string GetParallax(MapId mapId)
+ {
+ return GetParallax(_map.GetMapEntityId(mapId));
+ }
+
+ public string GetParallax(EntityUid mapUid)
+ {
+ return TryComp(mapUid, out var parallax) ? parallax.Parallax : Fallback;
+ }
+}
diff --git a/Content.Client/Physics/Controllers/MoverController.cs b/Content.Client/Physics/Controllers/MoverController.cs
index 6a75df3364..d9697ddea0 100644
--- a/Content.Client/Physics/Controllers/MoverController.cs
+++ b/Content.Client/Physics/Controllers/MoverController.cs
@@ -24,8 +24,6 @@ namespace Content.Client.Physics.Controllers
{
if (relayMover.RelayEntity != null)
HandleClientsideMovement(relayMover.RelayEntity.Value, frameTime);
-
- return;
}
HandleClientsideMovement(player, frameTime);
@@ -34,13 +32,26 @@ namespace Content.Client.Physics.Controllers
private void HandleClientsideMovement(EntityUid player, float frameTime)
{
if (!TryComp(player, out InputMoverComponent? mover) ||
- !TryComp(player, out TransformComponent? xform)) return;
+ !TryComp(player, out TransformComponent? xform))
+ {
+ return;
+ }
PhysicsComponent? body = null;
+ TransformComponent? xformMover = xform;
if (mover.ToParent && HasComp(xform.ParentUid))
{
- if (!TryComp(xform.ParentUid, out body)) return;
+ if (!TryComp(xform.ParentUid, out body) ||
+ !TryComp(xform.ParentUid, out xformMover))
+ {
+ return;
+ }
+
+ if (TryComp(xform.ParentUid, out var parentMover))
+ {
+ mover.LastGridAngle = parentMover.LastGridAngle;
+ }
}
else if (!TryComp(player, out body))
{
@@ -60,10 +71,13 @@ namespace Content.Client.Physics.Controllers
if (TryComp(player, out JointComponent? jointComponent))
{
- foreach (var joint in jointComponent.GetJoints)
+ foreach (var joint in jointComponent.GetJoints.Values)
{
- joint.BodyA.Predict = true;
- joint.BodyB.Predict = true;
+ if (TryComp(joint.BodyAUid, out PhysicsComponent? physics))
+ physics.Predict = true;
+
+ if (TryComp(joint.BodyBUid, out physics))
+ physics.Predict = true;
}
}
@@ -84,12 +98,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, xform, frameTime);
- }
-
- protected override Filter GetSoundPlayers(EntityUid mover)
- {
- return Filter.Local();
+ HandleMobMovement(mover, body, xformMover, frameTime);
}
protected override bool CanSound()
diff --git a/Content.Client/Popups/PopupSystem.cs b/Content.Client/Popups/PopupSystem.cs
index aea17fec80..3841164bcb 100644
--- a/Content.Client/Popups/PopupSystem.cs
+++ b/Content.Client/Popups/PopupSystem.cs
@@ -157,7 +157,8 @@ namespace Content.Client.Popups
public override void FrameUpdate(float frameTime)
{
- if (_aliveWorldLabels.Count == 0) return;
+ if (_aliveWorldLabels.Count == 0 && _aliveCursorLabels.Count == 0)
+ return;
var player = _playerManager.LocalPlayer?.ControlledEntity;
var playerPos = player != null ? Transform(player.Value).MapPosition : MapCoordinates.Nullspace;
@@ -234,13 +235,13 @@ namespace Content.Client.Popups
public CursorPopupLabel(ScreenCoordinates screenCoords) : base()
{
- InitialPos = screenCoords.Position / UIScale - DesiredSize / 2;
+ InitialPos = screenCoords.Position - DesiredSize / 2;
}
protected override void FrameUpdate(FrameEventArgs eventArgs)
{
base.FrameUpdate(eventArgs);
- LayoutContainer.SetPosition(this, InitialPos - (0, 20 * (TotalTime * TotalTime + TotalTime)));
+ LayoutContainer.SetPosition(this, InitialPos / UIScale - (0, 20 * (TotalTime * TotalTime + TotalTime)));
}
}
diff --git a/Content.Client/Shuttles/UI/RadarControl.cs b/Content.Client/Shuttles/UI/RadarControl.cs
index 004a040d6f..01bd88fc25 100644
--- a/Content.Client/Shuttles/UI/RadarControl.cs
+++ b/Content.Client/Shuttles/UI/RadarControl.cs
@@ -73,6 +73,7 @@ public sealed class RadarControl : Control
{
IoCManager.InjectDependencies(this);
MinSize = (SizeFull, SizeFull);
+ RectClipContent = true;
}
public void SetMatrix(EntityCoordinates? coordinates, Angle? angle)
diff --git a/Content.Client/Storage/StorageBoundUserInterface.cs b/Content.Client/Storage/StorageBoundUserInterface.cs
index 29a024af7f..4389663d42 100644
--- a/Content.Client/Storage/StorageBoundUserInterface.cs
+++ b/Content.Client/Storage/StorageBoundUserInterface.cs
@@ -31,7 +31,7 @@ namespace Content.Client.Storage
_window.StorageContainerButton.OnPressed += TouchedContainerButton;
_window.OnClose += Close;
- _window.OpenCentered();
+ _window.OpenCenteredLeft();
}
else
{
diff --git a/Content.Client/Storage/UI/StorageWindow.cs b/Content.Client/Storage/UI/StorageWindow.cs
index b8629eaa55..5a2b28a9ee 100644
--- a/Content.Client/Storage/UI/StorageWindow.cs
+++ b/Content.Client/Storage/UI/StorageWindow.cs
@@ -4,6 +4,7 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Content.Client.Items.Components;
using Content.Client.UserInterface.Controls;
+using Content.Shared.Item;
using Robust.Client.UserInterface;
using static Robust.Client.UserInterface.Controls.BoxContainer;
using static Content.Shared.Storage.SharedStorageComponent;
diff --git a/Content.Client/SurveillanceCamera/SurveillanceCameraMonitorSystem.cs b/Content.Client/SurveillanceCamera/SurveillanceCameraMonitorSystem.cs
index c946062128..f25cf4c30e 100644
--- a/Content.Client/SurveillanceCamera/SurveillanceCameraMonitorSystem.cs
+++ b/Content.Client/SurveillanceCamera/SurveillanceCameraMonitorSystem.cs
@@ -35,6 +35,6 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
public void RemoveTimer(EntityUid uid)
{
- EntityManager.RemoveComponent(uid);
+ EntityManager.RemoveComponentDeferred(uid);
}
}
diff --git a/Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorWindow.xaml.cs b/Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorWindow.xaml.cs
index 5f47fea140..1c6e18c7af 100644
--- a/Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorWindow.xaml.cs
+++ b/Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorWindow.xaml.cs
@@ -27,6 +27,7 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
public event Action? CameraDisconnect;
private string _currentAddress = string.Empty;
+ private bool _isSwitching;
private readonly FixedEye _defaultEye = new();
private readonly Dictionary _subnetMap = new();
@@ -135,27 +136,37 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
private void SetCameraView(IEye? eye)
{
+ var eyeChanged = eye != CameraView.Eye || CameraView.Eye == null;
CameraView.Eye = eye ?? _defaultEye;
- CameraView.Visible = eye != null;
- CameraViewBackground.Visible = true;
+ CameraView.Visible = !eyeChanged && !_isSwitching;
CameraDisconnectButton.Disabled = eye == null;
if (eye != null)
{
+ if (!eyeChanged)
+ {
+ return;
+ }
+
+ _isSwitching = true;
+ CameraViewBackground.Visible = true;
CameraStatus.Text = Loc.GetString("surveillance-camera-monitor-ui-status",
- ("status", Loc.GetString("surveillance-camera-monitor-ui-status-connecting")),
- ("address", _currentAddress));
+ ("status", Loc.GetString("surveillance-camera-monitor-ui-status-connecting")),
+ ("address", _currentAddress));
CameraSwitchTimer!();
}
else
{
+ CameraViewBackground.Visible = true;
CameraStatus.Text = Loc.GetString("surveillance-camera-monitor-ui-status-disconnected");
}
}
public void OnSwitchTimerComplete()
{
- CameraViewBackground.Visible = false;
+ _isSwitching = false;
+ CameraView.Visible = CameraView.Eye != _defaultEye;
+ CameraViewBackground.Visible = CameraView.Eye == _defaultEye;
CameraStatus.Text = Loc.GetString("surveillance-camera-monitor-ui-status",
("status", Loc.GetString("surveillance-camera-monitor-ui-status-connected")),
("address", _currentAddress));
diff --git a/Content.Client/Toggleable/ToggleableLightVisualsSystem.cs b/Content.Client/Toggleable/ToggleableLightVisualsSystem.cs
index f87a618dd5..6a3cccb0de 100644
--- a/Content.Client/Toggleable/ToggleableLightVisualsSystem.cs
+++ b/Content.Client/Toggleable/ToggleableLightVisualsSystem.cs
@@ -18,7 +18,7 @@ public sealed class ToggleableLightVisualsSystem : VisualizerSystem(OnGetHeldVisuals, after: new[] { typeof(ItemSystem) });
- SubscribeLocalEvent(OnGetEquipmentVisuals, after: new[] { typeof(ClothingSystem) });
+ SubscribeLocalEvent(OnGetEquipmentVisuals, after: new[] { typeof(ClothingVisualsSystem) });
}
protected override void OnAppearanceChange(EntityUid uid, ToggleableLightVisualsComponent component, ref AppearanceChangeEvent args)
diff --git a/Content.Client/Trigger/TimerTriggerVisualizer.cs b/Content.Client/Trigger/TimerTriggerVisualizer.cs
index dcad6c361c..ba26ef2479 100644
--- a/Content.Client/Trigger/TimerTriggerVisualizer.cs
+++ b/Content.Client/Trigger/TimerTriggerVisualizer.cs
@@ -1,13 +1,9 @@
-using System;
-using Content.Shared.Sound;
using Content.Shared.Trigger;
using JetBrains.Annotations;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
+using Robust.Shared.Audio;
using Robust.Shared.Serialization;
-using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Client.Trigger
{
diff --git a/Content.Client/VendingMachines/UI/VendingMachineVisualizer.cs b/Content.Client/VendingMachines/UI/VendingMachineVisualizer.cs
index 70167300b4..755a244cca 100644
--- a/Content.Client/VendingMachines/UI/VendingMachineVisualizer.cs
+++ b/Content.Client/VendingMachines/UI/VendingMachineVisualizer.cs
@@ -1,10 +1,13 @@
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;
@@ -139,7 +142,18 @@ namespace Content.Client.VendingMachines.UI
base.OnChangeData(component);
var entMan = IoCManager.Resolve();
- var sprite = entMan.GetComponent(component.Owner);
+ 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))
{
diff --git a/Content.Client/VendingMachines/VendingMachineComponent.cs b/Content.Client/VendingMachines/VendingMachineComponent.cs
new file mode 100644
index 0000000000..f7c16584a3
--- /dev/null
+++ b/Content.Client/VendingMachines/VendingMachineComponent.cs
@@ -0,0 +1,10 @@
+using Content.Shared.VendingMachines;
+
+namespace Content.Client.VendingMachines;
+
+[RegisterComponent]
+[ComponentReference(typeof(SharedVendingMachineComponent))]
+public sealed class VendingMachineComponent : SharedVendingMachineComponent
+{
+
+}
diff --git a/Content.Client/VendingMachines/VendingMachineSystem.cs b/Content.Client/VendingMachines/VendingMachineSystem.cs
new file mode 100644
index 0000000000..c08f5e10d2
--- /dev/null
+++ b/Content.Client/VendingMachines/VendingMachineSystem.cs
@@ -0,0 +1,7 @@
+using Content.Shared.VendingMachines;
+
+namespace Content.Client.VendingMachines;
+
+public sealed class VendingMachineSystem : SharedVendingMachineSystem
+{
+}
diff --git a/Content.Client/Wires/UI/WiresBoundUserInterface.cs b/Content.Client/Wires/UI/WiresBoundUserInterface.cs
index 61744fc232..1386c0d8c5 100644
--- a/Content.Client/Wires/UI/WiresBoundUserInterface.cs
+++ b/Content.Client/Wires/UI/WiresBoundUserInterface.cs
@@ -1,4 +1,4 @@
-using Content.Shared.Wires;
+using Content.Shared.Wires;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
@@ -18,7 +18,7 @@ namespace Content.Client.Wires.UI
_menu = new WiresMenu(this);
_menu.OnClose += Close;
- _menu.OpenCentered();
+ _menu.OpenCenteredLeft();
}
protected override void UpdateState(BoundUserInterfaceState state)
diff --git a/Content.Client/Wires/UI/WiresMenu.cs b/Content.Client/Wires/UI/WiresMenu.cs
index 08b84a6f90..bfbb2ebd22 100644
--- a/Content.Client/Wires/UI/WiresMenu.cs
+++ b/Content.Client/Wires/UI/WiresMenu.cs
@@ -267,16 +267,8 @@ namespace Content.Client.Wires.UI
_statusContainer.RemoveAllChildren();
- var originalStatuses = new List(state.Statuses); // TODO: maybe not this way?
- var shuffledStatuses = new List();
- for (var i = originalStatuses.Count; i > 0; i--)
- {
- var index = random.Next(originalStatuses.Count);
- shuffledStatuses.Add(originalStatuses[index]);
- originalStatuses.RemoveAt(index);
- }
- foreach (var status in shuffledStatuses)
+ foreach (var status in state.Statuses)
{
if (status.Value is StatusLightData statusLightData)
{
diff --git a/Content.IntegrationTests/DummyParallaxManager.cs b/Content.IntegrationTests/DummyParallaxManager.cs
index 7477fb1adb..91c739fa74 100644
--- a/Content.IntegrationTests/DummyParallaxManager.cs
+++ b/Content.IntegrationTests/DummyParallaxManager.cs
@@ -1,3 +1,5 @@
+using System;
+using System.Threading.Tasks;
using Content.Client.Parallax.Managers;
using Content.Client.Parallax;
using Robust.Shared.Maths;
@@ -6,13 +8,30 @@ namespace Content.IntegrationTests
{
public sealed class DummyParallaxManager : IParallaxManager
{
- public string ParallaxName { get; set; } = "";
public Vector2 ParallaxAnchor { get; set; }
- public ParallaxLayerPrepared[] ParallaxLayers { get; } = {};
-
- public void LoadParallax()
+ public bool IsLoaded(string name)
{
- ParallaxName = "default";
+ return true;
+ }
+
+ public ParallaxLayerPrepared[] GetParallaxLayers(string name)
+ {
+ return Array.Empty();
+ }
+
+ public void LoadDefaultParallax()
+ {
+ return;
+ }
+
+ public Task LoadParallaxByName(string name)
+ {
+ return Task.CompletedTask;
+ }
+
+ public void UnloadParallax(string name)
+ {
+ return;
}
}
}
diff --git a/Content.IntegrationTests/Tests/AI/BehaviorSetsTest.cs b/Content.IntegrationTests/Tests/AI/BehaviorSetsTest.cs
index 4f89e79791..1f21343c15 100644
--- a/Content.IntegrationTests/Tests/AI/BehaviorSetsTest.cs
+++ b/Content.IntegrationTests/Tests/AI/BehaviorSetsTest.cs
@@ -48,7 +48,7 @@ namespace Content.IntegrationTests.Tests.AI
{
foreach (var entity in protoManager.EnumeratePrototypes())
{
- if (!entity.TryGetComponent("UtilityAI", out var npcNode)) continue;
+ if (!entity.TryGetComponent("UtilityAI", out var npcNode)) continue;
foreach (var entry in npcNode.BehaviorSets)
{
diff --git a/Content.IntegrationTests/Tests/ClickableTest.cs b/Content.IntegrationTests/Tests/ClickableTest.cs
index 83cabc7282..943ee44ae3 100644
--- a/Content.IntegrationTests/Tests/ClickableTest.cs
+++ b/Content.IntegrationTests/Tests/ClickableTest.cs
@@ -1,9 +1,9 @@
-using System;
+using System;
using System.Threading.Tasks;
using Content.Client.Clickable;
using NUnit.Framework;
+using Robust.Client.GameObjects;
using Robust.Client.Graphics;
-using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
@@ -60,7 +60,6 @@ namespace Content.IntegrationTests.Tests
{
var ent = serverEntManager.SpawnEntity(prototype, testMap.GridCoords);
serverEntManager.GetComponent(ent).WorldRotation = angle;
- serverEntManager.GetComponent(ent).Scale = (scale, scale);
entity = ent;
});
@@ -71,6 +70,8 @@ namespace Content.IntegrationTests.Tests
await client.WaitPost(() =>
{
+ clientEntManager.GetComponent(entity).Scale = (scale, scale);
+
// these tests currently all assume player eye is 0
eyeManager.CurrentEye.Rotation = 0;
diff --git a/Content.IntegrationTests/Tests/Construction/ConstructionPrototypeTest.cs b/Content.IntegrationTests/Tests/Construction/ConstructionPrototypeTest.cs
index 2377b6abca..e9e3e64229 100644
--- a/Content.IntegrationTests/Tests/Construction/ConstructionPrototypeTest.cs
+++ b/Content.IntegrationTests/Tests/Construction/ConstructionPrototypeTest.cs
@@ -1,6 +1,10 @@
using System.Threading.Tasks;
+using Content.Server.Construction.Components;
using Content.Shared.Construction.Prototypes;
using NUnit.Framework;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Construction
@@ -10,6 +14,42 @@ namespace Content.IntegrationTests.Tests.Construction
{
// discount linter for construction graphs
// TODO: Create serialization validators for these?
+ // Top test definitely can be but writing a serializer takes ages.
+
+ ///
+ /// Checks every entity prototype with a construction component has a valid start node.
+ ///
+ [Test]
+ public async Task TestStartNodeValid()
+ {
+ await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true});
+ var server = pairTracker.Pair.Server;
+
+ var entMan = server.ResolveDependency();
+ var protoMan = server.ResolveDependency();
+
+ var map = await PoolManager.CreateTestMap(pairTracker);
+
+ await server.WaitAssertion(() =>
+ {
+ foreach (var proto in protoMan.EnumeratePrototypes())
+ {
+ if (!proto.Components.ContainsKey("Construction"))
+ continue;
+
+ var ent = entMan.SpawnEntity(proto.ID, new MapCoordinates(Vector2.Zero, map.MapId));
+ var construction = entMan.GetComponent(ent);
+
+ var graph = protoMan.Index(construction.Graph);
+ entMan.DeleteEntity(ent);
+
+ Assert.That(graph.Nodes.ContainsKey(construction.Node),
+ $"Found no startNode \"{construction.Node}\" on graph \"{graph.ID}\" for entity \"{proto.ID}\"!");
+ }
+ });
+
+ await pairTracker.CleanReturnAsync();
+ }
[Test]
public async Task TestStartIsValid()
diff --git a/Content.IntegrationTests/Tests/DeleteInventoryTest.cs b/Content.IntegrationTests/Tests/DeleteInventoryTest.cs
index 37eaedfb65..0bc69de7ea 100644
--- a/Content.IntegrationTests/Tests/DeleteInventoryTest.cs
+++ b/Content.IntegrationTests/Tests/DeleteInventoryTest.cs
@@ -1,7 +1,9 @@
-using System.Threading.Tasks;
+using System.Threading.Tasks;
using Content.Server.Clothing.Components;
using Content.Server.Inventory;
+using Content.Shared.Clothing.EntitySystems;
using Content.Shared.Inventory;
+using Content.Shared.Item;
using NUnit.Framework;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
@@ -35,8 +37,9 @@ namespace Content.IntegrationTests.Tests
entMgr.AddComponent(container);
var child = entMgr.SpawnEntity(null, MapCoordinates.Nullspace);
- var item = entMgr.AddComponent(child);
- item.SlotFlags = SlotFlags.HEAD;
+ var item = entMgr.AddComponent(child);
+
+ IoCManager.Resolve().GetEntitySystem().SetSlots(item.Owner, SlotFlags.HEAD, item);
// Equip item.
Assert.That(invSystem.TryEquip(container, child, "head"), Is.True);
diff --git a/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs b/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs
index a4d3cf1f31..3fe35dc9a7 100644
--- a/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs
+++ b/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs
@@ -42,7 +42,10 @@ namespace Content.IntegrationTests.Tests.DoAfter
});
await server.WaitRunTicks(1);
+ Assert.That(task.Status, Is.EqualTo(TaskStatus.RanToCompletion));
+#pragma warning disable RA0004
Assert.That(task.Result == DoAfterStatus.Finished);
+#pragma warning restore RA0004
await pairTracker.CleanReturnAsync();
}
@@ -69,7 +72,10 @@ namespace Content.IntegrationTests.Tests.DoAfter
});
await server.WaitRunTicks(3);
- Assert.That(task.Result == DoAfterStatus.Cancelled, $"Result was {task.Result}");
+ Assert.That(task.Status, Is.EqualTo(TaskStatus.RanToCompletion));
+#pragma warning disable RA0004
+ Assert.That(task.Result, Is.EqualTo(DoAfterStatus.Cancelled), $"Result was {task.Result}");
+#pragma warning restore RA0004
await pairTracker.CleanReturnAsync();
}
diff --git a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
index fd4fb903eb..caeab3c561 100644
--- a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
+++ b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
@@ -11,7 +11,7 @@ using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests.Gravity
{
[TestFixture]
- [TestOf(typeof(WeightlessSystem))]
+ [TestOf(typeof(GravitySystem))]
[TestOf(typeof(GravityGeneratorComponent))]
public sealed class WeightlessStatusTests
{
@@ -21,6 +21,9 @@ namespace Content.IntegrationTests.Tests.Gravity
id: HumanDummy
components:
- type: Alerts
+ - type: Physics
+ bodyType: Dynamic
+
- type: entity
name: GravityGeneratorDummy
id: GravityGeneratorDummy
@@ -38,7 +41,6 @@ namespace Content.IntegrationTests.Tests.Gravity
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes});
var server = pairTracker.Pair.Server;
- var mapManager = server.ResolveDependency();
var entityManager = server.ResolveDependency();
var alertsSystem = server.ResolveDependency().GetEntitySystem();
diff --git a/Content.IntegrationTests/Tests/GravityGridTest.cs b/Content.IntegrationTests/Tests/GravityGridTest.cs
index c09e9d4d61..d803118ec2 100644
--- a/Content.IntegrationTests/Tests/GravityGridTest.cs
+++ b/Content.IntegrationTests/Tests/GravityGridTest.cs
@@ -70,8 +70,8 @@ namespace Content.IntegrationTests.Tests
var grid1Entity = grid1.GridEntityId;
var grid2Entity = grid2.GridEntityId;
- Assert.That(!entityMan.GetComponent(grid1Entity).Enabled);
- Assert.That(entityMan.GetComponent(grid2Entity).Enabled);
+ Assert.That(!entityMan.GetComponent(grid1Entity).EnabledVV);
+ Assert.That(entityMan.GetComponent(grid2Entity).EnabledVV);
// Re-enable needs power so it turns off again.
// Charge rate is ridiculously high so it finishes in one tick.
@@ -88,7 +88,7 @@ namespace Content.IntegrationTests.Tests
var grid2Entity = grid2.GridEntityId;
- Assert.That(entityMan.GetComponent(grid2Entity).Enabled, Is.False);
+ Assert.That(entityMan.GetComponent(grid2Entity).EnabledVV, Is.False);
});
await pairTracker.CleanReturnAsync();
diff --git a/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs b/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs
index 4ebb5cd38c..bc1cce5222 100644
--- a/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs
+++ b/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs
@@ -26,7 +26,8 @@ namespace Content.IntegrationTests.Tests
id: UniformDummy
components:
- type: Clothing
- Slots: [innerclothing]
+ slots: [innerclothing]
+ - type: Item
size: 5
- type: entity
@@ -34,8 +35,9 @@ namespace Content.IntegrationTests.Tests
id: IDCardDummy
components:
- type: Clothing
- Slots:
+ slots:
- idcard
+ - type: Item
size: 5
- type: IdCard
diff --git a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs
index be084762cc..29de7ba73e 100644
--- a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs
+++ b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs
@@ -5,6 +5,7 @@ using Content.Server.Interaction;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
+using Content.Shared.Item;
using Content.Shared.Weapons.Melee;
using NUnit.Framework;
using Robust.Shared.Containers;
@@ -13,7 +14,6 @@ using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Reflection;
-using ItemComponent = Content.Server.Clothing.Components.ItemComponent;
namespace Content.IntegrationTests.Tests.Interaction.Click
{
diff --git a/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs b/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs
index a2cc176508..817442b4c5 100644
--- a/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs
@@ -12,7 +12,7 @@ namespace Content.IntegrationTests.Tests.Interaction
[TestOf(typeof(SharedInteractionSystem))]
public sealed class InRangeUnobstructed
{
- private const string HumanId = "MobHumanBase";
+ private const string HumanId = "MobHuman";
private const float InteractionRange = SharedInteractionSystem.InteractionRange;
diff --git a/Content.IntegrationTests/Tests/InventoryHelpersTest.cs b/Content.IntegrationTests/Tests/InventoryHelpersTest.cs
index 54400ab61d..5ef3c2990d 100644
--- a/Content.IntegrationTests/Tests/InventoryHelpersTest.cs
+++ b/Content.IntegrationTests/Tests/InventoryHelpersTest.cs
@@ -28,7 +28,7 @@ namespace Content.IntegrationTests.Tests
id: InventoryJumpsuitJanitorDummy
components:
- type: Clothing
- Slots: [innerclothing]
+ slots: [innerclothing]
- type: entity
name: InventoryIDCardDummy
@@ -36,7 +36,7 @@ namespace Content.IntegrationTests.Tests
components:
- type: Clothing
QuickEquip: false
- Slots:
+ slots:
- idcard
- type: PDA
";
diff --git a/Content.MapRenderer/Painters/MapPainter.cs b/Content.MapRenderer/Painters/MapPainter.cs
index ac93680015..bc5a0abd16 100644
--- a/Content.MapRenderer/Painters/MapPainter.cs
+++ b/Content.MapRenderer/Painters/MapPainter.cs
@@ -1,22 +1,19 @@
-#nullable enable
+#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.IntegrationTests;
-using Content.Shared.CCVar;
using Robust.Client.GameObjects;
using Robust.Server.GameObjects;
using Robust.Server.Player;
-using Robust.Shared;
-using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
-using SpriteComponent = Robust.Server.GameObjects.SpriteComponent;
+using SpriteComponent = Robust.Client.GameObjects.SpriteComponent;
namespace Content.MapRenderer.Painters
{
@@ -40,7 +37,7 @@ namespace Content.MapRenderer.Painters
await client.WaitPost(() =>
{
- if (cEntityManager.TryGetComponent(cPlayerManager.LocalPlayer!.ControlledEntity!, out Robust.Client.GameObjects.SpriteComponent? sprite))
+ if (cEntityManager.TryGetComponent(cPlayerManager.LocalPlayer!.ControlledEntity!, out SpriteComponent? sprite))
{
sprite.Visible = false;
}
@@ -49,14 +46,6 @@ namespace Content.MapRenderer.Painters
var sEntityManager = server.ResolveDependency();
var sPlayerManager = server.ResolveDependency();
- await server.WaitPost(() =>
- {
- if (sEntityManager.TryGetComponent(sPlayerManager.ServerSessions.Single().AttachedEntity!, out SpriteComponent? sprite))
- {
- sprite.Visible = false;
- }
- });
-
await PoolManager.RunTicksSync(pairTracker.Pair, 10);
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
diff --git a/Content.Server/AI/Commands/AddAiCommand.cs b/Content.Server/AI/Commands/AddAiCommand.cs
index af7e463289..d56996a704 100644
--- a/Content.Server/AI/Commands/AddAiCommand.cs
+++ b/Content.Server/AI/Commands/AddAiCommand.cs
@@ -1,5 +1,6 @@
using Content.Server.Administration;
using Content.Server.AI.Components;
+using Content.Server.AI.EntitySystems;
using Content.Server.AI.Utility;
using Content.Server.AI.Utility.AiLogic;
using Content.Shared.Administration;
@@ -35,22 +36,22 @@ namespace Content.Server.AI.Commands
return;
}
- if (_entities.HasComponent(entId))
+ if (_entities.HasComponent(entId))
{
shell.WriteLine("Entity already has an AI component.");
return;
}
- var comp = _entities.AddComponent(entId);
- var behaviorManager = IoCManager.Resolve();
+ var comp = _entities.AddComponent(entId);
+ var npcSystem = IoCManager.Resolve().EntitySysManager.GetEntitySystem();
for (var i = 1; i < args.Length; i++)
{
var bSet = args[i];
- behaviorManager.AddBehaviorSet(comp, bSet, false);
+ npcSystem.AddBehaviorSet(comp, bSet, false);
}
- behaviorManager.RebuildActions(comp);
+ npcSystem.RebuildActions(comp);
shell.WriteLine("AI component added.");
}
}
diff --git a/Content.Server/AI/Components/ActiveNPCComponent.cs b/Content.Server/AI/Components/ActiveNPCComponent.cs
new file mode 100644
index 0000000000..410c07e5f3
--- /dev/null
+++ b/Content.Server/AI/Components/ActiveNPCComponent.cs
@@ -0,0 +1,7 @@
+namespace Content.Server.AI.Components;
+
+///
+/// Added to NPCs that are actively being updated.
+///
+[RegisterComponent]
+public sealed class ActiveNPCComponent : Component {}
diff --git a/Content.Server/AI/Components/AiControllerComponent.cs b/Content.Server/AI/Components/AiControllerComponent.cs
deleted file mode 100644
index 68dac19adf..0000000000
--- a/Content.Server/AI/Components/AiControllerComponent.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using Content.Server.AI.EntitySystems;
-using Content.Server.Station.Systems;
-using Content.Shared.Movement.Components;
-using Content.Shared.Roles;
-using Robust.Shared.Map;
-using Robust.Shared.Prototypes;
-
-namespace Content.Server.AI.Components
-{
- [RegisterComponent]
- [Virtual]
- public class AiControllerComponent : Component
- {
- [DataField("logic")] private float _visionRadius = 8.0f;
-
- // TODO: Need to ECS a lot more of the AI first before we can ECS this
- ///
- /// Whether the AI is actively iterated.
- ///
- public bool Awake
- {
- get => _awake;
- set
- {
- if (_awake == value) return;
-
- _awake = value;
-
- if (_awake)
- EntitySystem.Get().WakeNPC(this);
- else
- EntitySystem.Get().SleepNPC(this);
- }
- }
-
- [DataField("awake")]
- private bool _awake = true;
-
- [ViewVariables(VVAccess.ReadWrite)]
- public float VisionRadius
- {
- get => _visionRadius;
- set => _visionRadius = value;
- }
-
- ///
- /// Is the entity Sprinting (running)?
- ///
- [ViewVariables]
- public bool Sprinting { get; } = true;
-
- ///
- /// Calculated linear velocity direction of the entity.
- ///
- [ViewVariables]
- public Vector2 VelocityDir { get; set; }
-
- public virtual void Update(float frameTime) {}
- }
-}
diff --git a/Content.Server/AI/Components/NPCComponent.cs b/Content.Server/AI/Components/NPCComponent.cs
new file mode 100644
index 0000000000..97e229f422
--- /dev/null
+++ b/Content.Server/AI/Components/NPCComponent.cs
@@ -0,0 +1,17 @@
+using Content.Server.AI.EntitySystems;
+
+namespace Content.Server.AI.Components
+{
+ [Access(typeof(NPCSystem))]
+ public abstract class NPCComponent : Component
+ {
+ // TODO: Soon. I didn't realise how much effort it was to deprecate the old one.
+ ///
+ /// Contains all of the world data for a particular NPC in terms of how it sees the world.
+ ///
+ //[ViewVariables, DataField("blackboardA")]
+ //public Dictionary BlackboardA = new();
+
+ public float VisionRadius => 7f;
+ }
+}
diff --git a/Content.Server/AI/Considerations/Bots/CanInjectCon.cs b/Content.Server/AI/Considerations/Bots/CanInjectCon.cs
new file mode 100644
index 0000000000..02169eac3a
--- /dev/null
+++ b/Content.Server/AI/Considerations/Bots/CanInjectCon.cs
@@ -0,0 +1,38 @@
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.Tracking;
+using Content.Shared.Damage;
+using Content.Shared.MobState.Components;
+using Content.Server.Silicons.Bots;
+
+namespace Content.Server.AI.Utility.Considerations.Bot
+{
+ public sealed class CanInjectCon : Consideration
+ {
+ protected override float GetScore(Blackboard context)
+ {
+ var entMan = IoCManager.Resolve();
+ var target = context.GetState().GetValue();
+
+ if (target == null || !entMan.TryGetComponent(target, out DamageableComponent? damageableComponent))
+ return 0;
+
+ if (entMan.TryGetComponent(target, out RecentlyInjectedComponent? recently))
+ return 0f;
+
+ if (!entMan.TryGetComponent(target, out MobStateComponent? mobState) || mobState.IsDead())
+ return 0f;
+
+ if (damageableComponent.TotalDamage == 0)
+ return 0f;
+
+ if (damageableComponent.TotalDamage <= MedibotComponent.StandardMedDamageThreshold)
+ return 1f;
+
+ if (damageableComponent.TotalDamage >= MedibotComponent.EmergencyMedDamageThreshold)
+ return 1f;
+
+ return 0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/EntitySystems/InjectNearbySystem.cs b/Content.Server/AI/EntitySystems/InjectNearbySystem.cs
new file mode 100644
index 0000000000..f702f5db48
--- /dev/null
+++ b/Content.Server/AI/EntitySystems/InjectNearbySystem.cs
@@ -0,0 +1,74 @@
+using Content.Server.Chemistry.Components.SolutionManager;
+using Content.Server.Chemistry.EntitySystems;
+using Content.Server.AI.Tracking;
+using Content.Server.Popups;
+using Content.Server.Chat.Systems;
+using Content.Server.Silicons.Bots;
+using Content.Shared.MobState.Components;
+using Content.Shared.Damage;
+using Content.Shared.Interaction;
+using Robust.Shared.Player;
+using Robust.Shared.Audio;
+
+namespace Content.Server.AI.EntitySystems
+{
+ public sealed class InjectNearbySystem : EntitySystem
+ {
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly ChatSystem _chat = default!;
+ [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
+
+ public EntityUid GetNearbyInjectable(EntityUid medibot, float range = 4)
+ {
+ foreach (var entity in _lookup.GetEntitiesInRange(medibot, range))
+ {
+ if (HasComp(entity) && HasComp(entity))
+ return entity;
+ }
+
+ return default;
+ }
+
+ public bool Inject(EntityUid medibot, EntityUid target)
+ {
+ if (!TryComp(medibot, out var botComp))
+ return false;
+
+ if (!TryComp(target, out var damage))
+ return false;
+
+ if (!_solutionSystem.TryGetInjectableSolution(target, out var injectable))
+ return false;
+
+ if (!_interactionSystem.InRangeUnobstructed(medibot, target))
+ return true; // return true lets the bot reattempt the action on the same target
+
+ if (damage.TotalDamage == 0)
+ return false;
+
+ if (damage.TotalDamage <= MedibotComponent.StandardMedDamageThreshold)
+ {
+ _solutionSystem.TryAddReagent(target, injectable, botComp.StandardMed, botComp.StandardMedInjectAmount, out var accepted);
+ EnsureComp(target);
+ _popupSystem.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, Filter.Entities(target));
+ SoundSystem.Play("/Audio/Items/hypospray.ogg", Filter.Pvs(target), target);
+ _chat.TrySendInGameICMessage(medibot, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, false);
+ return true;
+ }
+
+ if (damage.TotalDamage >= MedibotComponent.EmergencyMedDamageThreshold)
+ {
+ _solutionSystem.TryAddReagent(target, injectable, botComp.EmergencyMed, botComp.EmergencyMedInjectAmount, out var accepted);
+ EnsureComp(target);
+ _popupSystem.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, Filter.Entities(target));
+ SoundSystem.Play("/Audio/Items/hypospray.ogg", Filter.Pvs(target), target);
+ _chat.TrySendInGameICMessage(medibot, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, false);
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/Content.Server/AI/EntitySystems/NPCSystem.Blackboard.cs b/Content.Server/AI/EntitySystems/NPCSystem.Blackboard.cs
new file mode 100644
index 0000000000..b818aeb1a5
--- /dev/null
+++ b/Content.Server/AI/EntitySystems/NPCSystem.Blackboard.cs
@@ -0,0 +1,42 @@
+using System.Diagnostics.CodeAnalysis;
+using Content.Server.AI.Components;
+
+namespace Content.Server.AI.EntitySystems;
+
+public sealed partial class NPCSystem
+{
+ /*
+ ///
+ /// Tries to get the blackboard data for a particular key. Returns default if not found
+ ///
+ public T? GetValueOrDefault(NPCComponent component, string key)
+ {
+ if (component.BlackboardA.TryGetValue(key, out var value))
+ {
+ return (T) value;
+ }
+
+ return default;
+ }
+
+ ///
+ /// Tries to get the blackboard data for a particular key.
+ ///
+ public bool TryGetValue(NPCComponent component, string key, [NotNullWhen(true)] out T? value)
+ {
+ if (component.BlackboardA.TryGetValue(key, out var data))
+ {
+ value = (T) data;
+ return true;
+ }
+
+ value = default;
+ return false;
+ }
+
+ /*
+ * Constants to make development easier
+ */
+
+ public const string VisionRadius = "VisionRadius";
+}
diff --git a/Content.Server/AI/EntitySystems/NPCSystem.Utility.cs b/Content.Server/AI/EntitySystems/NPCSystem.Utility.cs
new file mode 100644
index 0000000000..10f96b3660
--- /dev/null
+++ b/Content.Server/AI/EntitySystems/NPCSystem.Utility.cs
@@ -0,0 +1,284 @@
+using System.Runtime.ExceptionServices;
+using System.Threading;
+using Content.Server.AI.Components;
+using Content.Server.AI.LoadBalancer;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Utility;
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.AiLogic;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Utility;
+using Content.Server.CPUJob.JobQueues;
+using Content.Server.CPUJob.JobQueues.Queues;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Reflection;
+
+namespace Content.Server.AI.EntitySystems;
+
+public sealed partial class NPCSystem
+{
+ /*
+ * Handles Utility AI, implemented via IAUS
+ */
+
+ private readonly NpcActionComparer _comparer = new();
+
+ private Dictionary> _behaviorSets = new();
+
+ private readonly AiActionJobQueue _aiRequestQueue = new();
+
+ private void InitializeUtility()
+ {
+ SubscribeLocalEvent(OnUtilityStartup);
+
+ foreach (var bSet in _prototypeManager.EnumeratePrototypes())
+ {
+ var actions = new List();
+
+ foreach (var act in bSet.Actions)
+ {
+ if (!_reflectionManager.TryLooseGetType(act, out var parsedType) ||
+ !typeof(IAiUtility).IsAssignableFrom(parsedType))
+ {
+ _sawmill.Error($"Unable to parse AI action for {act}");
+ }
+ else
+ {
+ actions.Add(parsedType);
+ }
+ }
+
+ _behaviorSets[bSet.ID] = actions;
+ }
+ }
+
+ private void OnUtilityStartup(EntityUid uid, UtilityNPCComponent component, ComponentStartup args)
+ {
+ if (component.BehaviorSets.Count > 0)
+ {
+ RebuildActions(component);
+ }
+
+ component._planCooldownRemaining = component.PlanCooldown;
+ component._blackboard = new Blackboard(component.Owner);
+ }
+
+ public AiActionRequestJob RequestAction(UtilityNPCComponent component, AiActionRequest request, CancellationTokenSource cancellationToken)
+ {
+ var job = new AiActionRequestJob(0.002, request, cancellationToken.Token);
+ // AI should already know if it shouldn't request again
+ _aiRequestQueue.EnqueueJob(job);
+ return job;
+ }
+
+ private void UpdateUtility(float frameTime)
+ {
+ foreach (var (_, comp) in EntityQuery())
+ {
+ if (_count >= _maxUpdates) break;
+
+ Update(comp, frameTime);
+ _count++;
+ }
+
+ _aiRequestQueue.Process();
+ }
+
+ private void ReceivedAction(UtilityNPCComponent component)
+ {
+ if (component._actionRequest == null)
+ {
+ return;
+ }
+
+ switch (component._actionRequest.Exception)
+ {
+ case null:
+ break;
+ default:
+ _sawmill.Fatal(component._actionRequest.Exception.ToString());
+ ExceptionDispatchInfo.Capture(component._actionRequest.Exception).Throw();
+ // The code never actually reaches here, because the above throws.
+ // This is to tell the compiler that the flow never leaves here.
+ throw component._actionRequest.Exception;
+ }
+ var action = component._actionRequest.Result;
+ component._actionRequest = null;
+ // Actions with lower scores should be implicitly dumped by GetAction
+ // If we're not allowed to replace the action with an action of the same type then dump.
+ if (action == null || !action.CanOverride && component.CurrentAction?.GetType() == action.GetType())
+ {
+ return;
+ }
+
+ var currentOp = component.CurrentAction?.ActionOperators.Peek();
+ if (currentOp != null && currentOp.HasStartup)
+ {
+ currentOp.Shutdown(Outcome.Failed);
+ }
+
+ component.CurrentAction = action;
+ action.SetupOperators(component._blackboard);
+ }
+
+ private void Update(UtilityNPCComponent component, float frameTime)
+ {
+ // If we asked for a new action we don't want to dump the existing one.
+ if (component._actionRequest != null)
+ {
+ if (component._actionRequest.Status != JobStatus.Finished)
+ {
+ return;
+ }
+
+ ReceivedAction(component);
+ // Do something next tick
+ return;
+ }
+
+ component._planCooldownRemaining -= frameTime;
+
+ // Might find a better action while we're doing one already
+ if (component._planCooldownRemaining <= 0.0f)
+ {
+ component._planCooldownRemaining = component.PlanCooldown;
+ component._actionCancellation = new CancellationTokenSource();
+ component._actionRequest = RequestAction(component, new AiActionRequest(component.Owner, component._blackboard, component.AvailableActions), component._actionCancellation);
+
+ return;
+ }
+
+ // When we spawn in we won't get an action for a bit
+ if (component.CurrentAction == null)
+ {
+ return;
+ }
+
+ var outcome = component.CurrentAction.Execute(frameTime);
+
+ switch (outcome)
+ {
+ case Outcome.Success:
+ if (component.CurrentAction.ActionOperators.Count == 0)
+ {
+ component.CurrentAction.Shutdown();
+ component.CurrentAction = null;
+ // Nothing to compare new action to
+ component._blackboard.GetState().SetValue(0.0f);
+ }
+ break;
+ case Outcome.Continuing:
+ break;
+ case Outcome.Failed:
+ component.CurrentAction.Shutdown();
+ component.CurrentAction = null;
+ component._blackboard.GetState().SetValue(0.0f);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ ///
+ /// Adds the BehaviorSet to the NPC.
+ ///
+ ///
+ ///
+ /// Set to false if you want to manually rebuild it after bulk updates.
+ public void AddBehaviorSet(UtilityNPCComponent npc, string behaviorSet, bool rebuild = true)
+ {
+ if (!_behaviorSets.ContainsKey(behaviorSet))
+ {
+ _sawmill.Error($"Tried to add BehaviorSet {behaviorSet} to {npc} but no such BehaviorSet found!");
+ return;
+ }
+
+ if (!npc.BehaviorSets.Add(behaviorSet))
+ {
+ _sawmill.Error($"Tried to add BehaviorSet {behaviorSet} to {npc} which already has the BehaviorSet!");
+ return;
+ }
+
+ if (rebuild)
+ RebuildActions(npc);
+
+ if (npc.BehaviorSets.Count == 1 && !IsAwake(npc))
+ WakeNPC(npc);
+ }
+
+ ///
+ /// Removes the BehaviorSet from the NPC.
+ ///
+ ///
+ ///
+ /// Set to false if yo uwant to manually rebuild it after bulk updates.
+ public void RemoveBehaviorSet(UtilityNPCComponent npc, string behaviorSet, bool rebuild = true)
+ {
+ if (!_behaviorSets.TryGetValue(behaviorSet, out var actions))
+ {
+ Logger.Error($"Tried to remove BehaviorSet {behaviorSet} from {npc} but no such BehaviorSet found!");
+ return;
+ }
+
+ if (!npc.BehaviorSets.Remove(behaviorSet))
+ {
+ Logger.Error($"Tried to remove BehaviorSet {behaviorSet} from {npc} but it doesn't have that BehaviorSet!");
+ return;
+ }
+
+ if (rebuild)
+ RebuildActions(npc);
+
+ if (npc.BehaviorSets.Count == 0)
+ SleepNPC(npc);
+ }
+
+ ///
+ /// Clear our actions and re-instantiate them from our BehaviorSets.
+ /// Will ensure each action is unique.
+ ///
+ ///
+ public void RebuildActions(UtilityNPCComponent npc)
+ {
+ npc.AvailableActions.Clear();
+ foreach (var bSet in npc.BehaviorSets)
+ {
+ foreach (var action in GetActions(bSet))
+ {
+ if (npc.AvailableActions.Contains(action)) continue;
+ // Setup
+ action.Owner = npc.Owner;
+
+ // Ad to actions.
+ npc.AvailableActions.Add(action);
+ }
+ }
+
+ SortActions(npc);
+ }
+
+ private IEnumerable GetActions(string behaviorSet)
+ {
+ foreach (var action in _behaviorSets[behaviorSet])
+ {
+ yield return (IAiUtility) _typeFactory.CreateInstance(action);
+ }
+ }
+
+ ///
+ /// Whenever the behavior sets are changed we'll re-sort the actions by bonus
+ ///
+ private void SortActions(UtilityNPCComponent npc)
+ {
+ npc.AvailableActions.Sort(_comparer);
+ }
+
+ private sealed class NpcActionComparer : Comparer
+ {
+ public override int Compare(IAiUtility? x, IAiUtility? y)
+ {
+ if (x == null || y == null) return 0;
+ return y.Bonus.CompareTo(x.Bonus);
+ }
+ }
+}
diff --git a/Content.Server/AI/EntitySystems/NPCSystem.cs b/Content.Server/AI/EntitySystems/NPCSystem.cs
index da4a7191fa..25da7d8621 100644
--- a/Content.Server/AI/EntitySystems/NPCSystem.cs
+++ b/Content.Server/AI/EntitySystems/NPCSystem.cs
@@ -1,10 +1,10 @@
-using System.Linq;
using Content.Server.AI.Components;
using Content.Shared.CCVar;
using Content.Shared.MobState;
using JetBrains.Annotations;
using Robust.Shared.Configuration;
-using Robust.Shared.Random;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Reflection;
namespace Content.Server.AI.EntitySystems
{
@@ -12,120 +12,103 @@ namespace Content.Server.AI.EntitySystems
/// Handles NPCs running every tick.
///
[UsedImplicitly]
- internal sealed class NPCSystem : EntitySystem
+ public sealed partial class NPCSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
- [Dependency] private readonly IRobustRandom _robustRandom = default!;
+ [Dependency] private readonly IDynamicTypeFactory _typeFactory = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IReflectionManager _reflectionManager = default!;
- ///
- /// To avoid iterating over dead AI continuously they can wake and sleep themselves when necessary.
- ///
- private readonly HashSet _awakeNPCs = new();
+ private ISawmill _sawmill = default!;
///
/// Whether any NPCs are allowed to run at all.
///
public bool Enabled { get; set; } = true;
+ private int _maxUpdates;
+
+ private int _count;
+
///
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent(OnMobStateChange);
- SubscribeLocalEvent(OnNPCInit);
- SubscribeLocalEvent(OnNPCShutdown);
+ _sawmill = Logger.GetSawmill("npc");
+ InitializeUtility();
+ SubscribeLocalEvent(OnMobStateChange);
+ SubscribeLocalEvent(OnNPCInit);
+ SubscribeLocalEvent(OnNPCShutdown);
_configurationManager.OnValueChanged(CCVars.NPCEnabled, SetEnabled, true);
-
- var maxUpdates = _configurationManager.GetCVar(CCVars.NPCMaxUpdates);
-
- if (maxUpdates < 1024)
- _awakeNPCs.EnsureCapacity(maxUpdates);
+ _configurationManager.OnValueChanged(CCVars.NPCMaxUpdates, SetMaxUpdates, true);
}
+ private void SetMaxUpdates(int obj) => _maxUpdates = obj;
private void SetEnabled(bool value) => Enabled = value;
public override void Shutdown()
{
base.Shutdown();
_configurationManager.UnsubValueChanged(CCVars.NPCEnabled, SetEnabled);
+ _configurationManager.UnsubValueChanged(CCVars.NPCMaxUpdates, SetMaxUpdates);
}
- private void OnNPCInit(EntityUid uid, AiControllerComponent component, ComponentInit args)
+ private void OnNPCInit(EntityUid uid, NPCComponent component, ComponentInit args)
{
- if (!component.Awake) return;
-
- _awakeNPCs.Add(component);
+ WakeNPC(component);
}
- private void OnNPCShutdown(EntityUid uid, AiControllerComponent component, ComponentShutdown args)
+ private void OnNPCShutdown(EntityUid uid, NPCComponent component, ComponentShutdown args)
{
- _awakeNPCs.Remove(component);
+ SleepNPC(component);
+ }
+
+ ///
+ /// Is the NPC awake and updating?
+ ///
+ public bool IsAwake(NPCComponent component, ActiveNPCComponent? active = null)
+ {
+ return Resolve(component.Owner, ref active, false);
}
///
/// Allows the NPC to actively be updated.
///
- ///
- public void WakeNPC(AiControllerComponent component)
+ public void WakeNPC(NPCComponent component)
{
- _awakeNPCs.Add(component);
+ _sawmill.Debug($"Waking {ToPrettyString(component.Owner)}");
+ EnsureComp(component.Owner);
}
///
/// Stops the NPC from actively being updated.
///
- ///
- public void SleepNPC(AiControllerComponent component)
+ public void SleepNPC(NPCComponent component)
{
- _awakeNPCs.Remove(component);
+ _sawmill.Debug($"Sleeping {ToPrettyString(component.Owner)}");
+ RemComp(component.Owner);
}
///
public override void Update(float frameTime)
{
+ base.Update(frameTime);
if (!Enabled) return;
- var cvarMaxUpdates = _configurationManager.GetCVar(CCVars.NPCMaxUpdates);
-
- if (cvarMaxUpdates <= 0) return;
-
- var npcs = _awakeNPCs.ToArray();
- var startIndex = 0;
-
- // If we're overcap we'll just update randomly so they all still at least do something
- // Didn't randomise the array (even though it'd probably be better) because god damn that'd be expensive.
- if (npcs.Length > cvarMaxUpdates)
- {
- startIndex = _robustRandom.Next(npcs.Length);
- }
-
- for (var i = 0; i < npcs.Length; i++)
- {
- MetaDataComponent? metadata = null;
- var index = (i + startIndex) % npcs.Length;
- var npc = npcs[index];
-
- if (Deleted(npc.Owner, metadata))
- continue;
-
- // Probably gets resolved in deleted for us already
- if (Paused(npc.Owner, metadata))
- continue;
-
- npc.Update(frameTime);
- }
+ _count = 0;
+ UpdateUtility(frameTime);
}
- private void OnMobStateChange(EntityUid uid, AiControllerComponent component, MobStateChangedEvent args)
+ private void OnMobStateChange(EntityUid uid, NPCComponent component, MobStateChangedEvent args)
{
switch (args.CurrentMobState)
{
case DamageState.Alive:
- component.Awake = true;
+ WakeNPC(component);
break;
case DamageState.Critical:
case DamageState.Dead:
- component.Awake = false;
+ SleepNPC(component);
break;
}
}
diff --git a/Content.Server/AI/LoadBalancer/AiActionRequestJob.cs b/Content.Server/AI/LoadBalancer/AiActionRequestJob.cs
index 64c0b6808e..452aca7961 100644
--- a/Content.Server/AI/LoadBalancer/AiActionRequestJob.cs
+++ b/Content.Server/AI/LoadBalancer/AiActionRequestJob.cs
@@ -35,7 +35,7 @@ namespace Content.Server.AI.LoadBalancer
var entity = _request.Context.GetState().GetValue();
- if (!IoCManager.Resolve().HasComponent(entity))
+ if (!IoCManager.Resolve().HasComponent(entity))
{
return null;
}
diff --git a/Content.Server/AI/LoadBalancer/AiActionSystem.cs b/Content.Server/AI/LoadBalancer/AiActionSystem.cs
deleted file mode 100644
index f6110ab268..0000000000
--- a/Content.Server/AI/LoadBalancer/AiActionSystem.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System.Threading;
-using Content.Server.CPUJob.JobQueues.Queues;
-
-namespace Content.Server.AI.LoadBalancer
-{
- ///
- /// This will queue up an AI's request for an action and give it one when possible
- ///
- public sealed class AiActionSystem : EntitySystem
- {
- private readonly AiActionJobQueue _aiRequestQueue = new();
-
- public AiActionRequestJob RequestAction(AiActionRequest request, CancellationTokenSource cancellationToken)
- {
- var job = new AiActionRequestJob(0.002, request, cancellationToken.Token);
- // AI should already know if it shouldn't request again
- _aiRequestQueue.EnqueueJob(job);
- return job;
- }
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
- _aiRequestQueue.Process();
- }
- }
-}
diff --git a/Content.Server/AI/Operators/Bots/InjectOperator.cs b/Content.Server/AI/Operators/Bots/InjectOperator.cs
new file mode 100644
index 0000000000..d8061763b9
--- /dev/null
+++ b/Content.Server/AI/Operators/Bots/InjectOperator.cs
@@ -0,0 +1,24 @@
+using Content.Server.AI.EntitySystems;
+
+namespace Content.Server.AI.Operators.Bots
+{
+ public sealed class InjectOperator : AiOperator
+ {
+ private EntityUid _medibot;
+ private EntityUid _target;
+ public InjectOperator(EntityUid medibot, EntityUid target)
+ {
+ _medibot = medibot;
+ _target = target;
+ }
+
+ public override Outcome Execute(float frameTime)
+ {
+ var injectSystem = IoCManager.Resolve().GetEntitySystem();
+ if (injectSystem.Inject(_medibot, _target))
+ return Outcome.Success;
+
+ return Outcome.Failed;
+ }
+ }
+}
diff --git a/Content.Server/AI/Operators/Inventory/PickupEntityOperator.cs b/Content.Server/AI/Operators/Inventory/PickupEntityOperator.cs
index 38479ac52f..ef76ed1ec9 100644
--- a/Content.Server/AI/Operators/Inventory/PickupEntityOperator.cs
+++ b/Content.Server/AI/Operators/Inventory/PickupEntityOperator.cs
@@ -25,7 +25,7 @@ namespace Content.Server.AI.Operators.Inventory
var handsSys = sysMan.GetEntitySystem();
if (entMan.Deleted(_target)
- || !entMan.HasComponent(_target)
+ || !entMan.HasComponent(_target)
|| _target.IsInContainer()
|| !interactionSystem.InRangeUnobstructed(_owner, _target, popup: true))
{
@@ -35,7 +35,7 @@ namespace Content.Server.AI.Operators.Inventory
// select empty hand
if (!handsSys.TrySelectEmptyHand(_owner))
return Outcome.Failed;
-
+
interactionSystem.InteractHand(_owner, _target);
return Outcome.Success;
}
diff --git a/Content.Server/AI/Operators/Speech/SpeechOperator.cs b/Content.Server/AI/Operators/Speech/SpeechOperator.cs
new file mode 100644
index 0000000000..19fa4698ae
--- /dev/null
+++ b/Content.Server/AI/Operators/Speech/SpeechOperator.cs
@@ -0,0 +1,22 @@
+using Content.Server.Chat.Systems;
+
+namespace Content.Server.AI.Operators.Speech
+{
+ public sealed class SpeakOperator : AiOperator
+ {
+ private EntityUid _speaker;
+ private string _speechString;
+ public SpeakOperator(EntityUid speaker, string speechString)
+ {
+ _speaker = speaker;
+ _speechString = speechString;
+ }
+
+ public override Outcome Execute(float frameTime)
+ {
+ var chatSystem = IoCManager.Resolve().GetEntitySystem();
+ chatSystem.TrySendInGameICMessage(_speaker, _speechString, InGameICChatType.Speak, false);
+ return Outcome.Success;
+ }
+ }
+}
diff --git a/Content.Server/AI/Pathfinding/Accessible/ReachableArgs.cs b/Content.Server/AI/Pathfinding/Accessible/ReachableArgs.cs
index 9df5c9cd1f..c1f4544c62 100644
--- a/Content.Server/AI/Pathfinding/Accessible/ReachableArgs.cs
+++ b/Content.Server/AI/Pathfinding/Accessible/ReachableArgs.cs
@@ -1,4 +1,5 @@
using Content.Server.AI.Components;
+using Content.Server.AI.EntitySystems;
using Content.Shared.Access.Systems;
using Robust.Shared.Physics;
@@ -33,7 +34,7 @@ namespace Content.Server.AI.Pathfinding.Accessible
var accessSystem = EntitySystem.Get();
var access = accessSystem.FindAccessTags(entity);
- var visionRadius = entMan.GetComponent(entity).VisionRadius;
+ var visionRadius = entMan.GetComponent(entity).VisionRadius;
return new ReachableArgs(visionRadius, access, collisionMask);
}
diff --git a/Content.Server/AI/Tracking/RecentlyInjectedComponent.cs b/Content.Server/AI/Tracking/RecentlyInjectedComponent.cs
new file mode 100644
index 0000000000..ffcc025baa
--- /dev/null
+++ b/Content.Server/AI/Tracking/RecentlyInjectedComponent.cs
@@ -0,0 +1,12 @@
+namespace Content.Server.AI.Tracking
+{
+ /// Added when a medibot injects someone
+ /// So they don't get injected again for at least a minute.
+ [RegisterComponent]
+ public sealed class RecentlyInjectedComponent : Component
+ {
+ public float Accumulator = 0f;
+
+ public TimeSpan RemoveTime = TimeSpan.FromMinutes(1);
+ }
+}
diff --git a/Content.Server/AI/Tracking/RecentlyInjectedSystem.cs b/Content.Server/AI/Tracking/RecentlyInjectedSystem.cs
new file mode 100644
index 0000000000..0a9ad16a4d
--- /dev/null
+++ b/Content.Server/AI/Tracking/RecentlyInjectedSystem.cs
@@ -0,0 +1,25 @@
+namespace Content.Server.AI.Tracking
+{
+ public sealed class RecentlyInjectedSystem : EntitySystem
+ {
+
+ Queue RemQueue = new();
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ foreach (var toRemove in RemQueue)
+ {
+ RemComp(toRemove);
+ }
+ RemQueue.Clear();
+ foreach (var entity in EntityQuery())
+ {
+ entity.Accumulator += frameTime;
+ if (entity.Accumulator < entity.RemoveTime.TotalSeconds)
+ continue;
+ entity.Accumulator = 0;
+ RemQueue.Enqueue(entity.Owner);
+ }
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Bots/InjectNearby.cs b/Content.Server/AI/Utility/Actions/Bots/InjectNearby.cs
new file mode 100644
index 0000000000..13ec3ab469
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Bots/InjectNearby.cs
@@ -0,0 +1,57 @@
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Generic;
+using Content.Server.AI.Operators.Movement;
+using Content.Server.AI.Operators.Bots;
+using Content.Server.AI.Operators.Speech;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.Utility.Considerations.Containers;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.ActionBlocker;
+using Content.Server.AI.WorldState.States.Movement;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.Utility.Considerations.Bot;
+
+
+namespace Content.Server.AI.Utility.Actions.Bots
+{
+ public sealed class InjectNearby : UtilityAction
+ {
+ public EntityUid Target { get; set; } = default!;
+
+ public override void SetupOperators(Blackboard context)
+ {
+ MoveToEntityOperator moveOperator = new MoveToEntityOperator(Owner, Target);
+ float waitTime = 3f;
+
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ moveOperator,
+ new SpeakOperator(Owner, Loc.GetString("medibot-start-inject")),
+ new WaitOperator(waitTime),
+ new InjectOperator(Owner, Target),
+ });
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(Target);
+ context.GetState().SetValue(Target);
+ }
+
+ protected override IReadOnlyCollection> GetConsiderations(Blackboard context)
+ {
+ var considerationsManager = IoCManager.Resolve();
+
+ return new[]
+ {
+ considerationsManager.Get()
+ .BoolCurve(context),
+ considerationsManager.Get()
+ .BoolCurve(context),
+ considerationsManager.Get()
+ .BoolCurve(context),
+ };
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/AiLogic/UtilityAI.cs b/Content.Server/AI/Utility/AiLogic/UtilityAI.cs
deleted file mode 100644
index 4eb4e47a71..0000000000
--- a/Content.Server/AI/Utility/AiLogic/UtilityAI.cs
+++ /dev/null
@@ -1,176 +0,0 @@
-using Content.Server.AI.Components;
-using Content.Server.AI.EntitySystems;
-using Content.Server.AI.LoadBalancer;
-using Content.Server.AI.Operators;
-using Content.Server.AI.Utility.Actions;
-using Content.Server.AI.WorldState;
-using Content.Server.AI.WorldState.States.Utility;
-using Content.Server.CPUJob.JobQueues;
-using Content.Shared.Movement.Components;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
-using System.Runtime.ExceptionServices;
-using System.Threading;
-
-namespace Content.Server.AI.Utility.AiLogic
-{
- // TODO: Need to split out the IMover stuff for NPC to a generic one that can be used for hoomans as well.
- [RegisterComponent]
- [ComponentProtoName("UtilityAI")]
- [ComponentReference(typeof(AiControllerComponent))]
- public sealed class UtilityAi : AiControllerComponent
- {
- // TODO: Look at having ParallelOperators (probably no more than that as then you'd have a full-blown BT)
- // Also RepeatOperators (e.g. if we're following an entity keep repeating MoveToEntity)
- private AiActionSystem _planner = default!;
- public Blackboard Blackboard => _blackboard;
- private Blackboard _blackboard = default!;
-
- ///
- /// The sum of all BehaviorSets gives us what actions the AI can take
- ///
- [DataField("behaviorSets", customTypeSerializer:typeof(PrototypeIdHashSetSerializer))]
- public HashSet BehaviorSets { get; } = new();
-
- public List AvailableActions { get; set; } = new();
-
- ///
- /// The currently running action; most importantly are the operators.
- ///
- public UtilityAction? CurrentAction { get; private set; }
-
- ///
- /// How frequently we can re-plan. If an AI's in combat you could decrease the cooldown,
- /// or if there's no players nearby increase it.
- ///
- public float PlanCooldown { get; } = 0.5f;
- private float _planCooldownRemaining;
-
- ///
- /// If we've requested a plan then wait patiently for the action
- ///
- private AiActionRequestJob? _actionRequest;
-
- private CancellationTokenSource? _actionCancellation;
-
- protected override void Initialize()
- {
- if (BehaviorSets.Count > 0)
- {
- var behaviorManager = IoCManager.Resolve();
- behaviorManager.RebuildActions(this);
- EntitySystem.Get().WakeNPC(this);
- }
-
- base.Initialize();
- _planCooldownRemaining = PlanCooldown;
- _blackboard = new Blackboard(Owner);
- _planner = EntitySystem.Get();
- }
-
- protected override void OnRemove()
- {
- base.OnRemove();
- var currentOp = CurrentAction?.ActionOperators.Peek();
- currentOp?.Shutdown(Outcome.Failed);
- CurrentAction?.Shutdown();
- CurrentAction = null;
- }
-
- private void ReceivedAction()
- {
- if (_actionRequest == null)
- {
- return;
- }
-
- switch (_actionRequest.Exception)
- {
- case null:
- break;
- default:
- Logger.FatalS("ai", _actionRequest.Exception.ToString());
- ExceptionDispatchInfo.Capture(_actionRequest.Exception).Throw();
- // The code never actually reaches here, because the above throws.
- // This is to tell the compiler that the flow never leaves here.
- throw _actionRequest.Exception;
- }
- var action = _actionRequest.Result;
- _actionRequest = null;
- // Actions with lower scores should be implicitly dumped by GetAction
- // If we're not allowed to replace the action with an action of the same type then dump.
- if (action == null || !action.CanOverride && CurrentAction?.GetType() == action.GetType())
- {
- return;
- }
-
- var currentOp = CurrentAction?.ActionOperators.Peek();
- if (currentOp != null && currentOp.HasStartup)
- {
- currentOp.Shutdown(Outcome.Failed);
- }
-
- CurrentAction = action;
- action.SetupOperators(_blackboard);
- }
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- // If we asked for a new action we don't want to dump the existing one.
- if (_actionRequest != null)
- {
- if (_actionRequest.Status != JobStatus.Finished)
- {
- return;
- }
-
- ReceivedAction();
- // Do something next tick
- return;
- }
-
- _planCooldownRemaining -= frameTime;
-
- // Might find a better action while we're doing one already
- if (_planCooldownRemaining <= 0.0f)
- {
- _planCooldownRemaining = PlanCooldown;
- _actionCancellation = new CancellationTokenSource();
- _actionRequest = _planner.RequestAction(new AiActionRequest(Owner, _blackboard, AvailableActions), _actionCancellation);
-
- return;
- }
-
- // When we spawn in we won't get an action for a bit
- if (CurrentAction == null)
- {
- return;
- }
-
- var outcome = CurrentAction.Execute(frameTime);
-
- switch (outcome)
- {
- case Outcome.Success:
- if (CurrentAction.ActionOperators.Count == 0)
- {
- CurrentAction.Shutdown();
- CurrentAction = null;
- // Nothing to compare new action to
- _blackboard.GetState().SetValue(0.0f);
- }
- break;
- case Outcome.Continuing:
- break;
- case Outcome.Failed:
- CurrentAction.Shutdown();
- CurrentAction = null;
- _blackboard.GetState().SetValue(0.0f);
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
- }
- }
-}
diff --git a/Content.Server/AI/Utility/AiLogic/UtilityNPCComponent.cs b/Content.Server/AI/Utility/AiLogic/UtilityNPCComponent.cs
new file mode 100644
index 0000000000..880512a8d4
--- /dev/null
+++ b/Content.Server/AI/Utility/AiLogic/UtilityNPCComponent.cs
@@ -0,0 +1,46 @@
+using Content.Server.AI.Components;
+using Content.Server.AI.LoadBalancer;
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.WorldState;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
+using System.Threading;
+using Content.Server.AI.EntitySystems;
+
+namespace Content.Server.AI.Utility.AiLogic
+{
+ [RegisterComponent, Access(typeof(NPCSystem))]
+ [ComponentReference(typeof(NPCComponent))]
+ public sealed class UtilityNPCComponent : NPCComponent
+ {
+ public Blackboard Blackboard => _blackboard;
+ public Blackboard _blackboard = default!;
+
+ ///
+ /// The sum of all BehaviorSets gives us what actions the AI can take
+ ///
+ [DataField("behaviorSets", customTypeSerializer:typeof(PrototypeIdHashSetSerializer))]
+ public HashSet BehaviorSets { get; } = new();
+
+ public List AvailableActions { get; set; } = new();
+
+ ///
+ /// The currently running action; most importantly are the operators.
+ ///
+ public UtilityAction? CurrentAction { get; set; }
+
+ ///
+ /// How frequently we can re-plan. If an AI's in combat you could decrease the cooldown,
+ /// or if there's no players nearby increase it.
+ ///
+ public float PlanCooldown { get; } = 0.5f;
+
+ public float _planCooldownRemaining;
+
+ ///
+ /// If we've requested a plan then wait patiently for the action
+ ///
+ public AiActionRequestJob? _actionRequest;
+
+ public CancellationTokenSource? _actionCancellation;
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Clothing/ClothingInInventoryCon.cs b/Content.Server/AI/Utility/Considerations/Clothing/ClothingInInventoryCon.cs
index d49f8d6394..dbedc4dc9a 100644
--- a/Content.Server/AI/Utility/Considerations/Clothing/ClothingInInventoryCon.cs
+++ b/Content.Server/AI/Utility/Considerations/Clothing/ClothingInInventoryCon.cs
@@ -29,7 +29,7 @@ namespace Content.Server.AI.Utility.Considerations.Clothing
continue;
}
- if ((clothingComponent.SlotFlags & slotDef.SlotFlags) != 0)
+ if ((clothingComponent.Slots & slotDef.SlotFlags) != 0)
{
return 1.0f;
}
diff --git a/Content.Server/AI/Utility/Considerations/Inventory/CanPutTargetInInventoryCon.cs b/Content.Server/AI/Utility/Considerations/Inventory/CanPutTargetInInventoryCon.cs
index 4d47356d45..0b5c86484f 100644
--- a/Content.Server/AI/Utility/Considerations/Inventory/CanPutTargetInInventoryCon.cs
+++ b/Content.Server/AI/Utility/Considerations/Inventory/CanPutTargetInInventoryCon.cs
@@ -14,7 +14,7 @@ namespace Content.Server.AI.Utility.Considerations.Inventory
// If not then check if we have a free hand
var target = context.GetState().GetValue();
- if (target == null || !IoCManager.Resolve().HasComponent(target))
+ if (target == null || !IoCManager.Resolve().HasComponent(target))
{
return 0.0f;
}
diff --git a/Content.Server/AI/Utility/Considerations/Inventory/TargetInOurInventoryCon.cs b/Content.Server/AI/Utility/Considerations/Inventory/TargetInOurInventoryCon.cs
index 0eaccfcfea..fc1ccd18a5 100644
--- a/Content.Server/AI/Utility/Considerations/Inventory/TargetInOurInventoryCon.cs
+++ b/Content.Server/AI/Utility/Considerations/Inventory/TargetInOurInventoryCon.cs
@@ -11,7 +11,7 @@ namespace Content.Server.AI.Utility.Considerations.Inventory
{
var target = context.GetState().GetValue();
- if (target == null || !IoCManager.Resolve().HasComponent(target))
+ if (target == null || !IoCManager.Resolve().HasComponent(target))
{
return 0.0f;
}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/EquipAnyGlovesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/EquipAnyGlovesExp.cs
index 79994cced2..65eeb013dd 100644
--- a/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/EquipAnyGlovesExp.cs
+++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/EquipAnyGlovesExp.cs
@@ -35,7 +35,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Gloves
foreach (var entity in context.GetState().GetValue())
{
if (IoCManager.Resolve().TryGetComponent(entity, out ClothingComponent? clothing) &&
- (clothing.SlotFlags & SlotFlags.GLOVES) != 0)
+ (clothing.Slots & SlotFlags.GLOVES) != 0)
{
yield return new EquipGloves {Owner = owner, Target = entity, Bonus = Bonus};
}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/PickUpAnyNearbyGlovesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/PickUpAnyNearbyGlovesExp.cs
index 50ea0ee9ca..1d7ec7637b 100644
--- a/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/PickUpAnyNearbyGlovesExp.cs
+++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/PickUpAnyNearbyGlovesExp.cs
@@ -33,7 +33,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Gloves
foreach (var entity in context.GetState().GetValue())
{
if (IoCManager.Resolve().TryGetComponent(entity, out ClothingComponent? clothing) &&
- (clothing.SlotFlags & SlotFlags.GLOVES) != 0)
+ (clothing.Slots & SlotFlags.GLOVES) != 0)
{
yield return new PickUpGloves {Owner = owner, Target = entity, Bonus = Bonus};
}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/EquipAnyHeadExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/EquipAnyHeadExp.cs
index ee2658ecde..c15ca9e80e 100644
--- a/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/EquipAnyHeadExp.cs
+++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/EquipAnyHeadExp.cs
@@ -34,7 +34,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Head
foreach (var entity in context.GetState().GetValue())
{
if (IoCManager.Resolve().TryGetComponent(entity, out ClothingComponent? clothing) &&
- (clothing.SlotFlags & SlotFlags.HEAD) != 0)
+ (clothing.Slots & SlotFlags.HEAD) != 0)
{
yield return new EquipHead {Owner = owner, Target = entity, Bonus = Bonus};
}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/PickUpAnyNearbyHeadExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/PickUpAnyNearbyHeadExp.cs
index 74b95c917b..d159a99741 100644
--- a/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/PickUpAnyNearbyHeadExp.cs
+++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/PickUpAnyNearbyHeadExp.cs
@@ -33,7 +33,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Head
foreach (var entity in context.GetState().GetValue())
{
if (IoCManager.Resolve().TryGetComponent(entity, out ClothingComponent? clothing) &&
- (clothing.SlotFlags & SlotFlags.HEAD) != 0)
+ (clothing.Slots & SlotFlags.HEAD) != 0)
{
yield return new PickUpHead {Owner = owner, Target = entity, Bonus = Bonus};
}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/EquipAnyOuterClothingExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/EquipAnyOuterClothingExp.cs
index d8f37644d9..bc20c66401 100644
--- a/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/EquipAnyOuterClothingExp.cs
+++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/EquipAnyOuterClothingExp.cs
@@ -35,7 +35,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.OuterClothing
foreach (var entity in context.GetState().GetValue())
{
if (IoCManager.Resolve().TryGetComponent(entity, out ClothingComponent? clothing) &&
- (clothing.SlotFlags & SlotFlags.OUTERCLOTHING) != 0)
+ (clothing.Slots & SlotFlags.OUTERCLOTHING) != 0)
{
yield return new EquipOuterClothing {Owner = owner, Target = entity, Bonus = Bonus};
}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/PickUpAnyNearbyOuterClothingExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/PickUpAnyNearbyOuterClothingExp.cs
index a1c6164337..156757f2ab 100644
--- a/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/PickUpAnyNearbyOuterClothingExp.cs
+++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/PickUpAnyNearbyOuterClothingExp.cs
@@ -34,7 +34,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.OuterClothing
foreach (var entity in context.GetState().GetValue())
{
if (IoCManager.Resolve().TryGetComponent(entity, out ClothingComponent? clothing) &&
- (clothing.SlotFlags & SlotFlags.OUTERCLOTHING) != 0)
+ (clothing.Slots & SlotFlags.OUTERCLOTHING) != 0)
{
yield return new PickUpOuterClothing {Owner = owner, Target = entity, Bonus = Bonus};
}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/EquipAnyShoesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/EquipAnyShoesExp.cs
index 84235e8cad..d0493fe736 100644
--- a/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/EquipAnyShoesExp.cs
+++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/EquipAnyShoesExp.cs
@@ -35,7 +35,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Shoes
foreach (var entity in context.GetState().GetValue())
{
if (IoCManager.Resolve().TryGetComponent(entity, out ClothingComponent? clothing) &&
- (clothing.SlotFlags & SlotFlags.FEET) != 0)
+ (clothing.Slots & SlotFlags.FEET) != 0)
{
yield return new EquipShoes {Owner = owner, Target = entity, Bonus = Bonus};
}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/PickUpAnyNearbyShoesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/PickUpAnyNearbyShoesExp.cs
index f60402a1d9..6512c16ebe 100644
--- a/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/PickUpAnyNearbyShoesExp.cs
+++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/PickUpAnyNearbyShoesExp.cs
@@ -34,7 +34,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Shoes
foreach (var entity in context.GetState().GetValue())
{
if (IoCManager.Resolve().TryGetComponent(entity, out ClothingComponent? clothing) &&
- (clothing.SlotFlags & SlotFlags.FEET) != 0)
+ (clothing.Slots & SlotFlags.FEET) != 0)
{
yield return new PickUpShoes {Owner = owner, Target = entity, Bonus = Bonus};
}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyExp.cs
index 55dd33bf86..4f2db88e76 100644
--- a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyExp.cs
+++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyExp.cs
@@ -27,7 +27,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
public override IEnumerable GetActions(Blackboard context)
{
var owner = context.GetState().GetValue();
- if (!IoCManager.Resolve().TryGetComponent(owner, out AiControllerComponent? controller))
+ if (!IoCManager.Resolve().TryGetComponent(owner, out NPCComponent? controller))
{
throw new InvalidOperationException();
}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyHostilesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyHostilesExp.cs
index bd8631ffeb..cf2b6120a2 100644
--- a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyHostilesExp.cs
+++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyHostilesExp.cs
@@ -27,7 +27,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
public override IEnumerable GetActions(Blackboard context)
{
var owner = context.GetState().GetValue();
- if (!IoCManager.Resolve().TryGetComponent(owner, out AiControllerComponent? controller))
+ if (!IoCManager.Resolve().TryGetComponent(owner, out NPCComponent? controller))
{
throw new InvalidOperationException();
}
diff --git a/Content.Server/AI/Utility/ExpendableActions/Bots/BufferNearbyPuddlesExp.cs b/Content.Server/AI/Utility/ExpendableActions/Bots/BufferNearbyPuddlesExp.cs
index d3cff226ec..0b7f56536e 100644
--- a/Content.Server/AI/Utility/ExpendableActions/Bots/BufferNearbyPuddlesExp.cs
+++ b/Content.Server/AI/Utility/ExpendableActions/Bots/BufferNearbyPuddlesExp.cs
@@ -29,7 +29,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Bots
public override IEnumerable GetActions(Blackboard context)
{
var owner = context.GetState().GetValue();
- if (!IoCManager.Resolve().TryGetComponent(owner, out AiControllerComponent? controller))
+ if (!IoCManager.Resolve().TryGetComponent(owner, out NPCComponent? controller))
{
throw new InvalidOperationException();
}
diff --git a/Content.Server/AI/Utility/ExpendableActions/Bots/InjectNearbyExp.cs b/Content.Server/AI/Utility/ExpendableActions/Bots/InjectNearbyExp.cs
new file mode 100644
index 0000000000..0e74889841
--- /dev/null
+++ b/Content.Server/AI/Utility/ExpendableActions/Bots/InjectNearbyExp.cs
@@ -0,0 +1,40 @@
+using Content.Server.AI.Components;
+using Content.Server.AI.EntitySystems;
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.Actions.Bots;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.Utility.Considerations.ActionBlocker;
+using Content.Server.Silicons.Bots;
+
+namespace Content.Server.AI.Utility.ExpandableActions.Bots
+{
+ public sealed class InjectNearbyExp : ExpandableUtilityAction
+ {
+ public override float Bonus => 30;
+ IEntityManager _entMan = IoCManager.Resolve();
+
+ protected override IReadOnlyCollection> GetCommonConsiderations(Blackboard context)
+ {
+ var considerationsManager = IoCManager.Resolve();
+
+ return new[]
+ {
+ considerationsManager.Get()
+ .BoolCurve(context),
+ };
+ }
+ public override IEnumerable GetActions(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+ if (!_entMan.TryGetComponent(owner, out NPCComponent? controller)
+ || !_entMan.TryGetComponent(owner, out MedibotComponent? bot))
+ {
+ throw new InvalidOperationException();
+ }
+
+ yield return new InjectNearby() {Owner = owner, Target = EntitySystem.Get().GetNearbyInjectable(Owner), Bonus = Bonus};
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/NpcBehaviorManager.cs b/Content.Server/AI/Utility/NpcBehaviorManager.cs
deleted file mode 100644
index 4e40203fa5..0000000000
--- a/Content.Server/AI/Utility/NpcBehaviorManager.cs
+++ /dev/null
@@ -1,165 +0,0 @@
-using Content.Server.AI.EntitySystems;
-using Content.Server.AI.Utility.Actions;
-using Content.Server.AI.Utility.AiLogic;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Reflection;
-
-namespace Content.Server.AI.Utility
-{
- internal interface INpcBehaviorManager
- {
- void Initialize();
-
- void AddBehaviorSet(UtilityAi npc, string behaviorSet, bool rebuild = true);
-
- void RemoveBehaviorSet(UtilityAi npc, string behaviorSet, bool rebuild = true);
-
- void RebuildActions(UtilityAi npc);
- }
-
- ///
- /// Handles BehaviorSets and adding / removing behaviors to NPCs
- ///
- internal sealed class NpcBehaviorManager : INpcBehaviorManager
- {
- [Dependency] private readonly IDynamicTypeFactory _typeFactory = default!;
-
- private readonly NpcActionComparer _comparer = new();
-
- private Dictionary> _behaviorSets = new();
-
- public void Initialize()
- {
- IoCManager.InjectDependencies(this);
- var protoManager = IoCManager.Resolve();
- var reflectionManager = IoCManager.Resolve();
-
- foreach (var bSet in protoManager.EnumeratePrototypes())
- {
- var actions = new List();
-
- foreach (var act in bSet.Actions)
- {
- if (!reflectionManager.TryLooseGetType(act, out var parsedType) ||
- !typeof(IAiUtility).IsAssignableFrom(parsedType))
- {
- Logger.Error($"Unable to parse AI action for {act}");
- }
- else
- {
- actions.Add(parsedType);
- }
- }
-
- _behaviorSets[bSet.ID] = actions;
- }
- }
-
- ///
- /// Adds the BehaviorSet to the NPC.
- ///
- ///
- ///
- /// Set to false if you want to manually rebuild it after bulk updates.
- public void AddBehaviorSet(UtilityAi npc, string behaviorSet, bool rebuild = true)
- {
- if (!_behaviorSets.ContainsKey(behaviorSet))
- {
- Logger.Error($"Tried to add BehaviorSet {behaviorSet} to {npc} but no such BehaviorSet found!");
- return;
- }
-
- if (!npc.BehaviorSets.Add(behaviorSet))
- {
- Logger.Error($"Tried to add BehaviorSet {behaviorSet} to {npc} which already has the BehaviorSet!");
- return;
- }
-
- if (rebuild)
- RebuildActions(npc);
-
- if (npc.BehaviorSets.Count == 1 && !npc.Awake)
- {
- EntitySystem.Get().WakeNPC(npc);
- }
- }
-
- ///
- /// Removes the BehaviorSet from the NPC.
- ///
- ///
- ///
- /// Set to false if yo uwant to manually rebuild it after bulk updates.
- public void RemoveBehaviorSet(UtilityAi npc, string behaviorSet, bool rebuild = true)
- {
- if (!_behaviorSets.TryGetValue(behaviorSet, out var actions))
- {
- Logger.Error($"Tried to remove BehaviorSet {behaviorSet} from {npc} but no such BehaviorSet found!");
- return;
- }
-
- if (!npc.BehaviorSets.Remove(behaviorSet))
- {
- Logger.Error($"Tried to remove BehaviorSet {behaviorSet} from {npc} but it doesn't have that BehaviorSet!");
- return;
- }
-
- if (rebuild)
- RebuildActions(npc);
-
- if (npc.BehaviorSets.Count == 0 && npc.Awake)
- {
- EntitySystem.Get().SleepNPC(npc);
- }
- }
-
- ///
- /// Clear our actions and re-instantiate them from our BehaviorSets.
- /// Will ensure each action is unique.
- ///
- ///
- public void RebuildActions(UtilityAi npc)
- {
- npc.AvailableActions.Clear();
- foreach (var bSet in npc.BehaviorSets)
- {
- foreach (var action in GetActions(bSet))
- {
- if (npc.AvailableActions.Contains(action)) continue;
- // Setup
- action.Owner = npc.Owner;
-
- // Ad to actions.
- npc.AvailableActions.Add(action);
- }
- }
-
- SortActions(npc);
- }
-
- private IEnumerable GetActions(string behaviorSet)
- {
- foreach (var action in _behaviorSets[behaviorSet])
- {
- yield return (IAiUtility) _typeFactory.CreateInstance(action);
- }
- }
-
- ///
- /// Whenever the behavior sets are changed we'll re-sort the actions by bonus
- ///
- private void SortActions(UtilityAi npc)
- {
- npc.AvailableActions.Sort(_comparer);
- }
-
- private sealed class NpcActionComparer : Comparer
- {
- public override int Compare(IAiUtility? x, IAiUtility? y)
- {
- if (x == null || y == null) return 0;
- return y.Bonus.CompareTo(x.Bonus);
- }
- }
- }
-}
diff --git a/Content.Server/AI/Utility/UtilityAiHelpers.cs b/Content.Server/AI/Utility/UtilityAiHelpers.cs
index b7b92f2111..7d46a71fd7 100644
--- a/Content.Server/AI/Utility/UtilityAiHelpers.cs
+++ b/Content.Server/AI/Utility/UtilityAiHelpers.cs
@@ -8,12 +8,12 @@ namespace Content.Server.AI.Utility
{
public static Blackboard? GetBlackboard(EntityUid entity)
{
- if (!IoCManager.Resolve().TryGetComponent(entity, out AiControllerComponent? aiControllerComponent))
+ if (!IoCManager.Resolve().TryGetComponent(entity, out NPCComponent? aiControllerComponent))
{
return null;
}
- if (aiControllerComponent is UtilityAi utilityAi)
+ if (aiControllerComponent is UtilityNPCComponent utilityAi)
{
return utilityAi.Blackboard;
}
diff --git a/Content.Server/AI/WorldState/States/Clothing/NearbyClothingState.cs b/Content.Server/AI/WorldState/States/Clothing/NearbyClothingState.cs
index d335c9a34d..b92f675cb7 100644
--- a/Content.Server/AI/WorldState/States/Clothing/NearbyClothingState.cs
+++ b/Content.Server/AI/WorldState/States/Clothing/NearbyClothingState.cs
@@ -17,7 +17,7 @@ namespace Content.Server.AI.WorldState.States.Clothing
var result = new List();
var entMan = IoCManager.Resolve();
- if (!entMan.TryGetComponent(Owner, out AiControllerComponent? controller))
+ if (!entMan.TryGetComponent(Owner, out NPCComponent? controller))
{
return result;
}
diff --git a/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyMeleeWeapons.cs b/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyMeleeWeapons.cs
index 34f11d4c29..f95e9461e2 100644
--- a/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyMeleeWeapons.cs
+++ b/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyMeleeWeapons.cs
@@ -15,7 +15,7 @@ namespace Content.Server.AI.WorldState.States.Combat.Nearby
var result = new List();
var entMan = IoCManager.Resolve();
- if (!entMan.TryGetComponent(Owner, out AiControllerComponent? controller))
+ if (!entMan.TryGetComponent(Owner, out NPCComponent? controller))
{
return result;
}
diff --git a/Content.Server/AI/WorldState/States/Mobs/NearbyBodiesState.cs b/Content.Server/AI/WorldState/States/Mobs/NearbyBodiesState.cs
index b4128ac7ca..3b1ea76830 100644
--- a/Content.Server/AI/WorldState/States/Mobs/NearbyBodiesState.cs
+++ b/Content.Server/AI/WorldState/States/Mobs/NearbyBodiesState.cs
@@ -15,7 +15,7 @@ namespace Content.Server.AI.WorldState.States.Mobs
var result = new List();
var entMan = IoCManager.Resolve();
- if (!entMan.TryGetComponent(Owner, out AiControllerComponent? controller))
+ if (!entMan.TryGetComponent(Owner, out NPCComponent? controller))
{
return result;
}
diff --git a/Content.Server/AI/WorldState/States/Mobs/NearbyPlayersState.cs b/Content.Server/AI/WorldState/States/Mobs/NearbyPlayersState.cs
index 0edc69377b..438db57262 100644
--- a/Content.Server/AI/WorldState/States/Mobs/NearbyPlayersState.cs
+++ b/Content.Server/AI/WorldState/States/Mobs/NearbyPlayersState.cs
@@ -15,7 +15,7 @@ namespace Content.Server.AI.WorldState.States.Mobs
var result = new List();
var entMan = IoCManager.Resolve();
- if (!entMan.TryGetComponent(Owner, out AiControllerComponent? controller))
+ if (!entMan.TryGetComponent(Owner, out NPCComponent? controller))
{
return result;
}
diff --git a/Content.Server/AI/WorldState/States/Nutrition/NearbyDrinkState.cs b/Content.Server/AI/WorldState/States/Nutrition/NearbyDrinkState.cs
index db0ebfff84..446b875653 100644
--- a/Content.Server/AI/WorldState/States/Nutrition/NearbyDrinkState.cs
+++ b/Content.Server/AI/WorldState/States/Nutrition/NearbyDrinkState.cs
@@ -17,7 +17,7 @@ namespace Content.Server.AI.WorldState.States.Nutrition
var result = new List();
var entMan = IoCManager.Resolve();
- if (!entMan.TryGetComponent(Owner, out AiControllerComponent? controller))
+ if (!entMan.TryGetComponent(Owner, out NPCComponent? controller))
{
return result;
}
diff --git a/Content.Server/AI/WorldState/States/Nutrition/NearbyFoodState.cs b/Content.Server/AI/WorldState/States/Nutrition/NearbyFoodState.cs
index ae25a92645..c1ab2c4479 100644
--- a/Content.Server/AI/WorldState/States/Nutrition/NearbyFoodState.cs
+++ b/Content.Server/AI/WorldState/States/Nutrition/NearbyFoodState.cs
@@ -17,7 +17,7 @@ namespace Content.Server.AI.WorldState.States.Nutrition
var result = new List();
var entMan = IoCManager.Resolve();
- if (!entMan.TryGetComponent(Owner, out AiControllerComponent? controller))
+ if (!entMan.TryGetComponent(Owner, out NPCComponent? controller))
{
return result;
}
diff --git a/Content.Server/AME/Components/AMEControllerComponent.cs b/Content.Server/AME/Components/AMEControllerComponent.cs
index cf9e01eec9..e9e8e274e7 100644
--- a/Content.Server/AME/Components/AMEControllerComponent.cs
+++ b/Content.Server/AME/Components/AMEControllerComponent.cs
@@ -4,7 +4,6 @@ using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Shared.AME;
using Content.Shared.Hands.EntitySystems;
-using Content.Shared.Sound;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
diff --git a/Content.Server/AME/Components/AMEPartComponent.cs b/Content.Server/AME/Components/AMEPartComponent.cs
index 19995d5b3c..925e2bc69e 100644
--- a/Content.Server/AME/Components/AMEPartComponent.cs
+++ b/Content.Server/AME/Components/AMEPartComponent.cs
@@ -1,4 +1,4 @@
-using Content.Shared.Sound;
+using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
diff --git a/Content.Server/Abilities/Boxer/Boxer/BoxerComponent.cs b/Content.Server/Abilities/Boxer/Boxer/BoxerComponent.cs
new file mode 100644
index 0000000000..cd3f5de0a7
--- /dev/null
+++ b/Content.Server/Abilities/Boxer/Boxer/BoxerComponent.cs
@@ -0,0 +1,23 @@
+using Content.Shared.Damage;
+
+namespace Content.Server.Abilities.Boxer
+{
+ ///
+ /// Added to the boxer on spawn.
+ ///
+ [RegisterComponent]
+ public sealed class BoxerComponent : Component
+ {
+ [DataField("modifiers", required: true)]
+ public DamageModifierSet UnarmedModifiers = default!;
+
+ [DataField("rangeBonus")]
+ public float RangeBonus = 1.5f;
+
+ ///
+ /// Damage modifier with boxing glove stam damage.
+ ///
+ [DataField("boxingGlovesModifier")]
+ public float BoxingGlovesModifier = 1.75f;
+ }
+}
diff --git a/Content.Server/Abilities/Boxer/Boxer/BoxingGlovesComponent.cs b/Content.Server/Abilities/Boxer/Boxer/BoxingGlovesComponent.cs
new file mode 100644
index 0000000000..92a9626013
--- /dev/null
+++ b/Content.Server/Abilities/Boxer/Boxer/BoxingGlovesComponent.cs
@@ -0,0 +1,11 @@
+using Content.Shared.Damage;
+
+namespace Content.Server.Abilities.Boxer
+{
+ ///
+ /// Boxer gets a bonus for these, and their fists, but not other unarmed weapons.
+ ///
+ [RegisterComponent]
+ public sealed class BoxingGlovesComponent : Component
+ {}
+}
diff --git a/Content.Server/Abilities/Boxer/BoxingSystem.cs b/Content.Server/Abilities/Boxer/BoxingSystem.cs
new file mode 100644
index 0000000000..a3a58a0619
--- /dev/null
+++ b/Content.Server/Abilities/Boxer/BoxingSystem.cs
@@ -0,0 +1,51 @@
+using Content.Server.Weapon.Melee;
+using Content.Server.Stunnable;
+using Content.Shared.Inventory.Events;
+using Content.Server.Weapon.Melee.Components;
+using Content.Server.Clothing.Components;
+using Content.Server.Damage.Components;
+using Content.Server.Damage.Events;
+using Robust.Shared.Audio;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+using Robust.Shared.Containers;
+
+namespace Content.Server.Abilities.Boxer
+{
+ public sealed class BoxingSystem : EntitySystem
+ {
+ [Dependency] private readonly StunSystem _stunSystem = default!;
+ [Dependency] private readonly IRobustRandom _robustRandom = default!;
+ [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(ApplyBoxerModifiers);
+ SubscribeLocalEvent(OnStamHit);
+ }
+
+ private void OnInit(EntityUid uid, BoxerComponent boxer, ComponentInit args)
+ {
+ if (TryComp(uid, out var meleeComp))
+ meleeComp.Range *= boxer.RangeBonus;
+ }
+ private void ApplyBoxerModifiers(EntityUid uid, BoxerComponent component, MeleeHitEvent args)
+ {
+ if (component.UnarmedModifiers == default!)
+ {
+ Logger.Warning("BoxerComponent on " + uid + " couldn't get damage modifiers. Know that adding components with damage modifiers through VV or similar is unsupported.");
+ return;
+ }
+
+ args.ModifiersList.Add(component.UnarmedModifiers);
+ }
+ private void OnStamHit(EntityUid uid, BoxingGlovesComponent component, StaminaMeleeHitEvent args)
+ {
+ _containerSystem.TryGetContainingContainer(uid, out var equipee);
+ if (TryComp(equipee?.Owner, out var boxer))
+ args.Multiplier *= boxer.BoxingGlovesModifier;
+ }
+ }
+}
diff --git a/Content.Server/AirlockPainter/AirlockPainterComponent.cs b/Content.Server/AirlockPainter/AirlockPainterComponent.cs
index 5a05d3433d..cd88795c12 100644
--- a/Content.Server/AirlockPainter/AirlockPainterComponent.cs
+++ b/Content.Server/AirlockPainter/AirlockPainterComponent.cs
@@ -1,4 +1,4 @@
-using Content.Shared.Sound;
+using Robust.Shared.Audio;
namespace Content.Server.AirlockPainter
{
diff --git a/Content.Server/Alert/Click/ToggleInternals.cs b/Content.Server/Alert/Click/ToggleInternals.cs
new file mode 100644
index 0000000000..3fa7e525f4
--- /dev/null
+++ b/Content.Server/Alert/Click/ToggleInternals.cs
@@ -0,0 +1,53 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Body.Components;
+using Content.Server.Body.Systems;
+using Content.Server.Popups;
+using Content.Server.Shuttles.Systems;
+using Content.Shared.Alert;
+using Content.Shared.Shuttles.Components;
+using JetBrains.Annotations;
+using Robust.Shared.Player;
+
+namespace Content.Server.Alert.Click;
+
+///
+/// Attempts to toggle the internals for a particular entity
+///
+[UsedImplicitly]
+[DataDefinition]
+public sealed class ToggleInternals : IAlertClick
+{
+ public void AlertClicked(EntityUid player)
+ {
+ var entManager = IoCManager.Resolve();
+
+ if (!entManager.TryGetComponent(player, out var internals)) return;
+
+ var popups = entManager.EntitySysManager.GetEntitySystem();
+ var internalsSystem = entManager.EntitySysManager.GetEntitySystem();
+
+ // Toggle off if they're on
+ if (internalsSystem.AreInternalsWorking(internals))
+ {
+ internalsSystem.DisconnectTank(internals);
+ return;
+ }
+
+ // If they're not on then check if we have a mask to use
+ if (internals.BreathToolEntity == null)
+ {
+ popups.PopupEntity(Loc.GetString("internals-no-breath-tool"), player, Filter.Entities(player));
+ return;
+ }
+
+ var tank = internalsSystem.FindBestGasTank(internals);
+
+ if (tank == null)
+ {
+ popups.PopupEntity(Loc.GetString("internals-no-tank"), player, Filter.Entities(player));
+ return;
+ }
+
+ entManager.EntitySysManager.GetEntitySystem().ConnectToInternals(tank);
+ }
+}
diff --git a/Content.Server/AlertLevel/AlertLevelPrototype.cs b/Content.Server/AlertLevel/AlertLevelPrototype.cs
index 56a81e1697..a048a9d9f0 100644
--- a/Content.Server/AlertLevel/AlertLevelPrototype.cs
+++ b/Content.Server/AlertLevel/AlertLevelPrototype.cs
@@ -1,5 +1,4 @@
-using System.Collections.Specialized;
-using Content.Shared.Sound;
+using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server.AlertLevel;
diff --git a/Content.Server/Animals/Components/EggLayerComponent.cs b/Content.Server/Animals/Components/EggLayerComponent.cs
index 84faa0ef27..a900c96947 100644
--- a/Content.Server/Animals/Components/EggLayerComponent.cs
+++ b/Content.Server/Animals/Components/EggLayerComponent.cs
@@ -1,7 +1,7 @@
-using Content.Shared.Actions;
+using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
-using Content.Shared.Sound;
using Content.Shared.Storage;
+using Robust.Shared.Audio;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
diff --git a/Content.Server/Announcements/RoundAnnouncementPrototype.cs b/Content.Server/Announcements/RoundAnnouncementPrototype.cs
index cc7ab3100c..68780886c7 100644
--- a/Content.Server/Announcements/RoundAnnouncementPrototype.cs
+++ b/Content.Server/Announcements/RoundAnnouncementPrototype.cs
@@ -1,5 +1,5 @@
using Content.Server.GameTicking.Presets;
-using Content.Shared.Sound;
+using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
diff --git a/Content.Server/Arcade/Components/SpaceVillainArcadeComponent.cs b/Content.Server/Arcade/Components/SpaceVillainArcadeComponent.cs
index aa93383678..fca97c2757 100644
--- a/Content.Server/Arcade/Components/SpaceVillainArcadeComponent.cs
+++ b/Content.Server/Arcade/Components/SpaceVillainArcadeComponent.cs
@@ -1,7 +1,6 @@
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Shared.Arcade;
-using Content.Shared.Sound;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Player;
diff --git a/Content.Server/Atmos/Components/BreathToolComponent.cs b/Content.Server/Atmos/Components/BreathToolComponent.cs
index 7f500ae5ae..edc494bb36 100644
--- a/Content.Server/Atmos/Components/BreathToolComponent.cs
+++ b/Content.Server/Atmos/Components/BreathToolComponent.cs
@@ -1,5 +1,4 @@
-using Content.Server.Body.Components;
-using Content.Shared.Inventory;
+using Content.Shared.Inventory;
namespace Content.Server.Atmos.Components
{
@@ -10,33 +9,12 @@ namespace Content.Server.Atmos.Components
[ComponentProtoName("BreathMask")]
public sealed class BreathToolComponent : Component
{
- [Dependency] private readonly IEntityManager _entities = default!;
-
///
/// Tool is functional only in allowed slots
///
[DataField("allowedSlots")]
public SlotFlags AllowedSlots = SlotFlags.MASK;
public bool IsFunctional;
- public EntityUid ConnectedInternalsEntity;
-
- protected override void Shutdown()
- {
- base.Shutdown();
- DisconnectInternals();
- }
-
- public void DisconnectInternals()
- {
- var old = ConnectedInternalsEntity;
- ConnectedInternalsEntity = default;
-
- if (old != default && _entities.TryGetComponent(old, out var internalsComponent))
- {
- internalsComponent.DisconnectBreathTool();
- }
-
- IsFunctional = false;
- }
+ public EntityUid? ConnectedInternalsEntity;
}
}
diff --git a/Content.Server/Atmos/Components/GasTankComponent.cs b/Content.Server/Atmos/Components/GasTankComponent.cs
index ddacdfd0b4..fd4b599d4c 100644
--- a/Content.Server/Atmos/Components/GasTankComponent.cs
+++ b/Content.Server/Atmos/Components/GasTankComponent.cs
@@ -1,49 +1,34 @@
-using Content.Server.Atmos.EntitySystems;
-using Content.Server.Body.Components;
-using Content.Server.Explosion.EntitySystems;
-using Content.Server.UserInterface;
-using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Atmos;
-using Content.Shared.Atmos.Components;
-using Content.Shared.Audio;
-using Content.Shared.Sound;
-using Robust.Server.GameObjects;
using Robust.Shared.Audio;
-using Robust.Shared.Containers;
-using Robust.Shared.Player;
namespace Content.Server.Atmos.Components
{
[RegisterComponent]
-#pragma warning disable 618
public sealed class GasTankComponent : Component, IGasMixtureHolder
-#pragma warning restore 618
{
- [Dependency] private readonly IEntityManager _entMan = default!;
-
- private const float MaxExplosionRange = 14f;
+ public const float MaxExplosionRange = 14f;
private const float DefaultOutputPressure = Atmospherics.OneAtmosphere;
- private int _integrity = 3;
+ public int Integrity = 3;
- [ViewVariables] public BoundUserInterface? UserInterface;
+ [ViewVariables(VVAccess.ReadWrite), DataField("ruptureSound")]
+ public SoundSpecifier RuptureSound = new SoundPathSpecifier("/Audio/Effects/spray.ogg");
- [ViewVariables(VVAccess.ReadWrite), DataField("ruptureSound")] private SoundSpecifier _ruptureSound = new SoundPathSpecifier("/Audio/Effects/spray.ogg");
-
- [ViewVariables(VVAccess.ReadWrite), DataField("connectSound")] private SoundSpecifier? _connectSound =
+ [ViewVariables(VVAccess.ReadWrite), DataField("connectSound")]
+ public SoundSpecifier? ConnectSound =
new SoundPathSpecifier("/Audio/Effects/internals.ogg")
{
- Params = AudioParams.Default.WithVolume(10f),
+ Params = AudioParams.Default.WithVolume(5f),
};
- [ViewVariables(VVAccess.ReadWrite), DataField("disconnectSound")] private SoundSpecifier? _disconnectSound;
+ [ViewVariables(VVAccess.ReadWrite), DataField("disconnectSound")]
+ public SoundSpecifier? DisconnectSound;
// Cancel toggles sounds if we re-toggle again.
- private IPlayingAudioStream? _connectStream;
- private IPlayingAudioStream? _disconnectStream;
-
+ public IPlayingAudioStream? ConnectStream;
+ public IPlayingAudioStream? DisconnectStream;
[DataField("air")] [ViewVariables] public GasMixture Air { get; set; } = new();
@@ -52,18 +37,13 @@ namespace Content.Server.Atmos.Components
///
[DataField("outputPressure")]
[ViewVariables]
- public float OutputPressure { get; private set; } = DefaultOutputPressure;
+ public float OutputPressure { get; set; } = DefaultOutputPressure;
///
/// Tank is connected to internals.
///
[ViewVariables] public bool IsConnected { get; set; }
- ///
- /// Represents that tank is functional and can be connected to internals.
- ///
- public bool IsFunctional => GetInternalsComponent() != null;
-
///
/// Pressure at which tanks start leaking.
///
@@ -90,212 +70,5 @@ namespace Content.Server.Atmos.Components
[DataField("toggleAction", required: true)]
public InstantAction ToggleAction = new();
-
- protected override void Initialize()
- {
- base.Initialize();
- UserInterface = Owner.GetUIOrNull(SharedGasTankUiKey.Key);
- if (UserInterface != null)
- {
- UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
- }
- }
-
- protected override void Shutdown()
- {
- base.Shutdown();
- DisconnectFromInternals();
- }
-
- public GasMixture? RemoveAir(float amount)
- {
- var gas = Air?.Remove(amount);
- CheckStatus();
- return gas;
- }
-
- public GasMixture RemoveAirVolume(float volume)
- {
- if (Air == null)
- return new GasMixture(volume);
-
- var tankPressure = Air.Pressure;
- if (tankPressure < OutputPressure)
- {
- OutputPressure = tankPressure;
- UpdateUserInterface();
- }
-
- var molesNeeded = OutputPressure * volume / (Atmospherics.R * Air.Temperature);
-
- var air = RemoveAir(molesNeeded);
-
- if (air != null)
- air.Volume = volume;
- else
- return new GasMixture(volume);
-
- return air;
- }
-
- public void ConnectToInternals()
- {
- if (IsConnected || !IsFunctional) return;
- var internals = GetInternalsComponent();
- if (internals == null) return;
- IsConnected = internals.TryConnectTank(Owner);
- EntitySystem.Get().SetToggled(ToggleAction, IsConnected);
-
- // Couldn't toggle!
- if (!IsConnected) return;
-
- _connectStream?.Stop();
-
- if (_connectSound != null)
- _connectStream = SoundSystem.Play(_connectSound.GetSound(), Filter.Pvs(Owner, entityManager: _entMan), Owner, _connectSound.Params);
-
- UpdateUserInterface();
- }
-
- public void DisconnectFromInternals(EntityUid? owner = null)
- {
- if (!IsConnected) return;
- IsConnected = false;
- EntitySystem.Get().SetToggled(ToggleAction, false);
-
- GetInternalsComponent(owner)?.DisconnectTank();
- _disconnectStream?.Stop();
-
- if (_disconnectSound != null)
- _disconnectStream = SoundSystem.Play(_disconnectSound.GetSound(), Filter.Pvs(Owner, entityManager: _entMan), Owner, _disconnectSound.Params);
-
- UpdateUserInterface();
- }
-
- public void UpdateUserInterface(bool initialUpdate = false)
- {
- var internals = GetInternalsComponent();
- UserInterface?.SetState(
- new GasTankBoundUserInterfaceState
- {
- TankPressure = Air?.Pressure ?? 0,
- OutputPressure = initialUpdate ? OutputPressure : null,
- InternalsConnected = IsConnected,
- CanConnectInternals = IsFunctional && internals != null
- });
- }
-
- private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
- {
- switch (message.Message)
- {
- case GasTankSetPressureMessage msg:
- OutputPressure = msg.Pressure;
- break;
- case GasTankToggleInternalsMessage _:
- ToggleInternals();
- break;
- }
- }
-
- internal void ToggleInternals()
- {
- if (IsConnected)
- {
- DisconnectFromInternals();
- return;
- }
-
- ConnectToInternals();
- }
-
- private InternalsComponent? GetInternalsComponent(EntityUid? owner = null)
- {
- if (_entMan.Deleted(Owner)) return null;
- if (owner != null) return _entMan.GetComponentOrNull(owner.Value);
- return Owner.TryGetContainer(out var container)
- ? _entMan.GetComponentOrNull(container.Owner)
- : null;
- }
-
- public void AssumeAir(GasMixture giver)
- {
- var atmos = EntitySystem.Get();
- atmos.Merge(Air, giver);
- CheckStatus(atmos);
- }
-
- public void CheckStatus(AtmosphereSystem? atmosphereSystem=null)
- {
- if (Air == null)
- return;
-
- atmosphereSystem ??= EntitySystem.Get();
-
- var pressure = Air.Pressure;
-
- if (pressure > TankFragmentPressure)
- {
- // Give the gas a chance to build up more pressure.
- for (var i = 0; i < 3; i++)
- {
- atmosphereSystem.React(Air, this);
- }
-
- pressure = Air.Pressure;
- var range = (pressure - TankFragmentPressure) / TankFragmentScale;
-
- // Let's cap the explosion, yeah?
- // !1984
- if (range > MaxExplosionRange)
- {
- range = MaxExplosionRange;
- }
-
- EntitySystem.Get().TriggerExplosive(Owner, radius: range);
-
- return;
- }
-
- if (pressure > TankRupturePressure)
- {
- if (_integrity <= 0)
- {
- var environment = atmosphereSystem.GetContainingMixture(Owner, false, true);
- if(environment != null)
- atmosphereSystem.Merge(environment, Air);
-
- SoundSystem.Play(_ruptureSound.GetSound(), Filter.Pvs(Owner), _entMan.GetComponent(Owner).Coordinates, AudioHelpers.WithVariation(0.125f));
-
- _entMan.QueueDeleteEntity(Owner);
- return;
- }
-
- _integrity--;
- return;
- }
-
- if (pressure > TankLeakPressure)
- {
- if (_integrity <= 0)
- {
- var environment = atmosphereSystem.GetContainingMixture(Owner, false, true);
- if (environment == null)
- return;
-
- var leakedGas = Air.RemoveRatio(0.25f);
- atmosphereSystem.Merge(environment, leakedGas);
- }
- else
- {
- _integrity--;
- }
-
- return;
- }
-
- if (_integrity < 3)
- _integrity++;
- }
}
}
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BreathTool.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BreathTool.cs
new file mode 100644
index 0000000000..22270cb541
--- /dev/null
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BreathTool.cs
@@ -0,0 +1,30 @@
+using Content.Server.Atmos.Components;
+using Content.Server.Body.Components;
+
+namespace Content.Server.Atmos.EntitySystems;
+
+public sealed partial class AtmosphereSystem
+{
+ private void InitializeBreathTool()
+ {
+ SubscribeLocalEvent(OnBreathToolShutdown);
+ }
+
+ private void OnBreathToolShutdown(EntityUid uid, BreathToolComponent component, ComponentShutdown args)
+ {
+ DisconnectInternals(component);
+ }
+
+ public void DisconnectInternals(BreathToolComponent component)
+ {
+ var old = component.ConnectedInternalsEntity;
+ component.ConnectedInternalsEntity = null;
+
+ if (TryComp(old, out var internalsComponent))
+ {
+ _internals.DisconnectBreathTool(internalsComponent);
+ }
+
+ component.IsFunctional = false;
+ }
+}
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
index 5089090e12..901154ba22 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
@@ -1,5 +1,6 @@
using Content.Server.Administration.Logs;
using Content.Server.Atmos.Components;
+using Content.Server.Body.Systems;
using Content.Server.NodeContainer.EntitySystems;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Maps;
@@ -21,6 +22,7 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IAdminLogManager _adminLog = default!;
+ [Dependency] private readonly InternalsSystem _internals = default!;
[Dependency] private readonly SharedContainerSystem _containers = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly GasTileOverlaySystem _gasTileOverlaySystem = default!;
@@ -36,6 +38,7 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
UpdatesAfter.Add(typeof(NodeGroupSystem));
+ InitializeBreathTool();
InitializeGases();
InitializeCommands();
InitializeCVars();
diff --git a/Content.Server/Atmos/EntitySystems/GasTankSystem.cs b/Content.Server/Atmos/EntitySystems/GasTankSystem.cs
index b8412c14dd..309a508d97 100644
--- a/Content.Server/Atmos/EntitySystems/GasTankSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/GasTankSystem.cs
@@ -1,10 +1,22 @@
using Content.Server.Atmos.Components;
+using Content.Server.Body.Components;
+using Content.Server.Body.Systems;
+using Content.Server.Explosion.EntitySystems;
using Content.Server.UserInterface;
using Content.Shared.Actions;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Audio;
using Content.Shared.Interaction.Events;
using Content.Shared.Toggleable;
using Content.Shared.Examine;
+using Content.Shared.Inventory;
using JetBrains.Annotations;
+using Robust.Server.GameObjects;
+using Robust.Server.Player;
+using Robust.Shared.Audio;
+using Robust.Shared.Containers;
+using Robust.Shared.Player;
namespace Content.Server.Atmos.EntitySystems
{
@@ -12,6 +24,11 @@ namespace Content.Server.Atmos.EntitySystems
public sealed class GasTankSystem : EntitySystem
{
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
+ [Dependency] private readonly ExplosionSystem _explosions = default!;
+ [Dependency] private readonly InternalsSystem _internals = default!;
+ [Dependency] private readonly SharedContainerSystem _containers = default!;
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+ [Dependency] private readonly UserInterfaceSystem _ui = default!;
private const float TimerDelay = 0.5f;
private float _timer = 0f;
@@ -19,22 +36,57 @@ namespace Content.Server.Atmos.EntitySystems
public override void Initialize()
{
base.Initialize();
+ SubscribeLocalEvent(OnGasShutdown);
SubscribeLocalEvent(BeforeUiOpen);
SubscribeLocalEvent(OnGetActions);
SubscribeLocalEvent(OnExamined);
SubscribeLocalEvent(OnActionToggle);
SubscribeLocalEvent(OnDropped);
+
+ SubscribeLocalEvent(OnGasTankSetPressure);
+ SubscribeLocalEvent(OnGasTankToggleInternals);
+ }
+
+ private void OnGasShutdown(EntityUid uid, GasTankComponent component, ComponentShutdown args)
+ {
+ DisconnectFromInternals(component);
+ }
+
+ private void OnGasTankToggleInternals(EntityUid uid, GasTankComponent component, GasTankToggleInternalsMessage args)
+ {
+ if (args.Session is not IPlayerSession playerSession ||
+ playerSession.AttachedEntity is not {} player) return;
+
+ ToggleInternals(component);
+ }
+
+ private void OnGasTankSetPressure(EntityUid uid, GasTankComponent component, GasTankSetPressureMessage args)
+ {
+ component.OutputPressure = args.Pressure;
+ }
+
+ public void UpdateUserInterface(GasTankComponent component, bool initialUpdate = false)
+ {
+ var internals = GetInternalsComponent(component);
+ _ui.GetUiOrNull(component.Owner, SharedGasTankUiKey.Key)?.SetState(
+ new GasTankBoundUserInterfaceState
+ {
+ TankPressure = component.Air?.Pressure ?? 0,
+ OutputPressure = initialUpdate ? component.OutputPressure : null,
+ InternalsConnected = component.IsConnected,
+ CanConnectInternals = IsFunctional(component) && internals != null
+ });
}
private void BeforeUiOpen(EntityUid uid, GasTankComponent component, BeforeActivatableUIOpenEvent args)
{
// Only initial update includes output pressure information, to avoid overwriting client-input as the updates come in.
- component.UpdateUserInterface(true);
+ UpdateUserInterface(component, true);
}
private void OnDropped(EntityUid uid, GasTankComponent component, DroppedEvent args)
{
- component.DisconnectFromInternals(args.User);
+ DisconnectFromInternals(component, args.User);
}
private void OnGetActions(EntityUid uid, GasTankComponent component, GetItemActionsEvent args)
@@ -55,8 +107,7 @@ namespace Content.Server.Atmos.EntitySystems
if (args.Handled)
return;
- component.ToggleInternals();
-
+ ToggleInternals(component);
args.Handled = true;
}
@@ -72,13 +123,185 @@ namespace Content.Server.Atmos.EntitySystems
foreach (var gasTank in EntityManager.EntityQuery())
{
_atmosphereSystem.React(gasTank.Air, gasTank);
- gasTank.CheckStatus(_atmosphereSystem);
-
- if (gasTank.UserInterface != null && gasTank.UserInterface.SubscribedSessions.Count > 0)
+ CheckStatus(gasTank);
+ if (_ui.IsUiOpen(gasTank.Owner, SharedGasTankUiKey.Key))
{
- gasTank.UpdateUserInterface();
+ UpdateUserInterface(gasTank);
}
}
}
+
+ private void ToggleInternals(GasTankComponent component)
+ {
+ if (component.IsConnected)
+ {
+ DisconnectFromInternals(component);
+ }
+ else
+ {
+ ConnectToInternals(component);
+ }
+ }
+
+ public GasMixture? RemoveAir(GasTankComponent component, float amount)
+ {
+ var gas = component.Air?.Remove(amount);
+ CheckStatus(component);
+ return gas;
+ }
+
+ public GasMixture RemoveAirVolume(GasTankComponent component, float volume)
+ {
+ if (component.Air == null)
+ return new GasMixture(volume);
+
+ var tankPressure = component.Air.Pressure;
+ if (tankPressure < component.OutputPressure)
+ {
+ component.OutputPressure = tankPressure;
+ UpdateUserInterface(component);
+ }
+
+ var molesNeeded = component.OutputPressure * volume / (Atmospherics.R * component.Air.Temperature);
+
+ var air = RemoveAir(component, molesNeeded);
+
+ if (air != null)
+ air.Volume = volume;
+ else
+ return new GasMixture(volume);
+
+ return air;
+ }
+
+ public bool CanConnectToInternals(GasTankComponent component)
+ {
+ return !component.IsConnected && IsFunctional(component);
+ }
+
+ public void ConnectToInternals(GasTankComponent component)
+ {
+ if (!CanConnectToInternals(component)) return;
+ var internals = GetInternalsComponent(component);
+ if (internals == null) return;
+ component.IsConnected = _internals.TryConnectTank(internals, component.Owner);
+ _actions.SetToggled(component.ToggleAction, component.IsConnected);
+
+ // Couldn't toggle!
+ if (!component.IsConnected) return;
+
+ component.ConnectStream?.Stop();
+
+ if (component.ConnectSound != null)
+ component.ConnectStream = SoundSystem.Play(component.ConnectSound.GetSound(), Filter.Pvs(component.Owner, entityManager: EntityManager), component.Owner, component.ConnectSound.Params);
+
+ UpdateUserInterface(component);
+ }
+
+ public void DisconnectFromInternals(GasTankComponent component, EntityUid? owner = null)
+ {
+ if (!component.IsConnected) return;
+ component.IsConnected = false;
+ _actions.SetToggled(component.ToggleAction, false);
+
+ _internals.DisconnectTank(GetInternalsComponent(component, owner));
+ component.DisconnectStream?.Stop();
+
+ if (component.DisconnectSound != null)
+ component.DisconnectStream = SoundSystem.Play(component.DisconnectSound.GetSound(), Filter.Pvs(component.Owner, entityManager: EntityManager), component.Owner, component.DisconnectSound.Params);
+
+ UpdateUserInterface(component);
+ }
+
+ private InternalsComponent? GetInternalsComponent(GasTankComponent component, EntityUid? owner = null)
+ {
+ if (Deleted(component.Owner)) return null;
+ if (owner != null) return CompOrNull(owner.Value);
+ return _containers.TryGetContainingContainer(component.Owner, out var container)
+ ? CompOrNull