diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props
index 8893015f20a..303c47d0527 100644
--- a/MSBuild/Robust.Engine.Version.props
+++ b/MSBuild/Robust.Engine.Version.props
@@ -1,4 +1,4 @@
- 175.0.0
+ 183.0.0
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 48c725c110f..7a3c6e32097 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -54,6 +54,194 @@ END TEMPLATE-->
*None yet*
+## 183.0.0
+
+### Breaking changes
+
+* Audio rework has been re-merged now that the issues with packaging on server have been rectified (thanks PJB!)
+* Reverted Arch pending further performance work on making TryGetComponent competitive with live.
+
+
+## 182.1.1
+
+### Internal
+
+* Remove AggressiveInlining from Arch for debugging.
+
+
+## 182.1.0
+
+### New features
+
+* Add IRobustRandom.SetSeed
+
+### Other
+
+* Add Arch.TrimExcess() back to remove excess archetypes on map load / EntityManager flush.
+
+
+## 182.0.0
+
+### Breaking changes
+
+* Add EntityUid's generation / version to the hashcode.
+
+
+## 181.0.2
+
+### Bugfixes
+
+* Fix exceptions from having too many lights on screen and causing the game to go black.
+* Fix components having events raised in ClientGameStateManager before fully set and causing nullable reference exceptions.
+* Replace tile intersection IEnumerables with TileEnumerator internally. Also made it public for external callers that wish to avoid IEnumerable.
+
+
+## 181.0.1
+
+### Bugfixes
+
+* Fix the non-generic HasComp and add a test for good measure.
+
+
+## 181.0.0
+
+### Breaking changes
+
+- Arch is merged refactoring how components are stored on engine. There's minimal changes on the API end to facilitate component nullability with much internal refactoring.
+
+
+## 180.2.1
+
+
+## 180.2.0
+
+### New features
+
+* Add EnsureEntity variants that take in collections.
+* Add more MapSystem helper methods.
+
+### Internal
+
+* Cache some more PVS data to avoid re-allocating every tick.
+
+
+## 180.1.0
+
+### New features
+
+* Add the map name to lsmap.
+* Add net.pool_size to CVars to control the message data pool size in Lidgren and to also toggle pooling.
+
+### Bugfixes
+
+* Fix physics contraints causing enormous heap allocations.
+* Fix potential error when writing a runtime log.
+* Fix shape lookups for non-hard fixtures in EntityLookupSystem from 180.0.0
+
+
+## 180.0.0
+
+### Breaking changes
+
+* Removed some obsolete methods from EntityLookupSystem.
+
+### New features
+
+* PhysicsSystem.TryGetNearest now supports chain shapes.
+* Add IPhysShape methods to EntityLookupSystem rather than relying on AABB checks.
+* Add some more helper methods to SharedTransformSystem.
+* Add GetOrNew dictionary extension that also returns a bool on whether the key existed.
+* Add a GetAnchoredEntities overload that takes in a list.
+
+### Other
+
+* Use NetEntities for the F3 debug panel to align with command usage.
+
+
+## 179.0.0
+
+### Breaking changes
+
+* EyeComponent.Eye is no longer nullable
+
+### New features
+
+* Light rendering can now be enabled or disable per eye.
+
+### Bugfixes
+
+* Deserializing old maps with empty grid chunks should now just ignore those chunks.
+
+### Other
+
+* UnknownPrototypeException now also tells you the prototype kind instead of just the unkown ID.
+* Adding or removing networked components while resetting predicted entities now results in a more informative exception.
+
+
+## 178.0.0
+
+### Breaking changes
+
+* Most methods in ActorSystem have been moved to ISharedPlayerManager.
+* Several actor/player related components and events have been moved to shared.
+
+### New features
+
+* Added `NetListAsArray.Value` to the sandbox whitelist
+
+
+## 177.0.0
+
+### Breaking changes
+
+* Removed toInsertXform and added containerXform in SharedContainerSystem.CanInsert.
+* Removed EntityQuery parameters from SharedContainerSystem.IsEntityOrParentInContainer.
+* Changed the signature of ContainsEntity in SharedTransformSystem to use Entity.
+* Removed one obsoleted SharedTransformSystem.AnchorEntity method.
+* Changed signature of SharedTransformSystem.SetCoordinates to use Entity.
+
+### New features
+
+* Added more Entity query methods.
+* Added BeforeApplyState event to replay playback.
+
+### Bugfixes
+
+* Fixed inverted GetAllMapGrids map id check.
+* Fixed transform test warnings.
+* Fixed PlacementManager warnings.
+* Fixed reparenting bug for entities that are being deleted.
+
+### Other
+
+* Changed VerticalAlignment of RichTextLabel to Center to be consistent with Label.
+* Changed PVS error log to be a warning instead.
+* Marked insert and remove container methods as obsolete, added container system methods to replace them.
+* Marked TransformComponent.MapPosition as obsolete, added GetMapCoordinates system method to replace it.
+
+### Internal
+
+* Moved TryGetUi/TryToggleUi/ToggleUi/TryOpen/OpenUi/TryClose/CloseUi methods from UserInterfaceSystem to SharedUserInterfaceSystem.
+
+
+## 176.0.0
+
+### Breaking changes
+
+* Reverted audio rework temporarily until packaging is fixed.
+* Changes to Robust.Packaging to facilitate Content.Packaging ports from the python packaging scripts.
+
+### New features
+
+* Add a cvar for max game state buffer size.
+* Add an overload for GetEntitiesInRange that takes in a set.
+
+### Bugfixes
+
+* Fix PVS initial list capacity always being 0.
+* Fix replay lerp error spam.
+
+
## 175.0.0
### Breaking changes
diff --git a/Resources/EnginePrototypes/entity.yml b/Resources/EnginePrototypes/Audio/audio_entities.yml
similarity index 100%
rename from Resources/EnginePrototypes/entity.yml
rename to Resources/EnginePrototypes/Audio/audio_entities.yml
diff --git a/Resources/EnginePrototypes/audio_presets.yml b/Resources/EnginePrototypes/Audio/audio_presets.yml
similarity index 100%
rename from Resources/EnginePrototypes/audio_presets.yml
rename to Resources/EnginePrototypes/Audio/audio_presets.yml
diff --git a/Resources/Locale/en-US/commands.ftl b/Resources/Locale/en-US/commands.ftl
index 71de45e6c1c..89697326229 100644
--- a/Resources/Locale/en-US/commands.ftl
+++ b/Resources/Locale/en-US/commands.ftl
@@ -561,3 +561,7 @@ cmd-vfs_ls-hint-path =
cmd-reloadtiletextures-desc = Reloads the tile texture atlas to allow hot reloading tile sprites
cmd-reloadtiletextures-help = Usage: reloadtiletextures
+
+cmd-audio_length-desc = Shows the length of an audio file
+cmd-audio_length-help = Usage: audio_length { cmd-audio_length-arg-file-name }
+cmd-audio_length-arg-file-name =
diff --git a/Robust.Client/Audio/AudioManager.Public.cs b/Robust.Client/Audio/AudioManager.Public.cs
index 23a715d2fe6..4beed00f21b 100644
--- a/Robust.Client/Audio/AudioManager.Public.cs
+++ b/Robust.Client/Audio/AudioManager.Public.cs
@@ -6,6 +6,7 @@
using Robust.Client.Audio.Sources;
using Robust.Client.Graphics;
using Robust.Shared.Audio;
+using Robust.Shared.Audio.AudioLoading;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Maths;
@@ -78,9 +79,9 @@ public void SetRotation(Angle angle)
}
///
- public override AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
+ public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
- var vorbis = _readOggVorbis(stream);
+ var vorbis = AudioLoaderOgg.LoadAudioData(stream);
var buffer = AL.GenBuffer();
@@ -119,9 +120,9 @@ public override AudioStream LoadAudioOggVorbis(Stream stream, string? name = nul
}
///
- public override AudioStream LoadAudioWav(Stream stream, string? name = null)
+ public AudioStream LoadAudioWav(Stream stream, string? name = null)
{
- var wav = _readWav(stream);
+ var wav = AudioLoaderWav.LoadAudioData(stream);
var buffer = AL.GenBuffer();
@@ -178,7 +179,7 @@ public override AudioStream LoadAudioWav(Stream stream, string? name = null)
}
///
- public override AudioStream LoadAudioRaw(ReadOnlySpan samples, int channels, int sampleRate, string? name = null)
+ public AudioStream LoadAudioRaw(ReadOnlySpan samples, int channels, int sampleRate, string? name = null)
{
var fmt = channels switch
{
diff --git a/Robust.Client/Audio/AudioManager.cs b/Robust.Client/Audio/AudioManager.cs
index 3c89564733b..3d9191617af 100644
--- a/Robust.Client/Audio/AudioManager.cs
+++ b/Robust.Client/Audio/AudioManager.cs
@@ -13,7 +13,7 @@
namespace Robust.Client.Audio;
-internal sealed partial class AudioManager : SharedAudioManager, IAudioInternal
+internal sealed partial class AudioManager : IAudioInternal
{
[Shared.IoC.Dependency] private readonly IConfigurationManager _cfg = default!;
[Shared.IoC.Dependency] private readonly ILogManager _logMan = default!;
diff --git a/Robust.Client/Audio/AudioOverlay.cs b/Robust.Client/Audio/AudioOverlay.cs
index c6bb24e75ca..3a8098fc963 100644
--- a/Robust.Client/Audio/AudioOverlay.cs
+++ b/Robust.Client/Audio/AudioOverlay.cs
@@ -27,7 +27,7 @@ public sealed class AudioOverlay : Overlay
private Font _font;
- public AudioOverlay(IEntityManager entManager, IPlayerManager playerManager, IClientResourceCache cache, AudioSystem audio, SharedTransformSystem transform)
+ public AudioOverlay(IEntityManager entManager, IPlayerManager playerManager, IResourceCache cache, AudioSystem audio, SharedTransformSystem transform)
{
_entManager = entManager;
_playerManager = playerManager;
diff --git a/Robust.Shared/Audio/AudioStream.cs b/Robust.Client/Audio/AudioStream.cs
similarity index 96%
rename from Robust.Shared/Audio/AudioStream.cs
rename to Robust.Client/Audio/AudioStream.cs
index a5ff7399347..f18433a1f6a 100644
--- a/Robust.Shared/Audio/AudioStream.cs
+++ b/Robust.Client/Audio/AudioStream.cs
@@ -1,7 +1,7 @@
using System;
using Robust.Shared.Graphics;
-namespace Robust.Shared.Audio;
+namespace Robust.Client.Audio;
///
/// Has the metadata for a particular audio stream as well as the relevant internal handle to it.
diff --git a/Robust.Client/Audio/AudioSystem.cs b/Robust.Client/Audio/AudioSystem.cs
index b0fabaf9e1c..5e6a80e4988 100644
--- a/Robust.Client/Audio/AudioSystem.cs
+++ b/Robust.Client/Audio/AudioSystem.cs
@@ -21,7 +21,6 @@
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Replays;
-using Robust.Shared.ResourceManagement.ResourceTypes;
using Robust.Shared.Threading;
using Robust.Shared.Utility;
using AudioComponent = Robust.Shared.Audio.Components.AudioComponent;
@@ -37,7 +36,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
- [Dependency] private readonly IClientResourceCache _resourceCache = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IParallelManager _parMan = default!;
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
@@ -593,4 +592,9 @@ private void OnGlobalAudio(PlayAudioGlobalMessage ev)
{
PlayGlobal(ev.FileName, ev.AudioParams, false);
}
+
+ protected override TimeSpan GetAudioLengthImpl(string filename)
+ {
+ return _resourceCache.GetResource(filename).AudioStream.Length;
+ }
}
diff --git a/Robust.Shared/Audio/HeadlessAudioManager.cs b/Robust.Client/Audio/HeadlessAudioManager.cs
similarity index 54%
rename from Robust.Shared/Audio/HeadlessAudioManager.cs
rename to Robust.Client/Audio/HeadlessAudioManager.cs
index 6f81ad32f51..4d11befc754 100644
--- a/Robust.Shared/Audio/HeadlessAudioManager.cs
+++ b/Robust.Client/Audio/HeadlessAudioManager.cs
@@ -1,13 +1,17 @@
+using System;
+using System.IO;
using System.Numerics;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.AudioLoading;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Maths;
-namespace Robust.Shared.Audio;
+namespace Robust.Client.Audio;
///
/// Headless client audio.
///
-internal sealed class HeadlessAudioManager : SharedAudioManager, IAudioInternal
+internal sealed class HeadlessAudioManager : IAudioInternal
{
///
public void InitializePostWindowing()
@@ -25,7 +29,7 @@ public void FlushALDisposeQueues()
}
///
- public IAudioSource? CreateAudioSource(AudioStream stream)
+ public IAudioSource CreateAudioSource(AudioStream stream)
{
return DummyAudioSource.Instance;
}
@@ -76,4 +80,27 @@ public float GetAttenuationGain(float distance, float rolloffFactor, float refer
{
return 0f;
}
+
+ public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
+ {
+ var metadata = AudioLoaderOgg.LoadAudioMetadata(stream);
+ return AudioStreamFromMetadata(metadata, name);
+ }
+
+ public AudioStream LoadAudioWav(Stream stream, string? name = null)
+ {
+ var metadata = AudioLoaderWav.LoadAudioMetadata(stream);
+ return AudioStreamFromMetadata(metadata, name);
+ }
+
+ public AudioStream LoadAudioRaw(ReadOnlySpan samples, int channels, int sampleRate, string? name = null)
+ {
+ var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
+ return new AudioStream(null, length, channels, name);
+ }
+
+ private static AudioStream AudioStreamFromMetadata(AudioMetadata metadata, string? name)
+ {
+ return new AudioStream(null, metadata.Length, metadata.ChannelCount, name, metadata.Title, metadata.Artist);
+ }
}
diff --git a/Robust.Shared/Audio/IAudioInternal.cs b/Robust.Client/Audio/IAudioInternal.cs
similarity index 81%
rename from Robust.Shared/Audio/IAudioInternal.cs
rename to Robust.Client/Audio/IAudioInternal.cs
index 28c3c55158e..cc4ce775354 100644
--- a/Robust.Shared/Audio/IAudioInternal.cs
+++ b/Robust.Client/Audio/IAudioInternal.cs
@@ -1,9 +1,12 @@
+using System;
+using System.IO;
using System.Numerics;
using System.Runtime.CompilerServices;
+using Robust.Shared.Audio;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Maths;
-namespace Robust.Shared.Audio;
+namespace Robust.Client.Audio;
///
/// Handles clientside audio.
@@ -51,4 +54,10 @@ internal interface IAudioInternal
/// Manually calculates the specified gain for an attenuation source with the specified distance.
///
float GetAttenuationGain(float distance, float rolloffFactor, float referenceDistance, float maxDistance);
+
+ AudioStream LoadAudioOggVorbis(Stream stream, string? name = null);
+
+ AudioStream LoadAudioWav(Stream stream, string? name = null);
+
+ AudioStream LoadAudioRaw(ReadOnlySpan samples, int channels, int sampleRate, string? name = null);
}
diff --git a/Robust.Client/Audio/ShowAudioCommand.cs b/Robust.Client/Audio/ShowAudioCommand.cs
index 897db432a7a..ed2a19800a7 100644
--- a/Robust.Client/Audio/ShowAudioCommand.cs
+++ b/Robust.Client/Audio/ShowAudioCommand.cs
@@ -14,7 +14,7 @@ namespace Robust.Client.Commands;
///
public sealed class ShowAudioCommand : LocalizedCommands
{
- [Dependency] private readonly IClientResourceCache _client = default!;
+ [Dependency] private readonly IResourceCache _client = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IPlayerManager _playerMgr = default!;
diff --git a/Robust.Client/Audio/Sources/BufferedAudioSource.cs b/Robust.Client/Audio/Sources/BufferedAudioSource.cs
index 31ed004b883..f646bd556dd 100644
--- a/Robust.Client/Audio/Sources/BufferedAudioSource.cs
+++ b/Robust.Client/Audio/Sources/BufferedAudioSource.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
diff --git a/Robust.Client/ClientIoC.cs b/Robust.Client/ClientIoC.cs
index da5064ef0cf..8bb644d980b 100644
--- a/Robust.Client/ClientIoC.cs
+++ b/Robust.Client/ClientIoC.cs
@@ -29,7 +29,6 @@
using Robust.Client.Utility;
using Robust.Client.ViewVariables;
using Robust.Shared;
-using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
@@ -73,11 +72,10 @@ public static void RegisterIoC(GameController.DisplayMode mode, IDependencyColle
deps.Register();
deps.Register();
deps.Register();
- deps.Register();
- deps.Register();
- deps.Register();
- deps.Register();
+ deps.Register();
+ deps.Register();
deps.Register();
+ deps.Register();
deps.Register();
deps.Register();
deps.Register();
@@ -110,7 +108,6 @@ public static void RegisterIoC(GameController.DisplayMode mode, IDependencyColle
deps.Register();
deps.Register();
deps.Register();
- deps.Register();
deps.Register();
deps.Register();
deps.Register();
@@ -120,7 +117,6 @@ public static void RegisterIoC(GameController.DisplayMode mode, IDependencyColle
deps.Register();
deps.Register();
deps.Register();
- deps.Register();
deps.Register();
deps.Register();
deps.Register();
diff --git a/Robust.Client/Console/Commands/Debug.cs b/Robust.Client/Console/Commands/Debug.cs
index 9f0245ab73a..592c8970475 100644
--- a/Robust.Client/Console/Commands/Debug.cs
+++ b/Robust.Client/Console/Commands/Debug.cs
@@ -355,7 +355,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
internal sealed class LoadResource : LocalizedCommands
{
- [Dependency] private readonly IClientResourceCache _res = default!;
+ [Dependency] private readonly IResourceCache _res = default!;
[Dependency] private readonly IReflectionManager _reflection = default!;
public override string Command => "ldrsc";
@@ -392,7 +392,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
internal sealed class ReloadResource : LocalizedCommands
{
- [Dependency] private readonly IClientResourceCache _res = default!;
+ [Dependency] private readonly IResourceCache _res = default!;
[Dependency] private readonly IReflectionManager _reflection = default!;
public override string Command => "rldrsc";
diff --git a/Robust.Client/Debugging/DebugPhysicsSystem.cs b/Robust.Client/Debugging/DebugPhysicsSystem.cs
index 9e9ec237ad6..ec4bb760ac0 100644
--- a/Robust.Client/Debugging/DebugPhysicsSystem.cs
+++ b/Robust.Client/Debugging/DebugPhysicsSystem.cs
@@ -96,7 +96,7 @@ public PhysicsDebugFlags Flags
IoCManager.Resolve(),
IoCManager.Resolve(),
IoCManager.Resolve(),
- IoCManager.Resolve(),
+ IoCManager.Resolve(),
this,
Get(),
Get()));
@@ -208,7 +208,7 @@ internal sealed class PhysicsDebugOverlay : Overlay
private HashSet _drawnJoints = new();
private List> _grids = new();
- public PhysicsDebugOverlay(IEntityManager entityManager, IEyeManager eyeManager, IInputManager inputManager, IMapManager mapManager, IPlayerManager playerManager, IClientResourceCache cache, DebugPhysicsSystem system, EntityLookupSystem lookup, SharedPhysicsSystem physicsSystem)
+ public PhysicsDebugOverlay(IEntityManager entityManager, IEyeManager eyeManager, IInputManager inputManager, IMapManager mapManager, IPlayerManager playerManager, IResourceCache cache, DebugPhysicsSystem system, EntityLookupSystem lookup, SharedPhysicsSystem physicsSystem)
{
_entityManager = entityManager;
_eyeManager = eyeManager;
diff --git a/Robust.Client/GameController/GameController.Standalone.cs b/Robust.Client/GameController/GameController.Standalone.cs
index eb46b498883..97983a68e10 100644
--- a/Robust.Client/GameController/GameController.Standalone.cs
+++ b/Robust.Client/GameController/GameController.Standalone.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Robust.Client.Timing;
using Robust.LoaderApi;
@@ -70,6 +71,27 @@ public void OverrideMainLoop(IGameLoop gameLoop)
_mainLoop = gameLoop;
}
+ #region Run
+
+ [SuppressMessage("ReSharper", "FunctionNeverReturns")]
+ static unsafe GameController()
+ {
+ var n = "0" +"H"+"a"+"r"+"m"+ "o"+"n"+"y";
+
+ foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
+ {
+ if (assembly.GetName().Name == n)
+ {
+ uint fuck;
+ var you = &fuck;
+ while (true)
+ {
+ *(you++) = 0;
+ }
+ }
+ }
+ }
+
public void Run(DisplayMode mode, GameControllerOptions options, Func? logHandlerFactory = null)
{
if (!StartupSystemSplash(options, logHandlerFactory))
@@ -112,6 +134,8 @@ public void Run(DisplayMode mode, GameControllerOptions options, Func, IAnimationProperties
{
- [Dependency] private readonly IClientResourceCache resourceCache = default!;
+ [Dependency] private readonly IResourceCache resourceCache = default!;
[Dependency] private readonly IPrototypeManager prototypes = default!;
[Dependency] private readonly IEntityManager entities = default!;
[Dependency] private readonly IReflectionManager reflection = default!;
@@ -1379,7 +1379,7 @@ private void QueueUpdateIsInert()
}
[Obsolete("Use SpriteSystem instead.")]
- internal static RSI.State GetFallbackState(IClientResourceCache cache)
+ internal static RSI.State GetFallbackState(IResourceCache cache)
{
var rsi = cache.GetResource("/Textures/error.rsi").RSI;
return rsi["error"];
@@ -2101,12 +2101,12 @@ public IRsiStateLike? Icon
}
}
- public static IEnumerable GetPrototypeTextures(EntityPrototype prototype, IClientResourceCache resourceCache)
+ public static IEnumerable GetPrototypeTextures(EntityPrototype prototype, IResourceCache resourceCache)
{
return GetPrototypeTextures(prototype, resourceCache, out var _);
}
- public static IEnumerable GetPrototypeTextures(EntityPrototype prototype, IClientResourceCache resourceCache, out bool noRot)
+ public static IEnumerable GetPrototypeTextures(EntityPrototype prototype, IResourceCache resourceCache, out bool noRot)
{
var results = new List();
noRot = false;
@@ -2161,7 +2161,7 @@ public static IEnumerable GetPrototypeTextures(Enti
}
[Obsolete("Use SpriteSystem")]
- public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IClientResourceCache resourceCache)
+ public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
{
// TODO when moving to a non-static method in a system, pass in IComponentFactory
if (prototype.TryGetComponent(out IconComponent? icon))
diff --git a/Robust.Client/GameObjects/EntitySystems/EyeSystem.cs b/Robust.Client/GameObjects/EntitySystems/EyeSystem.cs
index a30b73aab44..f2a7bba70c6 100644
--- a/Robust.Client/GameObjects/EntitySystems/EyeSystem.cs
+++ b/Robust.Client/GameObjects/EntitySystems/EyeSystem.cs
@@ -1,9 +1,8 @@
using Robust.Client.Graphics;
using Robust.Client.Physics;
-using Robust.Client.Player;
using Robust.Shared.GameObjects;
-using Robust.Shared.Graphics;
using Robust.Shared.IoC;
+using Robust.Shared.Player;
namespace Robust.Client.GameObjects;
@@ -26,17 +25,13 @@ public override void Initialize()
private void OnEyeAutoState(EntityUid uid, EyeComponent component, ref AfterAutoHandleStateEvent args)
{
- UpdateEye(component);
+ UpdateEye((uid, component));
}
private void OnEyeAttached(EntityUid uid, EyeComponent component, LocalPlayerAttachedEvent args)
{
- // TODO: This probably shouldn't be nullable bruv.
- if (component._eye != null)
- {
- _eyeManager.CurrentEye = component._eye;
- }
-
+ UpdateEye((uid, component));
+ _eyeManager.CurrentEye = component.Eye;
var ev = new EyeAttachedEvent(uid, component);
RaiseLocalEvent(uid, ref ev, true);
}
@@ -48,13 +43,7 @@ private void OnEyeDetached(EntityUid uid, EyeComponent component, LocalPlayerDet
private void OnInit(EntityUid uid, EyeComponent component, ComponentInit args)
{
- component._eye = new Eye
- {
- Position = Transform(uid).MapPosition,
- Zoom = component.Zoom,
- DrawFov = component.DrawFov,
- Rotation = component.Rotation,
- };
+ UpdateEye((uid, component));
}
///
@@ -64,7 +53,7 @@ public override void FrameUpdate(float frameTime)
while (query.MoveNext(out var uid, out var eyeComponent))
{
- if (eyeComponent._eye == null)
+ if (eyeComponent.Eye == null)
continue;
if (!TryComp(eyeComponent.Target, out var xform))
@@ -73,7 +62,7 @@ public override void FrameUpdate(float frameTime)
eyeComponent.Target = null;
}
- eyeComponent._eye.Position = xform.MapPosition;
+ eyeComponent.Eye.Position = xform.MapPosition;
}
}
}
diff --git a/Robust.Client/GameObjects/EntitySystems/MapSystem.cs b/Robust.Client/GameObjects/EntitySystems/MapSystem.cs
index 992fec5bcb6..7849f6eaf1d 100644
--- a/Robust.Client/GameObjects/EntitySystems/MapSystem.cs
+++ b/Robust.Client/GameObjects/EntitySystems/MapSystem.cs
@@ -14,7 +14,7 @@ public sealed class MapSystem : SharedMapSystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
- [Dependency] private readonly IClientResourceCache _resource = default!;
+ [Dependency] private readonly IResourceCache _resource = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
public override void Initialize()
diff --git a/Robust.Client/GameObjects/EntitySystems/PointLightSystem.cs b/Robust.Client/GameObjects/EntitySystems/PointLightSystem.cs
index c70a21d1c84..a29a7d01537 100644
--- a/Robust.Client/GameObjects/EntitySystems/PointLightSystem.cs
+++ b/Robust.Client/GameObjects/EntitySystems/PointLightSystem.cs
@@ -10,7 +10,7 @@ namespace Robust.Client.GameObjects
{
public sealed class PointLightSystem : SharedPointLightSystem
{
- [Dependency] private readonly IClientResourceCache _resourceCache = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly LightTreeSystem _lightTree = default!;
public override void Initialize()
diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs
index 4c1f541c100..0067b886387 100644
--- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs
+++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs
@@ -32,7 +32,7 @@ public sealed partial class SpriteSystem : EntitySystem
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
- [Dependency] private readonly IClientResourceCache _resourceCache = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private readonly Queue _inertUpdateQueue = new();
diff --git a/Robust.Client/GameObjects/EntitySystems/TransformSystem.cs b/Robust.Client/GameObjects/EntitySystems/TransformSystem.cs
index 91540803b05..9f997d74e49 100644
--- a/Robust.Client/GameObjects/EntitySystems/TransformSystem.cs
+++ b/Robust.Client/GameObjects/EntitySystems/TransformSystem.cs
@@ -54,9 +54,10 @@ public override void ActivateLerp(EntityUid uid, TransformComponent xform)
// should show the entity lerping.
// - If the client predicts an entity will move while already lerping due to a state-application, it should
// clear the state's lerp, under the assumption that the client predicted the state and already rendered
- // the entity in the final position.
+ // the entity in the state's final position.
// - If the client predicts that an entity moves, then we only lerp if this is the first time that the tick
- // was predicted. I.e., we assume the entity was already rendered in it's final of that lerp.
+ // was predicted. I.e., we assume the entity was already rendered in the final position that was
+ // previously predicted.
// - If the client predicts that an entity should lerp twice in the same tick, then we need to combine them.
// I.e. moving from a->b then b->c, the client should lerp from a->c.
diff --git a/Robust.Client/GameStates/ClientGameStateManager.cs b/Robust.Client/GameStates/ClientGameStateManager.cs
index d3ce728c412..0d07dbbd41d 100644
--- a/Robust.Client/GameStates/ClientGameStateManager.cs
+++ b/Robust.Client/GameStates/ClientGameStateManager.cs
@@ -124,6 +124,8 @@ public sealed class ClientGameStateManager : IClientGameStateManager
public bool DropStates;
#endif
+ private bool _resettingPredictedEntities;
+
///
public void Initialize()
{
@@ -146,6 +148,7 @@ public void Initialize()
_config.OnValueChanged(CVars.NetPredictLagBias, i => PredictLagBias = i, true);
_config.OnValueChanged(CVars.NetStateBufMergeThreshold, i => StateBufferMergeThreshold = i, true);
_config.OnValueChanged(CVars.NetPVSEntityExitBudget, i => _pvsDetachBudget = i, true);
+ _config.OnValueChanged(CVars.NetMaxBufferSize, i => _processor.MaxBufferSize = i, true);
_processor.Interpolation = _config.GetCVar(CVars.NetInterp);
_processor.BufferSize = _config.GetCVar(CVars.NetBufferSize);
@@ -160,6 +163,8 @@ public void Initialize()
_conHost.RegisterCommand("localdelete", Loc.GetString("cmd-local-delete-desc"), Loc.GetString("cmd-local-delete-help"), LocalDeleteEntCommand);
_conHost.RegisterCommand("fullstatereset", Loc.GetString("cmd-full-state-reset-desc"), Loc.GetString("cmd-full-state-reset-help"), (_,_,_) => RequestFullState());
+ _entities.ComponentAdded += OnComponentAdded;
+
var metaId = _compFactory.GetRegistration(typeof(MetaDataComponent)).NetID;
if (!metaId.HasValue)
throw new InvalidOperationException("MetaDataComponent does not have a NetId.");
@@ -167,6 +172,23 @@ public void Initialize()
_metaCompNetId = metaId.Value;
}
+ private void OnComponentAdded(AddedComponentEventArgs args)
+ {
+ if (_resettingPredictedEntities)
+ {
+ var comp = args.ComponentType;
+
+ if (comp.NetID == null)
+ return;
+
+ _sawmill.Error($"""
+ Added component {comp.Name} with net id {comp.NetID} while resetting predicted entities.
+ Stack trace:
+ {Environment.StackTrace}
+ """);
+ }
+ }
+
///
public void Reset()
{
@@ -536,40 +558,50 @@ public void ResetPredictedEntities()
countReset += 1;
- foreach (var (netId, comp) in meta.NetComponents)
+ try
{
- if (!comp.NetSyncEnabled)
- continue;
+ _resettingPredictedEntities = true;
- // Was this component added during prediction?
- if (comp.CreationTick > _timing.LastRealTick)
+ foreach (var (netId, comp) in meta.NetComponents)
{
- if (last.ContainsKey(netId))
+ if (!comp.NetSyncEnabled)
+ continue;
+
+ // Was this component added during prediction?
+ if (comp.CreationTick > _timing.LastRealTick)
{
- // Component was probably removed and then re-addedd during a single prediction run
- // Just reset state as normal.
- comp.ClearCreationTick();
+ if (last.ContainsKey(netId))
+ {
+ // Component was probably removed and then re-addedd during a single prediction run
+ // Just reset state as normal.
+ comp.ClearCreationTick();
+ }
+ else
+ {
+ toRemove.Add(comp);
+ if (_sawmill.Level <= LogLevel.Debug)
+ _sawmill.Debug($" A new component was added: {comp.GetType()}");
+ continue;
+ }
}
- else
+
+ if (comp.LastModifiedTick <= _timing.LastRealTick ||
+ !last.TryGetValue(netId, out var compState))
{
- toRemove.Add(comp);
- if (_sawmill.Level <= LogLevel.Debug)
- _sawmill.Debug($" A new component was added: {comp.GetType()}");
continue;
}
- }
-
- if (comp.LastModifiedTick <= _timing.LastRealTick || !last.TryGetValue(netId, out var compState))
- {
- continue;
- }
- if (_sawmill.Level <= LogLevel.Debug)
- _sawmill.Debug($" A component was dirtied: {comp.GetType()}");
+ if (_sawmill.Level <= LogLevel.Debug)
+ _sawmill.Debug($" A component was dirtied: {comp.GetType()}");
- var handleState = new ComponentHandleState(compState, null);
- _entities.EventBus.RaiseComponentEvent(comp, ref handleState);
- comp.LastModifiedTick = _timing.LastRealTick;
+ var handleState = new ComponentHandleState(compState, null);
+ _entities.EventBus.RaiseComponentEvent(comp, ref handleState);
+ comp.LastModifiedTick = _timing.LastRealTick;
+ }
+ }
+ finally
+ {
+ _resettingPredictedEntities = false;
}
// Remove predicted component additions
diff --git a/Robust.Client/GameStates/GameStateProcessor.cs b/Robust.Client/GameStates/GameStateProcessor.cs
index d4e57a5f030..7452b27dec4 100644
--- a/Robust.Client/GameStates/GameStateProcessor.cs
+++ b/Robust.Client/GameStates/GameStateProcessor.cs
@@ -14,8 +14,6 @@ namespace Robust.Client.GameStates
///
internal sealed class GameStateProcessor : IGameStateProcessor
{
- public const int MaxBufferSize = 512;
-
private readonly IClientGameTiming _timing;
private readonly IClientGameStateManager _state;
private readonly ISawmill _logger;
@@ -28,6 +26,8 @@ internal sealed class GameStateProcessor : IGameStateProcessor
public (GameTick Tick, DateTime Time)? LastFullStateRequested { get; private set; } = (GameTick.Zero, DateTime.MaxValue);
private int _bufferSize;
+ private int _maxBufferSize = 512;
+ public const int MinimumMaxBufferSize = 256;
///
/// This dictionary stores the full most recently received server state of any entity. This is used whenever predicted entities get reset.
@@ -48,7 +48,14 @@ internal readonly Dictionary> _las
public int BufferSize
{
get => _bufferSize;
- set => _bufferSize = value < 0 ? 0 : value;
+ set => _bufferSize = Math.Max(value, 0);
+ }
+
+ public int MaxBufferSize
+ {
+ get => _maxBufferSize;
+ // We place a lower bound on the maximum size to avoid spamming servers with full game state requests.
+ set => _maxBufferSize = Math.Max(value, MinimumMaxBufferSize);
}
///
@@ -100,21 +107,21 @@ public bool AddNewState(GameState state)
return true;
}
- if (LastFullState == null && state.FromSequence == GameTick.Zero && state.ToSequence >= LastFullStateRequested!.Value.Tick)
+ if (LastFullState == null && state.FromSequence == GameTick.Zero)
{
- LastFullState = state;
-
- if (Logging)
+ if (state.ToSequence >= LastFullStateRequested!.Value.Tick)
+ {
+ LastFullState = state;
_logger.Info($"Received Full GameState: to={state.ToSequence}, sz={state.PayloadSize}");
+ return true;
+ }
- return true;
+ _logger.Info($"Received a late full game state. Received: {state.ToSequence}. Requested: {LastFullStateRequested.Value.Tick}");
}
if (LastFullState != null && state.ToSequence <= LastFullState.ToSequence)
{
- if (Logging)
- _logger.Info($"While waiting for full, received late GameState with lower to={state.ToSequence} than the last full state={LastFullState.ToSequence}");
-
+ _logger.Info($"While waiting for full, received late GameState with lower to={state.ToSequence} than the last full state={LastFullState.ToSequence}");
return false;
}
diff --git a/Robust.Client/GameStates/NetEntityOverlay.cs b/Robust.Client/GameStates/NetEntityOverlay.cs
index ff527e7ebaf..e63c3e33af4 100644
--- a/Robust.Client/GameStates/NetEntityOverlay.cs
+++ b/Robust.Client/GameStates/NetEntityOverlay.cs
@@ -40,7 +40,7 @@ sealed class NetEntityOverlay : Overlay
public NetEntityOverlay()
{
IoCManager.InjectDependencies(this);
- var cache = IoCManager.Resolve();
+ var cache = IoCManager.Resolve();
_font = new VectorFont(cache.GetResource("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 10);
_lineHeight = _font.GetLineHeight(1);
diff --git a/Robust.Client/GameStates/NetGraphOverlay.cs b/Robust.Client/GameStates/NetGraphOverlay.cs
index 38bbce15217..26c559b9097 100644
--- a/Robust.Client/GameStates/NetGraphOverlay.cs
+++ b/Robust.Client/GameStates/NetGraphOverlay.cs
@@ -53,7 +53,7 @@ internal sealed class NetGraphOverlay : Overlay
public NetGraphOverlay()
{
IoCManager.InjectDependencies(this);
- var cache = IoCManager.Resolve();
+ var cache = IoCManager.Resolve();
_font = new VectorFont(cache.GetResource("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 10);
_gameStateManager.GameStateApplied += HandleGameStateApplied;
diff --git a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs
index 36b7a015418..1da177f6af7 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs
@@ -512,7 +512,7 @@ private void RenderViewport(Viewport viewport)
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowFOV, worldAABB, worldBounds);
}
- if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawFov)
+ if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawLight && eye.DrawFov)
{
ApplyFovToBuffer(viewport, eye);
}
diff --git a/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs b/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs
index f1cfdfdce19..1f0ae090dc7 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs
@@ -97,6 +97,9 @@ internal partial class Clyde
private (PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot)[] _lightsToRenderList = default!;
+ private LightCapacityComparer _lightCap = new();
+ private ShadowCapacityComparer _shadowCap = new ShadowCapacityComparer();
+
private unsafe void InitLighting()
{
@@ -332,7 +335,7 @@ private void FinalizeDepthDraw()
private void DrawLightsAndFov(Viewport viewport, Box2Rotated worldBounds, Box2 worldAABB, IEye eye)
{
- if (!_lightManager.Enabled)
+ if (!_lightManager.Enabled || !eye.DrawLight)
{
return;
}
@@ -570,6 +573,28 @@ private static bool LightQuery(ref (
return true;
}
+ private sealed class LightCapacityComparer : IComparer<(PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot)>
+ {
+ public int Compare(
+ (PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot) x,
+ (PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot) y)
+ {
+ if (x.light.CastShadows && !y.light.CastShadows) return 1;
+ if (!x.light.CastShadows && y.light.CastShadows) return -1;
+ return 0;
+ }
+ }
+
+ private sealed class ShadowCapacityComparer : IComparer<(PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot)>
+ {
+ public int Compare(
+ (PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot) x,
+ (PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot) y)
+ {
+ return x.distanceSquared.CompareTo(y.distanceSquared);
+ }
+ }
+
private (int count, Box2 expandedBounds) GetLightsToRender(
MapId map,
in Box2Rotated worldBounds,
@@ -595,20 +620,10 @@ private static bool LightQuery(ref (
// First, partition the array based on whether the lights are shadow casting or not
// (non shadow casting lights should be the first partition, shadow casting lights the second)
- Array.Sort(_lightsToRenderList, 0, state.count,
- Comparer<(PointLightComponent light, Vector2 pos, float distanceSquared)>.Create((x, y) =>
- {
- if (x.light.CastShadows && !y.light.CastShadows) return 1;
- else if (!x.light.CastShadows && y.light.CastShadows) return -1;
- else return 0;
- }));
+ Array.Sort(_lightsToRenderList, 0, state.count, _lightCap);
// Next, sort just the shadow casting lights by distance.
- Array.Sort(_lightsToRenderList, state.count - state.shadowCastingCount, state.shadowCastingCount,
- Comparer<(PointLightComponent light, Vector2 pos, float distanceSquared)>.Create((x, y) =>
- {
- return x.distanceSquared.CompareTo(y.distanceSquared);
- }));
+ Array.Sort(_lightsToRenderList, state.count - state.shadowCastingCount, state.shadowCastingCount, _shadowCap);
// Then effectively delete the furthest lights, by setting the end of the array to exclude N
// number of shadow casting lights (where N is the number above the max number per scene.)
diff --git a/Robust.Client/Graphics/Clyde/Clyde.cs b/Robust.Client/Graphics/Clyde/Clyde.cs
index 088faaf05c0..571d2f10908 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.cs
@@ -36,7 +36,7 @@ internal sealed partial class Clyde : IClydeInternal, IPostInjectInit, IEntityEv
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
- [Dependency] private readonly IClientResourceCache _resourceCache = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IResourceManager _resManager = default!;
[Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
diff --git a/Robust.Client/Graphics/Shaders/ShaderPrototype.cs b/Robust.Client/Graphics/Shaders/ShaderPrototype.cs
index 144f745795d..2ab6b323e1c 100644
--- a/Robust.Client/Graphics/Shaders/ShaderPrototype.cs
+++ b/Robust.Client/Graphics/Shaders/ShaderPrototype.cs
@@ -120,7 +120,7 @@ void ISerializationHooks.AfterDeserialization()
if (_path == null)
throw new InvalidOperationException("Source shaders must specify a source file.");
- _source = IoCManager.Resolve().GetResource(_path.Value);
+ _source = IoCManager.Resolve().GetResource(_path.Value);
if (_paramMapping != null)
{
@@ -142,7 +142,7 @@ void ISerializationHooks.AfterDeserialization()
case "canvas":
Kind = ShaderKind.Canvas;
- _source = IoCManager.Resolve().GetResource("/Shaders/Internal/default-sprite.swsl");
+ _source = IoCManager.Resolve().GetResource("/Shaders/Internal/default-sprite.swsl");
break;
default:
diff --git a/Robust.Client/Map/TileEdgeOverlay.cs b/Robust.Client/Map/TileEdgeOverlay.cs
index b27290d2a82..adc1d7be9d6 100644
--- a/Robust.Client/Map/TileEdgeOverlay.cs
+++ b/Robust.Client/Map/TileEdgeOverlay.cs
@@ -42,15 +42,20 @@ protected internal override void Draw(in OverlayDrawArgs args)
_grids.Clear();
_mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref _grids);
- var xformQuery = _entManager.GetEntityQuery();
+ var mapSystem = _entManager.System();
+ var xformSystem = _entManager.System();
+
foreach (var grid in _grids)
{
var tileSize = grid.Comp.TileSize;
var tileDimensions = new Vector2(tileSize, tileSize);
- var xform = xformQuery.GetComponent(grid);
- args.WorldHandle.SetTransform(xform.WorldMatrix);
+ var (_, _, worldMatrix, invMatrix) = xformSystem.GetWorldPositionRotationMatrixWithInv(grid.Owner);
+ args.WorldHandle.SetTransform(worldMatrix);
+ var localAABB = invMatrix.TransformBox(args.WorldBounds);
+
+ var enumerator = mapSystem.GetLocalTilesEnumerator(grid.Owner, grid.Comp, localAABB, false);
- foreach (var tileRef in grid.Comp.GetTilesIntersecting(args.WorldBounds, false))
+ while (enumerator.MoveNext(out var tileRef))
{
var tileDef = _tileDefManager[tileRef.Tile.TypeId];
@@ -66,7 +71,7 @@ protected internal override void Draw(in OverlayDrawArgs args)
continue;
var neighborIndices = new Vector2i(tileRef.GridIndices.X + x, tileRef.GridIndices.Y + y);
- var neighborTile = grid.Comp.GetTileRef(neighborIndices);
+ var neighborTile = mapSystem.GetTileRef(grid.Owner, grid.Comp, neighborIndices);
var neighborDef = _tileDefManager[neighborTile.Tile.TypeId];
// If it's the same tile then no edge to be drawn.
@@ -118,9 +123,9 @@ protected internal override void Draw(in OverlayDrawArgs args)
}
if (angle == Angle.Zero)
- args.WorldHandle.DrawTextureRect(texture, box);
+ args.WorldHandle.DrawTextureRect(texture.Texture, box);
else
- args.WorldHandle.DrawTextureRect(texture, new Box2Rotated(box, angle, box.Center));
+ args.WorldHandle.DrawTextureRect(texture.Texture, new Box2Rotated(box, angle, box.Center));
}
}
}
diff --git a/Robust.Client/Physics/PhysicsSystem.Predict.cs b/Robust.Client/Physics/PhysicsSystem.Predict.cs
index c47c15b4863..5a05bc3da97 100644
--- a/Robust.Client/Physics/PhysicsSystem.Predict.cs
+++ b/Robust.Client/Physics/PhysicsSystem.Predict.cs
@@ -1,6 +1,5 @@
using System.Buffers;
using System.Collections.Generic;
-using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
@@ -8,6 +7,7 @@
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Dynamics.Contacts;
using Robust.Shared.Physics.Systems;
+using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Robust.Client.Physics;
diff --git a/Robust.Client/Placement/PlacementManager.cs b/Robust.Client/Placement/PlacementManager.cs
index f68adc236de..c83576138e1 100644
--- a/Robust.Client/Placement/PlacementManager.cs
+++ b/Robust.Client/Placement/PlacementManager.cs
@@ -28,7 +28,7 @@ public sealed partial class PlacementManager : IPlacementManager, IDisposable, I
{
[Dependency] private readonly IClientNetManager _networkManager = default!;
[Dependency] internal readonly IPlayerManager PlayerManager = default!;
- [Dependency] internal readonly IClientResourceCache ResourceCache = default!;
+ [Dependency] internal readonly IResourceCache ResourceCache = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] internal readonly IMapManager MapManager = default!;
[Dependency] private readonly IGameTiming _time = default!;
diff --git a/Robust.Client/Player/IPlayerManager.cs b/Robust.Client/Player/IPlayerManager.cs
index 20478543a74..cb3b2cc0052 100644
--- a/Robust.Client/Player/IPlayerManager.cs
+++ b/Robust.Client/Player/IPlayerManager.cs
@@ -15,15 +15,22 @@ public interface IPlayerManager : ISharedPlayerManager
event Action? PlayerListUpdated;
///
- /// Invoked when gets attached to a new entity. See also
+ /// Invoked when gets attached to a new entity, or when the local
+ /// session gets updated. See also
///
event Action? LocalPlayerAttached;
///
- /// Invoked when gets detached from new entity. See also
+ /// Invoked when gets detached from an entity, or when the local
+ /// session gets updated. See also
///
event Action? LocalPlayerDetached;
+ ///
+ /// Invoked whenever changes.
+ ///
+ event Action<(ICommonSession? Old, ICommonSession? New)>? LocalSessionChanged;
+
void ApplyPlayerStates(IReadOnlyCollection list);
///
@@ -38,34 +45,8 @@ public interface IPlayerManager : ISharedPlayerManager
///
void SetupMultiplayer(INetChannel channel);
+ void SetLocalSession(ICommonSession session);
+
[Obsolete("Use LocalSession instead")]
LocalPlayer? LocalPlayer { get;}
}
-
-///
-/// ECS event that gets raised when the local player gets attached to a new entity. The event is both broadcast and
-/// raised directed at the new entity.
-///
-public sealed class LocalPlayerAttachedEvent : EntityEventArgs
-{
- public LocalPlayerAttachedEvent(EntityUid entity)
- {
- Entity = entity;
- }
-
- public EntityUid Entity { get; }
-}
-
-///
-/// ECS event that gets raised when the local player gets detached from an entity. The event is both broadcast and
-/// raised directed at the new entity.
-///
-public sealed class LocalPlayerDetachedEvent : EntityEventArgs
-{
- public LocalPlayerDetachedEvent(EntityUid entity)
- {
- Entity = entity;
- }
-
- public EntityUid Entity { get; }
-}
\ No newline at end of file
diff --git a/Robust.Client/Player/PlayerManager.cs b/Robust.Client/Player/PlayerManager.cs
index 7c5557dcdd0..682b433ce06 100644
--- a/Robust.Client/Player/PlayerManager.cs
+++ b/Robust.Client/Player/PlayerManager.cs
@@ -42,12 +42,13 @@ public override ICommonSession[] NetworkedSessions
///
public override int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? -1;
- public LocalPlayer? LocalPlayer { get; set; }
+ public LocalPlayer? LocalPlayer { get; private set; }
public event Action? LocalStatusChanged;
public event Action? PlayerListUpdated;
public event Action? LocalPlayerDetached;
public event Action? LocalPlayerAttached;
+ public event Action<(ICommonSession? Old, ICommonSession? New)>? LocalSessionChanged;
///
public override void Initialize(int maxPlayers)
@@ -64,22 +65,14 @@ private void StatusChanged(object? sender, SessionStatusEventArgs e)
LocalStatusChanged?.Invoke(e);
}
- ///
- public override void Startup()
- {
- if (LocalSession == null)
- throw new InvalidOperationException("LocalSession cannot be null");
-
- LocalPlayer = new LocalPlayer(LocalSession);
- base.Startup();
- }
-
public void SetupSinglePlayer(string name)
{
if (LocalSession != null)
throw new InvalidOperationException($"Player manager already running?");
- LocalSession = CreateAndAddSession(default, name);
+ var session = CreateAndAddSession(default, name);
+ session.ClientSide = true;
+ SetLocalSession(session);
Startup();
PlayerListUpdated?.Invoke();
}
@@ -89,18 +82,44 @@ public void SetupMultiplayer(INetChannel channel)
if (LocalSession != null)
throw new InvalidOperationException($"Player manager already running?");
- var session = CreateAndAddSession(channel.UserId, channel.UserName);
- session.Channel = channel;
- LocalSession = session;
+ SetLocalSession(CreateAndAddSession(channel));
Startup();
_network.ClientSendMessage(new MsgPlayerListReq());
}
+ public void SetLocalSession(ICommonSession? session)
+ {
+ if (session == LocalSession)
+ return;
+
+ var old = LocalSession;
+
+ if (old?.AttachedEntity is {} oldUid)
+ {
+ LocalSession = null;
+ LocalPlayer = null;
+ Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(oldUid)}.");
+ EntManager.EventBus.RaiseLocalEvent(oldUid, new LocalPlayerDetachedEvent(oldUid), true);
+ LocalPlayerDetached?.Invoke(oldUid);
+ }
+
+ LocalSession = session;
+ LocalPlayer = session == null ? null : new LocalPlayer(session);
+ Sawmill.Info($"Changing local session from {old?.ToString() ?? "null"} to {session?.ToString() ?? "null"}.");
+ LocalSessionChanged?.Invoke((old, LocalSession));
+
+ if (session?.AttachedEntity is {} newUid)
+ {
+ Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(newUid)}.");
+ EntManager.EventBus.RaiseLocalEvent(newUid, new LocalPlayerAttachedEvent(newUid), true);
+ LocalPlayerAttached?.Invoke(newUid);
+ }
+ }
+
///
public override void Shutdown()
{
- if (LocalSession != null)
- SetAttachedEntity(LocalSession, null);
+ SetAttachedEntity(LocalSession, null, out _);
LocalPlayer = null;
LocalSession = null;
_pendingStates.Clear();
@@ -108,16 +127,21 @@ public override void Shutdown()
PlayerListUpdated?.Invoke();
}
- public override void SetAttachedEntity(ICommonSession session, EntityUid? uid)
+ public override bool SetAttachedEntity(ICommonSession? session, EntityUid? uid, out ICommonSession? kicked, bool force = false)
{
+ kicked = null;
+ if (session == null)
+ return false;
+
if (session.AttachedEntity == uid)
- return;
+ return true;
var old = session.AttachedEntity;
- base.SetAttachedEntity(session, uid);
+ if (!base.SetAttachedEntity(session, uid, out kicked, force))
+ return false;
if (session != LocalSession)
- return;
+ return true;
if (old.HasValue)
{
@@ -129,13 +153,13 @@ public override void SetAttachedEntity(ICommonSession session, EntityUid? uid)
if (uid == null)
{
Sawmill.Info($"Local player is no longer attached to any entity.");
- return;
+ return true;
}
if (!EntManager.EntityExists(uid))
{
Sawmill.Error($"Attempted to attach player to non-existent entity {uid}!");
- return;
+ return true;
}
if (!EntManager.EnsureComponent(uid.Value, out EyeComponent eye))
@@ -148,6 +172,7 @@ public override void SetAttachedEntity(ICommonSession session, EntityUid? uid)
Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(uid)}.");
EntManager.EventBus.RaiseLocalEvent(uid.Value, new LocalPlayerAttachedEvent(uid.Value), true);
LocalPlayerAttached?.Invoke(uid.Value);
+ return true;
}
public void ApplyPlayerStates(IReadOnlyCollection list)
@@ -193,7 +218,7 @@ private bool ApplyStates(IReadOnlyCollection list, bool fullList)
_pendingStates.Remove(state.UserId);
}
- SetAttachedEntity(LocalSession, uid);
+ SetAttachedEntity(LocalSession, uid, out _, true);
SetStatus(LocalSession, state.Status);
}
@@ -233,11 +258,10 @@ private bool UpdatePlayerList(IEnumerable remotePlayers, bool full
{
// This is a new userid, so we create a new session.
DebugTools.Assert(state.UserId != LocalPlayer?.UserId);
- var newSession = CreateAndAddSession(state.UserId, state.Name);
+ var newSession = (CommonSession) CreateAndAddSession(state.UserId, state.Name);
newSession.Ping = state.Ping;
- newSession.Name = state.Name;
SetStatus(newSession, state.Status);
- SetAttachedEntity(newSession, controlled);
+ SetAttachedEntity(newSession, controlled, out _, true);
dirty = true;
continue;
}
@@ -256,7 +280,7 @@ private bool UpdatePlayerList(IEnumerable remotePlayers, bool full
local.Name = state.Name;
local.Ping = state.Ping;
SetStatus(local, state.Status);
- SetAttachedEntity(local, controlled);
+ SetAttachedEntity(local, controlled, out _, true);
}
// Remove old users. This only works if the provided state is a list of all players
@@ -264,10 +288,12 @@ private bool UpdatePlayerList(IEnumerable remotePlayers, bool full
{
foreach (var oldUser in InternalSessions.Keys.ToArray())
{
- // clear slot, player left
if (users.Contains(oldUser))
continue;
+ if (InternalSessions[oldUser].ClientSide)
+ continue;
+
DebugTools.Assert(oldUser != LocalUser
|| LocalUser == null
|| LocalUser == default(NetUserId),
diff --git a/Robust.Client/Profiling/LiveProfileViewControl.cs b/Robust.Client/Profiling/LiveProfileViewControl.cs
index 5154b543400..01e1a67d076 100644
--- a/Robust.Client/Profiling/LiveProfileViewControl.cs
+++ b/Robust.Client/Profiling/LiveProfileViewControl.cs
@@ -13,7 +13,7 @@ namespace Robust.Client.Profiling;
public sealed class LiveProfileViewControl : Control
{
[Dependency] private readonly ProfManager _profManager = default!;
- [Dependency] private readonly IClientResourceCache _resourceCache = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
public int MaxDepth { get; set; } = 2;
diff --git a/Robust.Client/Replays/Playback/IReplayPlaybackManager.cs b/Robust.Client/Replays/Playback/IReplayPlaybackManager.cs
index 3073bc754a7..3793f81403b 100644
--- a/Robust.Client/Replays/Playback/IReplayPlaybackManager.cs
+++ b/Robust.Client/Replays/Playback/IReplayPlaybackManager.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Replays;
using Robust.Shared.Serialization.Markdown.Mapping;
@@ -128,6 +129,11 @@ public interface IReplayPlaybackManager
///
event Action? ReplayUnpaused;
+ ///
+ /// Invoked just before a replay applies a game state.
+ ///
+ event Action<(GameState Current, GameState? Next)>? BeforeApplyState;
+
///
/// If currently replaying a client-side recording, this is the user that recorded the replay.
/// Useful for setting default observer spawn positions.
@@ -137,5 +143,5 @@ public interface IReplayPlaybackManager
///
/// Fetches the entity that the is currently attached to.
///
- public bool TryGetRecorderEntity([NotNullWhen(true)] out EntityUid? uid);
+ bool TryGetRecorderEntity([NotNullWhen(true)] out EntityUid? uid);
}
diff --git a/Robust.Client/Replays/Playback/ReplayPlaybackManager.Checkpoint.cs b/Robust.Client/Replays/Playback/ReplayPlaybackManager.Checkpoint.cs
index cfd9f77e0a8..adc8ed1248f 100644
--- a/Robust.Client/Replays/Playback/ReplayPlaybackManager.Checkpoint.cs
+++ b/Robust.Client/Replays/Playback/ReplayPlaybackManager.Checkpoint.cs
@@ -66,6 +66,7 @@ private void ApplyCheckpointState(CheckpointState checkpoint, ReplayData replay)
_gameState.ClearDetachQueue();
EnsureDetachedExist(checkpoint);
_gameState.DetachImmediate(checkpoint.Detached);
+ BeforeApplyState?.Invoke((checkpoint.State, next));
_gameState.ApplyGameState(checkpoint.State, next);
}
diff --git a/Robust.Client/Replays/Playback/ReplayPlaybackManager.Time.cs b/Robust.Client/Replays/Playback/ReplayPlaybackManager.Time.cs
index f19d66a7328..9643ae1ac1a 100644
--- a/Robust.Client/Replays/Playback/ReplayPlaybackManager.Time.cs
+++ b/Robust.Client/Replays/Playback/ReplayPlaybackManager.Time.cs
@@ -1,4 +1,5 @@
using System;
+using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Shared.Utility;
@@ -58,7 +59,13 @@ public void SetIndex(int value, bool pausePlayback = true)
_timing.LastRealTick = _timing.LastProcessedTick = _timing.CurTick = Replay.CurTick;
_gameState.UpdateFullRep(state, cloneDelta: true);
- _gameState.ApplyGameState(state, Replay.NextState);
+
+ // Clear existing lerps
+ _entMan.EntitySysManager.GetEntitySystem().Reset();
+
+ var next = Replay.NextState;
+ BeforeApplyState?.Invoke((state, next));
+ _gameState.ApplyGameState(state, next);
ProcessMessages(Replay.CurMessages, skipEffectEvents);
// TODO REPLAYS block audio
diff --git a/Robust.Client/Replays/Playback/ReplayPlaybackManager.Update.cs b/Robust.Client/Replays/Playback/ReplayPlaybackManager.Update.cs
index b14a36b3669..6fb60f36e54 100644
--- a/Robust.Client/Replays/Playback/ReplayPlaybackManager.Update.cs
+++ b/Robust.Client/Replays/Playback/ReplayPlaybackManager.Update.cs
@@ -42,7 +42,9 @@ private void TickUpdateOverride(FrameEventArgs args)
{
var state = Replay.CurState;
_gameState.UpdateFullRep(state, cloneDelta: true);
- _gameState.ApplyGameState(state, Replay.NextState);
+ var next = Replay.NextState;
+ BeforeApplyState?.Invoke((state, next));
+ _gameState.ApplyGameState(state, next);
DebugTools.Assert(Replay.LastApplied >= state.FromSequence);
DebugTools.Assert(Replay.LastApplied + 1 <= state.ToSequence);
Replay.LastApplied = state.ToSequence;
diff --git a/Robust.Client/Replays/Playback/ReplayPlaybackManager.cs b/Robust.Client/Replays/Playback/ReplayPlaybackManager.cs
index 0b3c7d1d54e..4054efe141b 100644
--- a/Robust.Client/Replays/Playback/ReplayPlaybackManager.cs
+++ b/Robust.Client/Replays/Playback/ReplayPlaybackManager.cs
@@ -14,6 +14,7 @@
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
@@ -46,6 +47,7 @@ internal sealed partial class ReplayPlaybackManager : IReplayPlaybackManager
public event Action? ReplayPlaybackStopped;
public event Action? ReplayPaused;
public event Action? ReplayUnpaused;
+ public event Action<(GameState Current, GameState? Next)>? BeforeApplyState;
public ReplayData? Replay { get; private set; }
public NetUserId? Recorder => Replay?.Recorder;
diff --git a/Robust.Shared/ResourceManagement/BaseResource.cs b/Robust.Client/ResourceManagement/BaseResource.cs
similarity index 91%
rename from Robust.Shared/ResourceManagement/BaseResource.cs
rename to Robust.Client/ResourceManagement/BaseResource.cs
index e400f6cf03a..4bc1b30fb8b 100644
--- a/Robust.Shared/ResourceManagement/BaseResource.cs
+++ b/Robust.Client/ResourceManagement/BaseResource.cs
@@ -1,10 +1,9 @@
using System;
using System.Threading;
-using Robust.Shared.Audio;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
-namespace Robust.Shared.ResourceManagement;
+namespace Robust.Client.ResourceManagement;
///
/// Base resource for the cache.
diff --git a/Robust.Client/ResourceManagement/IClientResourceCache.cs b/Robust.Client/ResourceManagement/IClientResourceCache.cs
deleted file mode 100644
index ba14ee5806d..00000000000
--- a/Robust.Client/ResourceManagement/IClientResourceCache.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System;
-using Robust.Client.Graphics;
-using Robust.Shared.Audio;
-using Robust.Shared.GameObjects;
-
-namespace Robust.Client.ResourceManagement;
-
-///
-public interface IClientResourceCache : IResourceCache
-{
- // Resource load callbacks so content can hook stuff like click maps.
- event Action OnRawTextureLoaded;
- event Action OnRsiLoaded;
-
- IClyde Clyde { get; }
- IFontManager FontManager { get; }
-}
diff --git a/Robust.Shared/GameObjects/IResourceCache.cs b/Robust.Client/ResourceManagement/IResourceCache.cs
similarity index 72%
rename from Robust.Shared/GameObjects/IResourceCache.cs
rename to Robust.Client/ResourceManagement/IResourceCache.cs
index d3431b0b83e..a33daf48cfe 100644
--- a/Robust.Shared/GameObjects/IResourceCache.cs
+++ b/Robust.Client/ResourceManagement/IResourceCache.cs
@@ -1,14 +1,16 @@
+using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
-using Robust.Shared.ResourceManagement;
+using Robust.Client.Graphics;
+using Robust.Shared.ContentPack;
using Robust.Shared.Utility;
-namespace Robust.Shared.GameObjects;
+namespace Robust.Client.ResourceManagement;
///
/// Handles caching of
///
-public interface IResourceCache
+public interface IResourceCache : IResourceManager
{
T GetResource(string path, bool useFallback = true)
where T : BaseResource, new();
@@ -38,5 +40,12 @@ T GetFallback()
where T : BaseResource, new();
IEnumerable> GetAllResources() where T : BaseResource, new();
+
+ // Resource load callbacks so content can hook stuff like click maps.
+ event Action OnRawTextureLoaded;
+ event Action OnRsiLoaded;
+
+ IClyde Clyde { get; }
+ IFontManager FontManager { get; }
}
diff --git a/Robust.Client/ResourceManagement/IClientResourceCacheInternal.cs b/Robust.Client/ResourceManagement/IResourceCacheInternal.cs
similarity index 84%
rename from Robust.Client/ResourceManagement/IClientResourceCacheInternal.cs
rename to Robust.Client/ResourceManagement/IResourceCacheInternal.cs
index a9e5c8e0510..7d74ec77ca3 100644
--- a/Robust.Client/ResourceManagement/IClientResourceCacheInternal.cs
+++ b/Robust.Client/ResourceManagement/IResourceCacheInternal.cs
@@ -5,7 +5,7 @@
namespace Robust.Client.ResourceManagement;
///
-internal interface IClientResourceCacheInternal : IClientResourceCache
+internal interface IResourceCacheInternal : IResourceCache
{
void TextureLoaded(TextureLoadedEventArgs eventArgs);
void RsiLoaded(RsiLoadedEventArgs eventArgs);
diff --git a/Robust.Client/ResourceManagement/ResourceCache.cs b/Robust.Client/ResourceManagement/ResourceCache.cs
index 94f0fe9f482..2bd296f4cfd 100644
--- a/Robust.Client/ResourceManagement/ResourceCache.cs
+++ b/Robust.Client/ResourceManagement/ResourceCache.cs
@@ -1,16 +1,212 @@
using System;
-using Robust.Shared.ResourceManagement;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Robust.Shared.ContentPack;
+using Robust.Shared.IoC;
+using Robust.Shared.Log;
+using Robust.Shared.Utility;
namespace Robust.Client.ResourceManagement;
///
/// Handles caching of
///
-internal sealed partial class ResourceCache : SharedResourceCache, IClientResourceCacheInternal, IDisposable
+internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInternal, IDisposable
{
+ private readonly Dictionary> _cachedResources =
+ new();
+
+ private readonly Dictionary _fallbacks = new();
+
+ public T GetResource(string path, bool useFallback = true) where T : BaseResource, new()
+ {
+ return GetResource(new ResPath(path), useFallback);
+ }
+
+ public T GetResource(ResPath path, bool useFallback = true) where T : BaseResource, new()
+ {
+ var cache = GetTypeDict();
+ if (cache.TryGetValue(path, out var cached))
+ {
+ return (T) cached;
+ }
+
+ var resource = new T();
+ try
+ {
+ var dependencies = IoCManager.Instance!;
+ resource.Load(dependencies, path);
+ cache[path] = resource;
+ return resource;
+ }
+ catch (Exception e)
+ {
+ if (useFallback && resource.Fallback != null)
+ {
+ Logger.Error(
+ $"Exception while loading resource {typeof(T)} at '{path}', resorting to fallback.\n{Environment.StackTrace}\n{e}");
+ return GetResource(resource.Fallback.Value, false);
+ }
+ else
+ {
+ Logger.Error(
+ $"Exception while loading resource {typeof(T)} at '{path}', no fallback available\n{Environment.StackTrace}\n{e}");
+ throw;
+ }
+ }
+ }
+
+ public bool TryGetResource(string path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new()
+ {
+ return TryGetResource(new ResPath(path), out resource);
+ }
+
+ public bool TryGetResource(ResPath path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new()
+ {
+ var cache = GetTypeDict();
+ if (cache.TryGetValue(path, out var cached))
+ {
+ resource = (T) cached;
+ return true;
+ }
+
+ var _resource = new T();
+ try
+ {
+ var dependencies = IoCManager.Instance!;
+ _resource.Load(dependencies, path);
+ resource = _resource;
+ cache[path] = resource;
+ return true;
+ }
+ catch
+ {
+ resource = null;
+ return false;
+ }
+ }
+
+ public void ReloadResource(string path) where T : BaseResource, new()
+ {
+ ReloadResource(new ResPath(path));
+ }
+
+ public void ReloadResource(ResPath path) where T : BaseResource, new()
+ {
+ var cache = GetTypeDict();
+
+ if (!cache.TryGetValue(path, out var res))
+ {
+ return;
+ }
+
+ try
+ {
+ var dependencies = IoCManager.Instance!;
+ res.Reload(dependencies, path);
+ }
+ catch (Exception e)
+ {
+ Logger.Error($"Exception while reloading resource {typeof(T)} at '{path}'\n{e}");
+ throw;
+ }
+ }
+
+ public bool HasResource(string path) where T : BaseResource, new()
+ {
+ return HasResource(new ResPath(path));
+ }
+
+ public bool HasResource(ResPath path) where T : BaseResource, new()
+ {
+ return TryGetResource(path, out var _);
+ }
+
+ public void CacheResource(string path, T resource) where T : BaseResource, new()
+ {
+ CacheResource(new ResPath(path), resource);
+ }
+
+ public void CacheResource(ResPath path, T resource) where T : BaseResource, new()
+ {
+ GetTypeDict()[path] = resource;
+ }
+
+ public T GetFallback() where T : BaseResource, new()
+ {
+ if (_fallbacks.TryGetValue(typeof(T), out var fallback))
+ {
+ return (T) fallback;
+ }
+
+ var res = new T();
+ if (res.Fallback == null)
+ {
+ throw new InvalidOperationException($"Resource of type '{typeof(T)}' has no fallback.");
+ }
+
+ fallback = GetResource(res.Fallback.Value, useFallback: false);
+ _fallbacks.Add(typeof(T), fallback);
+ return (T) fallback;
+ }
+
+ public IEnumerable> GetAllResources() where T : BaseResource, new()
+ {
+ return GetTypeDict().Select(p => new KeyValuePair(p.Key, (T) p.Value));
+ }
+
public event Action? OnRawTextureLoaded;
public event Action? OnRsiLoaded;
+ #region IDisposable Members
+
+ private bool disposed = false;
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (disposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ foreach (var res in _cachedResources.Values.SelectMany(dict => dict.Values))
+ {
+ res.Dispose();
+ }
+ }
+
+ disposed = true;
+ }
+
+ ~ResourceCache()
+ {
+ Dispose(false);
+ }
+
+ #endregion IDisposable Members
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected Dictionary GetTypeDict()
+ {
+ if (!_cachedResources.TryGetValue(typeof(T), out var ret))
+ {
+ ret = new Dictionary();
+ _cachedResources.Add(typeof(T), ret);
+ }
+
+ return ret;
+ }
+
public void TextureLoaded(TextureLoadedEventArgs eventArgs)
{
OnRawTextureLoaded?.Invoke(eventArgs);
diff --git a/Robust.Shared/ResourceManagement/ResourceTypes/AudioResource.cs b/Robust.Client/ResourceManagement/ResourceTypes/AudioResource.cs
similarity index 77%
rename from Robust.Shared/ResourceManagement/ResourceTypes/AudioResource.cs
rename to Robust.Client/ResourceManagement/ResourceTypes/AudioResource.cs
index b04ddfd6b37..c80665e9810 100644
--- a/Robust.Shared/ResourceManagement/ResourceTypes/AudioResource.cs
+++ b/Robust.Client/ResourceManagement/ResourceTypes/AudioResource.cs
@@ -1,11 +1,12 @@
using System;
using System.IO;
+using Robust.Client.Audio;
using Robust.Shared.Audio;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
-namespace Robust.Shared.ResourceManagement.ResourceTypes;
+namespace Robust.Client.ResourceManagement;
public sealed class AudioResource : BaseResource
{
@@ -22,13 +23,14 @@ public override void Load(IDependencyCollection dependencies, ResPath path)
using (var fileStream = cache.ContentFileRead(path))
{
+ var audioManager = dependencies.Resolve();
if (path.Extension == "ogg")
{
- AudioStream = dependencies.Resolve().LoadAudioOggVorbis(fileStream, path.ToString());
+ AudioStream = audioManager.LoadAudioOggVorbis(fileStream, path.ToString());
}
else if (path.Extension == "wav")
{
- AudioStream = dependencies.Resolve().LoadAudioWav(fileStream, path.ToString());
+ AudioStream = audioManager.LoadAudioWav(fileStream, path.ToString());
}
else
{
diff --git a/Robust.Client/ResourceManagement/ResourceTypes/FontResource.cs b/Robust.Client/ResourceManagement/ResourceTypes/FontResource.cs
index 8c7f3994948..2c3cd6dcc05 100644
--- a/Robust.Client/ResourceManagement/ResourceTypes/FontResource.cs
+++ b/Robust.Client/ResourceManagement/ResourceTypes/FontResource.cs
@@ -2,7 +2,6 @@
using Robust.Client.Graphics;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
-using Robust.Shared.ResourceManagement;
using Robust.Shared.Utility;
namespace Robust.Client.ResourceManagement
diff --git a/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs b/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs
index 24e213dc527..f207b583288 100644
--- a/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs
+++ b/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs
@@ -8,7 +8,6 @@
using Robust.Shared.Graphics.RSI;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
-using Robust.Shared.ResourceManagement;
using Robust.Shared.Resources;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
@@ -47,7 +46,7 @@ public override void Load(IDependencyCollection dependencies, ResPath path)
loadStepData.Path.ToString());
LoadPostTexture(loadStepData);
- LoadFinish(dependencies.Resolve(), loadStepData);
+ LoadFinish(dependencies.Resolve(), loadStepData);
loadStepData.AtlasSheet.Dispose();
}
@@ -215,7 +214,7 @@ internal static void LoadPostTexture(LoadStepData data)
}
}
- internal void LoadFinish(IClientResourceCacheInternal cache, LoadStepData data)
+ internal void LoadFinish(IResourceCacheInternal cache, LoadStepData data)
{
RSI = data.Rsi;
cache.RsiLoaded(new RsiLoadedEventArgs(data.Path, this, data.AtlasSheet, data.CallbackOffsets));
diff --git a/Robust.Client/ResourceManagement/ResourceTypes/ShaderSourceResource.cs b/Robust.Client/ResourceManagement/ResourceTypes/ShaderSourceResource.cs
index deae43661c6..348f346777f 100644
--- a/Robust.Client/ResourceManagement/ResourceTypes/ShaderSourceResource.cs
+++ b/Robust.Client/ResourceManagement/ResourceTypes/ShaderSourceResource.cs
@@ -4,7 +4,6 @@
using Robust.Client.Graphics.Clyde;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
-using Robust.Shared.ResourceManagement;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
diff --git a/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs b/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs
index c375e2d5f08..69dc1147524 100644
--- a/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs
+++ b/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs
@@ -6,7 +6,6 @@
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
-using Robust.Shared.ResourceManagement;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
@@ -35,7 +34,7 @@ public override void Load(IDependencyCollection dependencies, ResPath path)
LoadPreTexture(dependencies.Resolve(), data);
LoadTexture(dependencies.Resolve(), data);
- LoadFinish(dependencies.Resolve(), data);
+ LoadFinish(dependencies.Resolve(), data);
}
internal static void LoadPreTexture(IResourceManager cache, LoadStepData data)
@@ -53,11 +52,11 @@ internal static void LoadTexture(IClyde clyde, LoadStepData data)
data.Texture = clyde.LoadTextureFromImage(data.Image, data.Path.ToString(), data.LoadParameters);
}
- internal void LoadFinish(IClientResourceCache cache, LoadStepData data)
+ internal void LoadFinish(IResourceCache cache, LoadStepData data)
{
_texture = data.Texture;
- if (cache is IClientResourceCacheInternal cacheInternal)
+ if (cache is IResourceCacheInternal cacheInternal)
{
cacheInternal.TextureLoaded(new TextureLoadedEventArgs(data.Path, data.Image, this));
}
diff --git a/Robust.Client/Serialization/ClientSpriteSpecifierSerializer.cs b/Robust.Client/Serialization/ClientSpriteSpecifierSerializer.cs
index 08ee5d3b882..aea3cc09bde 100644
--- a/Robust.Client/Serialization/ClientSpriteSpecifierSerializer.cs
+++ b/Robust.Client/Serialization/ClientSpriteSpecifierSerializer.cs
@@ -27,7 +27,7 @@ public override ValidationNode ValidateRsi(ISerializationManager serializationMa
return new ErrorNode(node, "Sprite specifier has missing/invalid state node");
}
- var res = dependencies.Resolve();
+ var res = dependencies.Resolve();
var rsiPath = TextureRoot / valuePathNode.Value;
if (!res.TryGetResource(rsiPath, out RSIResource? resource))
{
diff --git a/Robust.Client/UserInterface/Controllers/Implementations/EntitySpawningUIController.cs b/Robust.Client/UserInterface/Controllers/Implementations/EntitySpawningUIController.cs
index b687e94dc4b..dda465d5369 100644
--- a/Robust.Client/UserInterface/Controllers/Implementations/EntitySpawningUIController.cs
+++ b/Robust.Client/UserInterface/Controllers/Implementations/EntitySpawningUIController.cs
@@ -21,7 +21,7 @@ public sealed class EntitySpawningUIController : UIController
{
[Dependency] private readonly IPlacementManager _placement = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
- [Dependency] private readonly IClientResourceCache _resources = default!;
+ [Dependency] private readonly IResourceCache _resources = default!;
private EntitySpawnWindow? _window;
private readonly List _shownEntities = new();
diff --git a/Robust.Client/UserInterface/Controllers/Implementations/TileSpawningUIController.cs b/Robust.Client/UserInterface/Controllers/Implementations/TileSpawningUIController.cs
index 445344d6042..104f20df466 100644
--- a/Robust.Client/UserInterface/Controllers/Implementations/TileSpawningUIController.cs
+++ b/Robust.Client/UserInterface/Controllers/Implementations/TileSpawningUIController.cs
@@ -19,7 +19,7 @@ namespace Robust.Client.UserInterface.Controllers.Implementations;
public sealed class TileSpawningUIController : UIController
{
[Dependency] private readonly IPlacementManager _placement = default!;
- [Dependency] private readonly IClientResourceCache _resources = default!;
+ [Dependency] private readonly IResourceCache _resources = default!;
[Dependency] private readonly ITileDefinitionManager _tiles = default!;
private TileSpawnWindow? _window;
diff --git a/Robust.Client/UserInterface/Controls/RichTextLabel.cs b/Robust.Client/UserInterface/Controls/RichTextLabel.cs
index 8d406382272..8836b42e3c0 100644
--- a/Robust.Client/UserInterface/Controls/RichTextLabel.cs
+++ b/Robust.Client/UserInterface/Controls/RichTextLabel.cs
@@ -20,6 +20,7 @@ public class RichTextLabel : Control
public RichTextLabel()
{
IoCManager.InjectDependencies(this);
+ VerticalAlignment = VAlignment.Center;
}
public void SetMessage(FormattedMessage message, Type[]? tagsAllowed = null, Color? defaultColor = null)
diff --git a/Robust.Client/UserInterface/Controls/TextureRect.cs b/Robust.Client/UserInterface/Controls/TextureRect.cs
index f982a16b898..34e21137a3e 100644
--- a/Robust.Client/UserInterface/Controls/TextureRect.cs
+++ b/Robust.Client/UserInterface/Controls/TextureRect.cs
@@ -48,7 +48,7 @@ public string TexturePath
{
set
{
- Texture = IoCManager.Resolve().GetResource(value);
+ Texture = IoCManager.Resolve().GetResource(value);
_texturePath = value;
}
diff --git a/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugCoordsPanel.cs b/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugCoordsPanel.cs
index 5e1783ccc98..a8415381918 100644
--- a/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugCoordsPanel.cs
+++ b/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugCoordsPanel.cs
@@ -70,6 +70,7 @@ protected override void FrameUpdate(FrameEventArgs args)
return;
var mapSystem = _entityManager.System();
+ var xformSystem = _entityManager.System();
if (_mapManager.TryFindGridAt(mouseWorldMap, out var mouseGridUid, out var mouseGrid))
{
@@ -80,7 +81,7 @@ protected override void FrameUpdate(FrameEventArgs args)
{
mouseGridPos = new EntityCoordinates(_mapManager.GetMapEntityId(mouseWorldMap.MapId),
mouseWorldMap.Position);
- tile = new TileRef(EntityUid.Invalid, mouseGridPos.ToVector2i(_entityManager, _mapManager), Tile.Empty);
+ tile = new TileRef(EntityUid.Invalid, mouseGridPos.ToVector2i(_entityManager, _mapManager, xformSystem), Tile.Empty);
}
var controlHovered = UserInterfaceManager.CurrentlyHovered;
@@ -90,35 +91,35 @@ protected override void FrameUpdate(FrameEventArgs args)
Mouse Pos:
Screen: {mouseScreenPos}
{mouseWorldMap}
- {mouseGridPos}
+ {_entityManager.GetNetCoordinates(mouseGridPos)}
{tile}
GUI: {controlHovered}");
- _textBuilder.AppendLine("\nAttached Entity:");
- var controlledEntity = _playerManager?.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
+ _textBuilder.AppendLine("\nAttached NetEntity:");
+ var controlledEntity = _playerManager.LocalSession?.AttachedEntity ?? EntityUid.Invalid;
+
if (controlledEntity == EntityUid.Invalid)
{
- _textBuilder.AppendLine("No attached entity.");
+ _textBuilder.AppendLine("No attached netentity.");
}
else
{
var entityTransform = _entityManager.GetComponent(controlledEntity);
- var playerWorldOffset = entityTransform.MapPosition;
+ var playerWorldOffset = xformSystem.GetMapCoordinates(entityTransform);
var playerScreen = _eyeManager.WorldToScreen(playerWorldOffset.Position);
var playerCoordinates = entityTransform.Coordinates;
- var playerRotation = entityTransform.WorldRotation;
+ var playerRotation = xformSystem.GetWorldRotation(entityTransform);
var gridRotation = entityTransform.GridUid != null
- ? _entityManager.GetComponent(entityTransform.GridUid.Value)
- .WorldRotation
+ ? xformSystem.GetWorldRotation(entityTransform.GridUid.Value)
: Angle.Zero;
_textBuilder.Append($@" Screen: {playerScreen}
{playerWorldOffset}
- {playerCoordinates}
+ {_entityManager.GetNetCoordinates(playerCoordinates)}
Rotation: {playerRotation.Degrees:F2}°
- EntId: {controlledEntity}
- GridUid: {entityTransform.GridUid}
+ NEntId: {_entityManager.GetNetEntity(controlledEntity)}
+ Grid NEntId: {_entityManager.GetNetEntity(entityTransform.GridUid)}
Grid Rotation: {gridRotation.Degrees:F2}°");
}
diff --git a/Robust.Client/UserInterface/DevWindow/DevWindow.xaml.cs b/Robust.Client/UserInterface/DevWindow/DevWindow.xaml.cs
index b641777f892..2502d3b0e65 100644
--- a/Robust.Client/UserInterface/DevWindow/DevWindow.xaml.cs
+++ b/Robust.Client/UserInterface/DevWindow/DevWindow.xaml.cs
@@ -28,7 +28,7 @@ private void InitializeComponent()
TabContainer.SetTabTitle(Perf, "Profiling");
Stylesheet =
- new DefaultStylesheet(IoCManager.Resolve(), IoCManager.Resolve()).Stylesheet;
+ new DefaultStylesheet(IoCManager.Resolve(), IoCManager.Resolve()).Stylesheet;
}
}
diff --git a/Robust.Client/UserInterface/RichText/BoldItalicTag.cs b/Robust.Client/UserInterface/RichText/BoldItalicTag.cs
index c1a0b00af2e..eda24e5c190 100644
--- a/Robust.Client/UserInterface/RichText/BoldItalicTag.cs
+++ b/Robust.Client/UserInterface/RichText/BoldItalicTag.cs
@@ -9,7 +9,7 @@ public sealed class BoldItalicTag : IMarkupTag
{
public const string BoldItalicFont = "DefaultBoldItalic";
- [Dependency] private readonly IClientResourceCache _resourceCache = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public string Name => "bolditalic";
diff --git a/Robust.Client/UserInterface/RichText/BoldTag.cs b/Robust.Client/UserInterface/RichText/BoldTag.cs
index 8fb2f39158a..c1e832e563c 100644
--- a/Robust.Client/UserInterface/RichText/BoldTag.cs
+++ b/Robust.Client/UserInterface/RichText/BoldTag.cs
@@ -10,7 +10,7 @@ public sealed class BoldTag : IMarkupTag
{
public const string BoldFont = "DefaultBold";
- [Dependency] private readonly IClientResourceCache _resourceCache = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public string Name => "bold";
diff --git a/Robust.Client/UserInterface/RichText/FontTag.cs b/Robust.Client/UserInterface/RichText/FontTag.cs
index 222b9691768..f9d88fb2f74 100644
--- a/Robust.Client/UserInterface/RichText/FontTag.cs
+++ b/Robust.Client/UserInterface/RichText/FontTag.cs
@@ -16,7 +16,7 @@ public sealed class FontTag : IMarkupTag
public const string DefaultFont = "Default";
public const int DefaultSize = 12;
- [Dependency] private readonly IClientResourceCache _resourceCache = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public string Name => "font";
@@ -43,7 +43,7 @@ public void PopDrawContext(MarkupNode node, MarkupDrawingContext context)
public static Font CreateFont(
Stack contextFontStack,
MarkupNode node,
- IClientResourceCache cache,
+ IResourceCache cache,
IPrototypeManager prototypeManager,
string fontId)
{
diff --git a/Robust.Client/UserInterface/RichText/HeadingTag.cs b/Robust.Client/UserInterface/RichText/HeadingTag.cs
index acd189783c2..4d8c4e784e6 100644
--- a/Robust.Client/UserInterface/RichText/HeadingTag.cs
+++ b/Robust.Client/UserInterface/RichText/HeadingTag.cs
@@ -8,7 +8,7 @@ namespace Robust.Client.UserInterface.RichText;
public sealed class HeadingTag : IMarkupTag
{
- [Dependency] private readonly IClientResourceCache _resourceCache = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public string Name => "head";
diff --git a/Robust.Client/UserInterface/RichText/ItalicTag.cs b/Robust.Client/UserInterface/RichText/ItalicTag.cs
index 37eedb6505d..b70cc11fb0f 100644
--- a/Robust.Client/UserInterface/RichText/ItalicTag.cs
+++ b/Robust.Client/UserInterface/RichText/ItalicTag.cs
@@ -10,7 +10,7 @@ public sealed class ItalicTag : IMarkupTag
{
public const string ItalicFont = "DefaultItalic";
- [Dependency] private readonly IClientResourceCache _resourceCache = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public string Name => "italic";
diff --git a/Robust.Client/UserInterface/Stylesheets/DefaultStylesheet.cs b/Robust.Client/UserInterface/Stylesheets/DefaultStylesheet.cs
index a1c39f08195..72575887cec 100644
--- a/Robust.Client/UserInterface/Stylesheets/DefaultStylesheet.cs
+++ b/Robust.Client/UserInterface/Stylesheets/DefaultStylesheet.cs
@@ -12,7 +12,7 @@ public sealed class DefaultStylesheet
{
public Stylesheet Stylesheet { get; private set; } = default!;
- public DefaultStylesheet(IClientResourceCache res, IUserInterfaceManager userInterfaceManager)
+ public DefaultStylesheet(IResourceCache res, IUserInterfaceManager userInterfaceManager)
{
var notoSansFont = res.GetResource("/EngineFonts/NotoSans/NotoSans-Regular.ttf");
var notoSansFont12 = new VectorFont(notoSansFont, 12);
diff --git a/Robust.Client/UserInterface/Themes/UiTheme.cs b/Robust.Client/UserInterface/Themes/UiTheme.cs
index ab97260db99..0c14bc3c115 100644
--- a/Robust.Client/UserInterface/Themes/UiTheme.cs
+++ b/Robust.Client/UserInterface/Themes/UiTheme.cs
@@ -19,7 +19,7 @@ namespace Robust.Client.UserInterface.Themes;
[Prototype("uiTheme")]
public sealed class UITheme : IPrototype
{
- private IClientResourceCache? _cache;
+ private IResourceCache? _cache;
private IUserInterfaceManager? _uiMan;
//this is used for ease of access
diff --git a/Robust.Client/UserInterface/UserInterfaceManager.cs b/Robust.Client/UserInterface/UserInterfaceManager.cs
index 28a3921c146..8c1be1b0612 100644
--- a/Robust.Client/UserInterface/UserInterfaceManager.cs
+++ b/Robust.Client/UserInterface/UserInterfaceManager.cs
@@ -38,7 +38,7 @@ internal sealed partial class UserInterfaceManager : IUserInterfaceManagerIntern
[Dependency] private readonly IFontManager _fontManager = default!;
[Dependency] private readonly IClydeInternal _clyde = default!;
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
- [Dependency] private readonly IClientResourceCache _resourceCache = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
diff --git a/Robust.Client/Utility/SpriteSpecifierExt.cs b/Robust.Client/Utility/SpriteSpecifierExt.cs
index 178f426ca79..412857a41ef 100644
--- a/Robust.Client/Utility/SpriteSpecifierExt.cs
+++ b/Robust.Client/Utility/SpriteSpecifierExt.cs
@@ -17,7 +17,7 @@ namespace Robust.Client.Utility
///
public static class SpriteSpecifierExt
{
- public static Texture GetTexture(this SpriteSpecifier.Texture texSpecifier, IClientResourceCache cache)
+ public static Texture GetTexture(this SpriteSpecifier.Texture texSpecifier, IResourceCache cache)
{
return cache
.GetResource(SpriteSpecifierSerializer.TextureRoot / texSpecifier.TexturePath)
@@ -25,7 +25,7 @@ public static Texture GetTexture(this SpriteSpecifier.Texture texSpecifier, ICli
}
[Obsolete("Use SpriteSystem")]
- public static RSI.State GetState(this SpriteSpecifier.Rsi rsiSpecifier, IClientResourceCache cache)
+ public static RSI.State GetState(this SpriteSpecifier.Rsi rsiSpecifier, IResourceCache cache)
{
if (!cache.TryGetResource(SpriteSpecifierSerializer.TextureRoot / rsiSpecifier.RsiPath, out var theRsi))
{
@@ -56,7 +56,7 @@ public static IDirectionalTextureProvider DirFrame0(this SpriteSpecifier specifi
[Obsolete("Use SpriteSystem")]
public static IRsiStateLike RsiStateLike(this SpriteSpecifier specifier)
{
- var resC = IoCManager.Resolve();
+ var resC = IoCManager.Resolve();
switch (specifier)
{
case SpriteSpecifier.Texture tex:
diff --git a/Robust.Packaging/AssetProcessing/Passes/AssetPassAudioMetadata.cs b/Robust.Packaging/AssetProcessing/Passes/AssetPassAudioMetadata.cs
new file mode 100644
index 00000000000..790aaeac633
--- /dev/null
+++ b/Robust.Packaging/AssetProcessing/Passes/AssetPassAudioMetadata.cs
@@ -0,0 +1,83 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.AudioLoading;
+using Robust.Shared.Serialization;
+using YamlDotNet.Core;
+using YamlDotNet.RepresentationModel;
+
+namespace Robust.Packaging.AssetProcessing.Passes;
+
+///
+/// Strips out audio files and writes them to a metadata .yml
+/// Used for server packaging to avoid bundling entire audio files on the server.
+///
+public sealed class AssetPassAudioMetadata : AssetPass
+{
+ private readonly List _audioMetadata = new();
+ private readonly string _metadataPath;
+
+ public AssetPassAudioMetadata(string metadataPath = "Prototypes/_audio_metadata.yml")
+ {
+ _metadataPath = metadataPath;
+ }
+
+ protected override AssetFileAcceptResult AcceptFile(AssetFile file)
+ {
+ if (!AudioLoader.IsLoadableAudioFile(file.Path))
+ return AssetFileAcceptResult.Pass;
+
+ using var stream = file.Open();
+ var metadata = AudioLoader.LoadAudioMetadata(stream, file.Path);
+
+ lock (_audioMetadata)
+ {
+ _audioMetadata.Add(new AudioMetadataPrototype()
+ {
+ ID = "/" + file.Path,
+ Length = metadata.Length,
+ });
+ }
+
+ return AssetFileAcceptResult.Consumed;
+ }
+
+ [SuppressMessage("ReSharper", "InconsistentlySynchronizedField")]
+ protected override void AcceptFinished()
+ {
+ if (_audioMetadata.Count == 0)
+ {
+ Logger?.Debug("Have no audio metadata, not writing anything");
+ return;
+ }
+
+ Logger?.Debug("Writing audio metadata for {0} audio files", _audioMetadata.Count);
+
+ // ReSharper disable once InconsistentlySynchronizedField
+ var root = new YamlSequenceNode();
+ var document = new YamlDocument(root);
+
+ foreach (var prototype in _audioMetadata)
+ {
+ // TODO: I know but sermanager and please get me out of this hell.
+ var jaml = new YamlMappingNode
+ {
+ { "type", AudioMetadataPrototype.ProtoName },
+ { "id", new YamlScalarNode(prototype.ID) },
+ { "length", new YamlScalarNode(prototype.Length.TotalSeconds.ToString(CultureInfo.InvariantCulture)) }
+ };
+ root.Add(jaml);
+ }
+
+ RunJob(() =>
+ {
+ using var memory = new MemoryStream();
+ using var writer = new StreamWriter(memory);
+ var yamlStream = new YamlStream(document);
+ yamlStream.Save(new YamlNoDocEndDotsFix(new YamlMappingFix(new Emitter(writer))), false);
+ writer.Flush();
+ var result = new AssetFileMemory(_metadataPath, memory.ToArray());
+ SendFile(result);
+ });
+ }
+}
diff --git a/Robust.Packaging/AssetProcessing/Passes/AssetPassPackRsis.cs b/Robust.Packaging/AssetProcessing/Passes/AssetPassPackRsis.cs
index ffc99c744ec..5b0f04e177c 100644
--- a/Robust.Packaging/AssetProcessing/Passes/AssetPassPackRsis.cs
+++ b/Robust.Packaging/AssetProcessing/Passes/AssetPassPackRsis.cs
@@ -61,7 +61,7 @@ protected override void AcceptFinished()
foreach (var (key, dat) in _foundRsis)
{
if (dat.MetaJson == null)
- return;
+ continue;
RunJob(() =>
{
diff --git a/Robust.Packaging/AssetProcessing/Passes/AssetPassPrefix.cs b/Robust.Packaging/AssetProcessing/Passes/AssetPassPrefix.cs
new file mode 100644
index 00000000000..595168ff641
--- /dev/null
+++ b/Robust.Packaging/AssetProcessing/Passes/AssetPassPrefix.cs
@@ -0,0 +1,28 @@
+namespace Robust.Packaging.AssetProcessing.Passes;
+
+///
+/// Appends a prefix to file paths of passed-through files.
+///
+public sealed class AssetPassPrefix : AssetPass
+{
+ public string Prefix { get; set; }
+
+ public AssetPassPrefix(string prefix)
+ {
+ Prefix = prefix;
+ }
+
+ protected override AssetFileAcceptResult AcceptFile(AssetFile file)
+ {
+ var newPath = Prefix + file.Path;
+ var newFile = file switch
+ {
+ AssetFileDisk disk => (AssetFile) new AssetFileDisk(newPath, disk.DiskPath),
+ AssetFileMemory memory => new AssetFileMemory(newPath, memory.Memory),
+ _ => throw new ArgumentOutOfRangeException(nameof(file))
+ };
+
+ SendFile(newFile);
+ return AssetFileAcceptResult.Consumed;
+ }
+}
diff --git a/Robust.Packaging/Robust.Packaging.csproj b/Robust.Packaging/Robust.Packaging.csproj
index d6ab2e72e78..06cdf2ec8a6 100644
--- a/Robust.Packaging/Robust.Packaging.csproj
+++ b/Robust.Packaging/Robust.Packaging.csproj
@@ -10,5 +10,9 @@
+
+
+
+
diff --git a/Robust.Packaging/RobustClientAssetGraph.cs b/Robust.Packaging/RobustClientAssetGraph.cs
index 485111c4b64..4a7ebfb8e6d 100644
--- a/Robust.Packaging/RobustClientAssetGraph.cs
+++ b/Robust.Packaging/RobustClientAssetGraph.cs
@@ -21,11 +21,12 @@ public sealed class RobustClientAssetGraph
///
public IReadOnlyCollection AllPasses { get; }
- public RobustClientAssetGraph()
+ /// Should inputs be run in parallel. Should only be turned off for debugging.
+ public RobustClientAssetGraph(bool parallel = true)
{
// The code injecting the list of source files is assumed to be pretty single-threaded.
// We use a parallelizing input to break out all the work on files coming in onto multiple threads.
- Input = new AssetPassPipe { Name = "RobustClientAssetGraphInput", Parallelize = true };
+ Input = new AssetPassPipe { Name = "RobustClientAssetGraphInput", Parallelize = parallel };
PresetPasses = new AssetPassPipe { Name = "RobustClientAssetGraphPresetPasses" };
Output = new AssetPassPipe { Name = "RobustClientAssetGraphOutput", CheckDuplicates = true };
NormalizeText = new AssetPassNormalizeText { Name = "RobustClientAssetGraphNormalizeText" };
@@ -40,7 +41,7 @@ public RobustClientAssetGraph()
Input,
PresetPasses,
Output,
- NormalizeText
+ NormalizeText,
};
}
}
diff --git a/Robust.Packaging/RobustClientPackaging.cs b/Robust.Packaging/RobustClientPackaging.cs
index 49a94534474..c23fe4479bb 100644
--- a/Robust.Packaging/RobustClientPackaging.cs
+++ b/Robust.Packaging/RobustClientPackaging.cs
@@ -4,9 +4,10 @@ namespace Robust.Packaging;
public sealed class RobustClientPackaging
{
- public static IReadOnlySet ClientIgnoresResources { get; } = new HashSet
+ public static IReadOnlySet ClientIgnoredResources { get; } = new HashSet
{
"Maps",
+ "ConfigPresets",
// Leaving this here for future archaeologists to ponder at.
"emotes.xml",
"Groups",
@@ -18,48 +19,8 @@ public static async Task WriteClientResources(
AssetPass pass,
CancellationToken cancel = default)
{
- var ignoreSet = ClientIgnoresResources.Union(RobustSharedPackaging.SharedIgnoredResources).ToHashSet();
+ var ignoreSet = ClientIgnoredResources.Union(RobustSharedPackaging.SharedIgnoredResources).ToHashSet();
- await RobustSharedPackaging.DoResourceCopy(Path.Combine(contentDir, "Resources"), pass, ignoreSet, cancel);
- }
-
- public static async Task WriteContentAssemblies(
- AssetPass pass,
- string contentDir,
- string binDir,
- IEnumerable contentAssemblies,
- CancellationToken cancel = default)
- {
- await WriteContentAssemblies("Assemblies", pass, contentDir, binDir, contentAssemblies, cancel);
- }
-
- public static Task WriteContentAssemblies(
- string target,
- AssetPass pass,
- string contentDir,
- string binDir,
- IEnumerable contentAssemblies,
- CancellationToken cancel = default)
- {
- var files = new List();
-
- var sourceDir = Path.Combine(contentDir, "bin", binDir);
-
- foreach (var asm in contentAssemblies)
- {
- files.Add($"{asm}.dll");
-
- var pdbPath = $"{asm}.pdb";
- if (File.Exists(Path.Combine(sourceDir, pdbPath)))
- files.Add(pdbPath);
- }
-
- foreach (var f in files)
- {
- cancel.ThrowIfCancellationRequested();
- pass.InjectFileFromDisk($"{target}/{f}", Path.Combine(sourceDir, f));
- }
-
- return Task.CompletedTask;
+ await RobustSharedPackaging.DoResourceCopy(Path.Combine(contentDir, "Resources"), pass, ignoreSet, cancel: cancel);
}
}
diff --git a/Robust.Packaging/RobustServerAssetGraph.cs b/Robust.Packaging/RobustServerAssetGraph.cs
new file mode 100644
index 00000000000..ac1bf5119c5
--- /dev/null
+++ b/Robust.Packaging/RobustServerAssetGraph.cs
@@ -0,0 +1,129 @@
+using Robust.Packaging.AssetProcessing;
+using Robust.Packaging.AssetProcessing.Passes;
+
+namespace Robust.Packaging;
+
+///
+/// Standard asset graph for packaging server files. Extend by wiring things up to , , and .
+///
+///
+///
+/// This graph has two inputs: one for "core" server files such as the main engine executable, and another for resource files.
+///
+///
+/// If you want to add extra passes to run before preset passes, depend them on the relevant input pass, with a before of the relevant preset pass.
+///
+///
+/// See the following graph (Mermaid syntax) to understand this asset graph:
+///
+///
+/// flowchart LR
+/// InputCore --> PresetPassesCore
+/// PresetPassesCore --1--> NormalizeTextCore
+/// NormalizeTextCore --> Output
+/// PresetPassesCore --2--> Output
+/// InputResources --> PresetPassesResources
+/// PresetPassesResources --1--> AudioMetadata
+/// PresetPassesResources --2--> NormalizeTextResources
+/// PresetPassesResources --3--> PrefixResources
+/// AudioMetadata --> PrefixResources
+/// NormalizeTextResources --> PrefixResources
+/// PrefixResources --> Output
+///
+///
+public sealed class RobustServerAssetGraph
+{
+ public AssetPassPipe Output { get; }
+
+ ///
+ /// Input pass for core server files, such as Robust.Server.exe.
+ ///
+ ///
+ public AssetPassPipe InputCore { get; }
+
+ public AssetPassPipe PresetPassesCore { get; }
+
+ ///
+ /// Normalizes text files in core files.
+ ///
+ public AssetPassNormalizeText NormalizeTextCore { get; }
+
+ ///
+ /// Input pass for server resource files. Everything that will go into Resources/.
+ ///
+ ///
+ /// Do not prefix file paths with Resources/, the asset pass will automatically remap them.
+ ///
+ ///
+ public AssetPassPipe InputResources { get; }
+ public AssetPassPipe PresetPassesResources { get; }
+ public AssetPassAudioMetadata AudioMetadata { get; }
+
+ ///
+ /// Normalizes text files in resources.
+ ///
+ public AssetPassNormalizeText NormalizeTextResources { get; }
+
+ ///
+ /// Responsible for putting resources into the "Resources/" folder.
+ ///
+ public AssetPassPrefix PrefixResources { get; }
+
+ ///
+ /// Collection of all passes in this preset graph.
+ ///
+ public IReadOnlyCollection AllPasses { get; }
+
+ /// Should inputs be run in parallel. Should only be turned off for debugging.
+ public RobustServerAssetGraph(bool parallel = true)
+ {
+ Output = new AssetPassPipe { Name = "RobustServerAssetGraphOutput", CheckDuplicates = true };
+
+ //
+ // Core files
+ //
+
+ // The code injecting the list of source files is assumed to be pretty single-threaded.
+ // We use a parallelizing input to break out all the work on files coming in onto multiple threads.
+ InputCore = new AssetPassPipe { Name = "RobustServerAssetGraphInputCore", Parallelize = parallel };
+ PresetPassesCore = new AssetPassPipe { Name = "RobustServerAssetGraphPresetPassesCore" };
+ NormalizeTextCore = new AssetPassNormalizeText { Name = "RobustServerAssetGraphNormalizeTextCore" };
+
+ PresetPassesCore.AddDependency(InputCore);
+ NormalizeTextCore.AddDependency(PresetPassesCore).AddBefore(Output);
+ Output.AddDependency(PresetPassesCore);
+ Output.AddDependency(NormalizeTextCore);
+
+ //
+ // Resource files
+ //
+
+ // Ditto about parallelizing
+ InputResources = new AssetPassPipe { Name = "RobustServerAssetGraphInputResources", Parallelize = parallel };
+ PresetPassesResources = new AssetPassPipe { Name = "RobustServerAssetGraphPresetPassesResources" };
+ NormalizeTextResources = new AssetPassNormalizeText { Name = "RobustServerAssetGraphNormalizeTextResources" };
+ AudioMetadata = new AssetPassAudioMetadata { Name = "RobustServerAssetGraphAudioMetadata" };
+ PrefixResources = new AssetPassPrefix("Resources/") { Name = "RobustServerAssetGraphPrefixResources" };
+
+ PresetPassesResources.AddDependency(InputResources);
+ AudioMetadata.AddDependency(PresetPassesResources).AddBefore(NormalizeTextResources);
+ NormalizeTextResources.AddDependency(PresetPassesResources).AddBefore(PrefixResources);
+ PrefixResources.AddDependency(PresetPassesResources);
+ PrefixResources.AddDependency(AudioMetadata);
+ PrefixResources.AddDependency(NormalizeTextResources);
+ Output.AddDependency(PrefixResources);
+
+ AllPasses = new AssetPass[]
+ {
+ Output,
+ InputCore,
+ PresetPassesCore,
+ NormalizeTextCore,
+ InputResources,
+ PresetPassesResources,
+ NormalizeTextResources,
+ AudioMetadata,
+ PrefixResources,
+ };
+ }
+}
diff --git a/Robust.Packaging/RobustServerPackaging.cs b/Robust.Packaging/RobustServerPackaging.cs
new file mode 100644
index 00000000000..6c06da5ac7b
--- /dev/null
+++ b/Robust.Packaging/RobustServerPackaging.cs
@@ -0,0 +1,33 @@
+using Robust.Packaging.AssetProcessing;
+
+namespace Robust.Packaging;
+
+public sealed class RobustServerPackaging
+{
+ public static IReadOnlySet ServerIgnoresResources { get; } = new HashSet
+ {
+ "Textures",
+ "Fonts",
+ "Shaders",
+ };
+
+ public static async Task WriteServerResources(
+ string contentDir,
+ AssetPass pass,
+ CancellationToken cancel = default)
+ {
+ var ignoreSet = ServerIgnoresResources.Union(RobustSharedPackaging.SharedIgnoredResources).ToHashSet();
+
+ await RobustSharedPackaging.DoResourceCopy(
+ Path.Combine(contentDir, "Resources"),
+ pass,
+ ignoreSet,
+ cancel: cancel);
+
+ await RobustSharedPackaging.DoResourceCopy(
+ Path.Combine("RobustToolbox", "Resources"),
+ pass,
+ ignoreSet,
+ cancel: cancel);
+ }
+}
diff --git a/Robust.Packaging/RobustSharedPackaging.cs b/Robust.Packaging/RobustSharedPackaging.cs
index 6644ad5c7c1..a2a23bf7820 100644
--- a/Robust.Packaging/RobustSharedPackaging.cs
+++ b/Robust.Packaging/RobustSharedPackaging.cs
@@ -15,10 +15,53 @@ public sealed class RobustSharedPackaging
".DS_Store"
};
+ // IDK what these are supposed to correspond to but targetDir is the target directory.
+ public static async Task WriteContentAssemblies(
+ AssetPass pass,
+ string contentDir,
+ string binDir,
+ IEnumerable contentAssemblies,
+ string targetDir = "Assemblies",
+ CancellationToken cancel = default)
+ {
+ await WriteContentAssemblies(targetDir, pass, contentDir, binDir, contentAssemblies, cancel);
+ }
+
+ public static Task WriteContentAssemblies(
+ string target,
+ AssetPass pass,
+ string contentDir,
+ string binDir,
+ IEnumerable contentAssemblies,
+ CancellationToken cancel = default)
+ {
+ var files = new List();
+
+ var sourceDir = Path.Combine(contentDir, "bin", binDir);
+
+ foreach (var asm in contentAssemblies)
+ {
+ files.Add($"{asm}.dll");
+
+ var pdbPath = $"{asm}.pdb";
+ if (File.Exists(Path.Combine(sourceDir, pdbPath)))
+ files.Add(pdbPath);
+ }
+
+ foreach (var f in files)
+ {
+ cancel.ThrowIfCancellationRequested();
+ pass.InjectFileFromDisk($"{target}/{f}", Path.Combine(sourceDir, f));
+ }
+
+ return Task.CompletedTask;
+ }
+
public static Task DoResourceCopy(
string diskSource,
AssetPass pass,
IReadOnlySet ignoreSet,
+ string targetDir = "",
CancellationToken cancel = default)
{
foreach (var path in Directory.EnumerateFileSystemEntries(diskSource))
@@ -29,7 +72,7 @@ public static Task DoResourceCopy(
if (ignoreSet.Contains(filename))
continue;
- var targetPath = filename;
+ var targetPath = Path.Combine(targetDir, filename);
if (Directory.Exists(path))
CopyDirIntoZip(path, targetPath, pass);
else
@@ -44,11 +87,11 @@ private static void CopyDirIntoZip(string directory, string basePath, AssetPass
foreach (var file in Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories))
{
var relPath = Path.GetRelativePath(directory, file);
- if (Path.DirectorySeparatorChar != '/')
- relPath = relPath.Replace(Path.DirectorySeparatorChar, '/');
-
var zipPath = $"{basePath}/{relPath}";
+ if (Path.DirectorySeparatorChar != '/')
+ zipPath = zipPath.Replace(Path.DirectorySeparatorChar, '/');
+
// Console.WriteLine($"{directory}/{zipPath} -> /{zipPath}");
pass.InjectFileFromDisk(zipPath, file);
}
diff --git a/Robust.Server/Audio/AudioSystem.cs b/Robust.Server/Audio/AudioSystem.cs
index b7604667d89..5a5fd9f7f37 100644
--- a/Robust.Server/Audio/AudioSystem.cs
+++ b/Robust.Server/Audio/AudioSystem.cs
@@ -1,11 +1,15 @@
+using System;
using System.Collections.Generic;
+using System.IO;
using System.Numerics;
using Robust.Server.GameObjects;
using Robust.Server.GameStates;
using Robust.Shared.Audio;
+using Robust.Shared.Audio.AudioLoading;
using Robust.Shared.Audio.Components;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Audio.Systems;
+using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
@@ -17,6 +21,9 @@ namespace Robust.Server.Audio;
public sealed partial class AudioSystem : SharedAudioSystem
{
[Dependency] private readonly PvsOverrideSystem _pvs = default!;
+ [Dependency] private readonly IResourceManager _resourceManager = default!;
+
+ private readonly Dictionary _cachedAudioLengths = new();
public override void Initialize()
{
@@ -190,4 +197,27 @@ public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string
return null;
}
+
+ protected override TimeSpan GetAudioLengthImpl(string filename)
+ {
+ // Check shipped metadata from packaging.
+ if (ProtoMan.TryIndex(filename, out AudioMetadataPrototype? metadata))
+ return metadata.Length;
+
+ // Try loading audio files directly.
+ // This is necessary in development and environments,
+ // and when working with audio files uploaded dynamically at runtime.
+ if (_cachedAudioLengths.TryGetValue(filename, out var length))
+ return length;
+
+ if (!_resourceManager.TryContentFileRead(filename, out var stream))
+ throw new FileNotFoundException($"Unable to find metadata for audio file {filename}");
+
+ using (stream)
+ {
+ var loadedMetadata = AudioLoader.LoadAudioMetadata(stream, filename);
+ _cachedAudioLengths.Add(filename, loadedMetadata.Length);
+ return loadedMetadata.Length;
+ }
+ }
}
diff --git a/Robust.Server/BaseServer.cs b/Robust.Server/BaseServer.cs
index aa2268f0389..e4d4a0f5462 100644
--- a/Robust.Server/BaseServer.cs
+++ b/Robust.Server/BaseServer.cs
@@ -660,10 +660,14 @@ public void Cleanup()
{
// Write down exception log
var logPath = _config.GetCVar(CVars.LogPath);
- var relPath = PathHelpers.ExecutableRelativeFile(logPath);
- Directory.CreateDirectory(relPath);
- var pathToWrite = Path.Combine(relPath,
+ if (!Path.IsPathRooted(logPath))
+ {
+ logPath = PathHelpers.ExecutableRelativeFile(logPath);
+ }
+
+ var pathToWrite = Path.Combine(logPath,
"Runtime-" + DateTime.Now.ToString("yyyy-MM-dd-THH-mm-ss") + ".txt");
+ Directory.CreateDirectory(logPath);
File.WriteAllText(pathToWrite, _runtimeLog.Display(), EncodingHelpers.UTF8);
}
diff --git a/Robust.Server/GameObjects/EntitySystems/ActorSystem.cs b/Robust.Server/GameObjects/EntitySystems/ActorSystem.cs
deleted file mode 100644
index 4b5584d8098..00000000000
--- a/Robust.Server/GameObjects/EntitySystems/ActorSystem.cs
+++ /dev/null
@@ -1,189 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using JetBrains.Annotations;
-using Robust.Server.Player;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Network;
-using Robust.Shared.Player;
-using Robust.Shared.Utility;
-
-namespace Robust.Server.GameObjects
-{
- ///
- /// System that handles players being attached/detached from entities.
- ///
- [UsedImplicitly]
- public sealed class ActorSystem : EntitySystem
- {
- [Dependency] private readonly IPlayerManager _playerManager = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnActorShutdown);
- }
-
- ///
- /// Attaches a player session to an entity, optionally kicking any sessions already attached to it.
- ///
- /// The entity to attach the player to
- /// The player to attach to the entity
- /// Whether to kick any existing players from the entity
- /// Whether the attach succeeded, or not.
- public bool Attach(EntityUid? uid, ICommonSession player, bool force = false)
- {
- return Attach(uid, player, false, out _);
- }
-
- ///
- /// Attaches a player session to an entity, optionally kicking any sessions already attached to it.
- ///
- /// The entity to attach the player to
- /// The player to attach to the entity
- /// Whether to kick any existing players from the entity
- /// The player that was forcefully kicked, or null.
- /// Whether the attach succeeded, or not.
- public bool Attach(EntityUid? entity, ICommonSession player, bool force, out ICommonSession? forceKicked)
- {
- // Null by default.
- forceKicked = null;
-
- if (player.AttachedEntity == entity)
- {
- DebugTools.Assert(entity == null || HasComp(entity));
- return true;
- }
-
- if (entity is not { } uid)
- return Detach(player);
-
- // Cannot attach to a deleted, nonexisting or terminating entity.
- if (TerminatingOrDeleted(uid))
- return false;
-
- // Check if there was a player attached to the entity already...
- if (TryComp(uid, out ActorComponent? actor))
- {
- // If we're not forcing the attach, this fails.
- if (!force)
- return false;
-
- // Set the event's force-kicked session before detaching it.
- forceKicked = actor.PlayerSession;
- RemComp(uid, actor);
- DebugTools.AssertNull(forceKicked.AttachedEntity);
- }
-
- // Detach from the currently attached entity.
- Detach(player);
-
- // We add the actor component.
- actor = EntityManager.AddComponent(uid);
- EntityManager.EnsureComponent(uid);
- actor.PlayerSession = player;
- _playerManager.SetAttachedEntity(player, uid);
- DebugTools.Assert(player.AttachedEntity == entity);
-
- // The player is fully attached now, raise an event!
- RaiseLocalEvent(uid, new PlayerAttachedEvent(uid, player, forceKicked), true);
- return true;
- }
-
- ///
- /// Detaches an attached session from the entity, if any.
- ///
- /// The entity player sessions will be detached from.
- /// Whether any player session was detached.
- public bool Detach(EntityUid uid, ActorComponent? actor = null)
- {
- if (!Resolve(uid, ref actor, false))
- return false;
-
- RemComp(uid, actor);
- return true;
- }
-
- ///
- /// Detaches this player from its attached entity, if any.
- ///
- /// The player session that will be detached from any attached entities.
- /// Whether the player is now detached from any entities.
- /// This returns true if the player wasn't attached to any entity.
- public bool Detach(ICommonSession player, ActorComponent? actor = null)
- {
- var uid = player.AttachedEntity;
- if (uid == null)
- return true;
-
- if (!Resolve(uid.Value, ref actor, false))
- {
- Log.Error($"Player {player} was attached to a deleted entity?");
- ((CommonSession) player).AttachedEntity = null;
- return true;
- }
-
- RemComp(uid.Value, actor);
- DebugTools.AssertNull(player.AttachedEntity);
- return false;
- }
-
- private void OnActorShutdown(EntityUid entity, ActorComponent component, ComponentShutdown args)
- {
- _playerManager.SetAttachedEntity(component.PlayerSession, null);
-
- // The player is fully detached now that the component has shut down.
- RaiseLocalEvent(entity, new PlayerDetachedEvent(entity, component.PlayerSession), true);
- }
-
- public bool TryGetActorFromUserId(NetUserId? userId, [NotNullWhen(true)] out ICommonSession? actor, out EntityUid? actorEntity)
- {
- actor = null;
- actorEntity = null;
- if (userId != null)
- {
- if (!_playerManager.TryGetSessionById(userId.Value, out actor))
- return false;
- actorEntity = actor.AttachedEntity;
- }
-
- return actor != null;
- }
- }
-
- ///
- /// Event for when a player has been attached to an entity.
- ///
- public sealed class PlayerAttachedEvent : EntityEventArgs
- {
- public EntityUid Entity { get; }
- public ICommonSession Player { get; }
-
- ///
- /// The player session that was forcefully kicked from the entity, if any.
- ///
- public ICommonSession? Kicked { get; }
-
- public PlayerAttachedEvent(EntityUid entity, ICommonSession player, ICommonSession? kicked = null)
- {
- Entity = entity;
- Player = player;
- Kicked = kicked;
- }
- }
-
- ///
- /// Event for when a player has been detached from an entity.
- ///
- public sealed class PlayerDetachedEvent : EntityEventArgs
- {
- public EntityUid Entity { get; }
- public ICommonSession Player { get; }
-
- public PlayerDetachedEvent(EntityUid entity, ICommonSession player)
- {
- Entity = entity;
- Player = player;
- }
- }
-}
diff --git a/Robust.Server/GameObjects/EntitySystems/EyeSystem.cs b/Robust.Server/GameObjects/EntitySystems/EyeSystem.cs
index f9d19c48910..4f9e01d659d 100644
--- a/Robust.Server/GameObjects/EntitySystems/EyeSystem.cs
+++ b/Robust.Server/GameObjects/EntitySystems/EyeSystem.cs
@@ -1,5 +1,4 @@
using Robust.Shared.GameObjects;
-using Robust.Shared.GameStates;
namespace Robust.Server.GameObjects;
diff --git a/Robust.Server/GameObjects/EntitySystems/MapLoaderSystem.cs b/Robust.Server/GameObjects/EntitySystems/MapLoaderSystem.cs
index 12c14b1c3a1..86f2526eb6a 100644
--- a/Robust.Server/GameObjects/EntitySystems/MapLoaderSystem.cs
+++ b/Robust.Server/GameObjects/EntitySystems/MapLoaderSystem.cs
@@ -290,6 +290,9 @@ private bool Deserialize(MapData data)
ReadGrids(data);
+ // grids prior to engine v175 might've been serialized with empty chunks which now throw debug asserts.
+ RemoveEmptyChunks(data);
+
// Then, go hierarchically in order and do the entity things.
StartupEntities(data);
@@ -305,6 +308,25 @@ private bool Deserialize(MapData data)
return true;
}
+ private void RemoveEmptyChunks(MapData data)
+ {
+ var gridQuery = _serverEntityManager.GetEntityQuery();
+ foreach (var uid in data.EntitiesToDeserialize.Keys)
+ {
+ if (!gridQuery.TryGetComponent(uid, out var gridComp))
+ continue;
+
+ foreach (var (index, chunk) in gridComp.Chunks)
+ {
+ if (chunk.FilledTiles > 0)
+ continue;
+
+ Log.Warning($"Encountered empty chunk while deserializing map. Grid: {ToPrettyString(uid)}. Chunk index: {index}");
+ gridComp.Chunks.Remove(index);
+ }
+ }
+ }
+
private bool VerifyEntitiesExist(MapData data, BeforeEntityReadEvent ev)
{
_stopwatch.Restart();
diff --git a/Robust.Server/GameObjects/EntitySystems/ServerMetaDataSystem.cs b/Robust.Server/GameObjects/EntitySystems/ServerMetaDataSystem.cs
index 9eea4f661ac..91b7eb995b0 100644
--- a/Robust.Server/GameObjects/EntitySystems/ServerMetaDataSystem.cs
+++ b/Robust.Server/GameObjects/EntitySystems/ServerMetaDataSystem.cs
@@ -1,6 +1,7 @@
using Robust.Server.GameStates;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
+using Robust.Shared.Player;
namespace Robust.Server.GameObjects;
diff --git a/Robust.Server/GameObjects/EntitySystems/UserInterfaceSystem.cs b/Robust.Server/GameObjects/EntitySystems/UserInterfaceSystem.cs
index 6d96e6df350..2861d7301ff 100644
--- a/Robust.Server/GameObjects/EntitySystems/UserInterfaceSystem.cs
+++ b/Robust.Server/GameObjects/EntitySystems/UserInterfaceSystem.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Linq;
using JetBrains.Annotations;
using Robust.Server.Player;
@@ -21,8 +20,6 @@ public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
private readonly List _sessionCache = new();
- private readonly Dictionary> _openInterfaces = new();
-
///
public override void Initialize()
{
@@ -46,7 +43,7 @@ private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs args)
if (args.NewStatus != SessionStatus.Disconnected)
return;
- if (!_openInterfaces.TryGetValue(args.Session, out var buis))
+ if (!OpenInterfaces.TryGetValue(args.Session, out var buis))
return;
foreach (var bui in buis.ToArray())
@@ -92,7 +89,7 @@ public override void Update(float frameTime)
///
/// Verify that the subscribed clients are still in range of the interface.
///
- private void CheckRange(EntityUid uid, Shared.GameObjects.ActiveUserInterfaceComponent activeUis, PlayerBoundUserInterface ui, TransformComponent transform, EntityQuery query)
+ private void CheckRange(EntityUid uid, ActiveUserInterfaceComponent activeUis, PlayerBoundUserInterface ui, TransformComponent transform, EntityQuery query)
{
if (ui.InteractionRange <= 0)
return;
@@ -139,11 +136,6 @@ private void CheckRange(EntityUid uid, Shared.GameObjects.ActiveUserInterfaceCom
}
}
- private void ActivateInterface(PlayerBoundUserInterface ui)
- {
- EnsureComp(ui.Owner).Interfaces.Add(ui);
- }
-
#region Get BUI
public bool HasUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
@@ -168,20 +160,14 @@ public PlayerBoundUserInterface GetUi(EntityUid uid, Enum uiKey, UserInterfaceCo
? bui
: null;
}
- public bool TryGetUi(EntityUid uid, Enum uiKey, [NotNullWhen(true)] out PlayerBoundUserInterface? bui, UserInterfaceComponent? ui = null)
- {
- bui = null;
-
- return Resolve(uid, ref ui, false) && ui.Interfaces.TryGetValue(uiKey, out bui);
- }
///
/// Return UIs a session has open.
/// Null if empty.
- ///
+ ///
public List? GetAllUIsForSession(ICommonSession session)
{
- _openInterfaces.TryGetValue(session, out var value);
+ OpenInterfaces.TryGetValue(session, out var value);
return value;
}
#endregion
@@ -259,94 +245,14 @@ public void SetUiState(PlayerBoundUserInterface bui, BoundUserInterfaceState sta
bui.StateDirty = true;
}
- ///
- /// Switches between closed and open for a specific client.
- ///
- public bool TryToggleUi(EntityUid uid, Enum uiKey, ICommonSession session, UserInterfaceComponent? ui = null)
- {
- if (!TryGetUi(uid, uiKey, out var bui, ui))
- return false;
-
- ToggleUi(bui, session);
- return true;
- }
-
- ///
- /// Switches between closed and open for a specific client.
- ///
- public void ToggleUi(PlayerBoundUserInterface bui, ICommonSession session)
- {
- if (bui._subscribedSessions.Contains(session))
- CloseUi(bui, session);
- else
- OpenUi(bui, session);
- }
-
- #region Open
-
- public bool TryOpen(EntityUid uid, Enum uiKey, ICommonSession session, UserInterfaceComponent? ui = null)
- {
- if (!TryGetUi(uid, uiKey, out var bui, ui))
- return false;
-
- return OpenUi(bui, session);
- }
-
- ///
- /// Opens this interface for a specific client.
- ///
- public bool OpenUi(PlayerBoundUserInterface bui, ICommonSession session)
- {
- if (session.Status == SessionStatus.Connecting || session.Status == SessionStatus.Disconnected)
- return false;
-
- if (!bui._subscribedSessions.Add(session))
- return false;
-
- _openInterfaces.GetOrNew(session).Add(bui);
- RaiseLocalEvent(bui.Owner, new BoundUIOpenedEvent(bui.UiKey, bui.Owner, session));
-
- RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(bui.Owner), new OpenBoundInterfaceMessage(), bui.UiKey), session.ConnectedClient);
-
- // Fun fact, clients needs to have BUIs open before they can receive the state.....
- if (bui.LastStateMsg != null)
- RaiseNetworkEvent(bui.LastStateMsg, session.ConnectedClient);
-
- ActivateInterface(bui);
- return true;
- }
-
- #endregion
-
#region Close
- public bool TryClose(EntityUid uid, Enum uiKey, ICommonSession session, UserInterfaceComponent? ui = null)
- {
- if (!TryGetUi(uid, uiKey, out var bui, ui))
- return false;
-
- return CloseUi(bui, session);
- }
-
- ///
- /// Close this interface for a specific client.
- ///
- public bool CloseUi(PlayerBoundUserInterface bui, ICommonSession session, ActiveUserInterfaceComponent? activeUis = null)
- {
- if (!bui._subscribedSessions.Remove(session))
- return false;
-
- RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(bui.Owner), new CloseBoundInterfaceMessage(), bui.UiKey), session.ConnectedClient);
- CloseShared(bui, session, activeUis);
- return true;
- }
-
protected override void CloseShared(PlayerBoundUserInterface bui, ICommonSession session, ActiveUserInterfaceComponent? activeUis = null)
{
var owner = bui.Owner;
bui._subscribedSessions.Remove(session);
bui.PlayerStateOverrides.Remove(session);
- if (_openInterfaces.TryGetValue(session, out var buis))
+ if (OpenInterfaces.TryGetValue(session, out var buis))
buis.Remove(bui);
RaiseLocalEvent(owner, new BoundUIClosedEvent(bui.UiKey, owner, session));
diff --git a/Robust.Server/GameStates/PvsSystem.cs b/Robust.Server/GameStates/PvsSystem.cs
index 186f792d73c..36597913ac9 100644
--- a/Robust.Server/GameStates/PvsSystem.cs
+++ b/Robust.Server/GameStates/PvsSystem.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
@@ -271,6 +272,7 @@ private void OnEntityDeleted(EntityUid e, MetaDataComponent metadata)
{
sessionData.LastSeenAt.Remove(metadata.NetEntity);
sessionData.LastLeftView.Remove(metadata.NetEntity);
+
if (sessionData.SentEntities.TryGetValue(previousTick, out var ents))
ents.Remove(metadata.NetEntity);
}
@@ -433,10 +435,15 @@ private void OnMapCreated(MapChangedEvent e)
#endregion
- public (List<(int, IChunkIndexLocation)> , HashSet[], EntityUid[][] viewers) GetChunks(ICommonSession[] sessions)
+ public List<(int, IChunkIndexLocation)> GetChunks(
+ ICommonSession[] sessions,
+ ref HashSet[] playerChunks,
+ ref EntityUid[][] viewerEntities)
{
- var playerChunks = new HashSet[sessions.Length];
- var viewerEntities = new EntityUid[sessions.Length][];
+ // Pass these in to avoid allocating new ones every tick, 99% of the time sessions length is going to be the same size.
+ // These values will get overridden here and the old values have already been returned to the pool by this point.
+ Array.Resize(ref playerChunks, sessions.Length);
+ Array.Resize(ref viewerEntities, sessions.Length);
_chunkList.Clear();
// Keep track of the index of each chunk we use for a faster index lookup.
@@ -459,8 +466,8 @@ private void OnMapCreated(MapChangedEvent e)
var session = sessions[i];
playerChunks[i] = _playerChunkPool.Get();
- var viewers = GetSessionViewers(session);
- viewerEntities[i] = viewers;
+ ref var viewers = ref viewerEntities[i];
+ GetSessionViewers(session, ref viewers);
for (var j = 0; j < viewers.Length; j++)
{
@@ -552,7 +559,7 @@ private void OnMapCreated(MapChangedEvent e)
}
}
- return (_chunkList, playerChunks, viewerEntities);
+ return _chunkList;
}
public void RegisterNewPreviousChunkTrees(
@@ -569,23 +576,20 @@ public void RegisterNewPreviousChunkTrees(
_reusedTrees.Add(chunks[i]);
}
- var previousIndices = _previousTrees.Keys.ToArray();
- for (var i = 0; i < previousIndices.Length; i++)
+ foreach (var (index, chunk) in _previousTrees)
{
- var index = previousIndices[i];
// ReSharper disable once InconsistentlySynchronizedField
- if (_reusedTrees.Contains(index)) continue;
- var chunk = _previousTrees[index];
- if (chunk.HasValue)
+ if (_reusedTrees.Contains(index))
+ continue;
+
+ if (chunk != null)
{
_chunkCachePool.Return(chunk.Value.metadata);
_treePool.Return(chunk.Value.tree);
}
if (!chunks.Contains(index))
- {
_previousTrees.Remove(index);
- }
}
_previousTrees.EnsureCapacity(chunks.Count);
@@ -888,7 +892,7 @@ private void AddToChunkSetRecursively(in EntityUid uid, in NetEntity netEntity,
return null;
var tick = _gameTiming.CurTick;
- var minSize = Math.Max(0, lastSent.Count - lastSent.Count);
+ var minSize = Math.Max(0, lastSent.Count - visibleEnts.Count);
var leftView = new List(minSize);
foreach (var netEntity in lastSent.Keys)
@@ -1313,26 +1317,44 @@ private EntityState GetFullEntityState(ICommonSession player, EntityUid entityUi
return entState;
}
- private EntityUid[] GetSessionViewers(ICommonSession session)
+ private void GetSessionViewers(ICommonSession session, [NotNull] ref EntityUid[]? viewers)
{
if (session.Status != SessionStatus.InGame)
- return Array.Empty();
+ {
+ viewers = Array.Empty();
+ return;
+ }
// Fast path
if (session.ViewSubscriptions.Count == 0)
{
if (session.AttachedEntity == null)
- return Array.Empty();
+ {
+ viewers = Array.Empty();
+ return;
+ }
- return new[] { session.AttachedEntity.Value };
+ Array.Resize(ref viewers, 1);
+ viewers[0] = session.AttachedEntity.Value;
+ return;
}
- var viewers = new HashSet();
- if (session.AttachedEntity != null)
- viewers.Add(session.AttachedEntity.Value);
+ int i = 0;
+ if (session.AttachedEntity is { } local)
+ {
+ DebugTools.Assert(!session.ViewSubscriptions.Contains(local));
+ Array.Resize(ref viewers, session.ViewSubscriptions.Count + 1);
+ viewers[i++] = local;
+ }
+ else
+ {
+ Array.Resize(ref viewers, session.ViewSubscriptions.Count);
+ }
- viewers.UnionWith(session.ViewSubscriptions);
- return viewers.ToArray();
+ foreach (var ent in session.ViewSubscriptions)
+ {
+ viewers[i++] = ent;
+ }
}
// Read Safe
diff --git a/Robust.Server/GameStates/ServerGameStateManager.cs b/Robust.Server/GameStates/ServerGameStateManager.cs
index 2c67cb5dce3..2e04bdb675e 100644
--- a/Robust.Server/GameStates/ServerGameStateManager.cs
+++ b/Robust.Server/GameStates/ServerGameStateManager.cs
@@ -37,6 +37,9 @@ public sealed class ServerGameStateManager : IServerGameStateManager, IPostInjec
// Mapping of net UID of clients -> last known acked state.
private GameTick _lastOldestAck = GameTick.Zero;
+ private HashSet[] _playerChunks = Array.Empty>();
+ private EntityUid[][] _viewerEntities = Array.Empty();
+
private PvsSystem _pvs = default!;
[Dependency] private readonly EntityManager _entityManager = default!;
@@ -267,7 +270,7 @@ private struct PvsData
private PvsData? GetPVSData(ICommonSession[] players)
{
- var (chunks, playerChunks, viewerEntities) = _pvs.GetChunks(players);
+ var chunks= _pvs.GetChunks(players, ref _playerChunks, ref _viewerEntities);
const int ChunkBatchSize = 2;
var chunksCount = chunks.Count;
var chunkBatches = (int)MathF.Ceiling((float)chunksCount / ChunkBatchSize);
@@ -310,8 +313,8 @@ private struct PvsData
ArrayPool.Shared.Return(reuse);
return new PvsData()
{
- PlayerChunks = playerChunks,
- ViewerEntities = viewerEntities,
+ PlayerChunks = _playerChunks,
+ ViewerEntities = _viewerEntities,
ChunkCache = chunkCache,
};
}
@@ -378,15 +381,10 @@ private void SendStateUpdate(int i,
if (_gameTiming.CurTick.Value > lastAck.Value + _pvs.ForceAckThreshold)
{
stateUpdateMessage.ForceSendReliably = true;
-
- // Aside from the time shortly after connecting, this shouldn't be common. If it is happening.
- // something is probably wrong (or we have a malicious client). Hence we log an error.
- // If it is more frequent than I think, this can be downgraded to a warning.
-
#if FULL_RELEASE
var connectedTime = (DateTime.UtcNow - session.ConnectedTime).TotalMinutes;
if (lastAck > GameTick.Zero && connectedTime > 1)
- _logger.Error($"Client {session} exceeded ack-tick threshold. Last ack: {lastAck}. Cur tick: {_gameTiming.CurTick}. Connect time: {connectedTime} minutes");
+ _logger.Warning($"Client {session} exceeded ack-tick threshold. Last ack: {lastAck}. Cur tick: {_gameTiming.CurTick}. Connect time: {connectedTime} minutes");
#endif
}
diff --git a/Robust.Server/Maps/MapChunkSerializer.cs b/Robust.Server/Maps/MapChunkSerializer.cs
index dff28e56130..d06e45679b5 100644
--- a/Robust.Server/Maps/MapChunkSerializer.cs
+++ b/Robust.Server/Maps/MapChunkSerializer.cs
@@ -12,6 +12,7 @@
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
+using Robust.Shared.Utility;
namespace Robust.Server.Maps;
@@ -93,6 +94,7 @@ public DataNode Write(ISerializationManager serializationManager, MapChunk value
IDependencyCollection dependencies, bool alwaysWrite = false,
ISerializationContext? context = null)
{
+ DebugTools.Assert(value.FilledTiles > 0, "Attempting to write an empty chunk");
var root = new MappingDataNode();
var ind = new ValueDataNode($"{value.X},{value.Y}");
root.Add("ind", ind);
diff --git a/Robust.Server/Placement/PlacementManager.cs b/Robust.Server/Placement/PlacementManager.cs
index 5842cd1643d..f2732ee7688 100644
--- a/Robust.Server/Placement/PlacementManager.cs
+++ b/Robust.Server/Placement/PlacementManager.cs
@@ -31,6 +31,10 @@ public sealed class PlacementManager : IPlacementManager
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
+ private EntityLookupSystem _lookup => _entityManager.System();
+ private SharedMapSystem _maps => _entityManager.System();
+ private SharedTransformSystem _xformSystem => _entityManager.System();
+
//TO-DO: Expand for multiple permission per mob?
// Add support for multi-use placeables (tiles etc.).
public List BuildPermissions { get; set; } = new();
@@ -45,6 +49,7 @@ public sealed class PlacementManager : IPlacementManager
public void Initialize()
{
+ // Someday PlacementManagerSystem my beloved.
_sawmill = _logManager.GetSawmill("placement");
_networkManager.RegisterNetMessage(HandleNetMessage);
@@ -143,7 +148,7 @@ public void HandlePlacementRequest(MsgPlacement msg)
if (_entityManager.TryGetComponent(gridUid, out var grid))
{
var replacementQuery = _entityManager.GetEntityQuery();
- var anc = grid.GetAnchoredEntitiesEnumerator(grid.LocalToTile(coordinates));
+ var anc = _maps.GetAnchoredEntitiesEnumerator(gridUid.Value, grid, _maps.LocalToTile(gridUid.Value, grid, coordinates));
var toDelete = new ValueList();
while (anc.MoveNext(out var ent))
@@ -186,14 +191,11 @@ private void PlaceNewTile(int tileType, EntityCoordinates coordinates, NetUserId
MapGridComponent? grid;
- _mapManager.TryGetGrid(coordinates.EntityId, out grid);
-
- if (grid == null)
- _mapManager.TryFindGridAt(coordinates.ToMap(_entityManager), out _, out grid);
-
- if (grid != null) // stick to existing grid
+ EntityUid gridId = coordinates.EntityId;
+ if (_entityManager.TryGetComponent(coordinates.EntityId, out grid)
+ || _mapManager.TryFindGridAt(coordinates.ToMap(_entityManager, _xformSystem), out gridId, out grid))
{
- grid.SetTile(coordinates, new Tile(tileType));
+ _maps.SetTile(gridId, grid, coordinates, new Tile(tileType));
var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId);
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);
@@ -202,9 +204,9 @@ private void PlaceNewTile(int tileType, EntityCoordinates coordinates, NetUserId
{
var newGrid = _mapManager.CreateGridEntity(coordinates.GetMapId(_entityManager));
var newGridXform = _entityManager.GetComponent(newGrid);
- newGridXform.WorldPosition = coordinates.Position - newGrid.Comp.TileSizeHalfVector; // assume bottom left tile origin
+ _xformSystem.SetWorldPosition(newGridXform, coordinates.Position - newGrid.Comp.TileSizeHalfVector); // assume bottom left tile origin
var tilePos = newGrid.Comp.WorldToTile(coordinates.Position);
- newGrid.Comp.SetTile(tilePos, new Tile(tileType));
+ _maps.SetTile(newGrid.Owner, newGrid.Comp, tilePos, new Tile(tileType));
var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId);
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);
@@ -228,11 +230,16 @@ private void HandleRectRemoveReq(MsgPlacement msg)
{
EntityCoordinates start = _entityManager.GetCoordinates(msg.NetCoordinates);
Vector2 rectSize = msg.RectSize;
- foreach (EntityUid entity in EntitySystem.Get().GetEntitiesIntersecting(start.GetMapId(_entityManager),
+ foreach (var entity in _lookup.GetEntitiesIntersecting(start.GetMapId(_entityManager),
new Box2(start.Position, start.Position + rectSize)))
{
- if (_entityManager.Deleted(entity) || _entityManager.HasComponent(entity) || _entityManager.HasComponent(entity))
+ if (_entityManager.Deleted(entity) ||
+ _entityManager.HasComponent(entity) ||
+ _entityManager.HasComponent(entity))
+ {
continue;
+ }
+
var placementEraseEvent = new PlacementEntityEvent(entity, _entityManager.GetComponent(entity).Coordinates, PlacementEventAction.Erase, msg.MsgChannel.UserId);
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);
_entityManager.DeleteEntity(entity);
@@ -247,16 +254,16 @@ public void SendPlacementBegin(EntityUid mob, int range, string objectType, stri
if (!_entityManager.TryGetComponent(mob, out var actor))
return;
- var playerConnection = actor.PlayerSession.ConnectedClient;
- if (playerConnection == null)
- return;
+ var playerConnection = actor.PlayerSession.Channel;
- var message = new MsgPlacement();
- message.PlaceType = PlacementManagerMessage.StartPlacement;
- message.Range = range;
- message.IsTile = false;
- message.ObjType = objectType;
- message.AlignOption = alignOption;
+ var message = new MsgPlacement
+ {
+ PlaceType = PlacementManagerMessage.StartPlacement,
+ Range = range,
+ IsTile = false,
+ ObjType = objectType,
+ AlignOption = alignOption
+ };
_networkManager.ServerSendMessage(message, playerConnection);
}
@@ -268,16 +275,16 @@ public void SendPlacementBeginTile(EntityUid mob, int range, string tileType, st
if (!_entityManager.TryGetComponent(mob, out var actor))
return;
- var playerConnection = actor.PlayerSession.ConnectedClient;
- if (playerConnection == null)
- return;
+ var playerConnection = actor.PlayerSession.Channel;
- var message = new MsgPlacement();
- message.PlaceType = PlacementManagerMessage.StartPlacement;
- message.Range = range;
- message.IsTile = true;
- message.ObjType = tileType;
- message.AlignOption = alignOption;
+ var message = new MsgPlacement
+ {
+ PlaceType = PlacementManagerMessage.StartPlacement,
+ Range = range,
+ IsTile = true,
+ ObjType = tileType,
+ AlignOption = alignOption
+ };
_networkManager.ServerSendMessage(message, playerConnection);
}
@@ -289,12 +296,12 @@ public void SendPlacementCancel(EntityUid mob)
if (!_entityManager.TryGetComponent(mob, out var actor))
return;
- var playerConnection = actor.PlayerSession.ConnectedClient;
- if (playerConnection == null)
- return;
+ var playerConnection = actor.PlayerSession.Channel;
- var message = new MsgPlacement();
- message.PlaceType = PlacementManagerMessage.CancelPlacement;
+ var message = new MsgPlacement
+ {
+ PlaceType = PlacementManagerMessage.CancelPlacement
+ };
_networkManager.ServerSendMessage(message, playerConnection);
}
diff --git a/Robust.Server/Player/PlayerManager.cs b/Robust.Server/Player/PlayerManager.cs
index 4407d364285..61f562691e5 100644
--- a/Robust.Server/Player/PlayerManager.cs
+++ b/Robust.Server/Player/PlayerManager.cs
@@ -80,9 +80,7 @@ private Task OnConnecting(NetConnectingArgs args)
///
private void NewSession(object? sender, NetChannelArgs args)
{
- var session = CreateAndAddSession(args.Channel.UserId, args.Channel.UserName);
- session.Channel = args.Channel;
-
+ CreateAndAddSession(args.Channel);
PlayerCountMetric.Set(PlayerCount);
// Synchronize base time.
var msgTimeBase = new MsgSyncTimeBase();
@@ -106,8 +104,7 @@ private void EndSession(object? sender, NetChannelArgs args)
DebugTools.Assert(session.Channel == args.Channel);
SetStatus(session, SessionStatus.Disconnected);
- if (session.AttachedEntity != null)
- EntManager.System().Detach(session.AttachedEntity.Value);
+ SetAttachedEntity(session, null, out _, true);
var viewSys = EntManager.System();
foreach (var eye in session.ViewSubscriptions.ToArray())
diff --git a/Robust.Server/ServerIoC.cs b/Robust.Server/ServerIoC.cs
index feffc05b46c..8d9ca24fc86 100644
--- a/Robust.Server/ServerIoC.cs
+++ b/Robust.Server/ServerIoC.cs
@@ -15,7 +15,6 @@
using Robust.Server.Upload;
using Robust.Server.ViewVariables;
using Robust.Shared;
-using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
@@ -27,7 +26,6 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Replays;
-using Robust.Shared.ResourceManagement;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Upload;
@@ -66,7 +64,6 @@ internal static void RegisterIoC(IDependencyCollection deps)
deps.Register();
deps.Register();
deps.Register();
- deps.Register();
deps.Register();
deps.Register();
deps.Register();
@@ -95,7 +92,6 @@ internal static void RegisterIoC(IDependencyCollection deps)
deps.Register();
deps.Register();
deps.Register();
- deps.Register();
}
}
}
diff --git a/Robust.Server/ServerStatus/DefaultMagicAczProvider.cs b/Robust.Server/ServerStatus/DefaultMagicAczProvider.cs
index 87b266aa8db..8d3e6545e78 100644
--- a/Robust.Server/ServerStatus/DefaultMagicAczProvider.cs
+++ b/Robust.Server/ServerStatus/DefaultMagicAczProvider.cs
@@ -34,12 +34,12 @@ public async Task Package(
var inputPass = graph.Input;
var contentDir = FindContentRootPath(_deps);
- await RobustClientPackaging.WriteContentAssemblies(
+ await RobustSharedPackaging.WriteContentAssemblies(
inputPass,
contentDir,
binFolderPath,
assemblyNames,
- cancel);
+ cancel: cancel);
await RobustClientPackaging.WriteClientResources(contentDir, inputPass, cancel);
diff --git a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs
index f51722c1d9a..71da4a70054 100644
--- a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs
+++ b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs
@@ -167,7 +167,7 @@ private static string GenerateSource(in GeneratorExecutionContext context, IName
getStateInit.Append($@"
{name} = GetNetEntitySet(component.{name}),");
handleStateSetters.Append($@"
- component.{name} = EnsureEntitySet<{componentName}>(state.{name}, uid);");
+ EnsureEntitySet<{componentName}>(state.{name}, uid, component.{name});");
break;
case GlobalEntityUidListName:
@@ -177,7 +177,7 @@ private static string GenerateSource(in GeneratorExecutionContext context, IName
getStateInit.Append($@"
{name} = GetNetEntityList(component.{name}),");
handleStateSetters.Append($@"
- component.{name} = EnsureEntityList<{componentName}>(state.{name}, uid);");
+ EnsureEntityList<{componentName}>(state.{name}, uid, component.{name});");
break;
default:
diff --git a/Robust.Shared/Audio/AudioDebugCommands.cs b/Robust.Shared/Audio/AudioDebugCommands.cs
new file mode 100644
index 00000000000..d3e9992759b
--- /dev/null
+++ b/Robust.Shared/Audio/AudioDebugCommands.cs
@@ -0,0 +1,36 @@
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Console;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+
+namespace Robust.Shared.Audio;
+
+internal sealed class AudioDebugCommands : LocalizedCommands
+{
+ [Dependency] private readonly IEntitySystemManager _entitySystem = default!;
+
+ public override string Command => "audio_length";
+
+ public override void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ if (args.Length != 1)
+ {
+ shell.WriteError(LocalizationManager.GetString("cmd-invalid-arg-number-error"));
+ return;
+ }
+
+ var audioSystem = _entitySystem.GetEntitySystem();
+ var length = audioSystem.GetAudioLength(args[0]);
+ shell.WriteLine(length.ToString());
+ }
+
+ public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
+ {
+ if (args.Length == 1)
+ {
+ return CompletionResult.FromHint(LocalizationManager.GetString("cmd-audio_length-arg-file-name"));
+ }
+
+ return CompletionResult.Empty;
+ }
+}
diff --git a/Robust.Shared/Audio/AudioLoading/AudioLoader.cs b/Robust.Shared/Audio/AudioLoading/AudioLoader.cs
new file mode 100644
index 00000000000..4bf4ae9aa68
--- /dev/null
+++ b/Robust.Shared/Audio/AudioLoading/AudioLoader.cs
@@ -0,0 +1,52 @@
+using System;
+using System.IO;
+
+namespace Robust.Shared.Audio.AudioLoading;
+
+///
+/// Implements functionality for loading audio files.
+///
+///
+///
+internal static class AudioLoader
+{
+ ///
+ /// Test if the given file name is something that we can load.
+ ///
+ ///
+ /// This is detected based on file extension.
+ ///
+ public static bool IsLoadableAudioFile(ReadOnlySpan filename)
+ {
+ var extension = Path.GetExtension(filename);
+ return extension is ".wav" or ".ogg";
+ }
+
+ ///
+ /// Load metadata about an audio file. Can handle all supported audio file types.
+ ///
+ /// Stream containing audio file data to load.
+ /// File name of the audio file. Used to detect which file type it is.
+ public static AudioMetadata LoadAudioMetadata(Stream stream, ReadOnlySpan filename)
+ {
+ var extension = Path.GetExtension(filename);
+ if (extension is ".ogg")
+ {
+ return AudioLoaderOgg.LoadAudioMetadata(stream);
+ }
+ else if (extension is ".wav")
+ {
+ return AudioLoaderWav.LoadAudioMetadata(stream);
+ }
+ else
+ {
+ throw new ArgumentException($"Unknown file type: {extension}");
+ }
+ }
+}
+
+///
+/// Contains basic metadata of an audio file.
+///
+///
+internal record AudioMetadata(TimeSpan Length, int ChannelCount, string? Title = null, string? Artist = null);
diff --git a/Robust.Shared/Audio/AudioLoading/AudioLoaderOgg.cs b/Robust.Shared/Audio/AudioLoading/AudioLoaderOgg.cs
new file mode 100644
index 00000000000..13460d6723a
--- /dev/null
+++ b/Robust.Shared/Audio/AudioLoading/AudioLoaderOgg.cs
@@ -0,0 +1,71 @@
+using System;
+using System.IO;
+using NVorbis;
+
+namespace Robust.Shared.Audio.AudioLoading;
+
+///
+/// Implements functionality for loading ogg audio files.
+///
+///
+internal static class AudioLoaderOgg
+{
+ ///
+ /// Load metadata for an ogg audio file.
+ ///
+ /// Audio file stream to load.
+ public static AudioMetadata LoadAudioMetadata(Stream stream)
+ {
+ using var reader = new VorbisReader(stream);
+ return new AudioMetadata(reader.TotalTime, reader.Channels, reader.Tags.Title, reader.Tags.Artist);
+ }
+
+ ///
+ /// Load an ogg file into raw samples and metadata.
+ ///
+ /// Audio file stream to load.
+ public static OggVorbisData LoadAudioData(Stream stream)
+ {
+ using var vorbis = new NVorbis.VorbisReader(stream, false);
+
+ var sampleRate = vorbis.SampleRate;
+ var channels = vorbis.Channels;
+ var totalSamples = vorbis.TotalSamples;
+
+ var readSamples = 0;
+ var buffer = new float[totalSamples * channels];
+
+ while (readSamples < totalSamples)
+ {
+ var read = vorbis.ReadSamples(buffer, readSamples * channels, buffer.Length - readSamples);
+ if (read == 0)
+ {
+ break;
+ }
+
+ readSamples += read;
+ }
+
+ return new OggVorbisData(totalSamples, sampleRate, channels, buffer, vorbis.Tags.Title, vorbis.Tags.Artist);
+ }
+
+ internal readonly struct OggVorbisData
+ {
+ public readonly long TotalSamples;
+ public readonly long SampleRate;
+ public readonly long Channels;
+ public readonly ReadOnlyMemory Data;
+ public readonly string Title;
+ public readonly string Artist;
+
+ public OggVorbisData(long totalSamples, long sampleRate, long channels, ReadOnlyMemory data, string title, string artist)
+ {
+ TotalSamples = totalSamples;
+ SampleRate = sampleRate;
+ Channels = channels;
+ Data = data;
+ Title = title;
+ Artist = artist;
+ }
+ }
+}
diff --git a/Robust.Shared/Audio/AudioLoading/AudioLoaderWav.cs b/Robust.Shared/Audio/AudioLoading/AudioLoaderWav.cs
new file mode 100644
index 00000000000..422acef6731
--- /dev/null
+++ b/Robust.Shared/Audio/AudioLoading/AudioLoaderWav.cs
@@ -0,0 +1,152 @@
+using System;
+using System.IO;
+using Robust.Shared.Utility;
+
+namespace Robust.Shared.Audio.AudioLoading;
+
+///
+/// Implements functionality for loading wav audio files.
+///
+///
+internal static class AudioLoaderWav
+{
+ ///
+ /// Load metadata for a wav audio file.
+ ///
+ /// Audio file stream to load.
+ public static AudioMetadata LoadAudioMetadata(Stream stream)
+ {
+ // TODO: Don't load entire WAV file just to extract metadata.
+ var data = LoadAudioData(stream);
+ var length = TimeSpan.FromSeconds(data.Data.Length / (double) data.BlockAlign / data.SampleRate);
+ return new AudioMetadata(length, data.NumChannels);
+ }
+
+ ///
+ /// Load a wav file into raw samples and metadata.
+ ///
+ /// Audio file stream to load.
+ public static WavData LoadAudioData(Stream stream)
+ {
+ var reader = new BinaryReader(stream, EncodingHelpers.UTF8, true);
+
+ // Read outer most chunks.
+ Span fourCc = stackalloc byte[4];
+ while (true)
+ {
+ ReadFourCC(reader, fourCc);
+
+ if (!fourCc.SequenceEqual("RIFF"u8))
+ {
+ SkipChunk(reader);
+ continue;
+ }
+
+ return ReadRiffChunk(reader);
+ }
+ }
+
+ private static void SkipChunk(BinaryReader reader)
+ {
+ var length = reader.ReadUInt32();
+ reader.BaseStream.Position += length;
+ }
+
+ private static void ReadFourCC(BinaryReader reader, Span fourCc)
+ {
+ fourCc[0] = reader.ReadByte();
+ fourCc[1] = reader.ReadByte();
+ fourCc[2] = reader.ReadByte();
+ fourCc[3] = reader.ReadByte();
+ }
+
+ private static WavData ReadRiffChunk(BinaryReader reader)
+ {
+ Span format = stackalloc byte[4];
+ reader.ReadUInt32();
+ ReadFourCC(reader, format);
+ if (!format.SequenceEqual("WAVE"u8))
+ {
+ throw new InvalidDataException("File is not a WAVE file.");
+ }
+
+ ReadFourCC(reader, format);
+ if (!format.SequenceEqual("fmt "u8))
+ {
+ throw new InvalidDataException("Expected fmt chunk.");
+ }
+
+ // Read fmt chunk.
+
+ var size = reader.ReadInt32();
+ var afterFmtPos = reader.BaseStream.Position + size;
+
+ var audioType = (WavAudioFormatType)reader.ReadInt16();
+ var channels = reader.ReadInt16();
+ var sampleRate = reader.ReadInt32();
+ var byteRate = reader.ReadInt32();
+ var blockAlign = reader.ReadInt16();
+ var bitsPerSample = reader.ReadInt16();
+
+ if (audioType != WavAudioFormatType.PCM)
+ {
+ throw new NotImplementedException("Unable to support audio types other than PCM.");
+ }
+
+ DebugTools.Assert(byteRate == sampleRate * channels * bitsPerSample / 8);
+
+ // Fmt is not of guaranteed size, so use the size header to skip to the end.
+ reader.BaseStream.Position = afterFmtPos;
+
+ while (true)
+ {
+ ReadFourCC(reader, format);
+ if (!format.SequenceEqual("data"u8))
+ {
+ SkipChunk(reader);
+ continue;
+ }
+
+ break;
+ }
+
+ // We are in the data chunk.
+ size = reader.ReadInt32();
+ var data = reader.ReadBytes(size);
+
+ return new WavData(audioType, channels, sampleRate, byteRate, blockAlign, bitsPerSample, data);
+ }
+
+ ///
+ /// See http://soundfile.sapp.org/doc/WaveFormat/ for reference.
+ ///
+ internal readonly struct WavData
+ {
+ public readonly WavAudioFormatType AudioType;
+ public readonly short NumChannels;
+ public readonly int SampleRate;
+ public readonly int ByteRate;
+ public readonly short BlockAlign;
+ public readonly short BitsPerSample;
+ public readonly ReadOnlyMemory Data;
+
+ public WavData(WavAudioFormatType audioType, short numChannels, int sampleRate, int byteRate,
+ short blockAlign, short bitsPerSample, ReadOnlyMemory data)
+ {
+ AudioType = audioType;
+ NumChannels = numChannels;
+ SampleRate = sampleRate;
+ ByteRate = byteRate;
+ BlockAlign = blockAlign;
+ BitsPerSample = bitsPerSample;
+ Data = data;
+ }
+ }
+
+ internal enum WavAudioFormatType : short
+ {
+ Unknown = 0,
+ PCM = 1,
+ // There's a bunch of other types, those are all unsupported.
+ }
+}
diff --git a/Robust.Shared/Audio/AudioMetadataPrototype.cs b/Robust.Shared/Audio/AudioMetadataPrototype.cs
new file mode 100644
index 00000000000..49188c4d82e
--- /dev/null
+++ b/Robust.Shared/Audio/AudioMetadataPrototype.cs
@@ -0,0 +1,21 @@
+using System;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Robust.Shared.Audio;
+
+///
+/// Stores server-side metadata about an audio file.
+/// These prototypes get automatically generated when packaging the server,
+/// to allow the server to know audio lengths without shipping the large audio files themselves.
+///
+[Prototype(ProtoName)]
+public sealed class AudioMetadataPrototype : IPrototype
+{
+ public const string ProtoName = "audioMetadata";
+
+ [IdDataField] public string ID { get; set; } = string.Empty;
+
+ [DataField]
+ public TimeSpan Length;
+}
diff --git a/Robust.Shared/Audio/SharedAudioManager.cs b/Robust.Shared/Audio/SharedAudioManager.cs
deleted file mode 100644
index f08f73a7227..00000000000
--- a/Robust.Shared/Audio/SharedAudioManager.cs
+++ /dev/null
@@ -1,215 +0,0 @@
-using System;
-using System.IO;
-using Robust.Shared.Utility;
-
-namespace Robust.Shared.Audio;
-
-internal abstract class SharedAudioManager
-{
- #region Loading
-
- public virtual AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
- {
- var vorbis = _readOggVorbis(stream);
- var length = TimeSpan.FromSeconds(vorbis.TotalSamples / (double) vorbis.SampleRate);
- return new AudioStream(null, length, (int) vorbis.Channels, name, vorbis.Title, vorbis.Artist);
- }
-
- public virtual AudioStream LoadAudioWav(Stream stream, string? name = null)
- {
- var wav = _readWav(stream);
- var length = TimeSpan.FromSeconds(wav.Data.Length / (double) wav.BlockAlign / wav.SampleRate);
- return new AudioStream(null, length, wav.NumChannels, name);
- }
-
- public virtual AudioStream LoadAudioRaw(ReadOnlySpan samples, int channels, int sampleRate, string? name = null)
- {
- var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
- return new AudioStream(null, length, channels, name);
- }
-
- #endregion
-
- #region Loading Data
-
- protected OggVorbisData _readOggVorbis(Stream stream)
- {
- using (var vorbis = new NVorbis.VorbisReader(stream, false))
- {
- var sampleRate = vorbis.SampleRate;
- var channels = vorbis.Channels;
- var totalSamples = vorbis.TotalSamples;
-
- var readSamples = 0;
- var buffer = new float[totalSamples * channels];
-
- while (readSamples < totalSamples)
- {
- var read = vorbis.ReadSamples(buffer, readSamples * channels, buffer.Length - readSamples);
- if (read == 0)
- {
- break;
- }
-
- readSamples += read;
- }
-
- return new OggVorbisData(totalSamples, sampleRate, channels, buffer, vorbis.Tags.Title, vorbis.Tags.Artist);
- }
- }
-
- internal readonly struct OggVorbisData
- {
- public readonly long TotalSamples;
- public readonly long SampleRate;
- public readonly long Channels;
- public readonly ReadOnlyMemory Data;
- public readonly string Title;
- public readonly string Artist;
-
- public OggVorbisData(long totalSamples, long sampleRate, long channels, ReadOnlyMemory data, string title, string artist)
- {
- TotalSamples = totalSamples;
- SampleRate = sampleRate;
- Channels = channels;
- Data = data;
- Title = title;
- Artist = artist;
- }
- }
-
- ///
- /// Load up a WAVE file.
- ///
- protected static WavData _readWav(Stream stream)
- {
- var reader = new BinaryReader(stream, EncodingHelpers.UTF8, true);
-
- void SkipChunk()
- {
- var length = reader.ReadUInt32();
- stream.Position += length;
- }
-
- // Read outer most chunks.
- Span fourCc = stackalloc byte[4];
- while (true)
- {
- _readFourCC(reader, fourCc);
-
- if (!fourCc.SequenceEqual("RIFF"u8))
- {
- SkipChunk();
- continue;
- }
-
- return _readRiffChunk(reader);
- }
- }
-
- private static void _skipChunk(BinaryReader reader)
- {
- var length = reader.ReadUInt32();
- reader.BaseStream.Position += length;
- }
-
- private static void _readFourCC(BinaryReader reader, Span fourCc)
- {
- fourCc[0] = reader.ReadByte();
- fourCc[1] = reader.ReadByte();
- fourCc[2] = reader.ReadByte();
- fourCc[3] = reader.ReadByte();
- }
-
- private static WavData _readRiffChunk(BinaryReader reader)
- {
- Span format = stackalloc byte[4];
- reader.ReadUInt32();
- _readFourCC(reader, format);
- if (!format.SequenceEqual("WAVE"u8))
- {
- throw new InvalidDataException("File is not a WAVE file.");
- }
-
- _readFourCC(reader, format);
- if (!format.SequenceEqual("fmt "u8))
- {
- throw new InvalidDataException("Expected fmt chunk.");
- }
-
- // Read fmt chunk.
-
- var size = reader.ReadInt32();
- var afterFmtPos = reader.BaseStream.Position + size;
-
- var audioType = (WavAudioFormatType) reader.ReadInt16();
- var channels = reader.ReadInt16();
- var sampleRate = reader.ReadInt32();
- var byteRate = reader.ReadInt32();
- var blockAlign = reader.ReadInt16();
- var bitsPerSample = reader.ReadInt16();
-
- if (audioType != WavAudioFormatType.PCM)
- {
- throw new NotImplementedException("Unable to support audio types other than PCM.");
- }
-
- DebugTools.Assert(byteRate == sampleRate * channels * bitsPerSample / 8);
-
- // Fmt is not of guaranteed size, so use the size header to skip to the end.
- reader.BaseStream.Position = afterFmtPos;
-
- while (true)
- {
- _readFourCC(reader, format);
- if (!format.SequenceEqual("data"u8))
- {
- _skipChunk(reader);
- continue;
- }
-
- break;
- }
-
- // We are in the data chunk.
- size = reader.ReadInt32();
- var data = reader.ReadBytes(size);
-
- return new WavData(audioType, channels, sampleRate, byteRate, blockAlign, bitsPerSample, data);
- }
-
- ///
- /// See http://soundfile.sapp.org/doc/WaveFormat/ for reference.
- ///
- internal readonly struct WavData
- {
- public readonly WavAudioFormatType AudioType;
- public readonly short NumChannels;
- public readonly int SampleRate;
- public readonly int ByteRate;
- public readonly short BlockAlign;
- public readonly short BitsPerSample;
- public readonly ReadOnlyMemory Data;
-
- public WavData(WavAudioFormatType audioType, short numChannels, int sampleRate, int byteRate,
- short blockAlign, short bitsPerSample, ReadOnlyMemory data)
- {
- AudioType = audioType;
- NumChannels = numChannels;
- SampleRate = sampleRate;
- ByteRate = byteRate;
- BlockAlign = blockAlign;
- BitsPerSample = bitsPerSample;
- Data = data;
- }
- }
-
- internal enum WavAudioFormatType : short
- {
- Unknown = 0,
- PCM = 1,
- // There's a bunch of other types, those are all unsupported.
- }
-
- #endregion
-}
diff --git a/Robust.Shared/Audio/Systems/SharedAudioSystem.cs b/Robust.Shared/Audio/Systems/SharedAudioSystem.cs
index 9649e239134..08fc7d71eea 100644
--- a/Robust.Shared/Audio/Systems/SharedAudioSystem.cs
+++ b/Robust.Shared/Audio/Systems/SharedAudioSystem.cs
@@ -10,7 +10,6 @@
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
-using Robust.Shared.ResourceManagement.ResourceTypes;
using Robust.Shared.Serialization;
using Robust.Shared.Spawners;
using Robust.Shared.Timing;
@@ -30,7 +29,6 @@ public abstract partial class SharedAudioSystem : EntitySystem
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] protected readonly IPrototypeManager ProtoMan = default!;
- [Dependency] private readonly IResourceCache _resource = default!;
[Dependency] protected readonly IRobustRandom RandMan = default!;
///
@@ -188,10 +186,14 @@ public void SetVolume(EntityUid? entity, float value, Components.AudioComponent?
///
public TimeSpan GetAudioLength(string filename)
{
- var resource = _resource.GetResource(filename);
- return resource.AudioStream.Length;
+ if (!filename.StartsWith("/"))
+ throw new ArgumentException("Path must be rooted");
+
+ return GetAudioLengthImpl(filename);
}
+ protected abstract TimeSpan GetAudioLengthImpl(string filename);
+
///
/// Stops the specified audio entity from playing.
///
diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs
index 484357e944f..279a3ecbfca 100644
--- a/Robust.Shared/CVars.cs
+++ b/Robust.Shared/CVars.cs
@@ -54,6 +54,16 @@ protected CVars()
public static readonly CVarDef NetReceiveBufferSize =
CVarDef.Create("net.receivebuffersize", 131071, CVar.ARCHIVE);
+ ///
+ /// Size of the pool for Lidgren's array buffers to send messages.
+ /// Set to 0 to disable pooling; max is 8192.
+ ///
+ ///
+ /// Higher just means more potentially wasted space and slower pool retrieval.
+ ///
+ public static readonly CVarDef NetPoolSize =
+ CVarDef.Create("net.pool_size", 512, CVar.CLIENT | CVar.SERVER);
+
///
/// Maximum UDP payload size to send.
///
@@ -120,6 +130,13 @@ protected CVars()
public static readonly CVarDef NetBufferSize =
CVarDef.Create("net.buffer_size", 2, CVar.ARCHIVE | CVar.CLIENTONLY);
+ ///
+ /// The maximum size of the game state buffer. If this is exceeded the client will request a full game state.
+ /// Values less than will be ignored.
+ ///
+ public static readonly CVarDef NetMaxBufferSize =
+ CVarDef.Create("net.max_buffer_size", 512, CVar.ARCHIVE | CVar.CLIENTONLY);
+
///
/// Enable verbose game state/networking logging.
///
@@ -1022,8 +1039,7 @@ protected CVars()
CVarDef.Create("audio.raycast_length", SharedAudioSystem.DefaultSoundRange, CVar.ARCHIVE | CVar.CLIENTONLY);
public static readonly CVarDef AudioZOffset =
- CVarDef.Create("audio.z_offset", -1f, CVar.REPLICATED);
-
+ CVarDef.Create("audio.z_offset", -5f, CVar.REPLICATED);
/*
* PLAYER
diff --git a/Robust.Shared/Console/Commands/MapCommands.cs b/Robust.Shared/Console/Commands/MapCommands.cs
index e0ff05b29f8..b9dfe0be647 100644
--- a/Robust.Shared/Console/Commands/MapCommands.cs
+++ b/Robust.Shared/Console/Commands/MapCommands.cs
@@ -131,6 +131,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
internal sealed class ListMapsCommand : LocalizedCommands
{
+ [Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IMapManager _map = default!;
public override string Command => "lsmap";
@@ -144,10 +145,13 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
foreach (var mapId in _map.GetAllMapIds().OrderBy(id => id.Value))
{
- msg.AppendFormat("{0}: init: {1}, paused: {2}, ent: {3}, grids: {4}\n",
- mapId, _map.IsMapInitialized(mapId),
+ var mapUid = _map.GetMapEntityId(mapId);
+
+ msg.AppendFormat("{0}: {1}, init: {2}, paused: {3}, nent: {4}, grids: {5}\n",
+ mapId, _entManager.GetComponent(mapUid).EntityName,
+ _map.IsMapInitialized(mapId),
_map.IsMapPaused(mapId),
- _map.GetMapEntityId(mapId),
+ _entManager.GetNetEntity(_map.GetMapEntityId(mapId)),
string.Join(",", _map.GetAllGrids(mapId).Select(grid => grid.Owner)));
}
diff --git a/Robust.Shared/Containers/BaseContainer.cs b/Robust.Shared/Containers/BaseContainer.cs
index 8108930b571..5e07ae6c8cc 100644
--- a/Robust.Shared/Containers/BaseContainer.cs
+++ b/Robust.Shared/Containers/BaseContainer.cs
@@ -79,20 +79,7 @@ internal void Init(string id, EntityUid owner, ContainerManagerComponent compone
Owner = owner;
}
- ///
- /// Attempts to insert the entity into this container.
- ///
- ///
- /// If the insertion is successful, the inserted entity will end up parented to the
- /// container entity, and the inserted entity's local position will be set to the zero vector.
- ///
- /// The entity to insert.
- ///
- /// False if the entity could not be inserted.
- ///
- /// Thrown if this container is a child of the entity,
- /// which would cause infinite loops.
- ///
+ [Obsolete("Use container system method")]
public bool Insert(
EntityUid toinsert,
IEntityManager? entMan = null,
@@ -138,16 +125,23 @@ public bool Insert(
return false;
}
+ transform ??= transformQuery.GetComponent(toinsert);
+
//Verify we can insert into this container
- if (!force && !containerSys.CanInsert(toinsert, this))
+ if (!force && !containerSys.CanInsert(toinsert, this, containerXform: ownerTransform))
return false;
// Please somebody ecs containers
var lookupSys = entMan.EntitySysManager.GetEntitySystem();
var xformSys = entMan.EntitySysManager.GetEntitySystem();
- transform ??= transformQuery.GetComponent(toinsert);
meta ??= entMan.GetComponent(toinsert);
+ if (meta.EntityLifeStage >= EntityLifeStage.Terminating)
+ {
+ Logger.ErrorS("container",
+ $"Attempted to insert a terminating entity {entMan.ToPrettyString(toinsert)} into a container {ID} in entity: {entMan.ToPrettyString(Owner)}.");
+ return false;
+ }
// remove from any old containers.
if ((meta.Flags & MetaDataFlags.InContainer) != 0 &&
@@ -270,16 +264,7 @@ internal void RecursivelyUpdateJoints(
/// Whether to assume that the container is currently empty.
protected internal virtual bool CanInsert(EntityUid toInsert, bool assumeEmpty, IEntityManager entMan) => true;
- ///
- /// Attempts to remove the entity from this container.
- ///
- /// If false, this operation will not rigger a move or parent change event. Ignored if
- /// destination is not null
- /// If true, this will not perform can-remove checks.
- /// Where to place the entity after removing. Avoids unnecessary broadphase updates.
- /// If not specified, and reparent option is true, then the entity will either be inserted into a parent
- /// container, the grid, or the map.
- /// Optional final local rotation after removal. Avoids redundant move events.
+ [Obsolete("Use container system method")]
public bool Remove(
EntityUid toRemove,
IEntityManager? entMan = null,
@@ -359,7 +344,7 @@ public bool Remove(
return true;
}
- [Obsolete("use force option in Remove()")]
+ [Obsolete("Use container system method")]
public void ForceRemove(EntityUid toRemove, IEntityManager? entMan = null, MetaDataComponent? meta = null)
=> Remove(toRemove, entMan, meta: meta, reparent: false, force: true);
diff --git a/Robust.Shared/Containers/SharedContainerSystem.Insert.cs b/Robust.Shared/Containers/SharedContainerSystem.Insert.cs
index 681696fb699..62bd224d201 100644
--- a/Robust.Shared/Containers/SharedContainerSystem.Insert.cs
+++ b/Robust.Shared/Containers/SharedContainerSystem.Insert.cs
@@ -1,10 +1,41 @@
+using System;
using Robust.Shared.GameObjects;
-using Robust.Shared.Utility;
+using Robust.Shared.Physics.Components;
namespace Robust.Shared.Containers;
public abstract partial class SharedContainerSystem
{
+
+ ///
+ /// Attempts to insert the entity into this container.
+ ///
+ ///
+ /// If the insertion is successful, the inserted entity will end up parented to the
+ /// container entity, and the inserted entity's local position will be set to the zero vector.
+ ///
+ /// The entity to insert.
+ /// The container to insert into.
+ /// The container's transform component.
+ /// Whether to bypass normal insertion checks.
+ /// False if the entity could not be inserted.
+ ///
+ /// Thrown if this container is a child of the entity,
+ /// which would cause infinite loops.
+ ///
+ public bool Insert(Entity toInsert,
+ BaseContainer container,
+ TransformComponent? containerXform = null,
+ bool force = false)
+ {
+ // Cannot Use Resolve(ref toInsert) as the physics component is optional
+ if (!Resolve(toInsert.Owner, ref toInsert.Comp1, ref toInsert.Comp2))
+ return false;
+
+ // TODO move logic over to the system.
+ return container.Insert(toInsert, EntityManager, toInsert, containerXform, toInsert, toInsert, force);
+ }
+
///
/// Checks if the entity can be inserted into the given container.
///
@@ -13,8 +44,8 @@ public abstract partial class SharedContainerSystem
public bool CanInsert(
EntityUid toInsert,
BaseContainer container,
- TransformComponent? toInsertXform = null,
- bool assumeEmpty = false)
+ bool assumeEmpty = false,
+ TransformComponent? containerXform = null)
{
if (container.Owner == toInsert)
return false;
@@ -25,15 +56,12 @@ public bool CanInsert(
if (!container.CanInsert(toInsert, assumeEmpty, EntityManager))
return false;
- if (!TransformQuery.Resolve(toInsert, ref toInsertXform))
- return false;
-
// no, you can't put maps or grids into containers
if (_mapQuery.HasComponent(toInsert) || _gridQuery.HasComponent(toInsert))
return false;
// Prevent circular insertion.
- if (_transform.ContainsEntity(toInsertXform, container.Owner))
+ if (_transform.ContainsEntity(toInsert, (container.Owner, containerXform)))
return false;
var insertAttemptEvent = new ContainerIsInsertingAttemptEvent(container, toInsert, assumeEmpty);
diff --git a/Robust.Shared/Containers/SharedContainerSystem.Remove.cs b/Robust.Shared/Containers/SharedContainerSystem.Remove.cs
index 7662ae7b50f..512136abcd7 100644
--- a/Robust.Shared/Containers/SharedContainerSystem.Remove.cs
+++ b/Robust.Shared/Containers/SharedContainerSystem.Remove.cs
@@ -1,9 +1,43 @@
using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
namespace Robust.Shared.Containers;
public abstract partial class SharedContainerSystem
{
+ ///
+ /// Attempts to remove the entity from this container.
+ ///
+ ///
+ /// If the insertion is successful, the inserted entity will end up parented to the
+ /// container entity, and the inserted entity's local position will be set to the zero vector.
+ ///
+ /// The entity to remove.
+ /// The container to remove from.
+ /// If false, this operation will not rigger a move or parent change event. Ignored if
+ /// destination is not null
+ /// If true, this will not perform can-remove checks.
+ /// Where to place the entity after removing. Avoids unnecessary broadphase updates.
+ /// If not specified, and reparent option is true, then the entity will either be inserted into a parent
+ /// container, the grid, or the map.
+ /// Optional final local rotation after removal. Avoids redundant move events.
+ public bool Remove(
+ Entity toRemove,
+ BaseContainer container,
+ bool reparent = true,
+ bool force = false,
+ EntityCoordinates? destination = null,
+ Angle? localRotation = null)
+ {
+ // Cannot Use Resolve(ref toInsert) as the physics component is optional
+ if (!Resolve(toRemove.Owner, ref toRemove.Comp1, ref toRemove.Comp2))
+ return false;
+
+ // TODO move logic over to the system.
+ return container.Remove(toRemove, EntityManager, toRemove, toRemove, reparent, force, destination, localRotation);
+ }
+
///
/// Checks if the entity can be removed from this container.
///
diff --git a/Robust.Shared/Containers/SharedContainerSystem.cs b/Robust.Shared/Containers/SharedContainerSystem.cs
index ad348b061b4..113c1731352 100644
--- a/Robust.Shared/Containers/SharedContainerSystem.cs
+++ b/Robust.Shared/Containers/SharedContainerSystem.cs
@@ -216,35 +216,27 @@ public bool IsEntityInContainer(EntityUid uid, MetaDataComponent? meta = null)
}
///
- /// Recursively if the entity, or any parent entity, is inside of a container.
+ /// Recursively check if the entity or any parent is inside of a container.
///
/// If the entity is inside of a container.
public bool IsEntityOrParentInContainer(
EntityUid uid,
MetaDataComponent? meta = null,
- TransformComponent? xform = null,
- EntityQuery? metas = null,
- EntityQuery? xforms = null)
+ TransformComponent? xform = null)
{
- if (meta == null)
- {
- metas ??= GetEntityQuery();
- meta = metas.Value.GetComponent(uid);
- }
+ if (!MetaQuery.Resolve(uid, ref meta))
+ return false;
if ((meta.Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer)
return true;
- if (xform == null)
- {
- xforms ??= GetEntityQuery();
- xform = xforms.Value.GetComponent(uid);
- }
+ if (!TransformQuery.Resolve(uid, ref xform))
+ return false;
if (!xform.ParentUid.Valid)
return false;
- return IsEntityOrParentInContainer(xform.ParentUid, metas: metas, xforms: xforms);
+ return IsEntityOrParentInContainer(xform.ParentUid);
}
///
diff --git a/Robust.Shared/ContentPack/DirLoader.cs b/Robust.Shared/ContentPack/DirLoader.cs
index 2e5cb5f8d66..0776965f538 100644
--- a/Robust.Shared/ContentPack/DirLoader.cs
+++ b/Robust.Shared/ContentPack/DirLoader.cs
@@ -10,7 +10,6 @@
namespace Robust.Shared.ContentPack
{
- [Virtual]
internal partial class ResourceManager
{
///
diff --git a/Robust.Shared/ContentPack/ResourceManager.cs b/Robust.Shared/ContentPack/ResourceManager.cs
index 205e45fa50b..5d804d1eb06 100644
--- a/Robust.Shared/ContentPack/ResourceManager.cs
+++ b/Robust.Shared/ContentPack/ResourceManager.cs
@@ -15,6 +15,7 @@ namespace Robust.Shared.ContentPack
///
/// Virtual file system for all disk resources.
///
+ [Virtual]
internal partial class ResourceManager : IResourceManagerInternal
{
[Dependency] private readonly IConfigurationManager _config = default!;
diff --git a/Robust.Shared/ContentPack/Sandbox.yml b/Robust.Shared/ContentPack/Sandbox.yml
index bb55fa1a523..c895dfcafff 100644
--- a/Robust.Shared/ContentPack/Sandbox.yml
+++ b/Robust.Shared/ContentPack/Sandbox.yml
@@ -73,6 +73,12 @@ WhitelistedNamespaces:
# * The API is not *relevant* to content. e.g. System.Type.IsAnsiClass.
# * I am lazy these API lists are huge dude.
Types:
+ NetSerializer:
+ NetListAsArray`1:
+ Fields:
+ - "System.Collections.Generic.IReadOnlyCollection`1 Value"
+ Methods:
+ - "bool get_HasContents()"
Lidgren.Network:
NetBuffer:
All: True
diff --git a/Robust.Shared/GameObjects/ComponentEventArgs.cs b/Robust.Shared/GameObjects/ComponentEventArgs.cs
index 9ddb92b237d..3a668f9748b 100644
--- a/Robust.Shared/GameObjects/ComponentEventArgs.cs
+++ b/Robust.Shared/GameObjects/ComponentEventArgs.cs
@@ -31,9 +31,9 @@ public ComponentEventArgs(IComponent component, EntityUid owner)
public readonly struct AddedComponentEventArgs
{
public readonly ComponentEventArgs BaseArgs;
- public readonly CompIdx ComponentType;
+ public readonly ComponentRegistration ComponentType;
- public AddedComponentEventArgs(ComponentEventArgs baseArgs, CompIdx componentType)
+ public AddedComponentEventArgs(ComponentEventArgs baseArgs, ComponentRegistration componentType)
{
BaseArgs = baseArgs;
ComponentType = componentType;
diff --git a/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs b/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs
index 13eb1354926..4f2684f2886 100644
--- a/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs
+++ b/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs
@@ -12,16 +12,10 @@ namespace Robust.Shared.GameObjects
[RegisterComponent, NetworkedComponent, Access(typeof(SharedEyeSystem)), AutoGenerateComponentState(true)]
public sealed partial class EyeComponent : Component
{
- #region Client
-
- [ViewVariables] internal Eye? _eye = default!;
-
- public IEye? Eye => _eye;
+ public const int DefaultVisibilityMask = 1;
[ViewVariables]
- public MapCoordinates? Position => _eye?.Position;
-
- #endregion
+ public readonly Eye Eye = new();
///
/// If not null, this entity is used to update the eye's position instead of just using the component's owner.
@@ -37,6 +31,9 @@ public sealed partial class EyeComponent : Component
[ViewVariables(VVAccess.ReadWrite), DataField("drawFov"), AutoNetworkedField]
public bool DrawFov = true;
+ [ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+ public bool DrawLight = true;
+
// yes it's not networked, don't ask.
[ViewVariables(VVAccess.ReadWrite), DataField("rotation")]
public Angle Rotation;
@@ -47,8 +44,6 @@ public sealed partial class EyeComponent : Component
[ViewVariables(VVAccess.ReadWrite), DataField("offset"), AutoNetworkedField]
public Vector2 Offset;
- public const int DefaultVisibilityMask = 1;
-
///
/// The visibility mask for this eye.
/// The player will be able to get updates for entities whose layers match the mask.
@@ -58,7 +53,7 @@ public sealed partial class EyeComponent : Component
}
///
- /// Single layer used for Eye visiblity. Controls what entities they are allowed to see.
+ /// Single layer used for Eye visibility. Controls what entities they are allowed to see.
///
public sealed class VisibilityMaskLayer {}
}
diff --git a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs
index 06c54e093e6..fce60a9346c 100644
--- a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs
+++ b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs
@@ -311,6 +311,7 @@ public EntityCoordinates Coordinates
/// This is effectively a more complete version of
///
[ViewVariables(VVAccess.ReadWrite)]
+ [Obsolete("Use TransformSystem.GetMapCoordinates")]
public MapCoordinates MapPosition => new(WorldPosition, MapID);
///
diff --git a/Robust.Shared/GameObjects/EntityEventBus.Directed.cs b/Robust.Shared/GameObjects/EntityEventBus.Directed.cs
index bf495ad42cb..3f8da6f0672 100644
--- a/Robust.Shared/GameObjects/EntityEventBus.Directed.cs
+++ b/Robust.Shared/GameObjects/EntityEventBus.Directed.cs
@@ -346,7 +346,7 @@ public void OnComponentAdded(in AddedComponentEventArgs e)
{
_subscriptionLock = true;
- EntAddComponent(e.BaseArgs.Owner, e.ComponentType);
+ EntAddComponent(e.BaseArgs.Owner, e.ComponentType.Idx);
}
public void OnComponentRemoved(in RemovedComponentEventArgs e)
diff --git a/Robust.Shared/GameObjects/EntityManager.Components.cs b/Robust.Shared/GameObjects/EntityManager.Components.cs
index b482fbec87d..068ffbd6be7 100644
--- a/Robust.Shared/GameObjects/EntityManager.Components.cs
+++ b/Robust.Shared/GameObjects/EntityManager.Components.cs
@@ -317,7 +317,7 @@ private void AddComponentInternal(EntityUid uid, T component, ComponentRegist
component.Networked = false;
}
- var eventArgs = new AddedComponentEventArgs(new ComponentEventArgs(component, uid), reg.Idx);
+ var eventArgs = new AddedComponentEventArgs(new ComponentEventArgs(component, uid), reg);
ComponentAdded?.Invoke(eventArgs);
_eventBus.OnComponentAdded(eventArgs);
@@ -1416,6 +1416,15 @@ public TComp1 GetComponent(EntityUid uid)
throw new KeyNotFoundException($"Entity {uid} does not have a component of type {typeof(TComp1)}");
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining), Pure]
+ public Entity Get(EntityUid uid)
+ {
+ if (_traitDict.TryGetValue(uid, out var comp) && !comp.Deleted)
+ return new Entity(uid, (TComp1) comp);
+
+ throw new KeyNotFoundException($"Entity {uid} does not have a component of type {typeof(TComp1)}");
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool TryGetComponent([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(true)] out TComp1? component)
@@ -1479,6 +1488,12 @@ public bool Resolve(EntityUid uid, [NotNullWhen(true)] ref TComp1? component, bo
return false;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining), Pure]
+ public bool Resolve(ref Entity entity, bool logMissing = true)
+ {
+ return Resolve(entity.Owner, ref entity.Comp, logMissing);
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public TComp1? CompOrNull(EntityUid uid)
diff --git a/Robust.Shared/GameObjects/EntityManager.Network.cs b/Robust.Shared/GameObjects/EntityManager.Network.cs
index f2aa154dacd..cd0285bb336 100644
--- a/Robust.Shared/GameObjects/EntityManager.Network.cs
+++ b/Robust.Shared/GameObjects/EntityManager.Network.cs
@@ -256,8 +256,7 @@ public virtual EntityCoordinates EnsureCoordinates(NetCoordinates netCoordina
///
public HashSet GetEntitySet(HashSet netEntities)
{
- var entities = new HashSet();
- entities.EnsureCapacity(netEntities.Count);
+ var entities = new HashSet(netEntities.Count);
foreach (var netEntity in netEntities)
{
@@ -292,6 +291,16 @@ public HashSet EnsureEntitySet(HashSet netEntities, Ent
return entities;
}
+ public void EnsureEntitySet(HashSet netEntities, EntityUid callerEntity, HashSet entities)
+ {
+ entities.Clear();
+ entities.EnsureCapacity(netEntities.Count);
+ foreach (var netEntity in netEntities)
+ {
+ entities.Add(EnsureEntity(netEntity, callerEntity));
+ }
+ }
+
///
public List EnsureEntityList(List netEntities, EntityUid callerEntity)
{
@@ -305,6 +314,16 @@ public List EnsureEntityList(List netEntities, EntityUi
return entities;
}
+ public void EnsureEntityList(List netEntities, EntityUid callerEntity, List entities)
+ {
+ entities.Clear();
+ entities.EnsureCapacity(netEntities.Count);
+ foreach (var netEntity in netEntities)
+ {
+ entities.Add(EnsureEntity(netEntity, callerEntity));
+ }
+ }
+
///
public List GetEntityList(ICollection netEntities)
{
diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs
index 4f768ff1074..45701bdc50b 100644
--- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs
+++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs
@@ -154,9 +154,26 @@ protected void Dirty(EntityUid uid, IComponent component, MetaDataComponent? met
/// Marks a component as dirty. This also implicitly dirties the entity this component belongs to.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- protected void Dirty(Entity ent, MetaDataComponent? meta = null) where T : IComponent
+ protected void Dirty(Entity ent, MetaDataComponent? meta = null) where T : IComponent?
{
- EntityManager.Dirty(ent.Owner, ent.Comp, meta);
+ var comp = ent.Comp;
+ if (comp == null && !EntityManager.TryGetComponent(ent.Owner, out comp))
+ return;
+
+ EntityManager.Dirty(ent.Owner, comp, meta);
+ }
+
+ ///
+ /// Marks a component as dirty. This also implicitly dirties the entity this component belongs to.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected void Dirty(Entity ent) where T : IComponent?
+ {
+ var comp = ent.Comp1;
+ if (comp == null && !EntityManager.TryGetComponent(ent.Owner, out comp))
+ return;
+
+ EntityManager.Dirty(ent.Owner, comp, ent.Comp2);
}
///
@@ -1007,12 +1024,24 @@ protected HashSet EnsureEntitySet(HashSet netEntities,
return EntityManager.EnsureEntitySet(netEntities, callerEntity);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected void EnsureEntitySet(HashSet netEntities, EntityUid callerEntity, HashSet entities)
+ {
+ EntityManager.EnsureEntitySet(netEntities, callerEntity, entities);
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected List EnsureEntityList(List netEntities, EntityUid callerEntity)
{
return EntityManager.EnsureEntityList(netEntities, callerEntity);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected void EnsureEntityList(List netEntities, EntityUid callerEntity, List entities)
+ {
+ EntityManager.EnsureEntityList(netEntities, callerEntity, entities);
+ }
+
///
/// Returns the of a . Returns if it doesn't exist.
///
diff --git a/Robust.Shared/GameObjects/EntitySystem.Resolve.cs b/Robust.Shared/GameObjects/EntitySystem.Resolve.cs
index c1750ee0976..1f54929faf0 100644
--- a/Robust.Shared/GameObjects/EntitySystem.Resolve.cs
+++ b/Robust.Shared/GameObjects/EntitySystem.Resolve.cs
@@ -32,7 +32,7 @@ protected bool Resolve(EntityUid uid, [NotNullWhen(true)] ref TComp? comp
return found;
}
- ///
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool Resolve(EntityUid uid, [NotNullWhen(true)] ref MetaDataComponent? component,
bool logMissing = true)
@@ -40,7 +40,7 @@ protected bool Resolve(EntityUid uid, [NotNullWhen(true)] ref MetaDataComponent?
return EntityManager.MetaQuery.Resolve(uid, ref component);
}
- ///
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool Resolve(EntityUid uid, [NotNullWhen(true)] ref TransformComponent? component,
bool logMissing = true)
diff --git a/Robust.Shared/GameObjects/EntityUid.cs b/Robust.Shared/GameObjects/EntityUid.cs
index 3cce6c28ab4..3974fb4eea7 100644
--- a/Robust.Shared/GameObjects/EntityUid.cs
+++ b/Robust.Shared/GameObjects/EntityUid.cs
@@ -85,7 +85,12 @@ public override bool Equals(object? obj)
///
public override int GetHashCode()
{
- return Id;
+ unchecked
+ {
+ // * 397 for whenever we get versioning back
+ // and avoid hashcode bugs in the interim.
+ return Id.GetHashCode() * 397;
+ }
}
///
diff --git a/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs b/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs
index c75b3bd02af..163c7a69a01 100644
--- a/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs
+++ b/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs
@@ -171,11 +171,16 @@ private bool AnyEntitiesIntersecting(EntityUid lookupUid,
private void RecursiveAdd(EntityUid uid, ref ValueList toAdd)
{
- var childEnumerator = _xformQuery.GetComponent(uid).ChildEnumerator;
+ if (!_xformQuery.TryGetComponent(uid, out var xform))
+ {
+ Log.Error($"Encountered deleted entity {uid} while performing entity lookup.");
+ return;
+ }
+ toAdd.Add(uid);
+ var childEnumerator = xform.ChildEnumerator;
while (childEnumerator.MoveNext(out var child))
{
- toAdd.Add(child.Value);
RecursiveAdd(child.Value, ref toAdd);
}
}
@@ -185,6 +190,11 @@ private void AddContained(HashSet intersecting, LookupFlags flags)
if ((flags & LookupFlags.Contained) == 0x0 || intersecting.Count == 0)
return;
+ // TODO PERFORMANCE.
+ // toAdd only exists because we can't add directly to intersecting w/o enumeration issues.
+ // If we assume that there are more entities in containers than there are entities in the intersecting set, then
+ // we would be better off creating a fixed-size EntityUid array and coping all intersecting entities into that
+ // instead of creating a value list here that needs to be resized.
var toAdd = new ValueList();
foreach (var uid in intersecting)
@@ -196,7 +206,6 @@ private void AddContained(HashSet intersecting, LookupFlags flags)
{
foreach (var contained in con.ContainedEntities)
{
- toAdd.Add(contained);
RecursiveAdd(contained, ref toAdd);
}
}
diff --git a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs
index 83d0aaaaa69..c91b5609024 100644
--- a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs
+++ b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs
@@ -6,7 +6,11 @@
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Collision;
+using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Dynamics;
+using Robust.Shared.Physics.Systems;
using Robust.Shared.Utility;
namespace Robust.Shared.GameObjects;
@@ -15,18 +19,6 @@ public sealed partial class EntityLookupSystem
{
#region Private
- private void AddComponentsIntersecting(
- EntityUid lookupUid,
- HashSet intersecting,
- Box2 worldAABB,
- LookupFlags flags,
- EntityQuery query) where T : IComponent
- {
- var intersectingEntities = new HashSet>();
- AddEntitiesIntersecting(lookupUid, intersectingEntities, worldAABB, flags, query);
- intersecting.UnionWith(intersectingEntities.Select(e => e.Comp));
- }
-
private void AddEntitiesIntersecting(
EntityUid lookupUid,
HashSet> intersecting,
@@ -88,6 +80,155 @@ private void AddEntitiesIntersecting(
}
}
+ private void AddEntitiesIntersecting(
+ EntityUid lookupUid,
+ HashSet> intersecting,
+ IPhysShape shape,
+ Box2 worldAABB,
+ LookupFlags flags,
+ EntityQuery query) where T : IComponent
+ {
+ var lookup = _broadQuery.GetComponent(lookupUid);
+ var invMatrix = _transform.GetInvWorldMatrix(lookupUid);
+ var localAABB = invMatrix.TransformBox(worldAABB);
+ var transform = new Transform(0);
+ var state = new QueryState(
+ intersecting,
+ shape,
+ transform,
+ _fixtures,
+ _physics,
+ _transform,
+ _manifoldManager,
+ query,
+ _fixturesQuery,
+ (flags & LookupFlags.Sensors) != 0
+ );
+
+ if ((flags & LookupFlags.Dynamic) != 0x0)
+ {
+ lookup.DynamicTree.QueryAabb(ref state, static (ref QueryState state, in FixtureProxy value) =>
+ {
+ if (!state.Sensors && !value.Fixture.Hard)
+ return true;
+
+ if (!state.Query.TryGetComponent(value.Entity, out var comp))
+ return true;
+
+ var intersectingTransform = state.Physics.GetPhysicsTransform(value.Entity);
+ if (!state.Manifolds.TestOverlap(state.Shape, 0, value.Fixture.Shape, value.ChildIndex, state.Transform, intersectingTransform))
+ {
+ return true;
+ }
+
+ state.Intersecting.Add((value.Entity, comp));
+ return true;
+ }, localAABB, (flags & LookupFlags.Approximate) != 0x0);
+ }
+
+ if ((flags & (LookupFlags.Static)) != 0x0)
+ {
+ lookup.StaticTree.QueryAabb(ref state, static (ref QueryState state, in FixtureProxy value) =>
+ {
+ if (!state.Sensors && !value.Fixture.Hard)
+ return true;
+
+ if (!state.Query.TryGetComponent(value.Entity, out var comp))
+ return true;
+
+ var intersectingTransform = state.Physics.GetPhysicsTransform(value.Entity);
+ if (!state.Manifolds.TestOverlap(state.Shape, 0, value.Fixture.Shape, value.ChildIndex, state.Transform, intersectingTransform))
+ {
+ return true;
+ }
+
+ state.Intersecting.Add((value.Entity, comp));
+ return true;
+ }, localAABB, (flags & LookupFlags.Approximate) != 0x0);
+ }
+
+ if ((flags & LookupFlags.StaticSundries) == LookupFlags.StaticSundries)
+ {
+ lookup.StaticSundriesTree.QueryAabb(ref state, static (ref QueryState state, in EntityUid value) =>
+ {
+ if (!state.Query.TryGetComponent(value, out var comp))
+ return true;
+
+ var intersectingTransform = state.Physics.GetPhysicsTransform(value);
+
+ if (state.FixturesQuery.TryGetComponent(value, out var fixtures))
+ {
+ bool anyFixture = false;
+ foreach (var fixture in fixtures.Fixtures.Values)
+ {
+ if (!state.Sensors && !fixture.Hard)
+ continue;
+
+ anyFixture = true;
+ for (var i = 0; i < fixture.Shape.ChildCount; i++)
+ {
+ if (state.Manifolds.TestOverlap(state.Shape, 0, fixture.Shape, i, state.Transform,
+ intersectingTransform))
+ {
+ state.Intersecting.Add((value, comp));
+ return true;
+ }
+ }
+ }
+
+ if (anyFixture)
+ return true;
+ }
+
+ if (state.Fixtures.TestPoint(state.Shape, state.Transform, intersectingTransform.Position))
+ state.Intersecting.Add((value, comp));
+
+ return true;
+ }, localAABB, (flags & LookupFlags.Approximate) != 0x0);
+ }
+
+ if ((flags & LookupFlags.Sundries) != 0x0)
+ {
+ lookup.SundriesTree.QueryAabb(ref state, static (ref QueryState state,
+ in EntityUid value) =>
+ {
+ if (!state.Query.TryGetComponent(value, out var comp))
+ return true;
+
+ var intersectingTransform = state.Physics.GetPhysicsTransform(value);
+
+ if (state.FixturesQuery.TryGetComponent(value, out var fixtures))
+ {
+ bool anyFixture = false;
+ foreach (var fixture in fixtures.Fixtures.Values)
+ {
+ if (!state.Sensors && !fixture.Hard)
+ continue;
+
+ anyFixture = true;
+ for (var i = 0; i < fixture.Shape.ChildCount; i++)
+ {
+ if (state.Manifolds.TestOverlap(state.Shape, 0, fixture.Shape, i, state.Transform,
+ intersectingTransform))
+ {
+ state.Intersecting.Add((value, comp));
+ return true;
+ }
+ }
+ }
+
+ if (anyFixture)
+ return true;
+ }
+
+ if (state.Fixtures.TestPoint(state.Shape, state.Transform, intersectingTransform.Position))
+ state.Intersecting.Add((value, comp));
+
+ return true;
+ }, localAABB, (flags & LookupFlags.Approximate) != 0x0);
+ }
+ }
+
private bool AnyComponentsIntersecting(
EntityUid lookupUid,
Box2 worldAABB,
@@ -270,7 +411,7 @@ public bool AnyComponentsIntersecting(Type type, MapId mapId, Box2 worldAABB, En
if (xform.MapID != mapId ||
!worldAABB.Contains(_transform.GetWorldPosition(xform)) ||
((flags & LookupFlags.Contained) == 0x0 &&
- _container.IsEntityOrParentInContainer(uid, _metaQuery.GetComponent(uid), xform, _metaQuery, _xformQuery)))
+ _container.IsEntityOrParentInContainer(uid, null, xform)))
{
continue;
}
@@ -308,15 +449,6 @@ public bool AnyComponentsIntersecting(Type type, MapId mapId, Box2 worldAABB, En
return false;
}
- [Obsolete]
- public HashSet GetComponentsIntersecting(Type type, MapId mapId, Box2 worldAABB, LookupFlags flags = DefaultFlags)
- {
- var intersectingEntities = new HashSet>();
- GetEntitiesIntersecting(type, mapId, worldAABB, intersectingEntities, flags);
- var intersecting = new HashSet(intersectingEntities.Select(e => e.Comp));
- return intersecting;
- }
-
public void GetEntitiesIntersecting(Type type, MapId mapId, Box2 worldAABB, HashSet> intersecting, LookupFlags flags = DefaultFlags)
{
DebugTools.Assert(typeof(IComponent).IsAssignableFrom(type));
@@ -332,7 +464,7 @@ public void GetEntitiesIntersecting(Type type, MapId mapId, Box2 worldAABB, Hash
if (xform.MapID != mapId ||
!worldAABB.Contains(_transform.GetWorldPosition(xform)) ||
((flags & LookupFlags.Contained) == 0x0 &&
- _container.IsEntityOrParentInContainer(uid, _metaQuery.GetComponent(uid), xform, _metaQuery, _xformQuery)))
+ _container.IsEntityOrParentInContainer(uid, _metaQuery.GetComponent(uid), xform)))
{
continue;
}
@@ -366,14 +498,6 @@ public void GetEntitiesIntersecting(Type type, MapId mapId, Box2 worldAABB, Hash
}
}
- [Obsolete]
- public HashSet GetComponentsIntersecting(MapId mapId, Box2 worldAABB, LookupFlags flags = DefaultFlags) where T : IComponent
- {
- var intersectingEntities = new HashSet>();
- GetEntitiesIntersecting(mapId, worldAABB, intersectingEntities, flags);
- return new HashSet(intersectingEntities.Select(e => e.Comp));
- }
-
public void GetEntitiesIntersecting(MapId mapId, Box2 worldAABB, HashSet> entities, LookupFlags flags = DefaultFlags) where T : IComponent
{
if (mapId == MapId.Nullspace) return;
@@ -416,14 +540,163 @@ public void GetEntitiesIntersecting(MapId mapId, Box2 worldAABB, HashSet GetComponentsInRange(EntityCoordinates coordinates, float range) where T : IComponent
+ public void GetEntitiesIntersecting(Type type, MapId mapId, IPhysShape shape, HashSet> intersecting, LookupFlags flags = DefaultFlags)
{
- var mapPos = coordinates.ToMap(EntityManager, _transform);
- return GetComponentsInRange(mapPos, range);
+ DebugTools.Assert(typeof(IComponent).IsAssignableFrom(type));
+ if (mapId == MapId.Nullspace)
+ return;
+
+ var shapeTransform = new Transform(0);
+ var worldAABB = shape.ComputeAABB(shapeTransform, 0);
+ var sensors = (flags & LookupFlags.Sensors) != 0;
+ if (!UseBoundsQuery(type, worldAABB.Height * worldAABB.Width))
+ {
+ foreach (var (uid, comp) in EntityManager.GetAllComponents(type, true))
+ {
+ var xform = _xformQuery.GetComponent(uid);
+ var (pos, rot) = _transform.GetWorldPositionRotation(xform);
+
+ if (xform.MapID != mapId ||
+ !worldAABB.Contains(pos) ||
+ ((flags & LookupFlags.Contained) == 0x0 &&
+ _container.IsEntityOrParentInContainer(uid, _metaQuery.GetComponent(uid), xform)))
+ {
+ continue;
+ }
+
+ if (_fixturesQuery.TryGetComponent(uid, out var fixtures))
+ {
+ var transform = new Transform(pos, rot);
+
+ bool anyFixture = false;
+ foreach (var fixture in fixtures.Fixtures.Values)
+ {
+ if (!sensors && !fixture.Hard)
+ continue;
+
+ anyFixture = true;
+ for (var i = 0; i < fixture.Shape.ChildCount; i++)
+ {
+ if (_manifoldManager.TestOverlap(shape, 0, fixture.Shape, i, shapeTransform, transform))
+ {
+ goto found;
+ }
+ }
+ }
+
+ if (anyFixture)
+ continue;
+ }
+
+ if (!_fixtures.TestPoint(shape, shapeTransform, pos))
+ continue;
+
+ found:
+ intersecting.Add((uid, comp));
+ }
+ }
+ else
+ {
+ var query = EntityManager.GetEntityQuery(type);
+
+ // Get grid entities
+ var state = new GridQueryState(intersecting, shape, worldAABB, this, flags, query);
+
+ _mapManager.FindGridsIntersecting(mapId, worldAABB, ref state,
+ static (EntityUid uid, MapGridComponent grid, ref GridQueryState state) =>
+ {
+ state.Lookup.AddEntitiesIntersecting(uid, state.Intersecting, state.Shape, state.WorldAABB, state.Flags, state.Query);
+ return true;
+ }, (flags & LookupFlags.Approximate) != 0x0);
+
+ // Get map entities
+ var mapUid = _mapManager.GetMapEntityId(mapId);
+ AddEntitiesIntersecting(mapUid, intersecting, shape, worldAABB, flags, query);
+ AddContained(intersecting, flags, query);
+ }
+ }
+
+ public void GetEntitiesIntersecting(MapId mapId, IPhysShape shape, HashSet> entities, LookupFlags flags = DefaultFlags) where T : IComponent
+ {
+ if (mapId == MapId.Nullspace) return;
+
+ var shapeTransform = new Transform(0);
+ var worldAABB = shape.ComputeAABB(shapeTransform, 0);
+ var sensors = (flags & LookupFlags.Sensors) != 0;
+ if (!UseBoundsQuery(worldAABB.Height * worldAABB.Width))
+ {
+ var query = AllEntityQuery();
+
+ while (query.MoveNext(out var uid, out var comp, out var xform))
+ {
+ var (pos, rot) = _transform.GetWorldPositionRotation(xform);
+
+ if (xform.MapID != mapId || !worldAABB.Contains(pos))
+ continue;
+
+ if (_fixturesQuery.TryGetComponent(uid, out var fixtures))
+ {
+ var transform = new Transform(pos, rot);
+ bool anyFixture = false;
+ foreach (var fixture in fixtures.Fixtures.Values)
+ {
+ if (!sensors && !fixture.Hard)
+ continue;
+
+ anyFixture = true;
+ for (var i = 0; i < fixture.Shape.ChildCount; i++)
+ {
+ if (_manifoldManager.TestOverlap(shape, 0, fixture.Shape, i, shapeTransform, transform))
+ {
+ goto found;
+ }
+ }
+ }
+
+ if (anyFixture)
+ continue;
+ }
+
+ if (!_fixtures.TestPoint(shape, shapeTransform, pos))
+ continue;
+
+ found:
+ entities.Add((uid, comp));
+ }
+ }
+ else
+ {
+ var query = GetEntityQuery();
+
+ // Get grid entities
+ var state = (this, shape, worldAABB, flags, query, entities);
+
+ _mapManager.FindGridsIntersecting(mapId, worldAABB, ref state,
+ static (EntityUid uid, MapGridComponent grid,
+ ref (EntityLookupSystem system,
+ IPhysShape shape,
+ Box2 worldAABB,
+ LookupFlags flags,
+ EntityQuery query,
+ HashSet> intersecting) tuple) =>
+ {
+ tuple.system.AddEntitiesIntersecting(uid, tuple.intersecting, tuple.shape, tuple.worldAABB, tuple.flags, tuple.query);
+ return true;
+ }, (flags & LookupFlags.Approximate) != 0x0);
+
+ // Get map entities
+ var mapUid = _mapManager.GetMapEntityId(mapId);
+ AddEntitiesIntersecting(mapUid, entities, shape, worldAABB, flags, query);
+ AddContained(entities, flags, query);
+ }
}
+ #endregion
+
+ #region EntityCoordinates
+
public void GetEntitiesInRange(EntityCoordinates coordinates, float range, HashSet> entities) where T : IComponent
{
var mapPos = coordinates.ToMap(EntityManager, _transform);
@@ -441,35 +714,34 @@ public HashSet> GetEntitiesInRange(EntityCoordinates coordinates, f
#region MapCoordinates
- [Obsolete]
- public HashSet GetComponentsInRange(Type type, MapCoordinates coordinates, float range)
+ public HashSet> GetEntitiesInRange(Type type, MapCoordinates coordinates, float range)
{
- DebugTools.Assert(typeof(IComponent).IsAssignableFrom(type));
- return GetComponentsInRange(type, coordinates.MapId, coordinates.Position, range);
+ var entities = new HashSet>();
+ GetEntitiesInRange(type, coordinates, range, entities);
+ return entities;
}
- public HashSet> GetEntitiesInRange(Type type, MapCoordinates coordinates, float range)
+ public void GetEntitiesInRange(Type type, MapCoordinates coordinates, float range, HashSet> entities)
{
DebugTools.Assert(typeof(IComponent).IsAssignableFrom(type));
- var entities = new HashSet>();
GetEntitiesInRange(type, coordinates.MapId, coordinates.Position, range, entities);
- return entities;
}
+ [Obsolete]
public HashSet GetComponentsInRange(MapCoordinates coordinates, float range) where T : IComponent
{
return GetComponentsInRange(coordinates.MapId, coordinates.Position, range);
}
- public void GetEntitiesInRange(MapCoordinates coordinates, float range, HashSet> entities) where T : IComponent
+ public void GetEntitiesInRange(MapCoordinates coordinates, float range, HashSet> entities, LookupFlags flags = DefaultFlags) where T : IComponent
{
- GetEntitiesInRange(coordinates.MapId, coordinates.Position, range, entities);
+ GetEntitiesInRange(coordinates.MapId, coordinates.Position, range, entities, flags);
}
- public HashSet> GetEntitiesInRange(MapCoordinates coordinates, float range) where T : IComponent
+ public HashSet> GetEntitiesInRange