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(MapCoordinates coordinates, float range, LookupFlags flags = DefaultFlags) where T : IComponent { var entities = new HashSet>(); - GetEntitiesInRange(coordinates.MapId, coordinates.Position, range, entities); + GetEntitiesInRange(coordinates.MapId, coordinates.Position, range, entities, flags); return entities; } @@ -477,40 +749,15 @@ public HashSet> GetEntitiesInRange(MapCoordinates coordinates, floa #region MapId - public bool AnyComponentsInRange(Type type, MapId mapId, Vector2 worldPos, float range) - { - DebugTools.Assert(typeof(IComponent).IsAssignableFrom(type)); - DebugTools.Assert(range > 0, "Range must be a positive float"); - - if (mapId == MapId.Nullspace) return false; - - // TODO: Actual circles - var rangeVec = new Vector2(range, range); - - var worldAABB = new Box2(worldPos - rangeVec, worldPos + rangeVec); - return AnyComponentsIntersecting(type, mapId, worldAABB); - } - - [Obsolete] - public HashSet GetComponentsInRange(Type type, MapId mapId, Vector2 worldPos, float range) - { - var entities = new HashSet>(); - GetEntitiesInRange(type, mapId, worldPos, range, entities); - return new HashSet(entities.Select(e => e.Comp)); - } - - public void GetEntitiesInRange(Type type, MapId mapId, Vector2 worldPos, float range, HashSet> entities) + public void GetEntitiesInRange(Type type, MapId mapId, Vector2 worldPos, float range, HashSet> entities, LookupFlags flags = DefaultFlags) { DebugTools.Assert(typeof(IComponent).IsAssignableFrom(type)); DebugTools.Assert(range > 0, "Range must be a positive float"); if (mapId == MapId.Nullspace) return; - // TODO: Actual circles - var rangeVec = new Vector2(range, range); - - var worldAABB = new Box2(worldPos - rangeVec, worldPos + rangeVec); - GetEntitiesIntersecting(type, mapId, worldAABB, entities); + var circle = new PhysShapeCircle(range, worldPos); + GetEntitiesIntersecting(type, mapId, circle, entities, flags); } [Obsolete] @@ -521,18 +768,41 @@ public HashSet GetComponentsInRange(MapId mapId, Vector2 worldPos, float r return new HashSet(entities.Select(e => e.Comp)); } - public void GetEntitiesInRange(MapId mapId, Vector2 worldPos, float range, HashSet> entities) where T : IComponent + public void GetEntitiesInRange(MapId mapId, Vector2 worldPos, float range, HashSet> entities, LookupFlags flags = DefaultFlags) where T : IComponent { - DebugTools.Assert(range > 0, "Range must be a positive float"); + GetEntitiesInRange(mapId, new PhysShapeCircle(range, worldPos), entities, flags); + } - if (mapId == MapId.Nullspace) return; + public void GetEntitiesInRange(MapId mapId, IPhysShape shape, HashSet> entities, LookupFlags flags = DefaultFlags) where T : IComponent + { + DebugTools.Assert(shape.Radius > 0, "Range must be a positive float"); - // TODO: Actual circles - var rangeVec = new Vector2(range, range); + if (mapId == MapId.Nullspace) return; - var worldAABB = new Box2(worldPos - rangeVec, worldPos + rangeVec); - GetEntitiesIntersecting(mapId, worldAABB, entities); + GetEntitiesIntersecting(mapId, shape, entities, flags); } #endregion + + private readonly record struct GridQueryState( + HashSet> Intersecting, + IPhysShape Shape, + Box2 WorldAABB, + EntityLookupSystem Lookup, + LookupFlags Flags, + EntityQuery Query + ) where T : IComponent; + + private readonly record struct QueryState( + HashSet> Intersecting, + IPhysShape Shape, + Transform Transform, + FixtureSystem Fixtures, + SharedPhysicsSystem Physics, + SharedTransformSystem TransformSystem, + IManifoldManager Manifolds, + EntityQuery Query, + EntityQuery FixturesQuery, + bool Sensors + ) where T : IComponent; } diff --git a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs index 1bb690a3c39..6a858ad5774 100644 --- a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs +++ b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs @@ -11,9 +11,11 @@ using Robust.Shared.Network; using Robust.Shared.Physics; using Robust.Shared.Physics.BroadPhase; +using Robust.Shared.Physics.Collision; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Dynamics; using Robust.Shared.Physics.Events; +using Robust.Shared.Physics.Systems; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -72,11 +74,14 @@ public record struct WorldAABBEvent public sealed partial class EntityLookupSystem : EntitySystem { + [Dependency] private readonly IManifoldManager _manifoldManager = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly INetManager _netMan = default!; [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly FixtureSystem _fixtures = default!; [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; private EntityQuery _broadQuery; @@ -368,7 +373,7 @@ private void AddOrUpdatePhysicsTree( FixturesComponent manager, EntityQuery xformQuery) { - DebugTools.Assert(!_container.IsEntityOrParentInContainer(body.Owner, null, xform, null, xformQuery)); + DebugTools.Assert(!_container.IsEntityOrParentInContainer(body.Owner, null, xform)); DebugTools.Assert(xform.Broadphase == null || xform.Broadphase == new BroadphaseData(broadphase.Owner, physicsMap.Owner, body.CanCollide, body.BodyType == BodyType.Static)); DebugTools.Assert(broadphase.Owner == broadUid); @@ -843,7 +848,7 @@ public bool TryFindBroadphase( TransformComponent xform, [NotNullWhen(true)] out BroadphaseComponent? broadphase) { - if (xform.MapID == MapId.Nullspace || _container.IsEntityOrParentInContainer(xform.Owner, null, xform, null, _xformQuery)) + if (xform.MapID == MapId.Nullspace || _container.IsEntityOrParentInContainer(xform.Owner, null, xform)) { broadphase = null; return false; diff --git a/Robust.Shared/GameObjects/Systems/SharedEyeSystem.cs b/Robust.Shared/GameObjects/Systems/SharedEyeSystem.cs index 6575f9a0ac8..41473b22c61 100644 --- a/Robust.Shared/GameObjects/Systems/SharedEyeSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedEyeSystem.cs @@ -6,20 +6,20 @@ namespace Robust.Shared.GameObjects; public abstract class SharedEyeSystem : EntitySystem { - [Dependency] protected readonly SharedTransformSystem TransformSystem = default!; - /// /// Refreshes all values for IEye with the component. /// - public void UpdateEye(EyeComponent component) + public void UpdateEye(Entity entity) { - if (component._eye == null) + var component = entity.Comp; + if (!Resolve(entity, ref component)) return; - component._eye.Offset = component.Offset; - component._eye.DrawFov = component.DrawFov; - component._eye.Rotation = component.Rotation; - component._eye.Zoom = component.Zoom; + component.Eye.Offset = component.Offset; + component.Eye.DrawFov = component.DrawFov; + component.Eye.DrawLight = component.DrawLight; + component.Eye.Rotation = component.Rotation; + component.Eye.Zoom = component.Zoom; } public void SetOffset(EntityUid uid, Vector2 value, EyeComponent? eyeComponent = null) @@ -31,10 +31,7 @@ public void SetOffset(EntityUid uid, Vector2 value, EyeComponent? eyeComponent = return; eyeComponent.Offset = value; - if (eyeComponent._eye != null) - { - eyeComponent._eye.Offset = value; - } + eyeComponent.Eye.Offset = value; Dirty(uid, eyeComponent); } @@ -47,13 +44,23 @@ public void SetDrawFov(EntityUid uid, bool value, EyeComponent? eyeComponent = n return; eyeComponent.DrawFov = value; - if (eyeComponent._eye != null) - { - eyeComponent._eye.DrawFov = value; - } + eyeComponent.Eye.DrawFov = value; Dirty(uid, eyeComponent); } + public void SetDrawLight(Entity entity, bool value) + { + if (!Resolve(entity, ref entity.Comp)) + return; + + if (entity.Comp.DrawLight == value) + return; + + entity.Comp.DrawLight = value; + entity.Comp.Eye.DrawLight = value; + Dirty(entity); + } + public void SetRotation(EntityUid uid, Angle rotation, EyeComponent? eyeComponent = null) { if (!Resolve(uid, ref eyeComponent)) @@ -63,10 +70,7 @@ public void SetRotation(EntityUid uid, Angle rotation, EyeComponent? eyeComponen return; eyeComponent.Rotation = rotation; - if (eyeComponent._eye != null) - { - eyeComponent._eye.Rotation = rotation; - } + eyeComponent.Eye.Rotation = rotation; } public void SetTarget(EntityUid uid, EntityUid? value, EyeComponent? eyeComponent = null) @@ -90,10 +94,7 @@ public void SetZoom(EntityUid uid, Vector2 value, EyeComponent? eyeComponent = n return; eyeComponent.Zoom = value; - if (eyeComponent._eye != null) - { - eyeComponent._eye.Zoom = value; - } + eyeComponent.Eye.Zoom = value; } public void SetVisibilityMask(EntityUid uid, int value, EyeComponent? eyeComponent = null) diff --git a/Robust.Shared/GameObjects/Systems/SharedGridTraversalSystem.cs b/Robust.Shared/GameObjects/Systems/SharedGridTraversalSystem.cs index e0bd3110076..05e038002a5 100644 --- a/Robust.Shared/GameObjects/Systems/SharedGridTraversalSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedGridTraversalSystem.cs @@ -103,7 +103,7 @@ private void HandleMove( // Attach them to map / they are on an invalid grid if (oldGridId != null) { - _transform.SetParent(entity, xform, _mapManager.GetMapEntityIdOrThrow(xform.MapID)); + _transform.SetParent(entity, xform, xform.MapUid!.Value); var ev = new ChangedGridEvent(entity, oldGridId, null); RaiseLocalEvent(entity, ref ev); } diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs index 57ad4a35eb4..b309d0ea807 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs @@ -689,9 +689,8 @@ internal TileRef GetTileRef(EntityUid uid, MapGridComponent grid, MapChunk mapCh public IEnumerable GetAllTiles(EntityUid uid, MapGridComponent grid, bool ignoreEmpty = true) { - foreach (var kvChunk in grid.Chunks) + foreach (var chunk in grid.Chunks.Values) { - var chunk = kvChunk.Value; for (ushort x = 0; x < grid.ChunkSize; x++) { for (ushort y = 0; y < grid.ChunkSize; y++) @@ -772,71 +771,81 @@ public void SetTiles(EntityUid uid, MapGridComponent grid, List<(Vector2i GridIn RegenerateCollision(uid, grid, modified); } - public IEnumerable GetLocalTilesIntersecting(EntityUid uid, MapGridComponent grid, Box2Rotated localArea, bool ignoreEmpty = true, + public TilesEnumerator GetLocalTilesEnumerator(EntityUid uid, MapGridComponent grid, Box2 aabb, + bool ignoreEmpty = true, Predicate? predicate = null) { - var localAABB = localArea.CalcBoundingBox(); - return GetLocalTilesIntersecting(uid, grid, localAABB, ignoreEmpty, predicate); + var enumerator = new TilesEnumerator(this, ignoreEmpty, predicate, uid, grid, aabb); + return enumerator; } - public IEnumerable GetTilesIntersecting(EntityUid uid, MapGridComponent grid, Box2Rotated worldArea, bool ignoreEmpty = true, + public TilesEnumerator GetTilesEnumerator(EntityUid uid, MapGridComponent grid, Box2 aabb, bool ignoreEmpty = true, Predicate? predicate = null) { - var matrix = _transform.GetInvWorldMatrix(uid); - var localArea = matrix.TransformBox(worldArea); + var invMatrix = _transform.GetInvWorldMatrix(uid); + var localAABB = invMatrix.TransformBox(aabb); + var enumerator = new TilesEnumerator(this, ignoreEmpty, predicate, uid, grid, localAABB); + return enumerator; + } - foreach (var tile in GetLocalTilesIntersecting(uid, grid, localArea, ignoreEmpty, predicate)) + public TilesEnumerator GetTilesEnumerator(EntityUid uid, MapGridComponent grid, Box2Rotated bounds, bool ignoreEmpty = true, + Predicate? predicate = null) + { + var invMatrix = _transform.GetInvWorldMatrix(uid); + var localAABB = invMatrix.TransformBox(bounds); + var enumerator = new TilesEnumerator(this, ignoreEmpty, predicate, uid, grid, localAABB); + return enumerator; + } + + public IEnumerable GetLocalTilesIntersecting(EntityUid uid, MapGridComponent grid, Box2 localAABB, bool ignoreEmpty = true, + Predicate? predicate = null) + { + var enumerator = new TilesEnumerator(this, ignoreEmpty, predicate, uid, grid, localAABB); + + while (enumerator.MoveNext(out var tileRef)) { - yield return tile; + yield return tileRef; } } - public IEnumerable GetTilesIntersecting(EntityUid uid, MapGridComponent grid, Box2 worldArea, bool ignoreEmpty = true, + public IEnumerable GetLocalTilesIntersecting(EntityUid uid, MapGridComponent grid, Box2Rotated localArea, bool ignoreEmpty = true, Predicate? predicate = null) { - var matrix = _transform.GetInvWorldMatrix(uid); - var localArea = matrix.TransformBox(worldArea); + var localAABB = localArea.CalcBoundingBox(); - foreach (var tile in GetLocalTilesIntersecting(uid, grid, localArea, ignoreEmpty, predicate)) + var enumerator = new TilesEnumerator(this, ignoreEmpty, predicate, uid, grid, localAABB); + + while (enumerator.MoveNext(out var tileRef)) { - yield return tile; + yield return tileRef; } } - public IEnumerable GetLocalTilesIntersecting(EntityUid uid, MapGridComponent grid, Box2 localArea, bool ignoreEmpty = true, + public IEnumerable GetTilesIntersecting(EntityUid uid, MapGridComponent grid, Box2Rotated worldArea, bool ignoreEmpty = true, Predicate? predicate = null) { - // TODO: Should move the intersecting calls onto mapmanager system and then allow people to pass in xform / xformquery - // that way we can avoid the GetComp here. - var gridTileLb = new Vector2i((int)Math.Floor(localArea.Left), (int)Math.Floor(localArea.Bottom)); - // If we have 20.1 we want to include that tile but if we have 20 then we don't. - var gridTileRt = new Vector2i((int)Math.Ceiling(localArea.Right), (int)Math.Ceiling(localArea.Top)); + var matrix = _transform.GetInvWorldMatrix(uid); + var localArea = matrix.TransformBox(worldArea); - for (var x = gridTileLb.X; x < gridTileRt.X; x++) - { - for (var y = gridTileLb.Y; y < gridTileRt.Y; y++) - { - var gridChunk = GridTileToChunkIndices(uid, grid, new Vector2i(x, y)); + var enumerator = new TilesEnumerator(this, ignoreEmpty, predicate, uid, grid, localArea); - if (grid.Chunks.TryGetValue(gridChunk, out var chunk)) - { - var chunkTile = chunk.GridTileToChunkTile(new Vector2i(x, y)); - var tile = GetTileRef(uid, grid, chunk, (ushort)chunkTile.X, (ushort)chunkTile.Y); + while (enumerator.MoveNext(out var tileRef)) + { + yield return tileRef; + } + } - if (ignoreEmpty && tile.Tile.IsEmpty) - continue; + public IEnumerable GetTilesIntersecting(EntityUid uid, MapGridComponent grid, Box2 worldArea, bool ignoreEmpty = true, + Predicate? predicate = null) + { + var matrix = _transform.GetInvWorldMatrix(uid); + var localArea = matrix.TransformBox(worldArea); - if (predicate == null || predicate(tile)) - yield return tile; - } - else if (!ignoreEmpty) - { - var tile = new TileRef(uid, x, y, Tile.Empty); + var enumerator = new TilesEnumerator(this, ignoreEmpty, predicate, uid, grid, localArea); - if (predicate == null || predicate(tile)) - yield return tile; - } - } + while (enumerator.MoveNext(out var tileRef)) + { + yield return tileRef; } } @@ -986,12 +995,25 @@ public IEnumerable GetAnchoredEntities(EntityUid uid, MapGridComponen // create an entire chunk for it. var gridChunkPos = GridTileToChunkIndices(uid, grid, pos); - if (!grid.Chunks.TryGetValue(gridChunkPos, out var chunk)) return Enumerable.Empty(); + if (!grid.Chunks.TryGetValue(gridChunkPos, out var chunk)) + return Enumerable.Empty(); var chunkTile = chunk.GridTileToChunkTile(pos); return chunk.GetSnapGridCell((ushort)chunkTile.X, (ushort)chunkTile.Y); } + public void GetAnchoredEntities(Entity grid, Vector2i pos, List list) + { + var gridChunkPos = GridTileToChunkIndices(grid.Owner, grid.Comp, pos); + if (!grid.Comp.Chunks.TryGetValue(gridChunkPos, out var chunk)) + return; + + var chunkTile = chunk.GridTileToChunkTile(pos); + var anchored = chunk.GetSnapGrid((ushort) chunkTile.X, (ushort) chunkTile.Y); + if (anchored != null) + list.AddRange(anchored); + } + public AnchoredEntitiesEnumerator GetAnchoredEntitiesEnumerator(EntityUid uid, MapGridComponent grid, Vector2i pos) { var gridChunkPos = GridTileToChunkIndices(uid, grid, pos); @@ -1008,22 +1030,32 @@ public AnchoredEntitiesEnumerator GetAnchoredEntitiesEnumerator(EntityUid uid, M public IEnumerable GetLocalAnchoredEntities(EntityUid uid, MapGridComponent grid, Box2 localAABB) { - foreach (var tile in GetLocalTilesIntersecting(uid, grid, localAABB, true, null)) + var enumerator = new TilesEnumerator(this, true, null, uid, grid, localAABB); + + while (enumerator.MoveNext(out var tileRef)) { - foreach (var ent in GetAnchoredEntities(uid, grid, tile.GridIndices)) + var anchoredEnumerator = GetAnchoredEntitiesEnumerator(uid, grid, tileRef.GridIndices); + + while (anchoredEnumerator.MoveNext(out var ent)) { - yield return ent; + yield return ent.Value; } } } public IEnumerable GetAnchoredEntities(EntityUid uid, MapGridComponent grid, Box2 worldAABB) { - foreach (var tile in GetTilesIntersecting(uid, grid, worldAABB)) + var invWorldMatrix = _transform.GetInvWorldMatrix(uid); + var localAABB = invWorldMatrix.TransformBox(worldAABB); + var enumerator = new TilesEnumerator(this, true, null, uid, grid, localAABB); + + while (enumerator.MoveNext(out var tileRef)) { - foreach (var ent in GetAnchoredEntities(uid, grid, tile.GridIndices)) + var anchoredEnumerator = GetAnchoredEntitiesEnumerator(uid, grid, tileRef.GridIndices); + + while (anchoredEnumerator.MoveNext(out var ent)) { - yield return ent; + yield return ent.Value; } } } @@ -1279,6 +1311,9 @@ public bool CollidesWithGrid(EntityUid uid, MapGridComponent grid, Vector2i indi } public Vector2i GridTileToChunkIndices(EntityUid uid, MapGridComponent grid, Vector2i gridTile) + => GridTileToChunkIndices(grid, gridTile); + + public Vector2i GridTileToChunkIndices(MapGridComponent grid, Vector2i gridTile) { var x = (int)Math.Floor(gridTile.X / (float) grid.ChunkSize); var y = (int)Math.Floor(gridTile.Y / (float) grid.ChunkSize); @@ -1321,6 +1356,36 @@ public bool TryGetTileRef(EntityUid uid, MapGridComponent grid, Vector2i indices return true; } + public bool TryGetTile(MapGridComponent grid, Vector2i indices, out Tile tile) + { + var chunkIndices = GridTileToChunkIndices(grid, indices); + if (!grid.Chunks.TryGetValue(chunkIndices, out var chunk)) + { + tile = default; + return false; + } + + var cTileIndices = chunk.GridTileToChunkTile(indices); + tile = chunk.Tiles[cTileIndices.X, cTileIndices.Y]; + return true; + } + + /// + /// Attempts to get the for the tile at the given grid indices. This will throw an + /// exception if the tile at this location has no registered tile definition. + /// + public bool TryGetTileDef(MapGridComponent grid, Vector2i indices, [NotNullWhen(true)] out ITileDefinition? tileDef) + { + if (!TryGetTile(grid, indices, out var tile)) + { + tileDef = null; + return false; + } + + tileDef = _tileMan[tile.TypeId]; + return true; + } + public bool TryGetTileRef(EntityUid uid, MapGridComponent grid, EntityCoordinates coords, out TileRef tile) { return TryGetTileRef(uid, grid, CoordinatesToTile(uid, grid, coords), out tile); @@ -1377,4 +1442,98 @@ private void OnTileModified(EntityUid uid, MapGridComponent grid, MapChunk mapCh RegenerateCollision(uid, grid, mapChunk); } } + + /// + /// Iterates the local tiles of the specified data. + /// + public struct TilesEnumerator + { + private readonly SharedMapSystem _mapSystem; + + private readonly EntityUid _uid; + private readonly MapGridComponent _grid; + private readonly bool _ignoreEmpty; + private readonly Predicate? _predicate; + + private readonly int _lowerY; + private readonly int _upperX; + private readonly int _upperY; + + private int _x; + private int _y; + + public TilesEnumerator( + SharedMapSystem mapSystem, + bool ignoreEmpty, + Predicate? predicate, + EntityUid uid, + MapGridComponent grid, + Box2 aabb) + { + _mapSystem = mapSystem; + + _uid = uid; + _grid = grid; + _ignoreEmpty = ignoreEmpty; + _predicate = predicate; + + // TODO: Should move the intersecting calls onto mapmanager system and then allow people to pass in xform / xformquery + // that way we can avoid the GetComp here. + var gridTileLb = new Vector2i((int)Math.Floor(aabb.Left), (int)Math.Floor(aabb.Bottom)); + // If we have 20.1 we want to include that tile but if we have 20 then we don't. + var gridTileRt = new Vector2i((int)Math.Ceiling(aabb.Right), (int)Math.Ceiling(aabb.Top)); + + _x = gridTileLb.X; + _y = gridTileLb.Y; + _lowerY = gridTileLb.Y; + _upperX = gridTileRt.X; + _upperY = gridTileRt.Y; + } + + public bool MoveNext(out TileRef tile) + { + if (_x >= _upperX) + { + tile = TileRef.Zero; + return false; + } + + var gridTile = new Vector2i(_x, _y); + + _y++; + + if (_y >= _upperY) + { + _x++; + _y = _lowerY; + } + + var gridChunk = _mapSystem.GridTileToChunkIndices(_uid, _grid, gridTile); + + if (_grid.Chunks.TryGetValue(gridChunk, out var chunk)) + { + var chunkTile = chunk.GridTileToChunkTile(gridTile); + tile = _mapSystem.GetTileRef(_uid, _grid, chunk, (ushort)chunkTile.X, (ushort)chunkTile.Y); + + if (_ignoreEmpty && tile.Tile.IsEmpty) + return MoveNext(out tile); + + if (_predicate == null || _predicate(tile)) + { + return true; + } + } + else if (!_ignoreEmpty) + { + tile = new TileRef(_uid, gridTile.X, gridTile.Y, Tile.Empty); + + if (_predicate == null || _predicate(tile)) + { + return true; + } + } + + return MoveNext(out tile); + } + } } diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs index 002235c59d7..42f7feac44d 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs @@ -12,6 +12,7 @@ namespace Robust.Shared.GameObjects { public abstract partial class SharedMapSystem : EntitySystem { + [Dependency] private readonly ITileDefinitionManager _tileMan = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] protected readonly IMapManager MapManager = default!; [Dependency] private readonly IMapManagerInternal _mapInternal = default!; diff --git a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs index 51baa68e999..de6d681042c 100644 --- a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs +++ b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs @@ -1,11 +1,8 @@ using JetBrains.Annotations; using Robust.Shared.GameStates; -using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Physics; -using Robust.Shared.Physics.Systems; -using Robust.Shared.Timing; using Robust.Shared.Utility; using System; using System.Linq; @@ -65,12 +62,7 @@ internal void ReAnchor( RaiseLocalEvent(uid, ref ev); } - [Obsolete("Use overload that takes an explicit EntityUid for the grid instead.")] - public bool AnchorEntity(EntityUid uid, TransformComponent xform, MapGridComponent grid, Vector2i tileIndices) - { - return AnchorEntity(uid, xform, grid.Owner, grid, tileIndices); - } - + [Obsolete("Use Entity variant")] public bool AnchorEntity( EntityUid uid, TransformComponent xform, @@ -78,12 +70,22 @@ public bool AnchorEntity( MapGridComponent grid, Vector2i tileIndices) { - if (!_map.AddToSnapGridCell(gridUid, grid, tileIndices, uid)) + return AnchorEntity((uid, xform), (gridUid, grid), tileIndices); + } + + public bool AnchorEntity( + Entity entity, + Entity grid, + Vector2i tileIndices) + { + var (uid, xform) = entity; + if (!_map.AddToSnapGridCell(grid, grid, tileIndices, uid)) return false; - var wasAnchored = xform._anchored; - Dirty(uid, xform); + var wasAnchored = entity.Comp._anchored; xform._anchored = true; + var meta = MetaData(uid); + Dirty(entity, meta); // Mark as static before doing position changes, to avoid the velocity change on parent change. _physics.TrySetBodyType(uid, BodyType.Static, xform: xform); @@ -95,22 +97,36 @@ public bool AnchorEntity( } // Anchor snapping. If there is a coordinate change, it will dirty the component for us. - var pos = new EntityCoordinates(gridUid, _map.GridTileToLocal(gridUid, grid, tileIndices).Position); - SetCoordinates(uid, xform, pos, unanchor: false); - + var pos = new EntityCoordinates(grid, _map.GridTileToLocal(grid, grid, tileIndices).Position); + SetCoordinates((uid, xform, meta), pos, unanchor: false); return true; } + [Obsolete("Use Entity variants")] public bool AnchorEntity(EntityUid uid, TransformComponent xform, MapGridComponent grid) { var tileIndices = _map.TileIndicesFor(grid.Owner, grid, xform.Coordinates); - return AnchorEntity(uid, xform, grid, tileIndices); + return AnchorEntity(uid, xform, grid.Owner, grid, tileIndices); } public bool AnchorEntity(EntityUid uid, TransformComponent xform) { - return _mapManager.TryGetGrid(xform.GridUid, out var grid) - && AnchorEntity(uid, xform, grid, _map.TileIndicesFor(xform.GridUid.Value, grid, xform.Coordinates)); + return AnchorEntity((uid, xform)); + } + + public bool AnchorEntity(Entity entity, Entity? grid = null) + { + DebugTools.Assert(grid == null || grid.Value.Owner == entity.Comp.GridUid); + + if (grid == null) + { + if (!TryComp(entity.Comp.GridUid, out MapGridComponent? gridComp)) + return false; + grid = (entity.Comp.GridUid.Value, gridComp); + } + + var tileIndices = _map.TileIndicesFor(grid.Value, grid.Value, entity.Comp.Coordinates); + return AnchorEntity(entity, grid.Value, tileIndices); } public void Unanchor(EntityUid uid, TransformComponent xform, bool setPhysics = true) @@ -145,39 +161,23 @@ public void Unanchor(EntityUid uid, TransformComponent xform, bool setPhysics = #region Contains /// - /// Returns whether the given entity is a child of this transform or one of its descendants. + /// Checks whether the first entity or one of it's children is the parent of some other entity. /// - public bool ContainsEntity(TransformComponent xform, EntityUid entity) + public bool ContainsEntity(EntityUid parent, Entity child) { - return ContainsEntity(xform, entity, XformQuery); - } - - /// - public bool ContainsEntity(TransformComponent xform, EntityUid entity, EntityQuery xformQuery) - { - return ContainsEntity(xform, xformQuery.GetComponent(entity), xformQuery); - } - - /// - public bool ContainsEntity(TransformComponent xform, TransformComponent entityTransform) - { - return ContainsEntity(xform, entityTransform, XformQuery); - } + if (!Resolve(child.Owner, ref child.Comp)) + return false; - /// - public bool ContainsEntity(TransformComponent xform, TransformComponent entityTransform, EntityQuery xformQuery) - { - // Is the entity the scene root - if (!entityTransform.ParentUid.IsValid()) + if (!child.Comp.ParentUid.IsValid()) return false; - // Is this the direct parent of the entity - if (xform.Owner == entityTransform.ParentUid) + if (parent == child.Comp.ParentUid) return true; - // Recursively search up the parents for this object - var parentXform = xformQuery.GetComponent(entityTransform.ParentUid); - return ContainsEntity(xform, parentXform, xformQuery); + if (!XformQuery.TryGetComponent(child.Comp.ParentUid, out var parentXform)) + return false; + + return ContainsEntity(parent, (child.Comp.ParentUid, parentXform)); } #endregion @@ -237,7 +237,7 @@ static MapId FindMapIdAndSet(EntityUid uid, TransformComponent xform, IEntityMan { var msg = $"Attempted to re-parent to a terminating object. Entity: {ToPrettyString(component.ParentUid)}, new parent: {ToPrettyString(uid)}"; #if EXCEPTION_TOLERANCE - Logger.Error(msg); + Log.Error(msg); Del(uid); #else throw new InvalidOperationException(msg); @@ -432,7 +432,7 @@ public void SetLocalRotation(TransformComponent xform, Angle value) public void SetCoordinates(EntityUid uid, EntityCoordinates value) { - SetCoordinates(uid, Transform(uid), value); + SetCoordinates((uid, Transform(uid), MetaData(uid)), value); } /// @@ -443,8 +443,15 @@ public void SetCoordinates(EntityUid uid, EntityCoordinates value) /// Whether or not to unanchor the entity before moving. Note that this will still move the /// entity even when false. If you set this to false, you need to manually manage the grid lookup changes and ensure /// the final position is valid - public void SetCoordinates(EntityUid uid, TransformComponent xform, EntityCoordinates value, Angle? rotation = null, bool unanchor = true, TransformComponent? newParent = null, TransformComponent? oldParent = null) - { + public void SetCoordinates( + Entity entity, + EntityCoordinates value, + Angle? rotation = null, + bool unanchor = true, + TransformComponent? newParent = null, + TransformComponent? oldParent = null) + { + var (uid, xform, meta) = entity; // NOTE: This setter must be callable from before initialize. if (xform.ParentUid == value.EntityId @@ -460,8 +467,23 @@ public void SetCoordinates(EntityUid uid, TransformComponent xform, EntityCoordi if (xform.Anchored && unanchor) Unanchor(uid, xform); + if (value.EntityId != xform.ParentUid && value.EntityId.IsValid()) + { + if (meta.EntityLifeStage >= EntityLifeStage.Terminating) + { + Log.Error($"{ToPrettyString(uid)} is attempting to move while terminating. New parent: {ToPrettyString(value.EntityId)}. Trace: {Environment.StackTrace}"); + return; + } + + if (TerminatingOrDeleted(value.EntityId)) + { + Log.Error($"{ToPrettyString(uid)} is attempting to attach itself to a terminating entity {ToPrettyString(value.EntityId)}. Trace: {Environment.StackTrace}"); + return; + } + } + // Set new values - Dirty(uid, xform); + Dirty(uid, xform, meta); xform.MatricesDirty = true; xform._localPosition = value.Position; @@ -583,6 +605,18 @@ public void SetCoordinates(EntityUid uid, TransformComponent xform, EntityCoordi RaiseLocalEvent(uid, ref moveEvent, true); } + public void SetCoordinates( + EntityUid uid, + TransformComponent xform, + EntityCoordinates value, + Angle? rotation = null, + bool unanchor = true, + TransformComponent? newParent = null, + TransformComponent? oldParent = null) + { + SetCoordinates((uid, xform, MetaData(uid)), value, rotation, unanchor, newParent, oldParent); + } + #endregion #region Parent @@ -827,6 +861,27 @@ public Vector2 GetWorldPosition(TransformComponent component, EntityQuery entity) + { + return GetMapCoordinates(entity.Comp); + } + [Pure] public (Vector2 WorldPosition, Angle WorldRotation) GetWorldPositionRotation(EntityUid uid) { diff --git a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.cs b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.cs index 038b1ce3f8f..0081ca146de 100644 --- a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.cs @@ -49,21 +49,6 @@ public override void Initialize() SubscribeLocalEvent(OnGetState); SubscribeLocalEvent(OnHandleState); SubscribeLocalEvent(OnGridAdd); - SubscribeLocalEvent(OnParentChange); - } - - private void OnParentChange(ref EntParentChangedMessage ev) - { - // TODO: when PVS errors on live servers get fixed, wrap this whole subscription in an #if DEBUG block to speed up parent changes & entity deletion. - if (ev.Transform.ParentUid == EntityUid.Invalid) - return; - - if (LifeStage(ev.Entity) >= EntityLifeStage.Terminating) - Log.Error($"Entity {ToPrettyString(ev.Entity)} is getting attached to a new parent while terminating. New parent: {ToPrettyString(ev.Transform.ParentUid)}. Trace: {Environment.StackTrace}"); - - - if (LifeStage(ev.Transform.ParentUid) >= EntityLifeStage.Terminating) - Log.Error($"Entity {ToPrettyString(ev.Entity)} is attaching itself to a terminating entity {ToPrettyString(ev.Transform.ParentUid)}. Trace: {Environment.StackTrace}"); } private void MapManagerOnTileChanged(ref TileChangedEvent e) @@ -261,6 +246,39 @@ public Vector2i GetGridOrMapTilePosition(EntityUid uid, TransformComponent? xfor // We're on a grid, need to convert the coordinates to grid tiles. return _map.CoordinatesToTile(xform.GridUid.Value, Comp(xform.GridUid.Value), xform.Coordinates); } + + /// + /// Helper method that returns the grid tile an entity is on. + /// + public Vector2i GetGridTilePositionOrDefault(Entity entity, MapGridComponent? grid = null) + { + var xform = entity.Comp; + if(!Resolve(entity.Owner, ref xform) || xform.GridUid == null) + return Vector2i.Zero; + + if (!Resolve(xform.GridUid.Value, ref grid)) + return Vector2i.Zero; + + return _map.CoordinatesToTile(xform.GridUid.Value, grid, xform.Coordinates); + } + + /// + /// Helper method that returns the grid tile an entity is on. + /// + public bool TryGetGridTilePosition(Entity entity, out Vector2i indices, MapGridComponent? grid = null) + { + indices = default; + var xform = entity.Comp; + if(!Resolve(entity.Owner, ref xform) || xform.GridUid == null) + return false; + + if (!Resolve(xform.GridUid.Value, ref grid)) + return false; + + indices = _map.CoordinatesToTile(xform.GridUid.Value, grid, xform.Coordinates); + return true; + } + } [ByRefEvent] diff --git a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs index 241085d5ed1..2b6bc8ea11b 100644 --- a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs @@ -1,10 +1,16 @@ using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Robust.Shared.Enums; using Robust.Shared.Player; +using Robust.Shared.Utility; namespace Robust.Shared.GameObjects; public abstract class SharedUserInterfaceSystem : EntitySystem { + protected readonly Dictionary> OpenInterfaces = new(); + public override void Initialize() { base.Initialize(); @@ -99,6 +105,73 @@ protected virtual void CloseShared(PlayerBoundUserInterface bui, ICommonSession { } + 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); + } + + /// + /// Switches between closed and open for a specific client. + /// + public virtual 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); + } + + 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.Channel); + + // Fun fact, clients needs to have BUIs open before they can receive the state..... + if (bui.LastStateMsg != null) + RaiseNetworkEvent(bui.LastStateMsg, session.Channel); + + ActivateInterface(bui); + return true; + } + + private void ActivateInterface(PlayerBoundUserInterface ui) + { + EnsureComp(ui.Owner).Interfaces.Add(ui); + } + internal bool TryCloseUi(ICommonSession? session, EntityUid uid, Enum uiKey, bool remoteCall = false, UserInterfaceComponent? uiComp = null) { if (!Resolve(uid, ref uiComp)) @@ -119,6 +192,27 @@ internal bool TryCloseUi(ICommonSession? session, EntityUid uid, Enum uiKey, boo return true; } + 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.Channel); + CloseShared(bui, session, activeUis); + return true; + } + /// /// Raised by client-side UIs to send to server. /// diff --git a/Robust.Shared/Graphics/Eye.cs b/Robust.Shared/Graphics/Eye.cs index 3966fbf5102..aca7eea5245 100644 --- a/Robust.Shared/Graphics/Eye.cs +++ b/Robust.Shared/Graphics/Eye.cs @@ -19,6 +19,10 @@ public class Eye : IEye [ViewVariables(VVAccess.ReadWrite)] public bool DrawFov { get; set; } = true; + /// + [ViewVariables] + public bool DrawLight { get; set; } = true; + /// [ViewVariables(VVAccess.ReadWrite)] public virtual MapCoordinates Position diff --git a/Robust.Shared/Graphics/IEye.cs b/Robust.Shared/Graphics/IEye.cs index 0cdf769a25f..d2e33614de9 100644 --- a/Robust.Shared/Graphics/IEye.cs +++ b/Robust.Shared/Graphics/IEye.cs @@ -17,6 +17,11 @@ public interface IEye /// bool DrawFov { get; set; } + /// + /// Whether to draw lights for this eye. + /// + bool DrawLight { get; set; } + /// /// Current position of the center of the eye in the game world. /// diff --git a/Robust.Shared/Map/Enumerators/GridTileEnumerator.cs b/Robust.Shared/Map/Enumerators/GridTileEnumerator.cs index 22480039d53..76eedfff8fe 100644 --- a/Robust.Shared/Map/Enumerators/GridTileEnumerator.cs +++ b/Robust.Shared/Map/Enumerators/GridTileEnumerator.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using Robust.Shared.GameObjects; using Robust.Shared.Maths; +using Robust.Shared.Utility; namespace Robust.Shared.Map.Enumerators; @@ -27,32 +28,33 @@ internal GridTileEnumerator(EntityUid gridUid, Dictionary.En public bool MoveNext([NotNullWhen(true)] out TileRef? tileRef) { - if (_index == _chunkSize * _chunkSize) + while (true) { - if (!_chunkEnumerator.MoveNext()) + if (_index == _chunkSize * _chunkSize) { - tileRef = null; - return false; - } + if (!_chunkEnumerator.MoveNext()) + { + tileRef = null; + return false; + } - _index = 0; - } + _index = 0; + } - var (chunkOrigin, chunk) = _chunkEnumerator.Current; + var (chunkOrigin, chunk) = _chunkEnumerator.Current; + DebugTools.Assert(chunk.FilledTiles > 0, $"Encountered empty chunk while enumerating tiles"); + var x = (ushort) (_index / _chunkSize); + var y = (ushort) (_index % _chunkSize); + var tile = chunk.GetTile(x, y); + _index++; - var x = (ushort) (_index / _chunkSize); - var y = (ushort) (_index % _chunkSize); - var tile = chunk.GetTile(x, y); - _index++; + if (_ignoreEmpty && tile.IsEmpty) + continue; - if (_ignoreEmpty && tile.IsEmpty) - { - return MoveNext(out tileRef); + var gridX = x + chunkOrigin.X * _chunkSize; + var gridY = y + chunkOrigin.Y * _chunkSize; + tileRef = new TileRef(_gridUid, gridX, gridY, tile); + return true; } - - var gridX = x + chunkOrigin.X * _chunkSize; - var gridY = y + chunkOrigin.Y * _chunkSize; - tileRef = new TileRef(_gridUid, gridX, gridY, tile); - return true; } -} \ No newline at end of file +} diff --git a/Robust.Shared/Map/ITileDefinitionManager.cs b/Robust.Shared/Map/ITileDefinitionManager.cs index 08eff0bb80a..4175350f5ba 100644 --- a/Robust.Shared/Map/ITileDefinitionManager.cs +++ b/Robust.Shared/Map/ITileDefinitionManager.cs @@ -30,6 +30,7 @@ public interface ITileDefinitionManager : IEnumerable /// The ID of the tile definition. /// The tile definition. ITileDefinition this[int id] { get; } + // TODO add a try get and get-or-null variant. /// /// The number of tile definitions contained inside of this manager. diff --git a/Robust.Shared/Map/MapManager.GridCollection.cs b/Robust.Shared/Map/MapManager.GridCollection.cs index a34ef91c8ef..4d142a84b65 100644 --- a/Robust.Shared/Map/MapManager.GridCollection.cs +++ b/Robust.Shared/Map/MapManager.GridCollection.cs @@ -89,7 +89,7 @@ public IEnumerable GetAllMapGrids(MapId mapId) var query = EntityManager.AllEntityQueryEnumerator(); while (query.MoveNext(out var grid, out var xform)) { - if (xform.MapID != mapId) + if (xform.MapID == mapId) yield return grid; } } diff --git a/Robust.Shared/Network/NetManager.cs b/Robust.Shared/Network/NetManager.cs index 2b9a4623ce7..7f0ef8b2a9c 100644 --- a/Robust.Shared/Network/NetManager.cs +++ b/Robust.Shared/Network/NetManager.cs @@ -576,6 +576,17 @@ private NetPeerConfiguration _getBaseNetPeerConfig() // ping the client once per second. netConfig.PingInterval = 1f; + var poolSize = _config.GetCVar(CVars.NetPoolSize); + + if (poolSize <= 0) + { + netConfig.UseMessageRecycling = false; + } + else + { + netConfig.RecycledCacheMaxCount = Math.Min(poolSize, 8192); + } + netConfig.SendBufferSize = _config.GetCVar(CVars.NetSendBufferSize); netConfig.ReceiveBufferSize = _config.GetCVar(CVars.NetReceiveBufferSize); netConfig.MaximumHandshakeAttempts = 5; diff --git a/Robust.Shared/Physics/Dynamics/Contacts/ContactPositionConstraint.cs b/Robust.Shared/Physics/Dynamics/Contacts/ContactPositionConstraint.cs index f3f2cae0255..4d96a81801f 100644 --- a/Robust.Shared/Physics/Dynamics/Contacts/ContactPositionConstraint.cs +++ b/Robust.Shared/Physics/Dynamics/Contacts/ContactPositionConstraint.cs @@ -21,7 +21,6 @@ */ using System.Numerics; -using Robust.Shared.Maths; using Robust.Shared.Physics.Collision; namespace Robust.Shared.Physics.Dynamics.Contacts diff --git a/Robust.Shared/Physics/Dynamics/Contacts/ContactVelocityConstraint.cs b/Robust.Shared/Physics/Dynamics/Contacts/ContactVelocityConstraint.cs index 14e325e672a..a1e876c3bc1 100644 --- a/Robust.Shared/Physics/Dynamics/Contacts/ContactVelocityConstraint.cs +++ b/Robust.Shared/Physics/Dynamics/Contacts/ContactVelocityConstraint.cs @@ -21,7 +21,6 @@ */ using System.Numerics; -using Robust.Shared.Maths; namespace Robust.Shared.Physics.Dynamics.Contacts { diff --git a/Robust.Shared/Physics/Systems/SharedJointSystem.Relay.cs b/Robust.Shared/Physics/Systems/SharedJointSystem.Relay.cs index 03443b8c931..65c1f48177c 100644 --- a/Robust.Shared/Physics/Systems/SharedJointSystem.Relay.cs +++ b/Robust.Shared/Physics/Systems/SharedJointSystem.Relay.cs @@ -43,8 +43,7 @@ private void OnRelayHandleState(EntityUid uid, JointRelayTargetComponent compone if (args.Current is not JointRelayComponentState state) return; - component.Relayed.Clear(); - component.Relayed.UnionWith(EnsureEntitySet(state.Entities, uid)); + EnsureEntitySet(state.Entities, uid, component.Relayed); } private void OnRelayShutdown(EntityUid uid, JointRelayTargetComponent component, ComponentShutdown args) diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Queries.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Queries.cs index 5d18bff6c98..5fe04db79fb 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Queries.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Queries.cs @@ -457,14 +457,14 @@ public bool TryGetNearestPoints(EntityUid uidA, EntityUid uidB, } public bool TryGetNearest(EntityUid uidA, EntityUid uidB, - out Vector2 point, + out Vector2 pointA, out Vector2 pointB, out float distance, Transform xfA, Transform xfB, FixturesComponent? managerA = null, FixturesComponent? managerB = null, PhysicsComponent? bodyA = null, PhysicsComponent? bodyB = null) { - point = Vector2.Zero; + pointA = Vector2.Zero; pointB = Vector2.Zero; if (!Resolve(uidA, ref managerA, ref bodyA) || @@ -477,11 +477,12 @@ public bool TryGetNearest(EntityUid uidA, EntityUid uidB, } distance = float.MaxValue; - var input = new DistanceInput(); - - input.TransformA = xfA; - input.TransformB = xfB; - input.UseRadii = true; + var input = new DistanceInput + { + TransformA = xfA, + TransformB = xfB, + UseRadii = true + }; // No requirement on collision being enabled so chainshapes will fail foreach (var fixtureA in managerA.Fixtures.Values) @@ -489,24 +490,28 @@ public bool TryGetNearest(EntityUid uidA, EntityUid uidB, if (bodyA.Hard && !fixtureA.Hard) continue; - DebugTools.Assert(fixtureA.ProxyCount <= 1); - - foreach (var fixtureB in managerB.Fixtures.Values) + for (var i = 0; i < fixtureA.Shape.ChildCount; i++) { - if (bodyB.Hard && !fixtureB.Hard) - continue; + input.ProxyA.Set(fixtureA.Shape, i); - DebugTools.Assert(fixtureB.ProxyCount <= 1); - input.ProxyA.Set(fixtureA.Shape, 0); - input.ProxyB.Set(fixtureB.Shape, 0); - DistanceManager.ComputeDistance(out var output, out _, input); + foreach (var fixtureB in managerB.Fixtures.Values) + { + if (bodyB.Hard && !fixtureB.Hard) + continue; - if (distance < output.Distance) - continue; + for (var j = 0; j < fixtureB.Shape.ChildCount; j++) + { + input.ProxyB.Set(fixtureB.Shape, j); + DistanceManager.ComputeDistance(out var output, out _, input); + + if (distance < output.Distance) + continue; - point = output.PointA; - pointB = output.PointB; - distance = output.Distance; + pointA = output.PointA; + pointB = output.PointB; + distance = output.Distance; + } + } } } diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Solver.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Solver.cs index 5130a8e235d..d1b8d9b3df2 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Solver.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Solver.cs @@ -67,12 +67,8 @@ private void ResetSolver( velocityConstraint.TangentSpeed = contact.TangentSpeed; velocityConstraint.IndexA = bodyA.IslandIndex[island.Index]; velocityConstraint.IndexB = bodyB.IslandIndex[island.Index]; - velocityConstraint.Points = new VelocityConstraintPoint[2]; - - for (var j = 0; j < 2; j++) - { - velocityConstraint.Points[j] = new VelocityConstraintPoint(); - } + Array.Resize(ref velocityConstraint.Points, 2); + // Don't need to reset point data as it all gets set below. var (invMassA, invMassB) = GetInvMass(bodyA, bodyB); @@ -91,7 +87,7 @@ private void ResetSolver( (positionConstraint.InvMassA, positionConstraint.InvMassB) = (invMassA, invMassB); positionConstraint.LocalCenterA = bodyA.LocalCenter; positionConstraint.LocalCenterB = bodyB.LocalCenter; - positionConstraint.LocalPoints = new Vector2[2]; + Array.Resize(ref positionConstraint.LocalPoints, 2); positionConstraint.InvIA = bodyA.InvI; positionConstraint.InvIB = bodyB.InvI; diff --git a/Robust.Shared/Player/ActorSystem.cs b/Robust.Shared/Player/ActorSystem.cs new file mode 100644 index 00000000000..b725556e1ee --- /dev/null +++ b/Robust.Shared/Player/ActorSystem.cs @@ -0,0 +1,23 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; + +namespace Robust.Shared.Player; + +/// +/// System that handles . +/// +public sealed class ActorSystem : EntitySystem +{ + [Dependency] private readonly ISharedPlayerManager _playerManager = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnActorShutdown); + } + + private void OnActorShutdown(EntityUid entity, ActorComponent component, ComponentShutdown args) + { + _playerManager.SetAttachedEntity(component.PlayerSession, null); + } +} diff --git a/Robust.Shared/Player/CommonSession.cs b/Robust.Shared/Player/CommonSession.cs index e70be2cd84d..3dbc40459c7 100644 --- a/Robust.Shared/Player/CommonSession.cs +++ b/Robust.Shared/Player/CommonSession.cs @@ -17,10 +17,10 @@ internal sealed class CommonSession : ICommonSession public NetUserId UserId { get; } [ViewVariables] - public string Name { get; set; } = ""; + public string Name { get; internal set; } = ""; [ViewVariables] - public short Ping { get; set; } + public short Ping { get; internal set; } [ViewVariables] public DateTime ConnectedTime { get; set; } @@ -34,6 +34,8 @@ internal sealed class CommonSession : ICommonSession [ViewVariables] public SessionData Data { get; } + public bool ClientSide { get; set; } + [ViewVariables] public INetChannel Channel { get; set; } = default!; @@ -54,4 +56,4 @@ public CommonSession(NetUserId user, string name, SessionData data) Name = name; Data = data; } -} \ No newline at end of file +} diff --git a/Robust.Shared/Player/Events.cs b/Robust.Shared/Player/Events.cs new file mode 100644 index 00000000000..22b6db449cc --- /dev/null +++ b/Robust.Shared/Player/Events.cs @@ -0,0 +1,63 @@ +using Robust.Shared.GameObjects; + +namespace Robust.Shared.Player; + +/// +/// Event that gets raised when a player has been attached to an entity. This event is both raised directed at the +/// entity and broadcast. +/// +public sealed class PlayerAttachedEvent : EntityEventArgs +{ + public readonly EntityUid Entity; + public readonly ICommonSession Player; + + public PlayerAttachedEvent(EntityUid entity, ICommonSession player) + { + Entity = entity; + Player = player; + } +} + +/// +/// Event that gets raised when a player has been detached from an entity. This event is both raised directed at the +/// entity and broadcast. +/// +public sealed class PlayerDetachedEvent : EntityEventArgs +{ + public readonly EntityUid Entity; + public readonly ICommonSession Player; + + public PlayerDetachedEvent(EntityUid entity, ICommonSession player) + { + Entity = entity; + Player = player; + } +} + +/// +/// Variant of that gets raised by the client when the local session gets attached to +/// a new entity. This event will also get raised if the local session changes. +/// +public sealed class LocalPlayerAttachedEvent : EntityEventArgs +{ + public readonly EntityUid Entity; + + public LocalPlayerAttachedEvent(EntityUid entity) + { + Entity = entity; + } +} + +/// +/// Variant of that gets raised by the client when the local session gets attached to +/// a new entity. This event will also get raised if the local session changes. +/// +public sealed class LocalPlayerDetachedEvent : EntityEventArgs +{ + public readonly EntityUid Entity; + + public LocalPlayerDetachedEvent(EntityUid entity) + { + Entity = entity; + } +} diff --git a/Robust.Shared/Player/ICommonSession.cs b/Robust.Shared/Player/ICommonSession.cs index 4fb96d9ffc8..3ff1ff00e5a 100644 --- a/Robust.Shared/Player/ICommonSession.cs +++ b/Robust.Shared/Player/ICommonSession.cs @@ -30,12 +30,12 @@ public interface ICommonSession /// /// Current name of this player. /// - string Name { get; set; } + string Name { get; } /// /// Current connection latency of this session from the server to their client. /// - short Ping { get; internal set; } + short Ping { get; } /// /// The current network channel for this session. @@ -43,8 +43,9 @@ public interface ICommonSession /// /// On the Server every player has a network channel, /// on the Client only the LocalPlayer has a network channel, and that channel points to the server. + /// Unless you know what you are doing, you shouldn't be modifying this directly. /// - INetChannel Channel { get; } + INetChannel Channel { get; set; } LoginType AuthType { get; } @@ -67,4 +68,10 @@ public interface ICommonSession [Obsolete("Just use the Channel field instead.")] INetChannel ConnectedClient => Channel; -} \ No newline at end of file + + /// + /// If true, this indicates that this is a client-side session, and should be ignored when applying a server's + /// game state. + /// + bool ClientSide { get; set; } +} diff --git a/Robust.Shared/Player/ISharedPlayerManager.cs b/Robust.Shared/Player/ISharedPlayerManager.cs index aaf465a1aee..c1aaf33834a 100644 --- a/Robust.Shared/Player/ISharedPlayerManager.cs +++ b/Robust.Shared/Player/ISharedPlayerManager.cs @@ -87,7 +87,7 @@ public interface ISharedPlayerManager /// /// Attempts to get the session with the given . /// - bool TryGetSessionById(NetUserId user, [NotNullWhen(true)] out ICommonSession? session); + bool TryGetSessionById([NotNullWhen(true)] NetUserId? user, [NotNullWhen(true)] out ICommonSession? session); /// /// Attempts to get the session with the given . @@ -121,16 +121,49 @@ public interface ISharedPlayerManager void RemoveSession(ICommonSession session, bool removeData = false); void RemoveSession(NetUserId user, bool removeData = false); + ICommonSession CreateAndAddSession(INetChannel channel); + + ICommonSession CreateAndAddSession(NetUserId user, string name); + + /// + /// Sets a session's attached entity, optionally kicking any sessions already attached to it. + /// + /// The player whose attached entity should get updated + /// The entity to attach the player to, if any. + /// Whether to kick any existing players that are already attached to the entity + /// The player that was forcefully kicked, if any. + /// Whether the attach succeeded, or not. + bool SetAttachedEntity( + [NotNullWhen(true)] ICommonSession? session, + EntityUid? entity, + out ICommonSession? kicked, + bool force = false); + /// - /// Updates a session's + /// Sets a session's attached entity, optionally kicking any sessions already attached to it. /// - void SetAttachedEntity(ICommonSession session, EntityUid? uid); + /// The player whose attached entity should get updated + /// The entity to attach the player to, if any. + /// Whether to kick any existing players that are already attached to the entity + /// Whether the attach succeeded, or not. + bool SetAttachedEntity([NotNullWhen(true)] ICommonSession? session, EntityUid? entity, bool force = false) + => SetAttachedEntity(session, entity, out _, force); /// /// Updates a session's /// void SetStatus(ICommonSession session, SessionStatus status); + /// + /// Updates a session's + /// + void SetPing(ICommonSession session, short ping); + + /// + /// Updates a session's + /// + public void SetName(ICommonSession session, string name); + /// /// Set the session's status to . /// @@ -138,8 +171,4 @@ public interface ISharedPlayerManager [Obsolete("Use GetSessionById()")] ICommonSession GetSessionByUserId(NetUserId user) => GetSessionById(user); - - [Obsolete("Use TryGetSessionById()")] - bool TryGetSessionByUserId(NetUserId user, [NotNullWhen(true)] out ICommonSession? session) - => TryGetSessionById(user, out session); } diff --git a/Robust.Shared/Player/SharedPlayerManager.Sessions.cs b/Robust.Shared/Player/SharedPlayerManager.Sessions.cs index bcaec3769c0..b615514814d 100644 --- a/Robust.Shared/Player/SharedPlayerManager.Sessions.cs +++ b/Robust.Shared/Player/SharedPlayerManager.Sessions.cs @@ -50,12 +50,18 @@ public ICommonSession[] Sessions } } - public bool TryGetSessionById(NetUserId user, [NotNullWhen(true)] out ICommonSession? session) + public bool TryGetSessionById([NotNullWhen(true)] NetUserId? user, [NotNullWhen(true)] out ICommonSession? session) { + if (user == null) + { + session = null; + return false; + } + Lock.EnterReadLock(); try { - return InternalSessions.TryGetValue(user, out session); + return InternalSessions.TryGetValue(user.Value, out session); } finally { @@ -93,7 +99,14 @@ protected virtual CommonSession CreateSession(NetUserId user, string name, Sessi return new CommonSession(user, name, data); } - internal CommonSession CreateAndAddSession(NetUserId user, string name) + public ICommonSession CreateAndAddSession(INetChannel channel) + { + var session = CreateAndAddSession(channel.UserId, channel.UserName); + session.Channel = channel; + return session; + } + + public ICommonSession CreateAndAddSession(NetUserId user, string name) { Lock.EnterWriteLock(); CommonSession session; @@ -134,13 +147,78 @@ public void RemoveSession(NetUserId user, bool removeData = false) } } - public virtual void SetAttachedEntity(ICommonSession session, EntityUid? uid) + /// + public virtual bool SetAttachedEntity( + [NotNullWhen(true)] ICommonSession? session, + EntityUid? uid, + out ICommonSession? kicked, + bool force = false) { + kicked = null; + if (session == null) + return false; + if (session.AttachedEntity == uid) + { + DebugTools.Assert(uid == null || EntManager.HasComponent(uid)); + return true; + } + + if (uid != null) + return Attach(session, uid.Value, out kicked, force); + + Detach(session); + return true; + } + + private void Detach(ICommonSession session) + { + if (session.AttachedEntity is not {} uid) return; + ((CommonSession) session).AttachedEntity = null; + UpdateState(session); + + if (EntManager.TryGetComponent(uid, out ActorComponent? actor) && actor.LifeStage <= ComponentLifeStage.Running) + { + actor.PlayerSession = default!; + EntManager.RemoveComponent(uid, actor); + } + + EntManager.EventBus.RaiseLocalEvent(uid, new PlayerDetachedEvent(uid, session), true); + } + + private bool Attach(ICommonSession session, EntityUid uid, out ICommonSession? kicked, bool force = false) + { + kicked = null; + if (!EntManager.TryGetComponent(uid, out MetaDataComponent? meta)) + return false; + + if (meta.EntityLifeStage >= EntityLifeStage.Terminating) + return false; + + if (EntManager.EnsureComponent(uid, out var actor)) + { + // component already existed. + DebugTools.AssertNotNull(actor.PlayerSession); + if (!force) + return false; + + kicked = actor.PlayerSession; + Detach(kicked); + } + + if (_netMan.IsServer) + EntManager.EnsureComponent(uid); + + if (session.AttachedEntity != null) + Detach(session); + ((CommonSession) session).AttachedEntity = uid; + actor.PlayerSession = session; UpdateState(session); + EntManager.EventBus.RaiseLocalEvent(uid, new PlayerAttachedEvent(uid, session), true); + return true; } public void SetStatus(ICommonSession session, SessionStatus status) @@ -155,6 +233,18 @@ public void SetStatus(ICommonSession session, SessionStatus status) PlayerStatusChanged?.Invoke(this, new SessionStatusEventArgs(session, old, status)); } + public void SetPing(ICommonSession session, short ping) + { + ((CommonSession) session).Ping = ping; + UpdateState(session); + } + + public void SetName(ICommonSession session, string name) + { + ((CommonSession) session).Name = name; + UpdateState(session); + } + public void JoinGame(ICommonSession session) { // This currently just directly sets the session's status, as this was the old behaviour. diff --git a/Robust.Shared/Player/SharedPlayerManager.cs b/Robust.Shared/Player/SharedPlayerManager.cs index 9a9deedada8..92f14fd4cdc 100644 --- a/Robust.Shared/Player/SharedPlayerManager.cs +++ b/Robust.Shared/Player/SharedPlayerManager.cs @@ -14,6 +14,7 @@ internal abstract partial class SharedPlayerManager : ISharedPlayerManager [Dependency] protected readonly IEntityManager EntManager = default!; [Dependency] protected readonly ILogManager LogMan = default!; [Dependency] protected readonly IGameTiming Timing = default!; + [Dependency] private readonly INetManager _netMan = default!; protected ISawmill Sawmill = default!; @@ -26,7 +27,7 @@ internal abstract partial class SharedPlayerManager : ISharedPlayerManager public int PlayerCount => InternalSessions.Count; [ViewVariables] - public ICommonSession? LocalSession { get; set; } + public ICommonSession? LocalSession { get; protected set; } [ViewVariables] public NetUserId? LocalUser => LocalSession?.UserId; diff --git a/Robust.Shared/Prototypes/Exceptions.cs b/Robust.Shared/Prototypes/Exceptions.cs index 96bf1e1b122..3f0f185d603 100644 --- a/Robust.Shared/Prototypes/Exceptions.cs +++ b/Robust.Shared/Prototypes/Exceptions.cs @@ -28,22 +28,13 @@ public PrototypeLoadException(SerializationInfo info, StreamingContext context) [Virtual] public class UnknownPrototypeException : Exception { - public override string Message => "Unknown prototype: " + Prototype; - public readonly string? Prototype; + public override string Message => $"Unknown {Kind.Name} prototype: {Prototype}" ; + public readonly string Prototype; + public readonly Type Kind; - public UnknownPrototypeException(string prototype) + public UnknownPrototypeException(string prototype, Type kind) { Prototype = prototype; - } - - public UnknownPrototypeException(SerializationInfo info, StreamingContext context) : base(info, context) - { - Prototype = (string?) info.GetValue("prototype", typeof(string)); - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - info.AddValue("prototype", Prototype, typeof(string)); + Kind = kind; } } diff --git a/Robust.Shared/Prototypes/PrototypeManager.cs b/Robust.Shared/Prototypes/PrototypeManager.cs index 3c642a4d176..9d89f74fe89 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.cs @@ -196,7 +196,7 @@ public T Index(string id) where T : class, IPrototype } catch (KeyNotFoundException) { - throw new UnknownPrototypeException(id); + throw new UnknownPrototypeException(id, typeof(T)); } } @@ -594,7 +594,7 @@ public bool HasIndex(string id) where T : class, IPrototype { if (!_kinds.TryGetValue(typeof(T), out var index)) { - throw new UnknownPrototypeException(id); + throw new UnknownPrototypeException(id, typeof(T)); } return index.Instances.ContainsKey(id); @@ -625,7 +625,7 @@ public bool TryIndex(Type kind, string id, [NotNullWhen(true)] out IPrototype? p { if (!_kinds.TryGetValue(kind, out var index)) { - throw new UnknownPrototypeException(id); + throw new UnknownPrototypeException(id, kind); } return index.Instances.TryGetValue(id, out prototype); @@ -648,7 +648,7 @@ public bool HasMapping(string id) { if (!_kinds.TryGetValue(typeof(T), out var index)) { - throw new UnknownPrototypeException(id); + throw new UnknownPrototypeException(id, typeof(T)); } return index.Results.ContainsKey(id); diff --git a/Robust.Shared/Random/IRobustRandom.cs b/Robust.Shared/Random/IRobustRandom.cs index b6fafe5e4ce..4c50b929233 100644 --- a/Robust.Shared/Random/IRobustRandom.cs +++ b/Robust.Shared/Random/IRobustRandom.cs @@ -14,6 +14,7 @@ public interface IRobustRandom /// /// System.Random GetRandom(); + void SetSeed(int seed); float NextFloat(); public float NextFloat(float minValue, float maxValue) diff --git a/Robust.Shared/Random/RobustRandom.cs b/Robust.Shared/Random/RobustRandom.cs index 82f325da4c3..28981c76fb6 100644 --- a/Robust.Shared/Random/RobustRandom.cs +++ b/Robust.Shared/Random/RobustRandom.cs @@ -5,10 +5,15 @@ namespace Robust.Shared.Random { public sealed class RobustRandom : IRobustRandom { - private readonly System.Random _random = new(); + private System.Random _random = new(); public System.Random GetRandom() => _random; + public void SetSeed(int seed) + { + _random = new(seed); + } + public float NextFloat() { return _random.NextFloat(); diff --git a/Robust.Shared/ResourceManagement/SharedResourceCache.cs b/Robust.Shared/ResourceManagement/SharedResourceCache.cs deleted file mode 100644 index e80f9c3907c..00000000000 --- a/Robust.Shared/ResourceManagement/SharedResourceCache.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.CompilerServices; -using Robust.Shared.Audio; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Log; -using Robust.Shared.Utility; - -namespace Robust.Shared.ResourceManagement; - -[Virtual] -public class SharedResourceCache : IResourceCache -{ -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)); - } - - #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; - } - - ~SharedResourceCache() - { - 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; - } -} diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.cs b/Robust.Shared/Serialization/Manager/SerializationManager.cs index 6556b5827f9..54410610da4 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.cs @@ -18,7 +18,7 @@ namespace Robust.Shared.Serialization.Manager { public sealed partial class SerializationManager : ISerializationManager { - [IoC.Dependency] private readonly IReflectionManager _reflectionManager = default!; + [Dependency] private readonly IReflectionManager _reflectionManager = default!; public IReflectionManager ReflectionManager => _reflectionManager; diff --git a/Robust.Shared/Utility/CollectionExtensions.cs b/Robust.Shared/Utility/CollectionExtensions.cs index 438a55f8139..7907dfc6462 100644 --- a/Robust.Shared/Utility/CollectionExtensions.cs +++ b/Robust.Shared/Utility/CollectionExtensions.cs @@ -222,6 +222,17 @@ public static TValue GetOrNew(this Dictionary dict, return entry!; } + public static TValue GetOrNew(this Dictionary dict, TKey key, out bool exists) + where TValue : new() + where TKey : notnull + { + ref var entry = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out exists); + if (!exists) + entry = new TValue(); + + return entry!; + } + // More efficient than LINQ. public static KeyValuePair[] ToArray(this Dictionary dict) where TKey : notnull diff --git a/Robust.UnitTesting/Server/GameObjects/Components/Transform_Test.cs b/Robust.UnitTesting/Server/GameObjects/Components/Transform_Test.cs index d9e449b728f..5272562cd41 100644 --- a/Robust.UnitTesting/Server/GameObjects/Components/Transform_Test.cs +++ b/Robust.UnitTesting/Server/GameObjects/Components/Transform_Test.cs @@ -21,6 +21,7 @@ sealed class Transform_Test : RobustUnitTest private IServerEntityManagerInternal EntityManager = default!; private IMapManager MapManager = default!; + private SharedTransformSystem XformSystem => EntityManager.System(); const string Prototypes = @" - type: entity @@ -35,9 +36,9 @@ sealed class Transform_Test : RobustUnitTest "; private MapId MapA; - private Entity GridA = default!; + private Entity GridA; private MapId MapB; - private Entity GridB = default!; + private Entity GridB; private static readonly EntityCoordinates InitialPos = new(new EntityUid(1), new Vector2(0, 0)); @@ -84,39 +85,38 @@ public void ParentMapSwitchTest() var childTrans = EntityManager.GetComponent(child); // that are not on the same map - parentTrans.Coordinates = new EntityCoordinates(GridA, new Vector2(5, 5)); - childTrans.Coordinates = new EntityCoordinates(GridB, new Vector2(4, 4)); + XformSystem.SetCoordinates(parent, parentTrans, new EntityCoordinates(GridA, new Vector2(5, 5))); + XformSystem.SetCoordinates(child, childTrans, new EntityCoordinates(GridB, new Vector2(4, 4))); // if they are parented, the child keeps its world position, but moves to the parents map - childTrans.AttachParent(parentTrans); - + XformSystem.SetParent(child, childTrans, parent, parentXform: parentTrans); Assert.Multiple(() => { - Assert.That(childTrans.MapID, Is.EqualTo(parentTrans.MapID)); - Assert.That(childTrans.GridUid, Is.EqualTo(parentTrans.GridUid)); - Assert.That(childTrans.Coordinates, Is.EqualTo(new EntityCoordinates(parent, new Vector2(-1, -1)))); - Assert.That(childTrans.WorldPosition, Is.EqualTo(new Vector2(4, 4))); + Assert.That(childTrans.MapID, NUnit.Framework.Is.EqualTo(parentTrans.MapID)); + Assert.That(childTrans.GridUid, NUnit.Framework.Is.EqualTo(parentTrans.GridUid)); + Assert.That(childTrans.Coordinates, NUnit.Framework.Is.EqualTo(new EntityCoordinates(parent, new Vector2(-1, -1)))); + Assert.That(XformSystem.GetWorldPosition(childTrans), NUnit.Framework.Is.EqualTo(new Vector2(4, 4))); }); // move the parent, and the child should move with it - childTrans.LocalPosition = new Vector2(6, 6); - parentTrans.WorldPosition = new Vector2(-8, -8); + XformSystem.SetLocalPosition(child, new Vector2(6, 6), childTrans); + XformSystem.SetWorldPosition(parentTrans, new Vector2(-8, -8)); - Assert.That(childTrans.WorldPosition, Is.EqualTo(new Vector2(-2, -2))); + Assert.That(XformSystem.GetWorldPosition(childTrans), NUnit.Framework.Is.EqualTo(new Vector2(-2, -2))); // if we detach parent, the child should be left where it was, still relative to parents grid var oldLpos = new Vector2(-2, -2); - var oldWpos = childTrans.WorldPosition; + var oldWpos = XformSystem.GetWorldPosition(childTrans); - childTrans.AttachToGridOrMap(); + XformSystem.AttachToGridOrMap(child, childTrans); // the gridId won't match, because we just detached from the grid entity Assert.Multiple(() => { - Assert.That(childTrans.Coordinates.Position, Is.EqualTo(oldLpos)); - Assert.That(childTrans.WorldPosition, Is.EqualTo(oldWpos)); + Assert.That(childTrans.Coordinates.Position, NUnit.Framework.Is.EqualTo(oldLpos)); + Assert.That(XformSystem.GetWorldPosition(childTrans), NUnit.Framework.Is.EqualTo(oldWpos)); }); } @@ -131,16 +131,16 @@ public void ParentAttachMoveTest() var child = EntityManager.SpawnEntity(null, InitialPos); var parentTrans = EntityManager.GetComponent(parent); var childTrans = EntityManager.GetComponent(child); - parentTrans.WorldPosition = new Vector2(5, 5); - childTrans.WorldPosition = new Vector2(6, 6); + XformSystem.SetWorldPosition(parentTrans, new Vector2(5, 5)); + XformSystem.SetWorldPosition(childTrans, new Vector2(6, 6)); // Act - var oldWpos = childTrans.WorldPosition; - childTrans.AttachParent(parentTrans); - var newWpos = childTrans.WorldPosition; + var oldWpos = XformSystem.GetWorldPosition(childTrans); + XformSystem.SetParent(child, childTrans, parent, parentXform: parentTrans); + var newWpos = XformSystem.GetWorldPosition(childTrans); // Assert - Assert.That(oldWpos == newWpos); + Assert.That(oldWpos, NUnit.Framework.Is.EqualTo(newWpos)); } /// @@ -156,25 +156,25 @@ public void ParentDoubleAttachMoveTest() var parentTrans = EntityManager.GetComponent(parent); var childOneTrans = EntityManager.GetComponent(childOne); var childTwoTrans = EntityManager.GetComponent(childTwo); - parentTrans.WorldPosition = new Vector2(1, 1); - childOneTrans.WorldPosition = new Vector2(2, 2); - childTwoTrans.WorldPosition = new Vector2(3, 3); + XformSystem.SetWorldPosition(parentTrans, new Vector2(1, 1)); + XformSystem.SetWorldPosition(childOneTrans, new Vector2(2, 2)); + XformSystem.SetWorldPosition(childTwoTrans, new Vector2(3, 3)); // Act - var oldWpos = childOneTrans.WorldPosition; - childOneTrans.AttachParent(parentTrans); - var newWpos = childOneTrans.WorldPosition; - Assert.That(oldWpos, Is.EqualTo(newWpos)); - - oldWpos = childTwoTrans.WorldPosition; - childTwoTrans.AttachParent(parentTrans); - newWpos = childTwoTrans.WorldPosition; - Assert.That(oldWpos, Is.EqualTo(newWpos)); - - oldWpos = childTwoTrans.WorldPosition; - childTwoTrans.AttachParent(childOneTrans); - newWpos = childTwoTrans.WorldPosition; - Assert.That(oldWpos, Is.EqualTo(newWpos)); + var oldWpos = XformSystem.GetWorldPosition(childOneTrans); + XformSystem.SetParent(childOne, childOneTrans, parent, parentXform: parentTrans); + var newWpos = XformSystem.GetWorldPosition(childOneTrans); + Assert.That(oldWpos, NUnit.Framework.Is.EqualTo(newWpos)); + + oldWpos = XformSystem.GetWorldPosition(childTwoTrans); + XformSystem.SetParent(childOne, childOneTrans, parent, parentXform: parentTrans); + newWpos = XformSystem.GetWorldPosition(childTwoTrans); + Assert.That(oldWpos, NUnit.Framework.Is.EqualTo(newWpos)); + + oldWpos = XformSystem.GetWorldPosition(childTwoTrans); + XformSystem.SetParent(childTwo, childTwoTrans, childOne, parentXform: childOneTrans); + newWpos = XformSystem.GetWorldPosition(childTwoTrans); + Assert.That(oldWpos, NUnit.Framework.Is.EqualTo(newWpos)); } /// @@ -188,15 +188,15 @@ public void ParentRotateTest() var child = EntityManager.SpawnEntity(null, InitialPos); var parentTrans = EntityManager.GetComponent(parent); var childTrans = EntityManager.GetComponent(child); - parentTrans.WorldPosition = new Vector2(0, 0); - childTrans.WorldPosition = new Vector2(2, 0); - childTrans.AttachParent(parentTrans); + XformSystem.SetWorldPosition(parentTrans, new Vector2(0, 0)); + XformSystem.SetWorldPosition(childTrans, new Vector2(2, 0)); + XformSystem.SetParent(child, childTrans, parent, parentXform: parentTrans); //Act parentTrans.LocalRotation = new Angle(MathHelper.Pi / 2); //Assert - var result = childTrans.WorldPosition; + var result = XformSystem.GetWorldPosition(childTrans); Assert.Multiple(() => { Assert.That(MathHelper.CloseToPercent(result.X, 0)); @@ -215,15 +215,15 @@ public void ParentTransRotateTest() var child = EntityManager.SpawnEntity(null, InitialPos); var parentTrans = EntityManager.GetComponent(parent); var childTrans = EntityManager.GetComponent(child); - parentTrans.WorldPosition = new Vector2(1, 1); - childTrans.WorldPosition = new Vector2(2, 1); - childTrans.AttachParent(parentTrans); + XformSystem.SetWorldPosition(parentTrans, new Vector2(1, 1)); + XformSystem.SetWorldPosition(childTrans, new Vector2(2, 1)); + XformSystem.SetParent(child, childTrans, parent, parentXform: parentTrans); //Act parentTrans.LocalRotation = new Angle(MathHelper.Pi / 2); //Assert - var result = childTrans.WorldPosition; + var result = XformSystem.GetWorldPosition(childTrans); Assert.Multiple(() => { Assert.That(MathHelper.CloseToPercent(result.X, 1)); @@ -248,20 +248,20 @@ public void PositionCompositionTest() var node3Trans = EntityManager.GetComponent(node3); var node4Trans = EntityManager.GetComponent(node4); - node1Trans.WorldPosition = new Vector2(0, 0); - node2Trans.WorldPosition = new Vector2(1, 1); - node3Trans.WorldPosition = new Vector2(2, 2); - node4Trans.WorldPosition = new Vector2(0, 2); + XformSystem.SetWorldPosition(node1Trans, new Vector2(0, 0)); + XformSystem.SetWorldPosition(node2Trans, new Vector2(1, 1)); + XformSystem.SetWorldPosition(node3Trans, new Vector2(2, 2)); + XformSystem.SetWorldPosition(node4Trans, new Vector2(0, 2)); - node2Trans.AttachParent(node1Trans); - node3Trans.AttachParent(node2Trans); - node4Trans.AttachParent(node3Trans); + XformSystem.SetParent(node2, node2Trans, node1, parentXform: node1Trans); + XformSystem.SetParent(node3, node3Trans, node2, parentXform: node2Trans); + XformSystem.SetParent(node4, node4Trans, node3, parentXform: node3Trans); //Act node1Trans.LocalRotation = new Angle(MathHelper.Pi / 2); //Assert - var result = node4Trans.WorldPosition; + var result = XformSystem.GetWorldPosition(node4Trans); Assert.Multiple(() => { @@ -285,25 +285,25 @@ public void ParentLocalPositionRoundingErrorTest() var node2Trans = EntityManager.GetComponent(node2); var node3Trans = EntityManager.GetComponent(node3); - node1Trans.WorldPosition = new Vector2(0, 0); - node2Trans.WorldPosition = new Vector2(1, 1); - node3Trans.WorldPosition = new Vector2(2, 2); + XformSystem.SetWorldPosition(node1Trans, new Vector2(0, 0)); + XformSystem.SetWorldPosition(node2Trans, new Vector2(1, 1)); + XformSystem.SetWorldPosition(node3Trans, new Vector2(2, 2)); - node2Trans.AttachParent(node1Trans); - node3Trans.AttachParent(node2Trans); + XformSystem.SetParent(node1, node1Trans, node2, parentXform: node2Trans); + XformSystem.SetParent(node2, node2Trans, node3, parentXform: node3Trans); // Act - var oldWpos = node3Trans.WorldPosition; + var oldWpos = XformSystem.GetWorldPosition(node3Trans); for (var i = 0; i < 10000; i++) { var dx = i % 2 == 0 ? 5 : -5; - node1Trans.LocalPosition += new Vector2(dx, dx); - node2Trans.LocalPosition += new Vector2(dx, dx); - node3Trans.LocalPosition += new Vector2(dx, dx); + XformSystem.SetLocalPosition(node1, node1Trans.LocalPosition + new Vector2(dx, dx), node1Trans); + XformSystem.SetLocalPosition(node2, node2Trans.LocalPosition + new Vector2(dx, dx), node2Trans); + XformSystem.SetLocalPosition(node3, node3Trans.LocalPosition + new Vector2(dx, dx), node3Trans); } - var newWpos = node3Trans.WorldPosition; + var newWpos = XformSystem.GetWorldPosition(node3Trans); // Assert Assert.Multiple(() => @@ -330,15 +330,15 @@ public void ParentRotationRoundingErrorTest() var node2Trans = EntityManager.GetComponent(node2); var node3Trans = EntityManager.GetComponent(node3); - node1Trans.WorldPosition = new Vector2(0, 0); - node2Trans.WorldPosition = new Vector2(1, 1); - node3Trans.WorldPosition = new Vector2(2, 2); + XformSystem.SetWorldPosition(node1Trans, new Vector2(0, 0)); + XformSystem.SetWorldPosition(node2Trans, new Vector2(1, 1)); + XformSystem.SetWorldPosition(node3Trans, new Vector2(2, 2)); - node2Trans.AttachParent(node1Trans); - node3Trans.AttachParent(node2Trans); + XformSystem.SetParent(node2, node2Trans, node1, parentXform: node1Trans); + XformSystem.SetParent(node3, node3Trans, node2, parentXform: node2Trans); // Act - var oldWpos = node3Trans.WorldPosition; + var oldWpos = XformSystem.GetWorldPosition(node3Trans); for (var i = 0; i < 100; i++) { @@ -347,7 +347,7 @@ public void ParentRotationRoundingErrorTest() node3Trans.LocalRotation += new Angle(MathHelper.Pi); } - var newWpos = node3Trans.WorldPosition; + var newWpos = XformSystem.GetWorldPosition(node3Trans); //NOTE: Yes, this does cause a non-zero error @@ -355,8 +355,8 @@ public void ParentRotationRoundingErrorTest() Assert.Multiple(() => { - Assert.That(MathHelper.CloseToPercent(oldWpos.X, newWpos.Y)); - Assert.That(MathHelper.CloseToPercent(oldWpos.Y, newWpos.Y)); + Assert.That(MathHelper.CloseToPercent(oldWpos.X, newWpos.Y, 0.0001f)); + Assert.That(MathHelper.CloseToPercent(oldWpos.Y, newWpos.Y, 0.0001f)); }); } @@ -379,21 +379,21 @@ public void TreeComposeWorldMatricesTest() var node3Trans = EntityManager.GetComponent(node3); var node4Trans = EntityManager.GetComponent(node4); - node1Trans.WorldPosition = new Vector2(0, 0); - node2Trans.WorldPosition = new Vector2(1, 1); - node3Trans.WorldPosition = new Vector2(2, 2); - node4Trans.WorldPosition = new Vector2(0, 2); + XformSystem.SetWorldPosition(node1Trans, new Vector2(0, 0)); + XformSystem.SetWorldPosition(node2Trans, new Vector2(1, 1)); + XformSystem.SetWorldPosition(node3Trans, new Vector2(2, 2)); + XformSystem.SetWorldPosition(node4Trans, new Vector2(0, 2)); - node2Trans.AttachParent(node1Trans); - node3Trans.AttachParent(node2Trans); - node4Trans.AttachParent(node3Trans); + XformSystem.SetParent(node2, node2Trans, node1, parentXform: node1Trans); + XformSystem.SetParent(node3, node3Trans, node2, parentXform: node2Trans); + XformSystem.SetParent(node4, node4Trans, node3, parentXform: node3Trans); //Act node1Trans.LocalRotation = new Angle(MathHelper.Pi / 6.37); - node1Trans.WorldPosition = new Vector2(1, 1); + XformSystem.SetWorldPosition(node1Trans, new Vector2(1, 1)); - var worldMat = node4Trans.WorldMatrix; - var invWorldMat = node4Trans.InvWorldMatrix; + var worldMat = XformSystem.GetWorldMatrix(node4Trans); + var invWorldMat = XformSystem.GetInvWorldMatrix(node4Trans); Matrix3.Multiply(in worldMat, in invWorldMat, out var leftVerifyMatrix); Matrix3.Multiply(in invWorldMat, in worldMat, out var rightVerifyMatrix); @@ -425,8 +425,8 @@ public void WorldRotationTest() var node2Trans = EntityManager.GetComponent(node2); var node3Trans = EntityManager.GetComponent(node3); - node2Trans.AttachParent(node1Trans); - node3Trans.AttachParent(node2Trans); + XformSystem.SetParent(node2, node2Trans, node1, parentXform: node1Trans); + XformSystem.SetParent(node3, node3Trans, node2, parentXform: node2Trans); node1Trans.LocalRotation = Angle.FromDegrees(0); node2Trans.LocalRotation = Angle.FromDegrees(45); @@ -436,7 +436,7 @@ public void WorldRotationTest() node1Trans.LocalRotation = Angle.FromDegrees(135); // Assert (135 + 45 + 45 = 225) - var result = node3Trans.WorldRotation; + var result = XformSystem.GetWorldRotation(node3Trans); Assert.That(result, new ApproxEqualityConstraint(Angle.FromDegrees(225))); } @@ -454,14 +454,14 @@ public void MatrixUpdateTest() var node2Trans = EntityManager.GetComponent(node2); var node3Trans = EntityManager.GetComponent(node3); - node2Trans.AttachParent(node1Trans); - node3Trans.AttachParent(node2Trans); + XformSystem.SetParent(node2, node2Trans, node1, parentXform: node1Trans); + XformSystem.SetParent(node3, node3Trans, node2, parentXform: node2Trans); - node3Trans.LocalPosition = new Vector2(5, 5); - node2Trans.LocalPosition = new Vector2(5, 5); - node1Trans.LocalPosition = new Vector2(5, 5); + XformSystem.SetLocalPosition(node3, new Vector2(5, 5), node3Trans); + XformSystem.SetLocalPosition(node2, new Vector2(5, 5), node2Trans); + XformSystem.SetLocalPosition(node1, new Vector2(5, 5), node1Trans); - Assert.That(node3Trans.WorldPosition, new ApproxEqualityConstraint(new Vector2(15, 15))); + Assert.That(XformSystem.GetWorldPosition(node3Trans), new ApproxEqualityConstraint(new Vector2(15, 15))); } /* diff --git a/Robust.UnitTesting/Server/GameStates/DefaultEntityTest.cs b/Robust.UnitTesting/Server/GameStates/DefaultEntityTest.cs index bfded2e559c..5c4186abd83 100644 --- a/Robust.UnitTesting/Server/GameStates/DefaultEntityTest.cs +++ b/Robust.UnitTesting/Server/GameStates/DefaultEntityTest.cs @@ -1,7 +1,6 @@ using System.Linq; using System.Threading.Tasks; using NUnit.Framework; -using Robust.Server.GameObjects; using Robust.Server.Player; using Robust.Shared; using Robust.Shared.Configuration; @@ -81,7 +80,7 @@ await server.WaitPost(() => coords = new(map, default); var playerUid = sEntMan.SpawnEntity(null, coords); player = sEntMan.GetNetEntity(playerUid); - sEntMan.System().Attach(playerUid, session); + server.PlayerMan.SetAttachedEntity(session, playerUid); }); for (int i = 0; i < 10; i++) diff --git a/Robust.UnitTesting/Server/GameStates/PvsReEntryTest.cs b/Robust.UnitTesting/Server/GameStates/PvsReEntryTest.cs index 8a4abaf809c..c5c3534f461 100644 --- a/Robust.UnitTesting/Server/GameStates/PvsReEntryTest.cs +++ b/Robust.UnitTesting/Server/GameStates/PvsReEntryTest.cs @@ -4,15 +4,13 @@ using NUnit.Framework; using Robust.Client.GameStates; using Robust.Client.Timing; -using Robust.Server.GameObjects; using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Network; +using Robust.Shared.Player; using Robust.Shared.Timing; -using cIPlayerManager = Robust.Client.Player.IPlayerManager; -using sIPlayerManager = Robust.Server.Player.IPlayerManager; namespace Robust.UnitTesting.Server.GameStates; @@ -33,13 +31,13 @@ public async Task TestLossyReEntry() var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); - var sPlayerMan = server.ResolveDependency(); + var sPlayerMan = server.ResolveDependency(); var xforms = sEntMan.System(); var stateMan = (ClientGameStateManager) client.ResolveDependency(); var cEntMan = client.ResolveDependency(); var netMan = client.ResolveDependency(); - var cPlayerMan = client.ResolveDependency(); + var cPlayerMan = client.ResolveDependency(); Assert.DoesNotThrow(() => client.SetConnectTarget(server)); client.Post(() => netMan.ClientConnect(null!, 0, null!)); @@ -87,7 +85,7 @@ await server.WaitPost(() => // Attach player. var session = sPlayerMan.Sessions.First(); - sEntMan.System().Attach(playerUid, session); + server.PlayerMan.SetAttachedEntity(session, playerUid); sPlayerMan.JoinGame(session); }); @@ -106,7 +104,7 @@ await client.WaitPost(() => { Assert.That(cEntMan.TryGetEntityData(entity, out _, out meta)); Assert.That(cEntMan.TryGetEntity(player, out var cPlayerUid)); - Assert.That(cPlayerMan.LocalPlayer?.ControlledEntity, Is.EqualTo(cPlayerUid)); + Assert.That(cPlayerMan.LocalEntity, Is.EqualTo(cPlayerUid)); Assert.That(meta!.Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.None)); Assert.That(stateMan.IsQueuedForDetach(entity), Is.False); }); diff --git a/Robust.UnitTesting/Server/GameStates/PvsSystemTests.cs b/Robust.UnitTesting/Server/GameStates/PvsSystemTests.cs index 9c14b159707..84fa99d82a2 100644 --- a/Robust.UnitTesting/Server/GameStates/PvsSystemTests.cs +++ b/Robust.UnitTesting/Server/GameStates/PvsSystemTests.cs @@ -2,15 +2,13 @@ using System.Numerics; using System.Threading.Tasks; using NUnit.Framework; -using Robust.Server.GameObjects; using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Network; -using cIPlayerManager = Robust.Client.Player.IPlayerManager; -using sIPlayerManager = Robust.Server.Player.IPlayerManager; +using Robust.Shared.Player; namespace Robust.UnitTesting.Server.GameStates; @@ -30,12 +28,12 @@ public async Task TestMultipleIndexChange() var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); - var sPlayerMan = server.ResolveDependency(); + var sPlayerMan = server.ResolveDependency(); var xforms = sEntMan.System(); var cEntMan = client.ResolveDependency(); var netMan = client.ResolveDependency(); - var cPlayerMan = client.ResolveDependency(); + var cPlayerMan = client.ResolveDependency(); Assert.DoesNotThrow(() => client.SetConnectTarget(server)); client.Post(() => netMan.ClientConnect(null!, 0, null!)); @@ -76,7 +74,7 @@ await server.WaitPost(() => // Attach player. var session = sPlayerMan.Sessions.First(); - sEntMan.System().Attach(player, session); + server.PlayerMan.SetAttachedEntity(session, player); sPlayerMan.JoinGame(session); }); @@ -89,7 +87,7 @@ await server.WaitPost(() => // Check player got properly attached await client.WaitPost(() => { - var ent = cEntMan.GetNetEntity(cPlayerMan.LocalPlayer?.ControlledEntity); + var ent = cEntMan.GetNetEntity(cPlayerMan.LocalEntity); Assert.That(ent, Is.EqualTo(sEntMan.GetNetEntity(player))); }); diff --git a/Robust.UnitTesting/Shared/GameObjects/ContainerTests.cs b/Robust.UnitTesting/Shared/GameObjects/ContainerTests.cs index 19c2136e1dd..19ed261e8fd 100644 --- a/Robust.UnitTesting/Shared/GameObjects/ContainerTests.cs +++ b/Robust.UnitTesting/Shared/GameObjects/ContainerTests.cs @@ -72,7 +72,7 @@ await server.WaitAssertion(() => // Setup PVS sEntManager.AddComponent(entityUid); var player = sPlayerManager.Sessions.First(); - sEntManager.System().Attach(entityUid, player); + server.PlayerMan.SetAttachedEntity(player, entityUid); sPlayerManager.JoinGame(player); }); @@ -200,7 +200,7 @@ await server.WaitAssertion(() => // Setup PVS sEntManager.AddComponent(sEntityUid); var player = sPlayerManager.Sessions.First(); - sEntManager.System().Attach(sEntityUid, player); + server.PlayerMan.SetAttachedEntity(player, sEntityUid); sPlayerManager.JoinGame(player); }); diff --git a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.ComponentEvent.cs b/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.ComponentEvent.cs index e6ad5b21bd2..0bee356c5c5 100644 --- a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.ComponentEvent.cs +++ b/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.ComponentEvent.cs @@ -43,7 +43,9 @@ public void SubscribeCompEvent() // add a component to the system bus.OnEntityAdded(entUid); - bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(compInstance, entUid), CompIdx.Index())); + + var reg = compFactory.GetRegistration(CompIdx.Index()); + bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(compInstance, entUid), reg)); // Raise var evntArgs = new TestEvent(5); @@ -98,7 +100,9 @@ public void UnsubscribeCompEvent() // add a component to the system bus.OnEntityAdded(entUid); - bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(compInstance, entUid), CompIdx.Index())); + + var reg = compFacMock.Object.GetRegistration(CompIdx.Index()); + bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(compInstance, entUid), reg)); // Raise var evntArgs = new TestEvent(5); @@ -151,7 +155,9 @@ public void SubscribeCompLifeEvent() // add a component to the system entManMock.Raise(m => m.EntityAdded += null, entUid); - entManMock.Raise(m => m.ComponentAdded += null, new AddedComponentEventArgs(new ComponentEventArgs(compInstance, entUid), CompIdx.Index())); + + var reg = compFacMock.Object.GetRegistration(); + entManMock.Raise(m => m.ComponentAdded += null, new AddedComponentEventArgs(new ComponentEventArgs(compInstance, entUid), reg)); // Raise ((IEventBus)bus).RaiseComponentEvent(compInstance, new ComponentInit()); @@ -228,9 +234,14 @@ void HandlerB(EntityUid uid, Component comp, TestEvent ev) // add a component to the system bus.OnEntityAdded(entUid); - bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instA, entUid), CompIdx.Index())); - bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instB, entUid), CompIdx.Index())); - bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instC, entUid), CompIdx.Index())); + + var regA = compFacMock.Object.GetRegistration(CompIdx.Index()); + var regB = compFacMock.Object.GetRegistration(CompIdx.Index()); + var regC = compFacMock.Object.GetRegistration(CompIdx.Index()); + + bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instA, entUid), regA)); + bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instB, entUid), regB)); + bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instC, entUid), regC)); // Raise var evntArgs = new TestEvent(5); diff --git a/Robust.UnitTesting/Shared/GameState/ComponentStateTests.cs b/Robust.UnitTesting/Shared/GameState/ComponentStateTests.cs index 1bc443a429f..1238a6ede7d 100644 --- a/Robust.UnitTesting/Shared/GameState/ComponentStateTests.cs +++ b/Robust.UnitTesting/Shared/GameState/ComponentStateTests.cs @@ -2,7 +2,6 @@ using System.Numerics; using System.Threading.Tasks; using NUnit.Framework; -using Robust.Server.GameObjects; using Robust.Shared; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; @@ -74,7 +73,7 @@ await server.WaitPost(() => // Attach player. player = server.EntMan.Spawn(); var session = server.PlayerMan.Sessions.First(); - server.System().Attach(player, session); + server.PlayerMan.SetAttachedEntity(session, player); server.PlayerMan.JoinGame(session); // Spawn test entities. @@ -210,7 +209,7 @@ await server.WaitPost(() => // Attach player. player = server.EntMan.Spawn(); var session = server.PlayerMan.Sessions.First(); - server.System().Attach(player, session); + server.PlayerMan.SetAttachedEntity(session, player); server.PlayerMan.JoinGame(session); // Spawn test entities. diff --git a/Robust.UnitTesting/Shared/GameState/DeletionNetworkingTests.cs b/Robust.UnitTesting/Shared/GameState/DeletionNetworkingTests.cs index 210edf1861b..d73656ea04e 100644 --- a/Robust.UnitTesting/Shared/GameState/DeletionNetworkingTests.cs +++ b/Robust.UnitTesting/Shared/GameState/DeletionNetworkingTests.cs @@ -2,15 +2,13 @@ using System.Numerics; using System.Threading.Tasks; using NUnit.Framework; -using Robust.Server.GameObjects; using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Network; -using cIPlayerManager = Robust.Client.Player.IPlayerManager; -using sIPlayerManager = Robust.Server.Player.IPlayerManager; +using Robust.Shared.Player; // ReSharper disable AccessToStaticMemberViaDerivedType @@ -37,8 +35,8 @@ public async Task DeletionNetworkingTest() var cEntMan = client.ResolveDependency(); var netMan = client.ResolveDependency(); var confMan = server.ResolveDependency(); - var cPlayerMan = client.ResolveDependency(); - var sPlayerMan = server.ResolveDependency(); + var cPlayerMan = client.ResolveDependency(); + var sPlayerMan = server.ResolveDependency(); var xformSys = sEntMan.EntitySysManager.GetEntitySystem(); Assert.DoesNotThrow(() => client.SetConnectTarget(server)); @@ -85,7 +83,7 @@ await server.WaitPost(() => var coords = new EntityCoordinates(grid1, new Vector2(0.5f, 0.5f)); player = sEntMan.SpawnEntity(null, coords); var session = sPlayerMan.Sessions.First(); - sEntMan.System().Attach(player, session); + server.PlayerMan.SetAttachedEntity(session, player); sPlayerMan.JoinGame(session); }); @@ -94,7 +92,7 @@ await server.WaitPost(() => // Check player got properly attached await client.WaitPost(() => { - var ent = cEntMan.GetNetEntity(cPlayerMan.LocalPlayer?.ControlledEntity); + var ent = cEntMan.GetNetEntity(cPlayerMan.LocalEntity); Assert.That(ent, Is.EqualTo(sEntMan.GetNetEntity(player))); }); diff --git a/Robust.UnitTesting/Shared/Physics/BroadphaseNetworkingTest.cs b/Robust.UnitTesting/Shared/Physics/BroadphaseNetworkingTest.cs index 16046605e1e..e3a33cc6377 100644 --- a/Robust.UnitTesting/Shared/Physics/BroadphaseNetworkingTest.cs +++ b/Robust.UnitTesting/Shared/Physics/BroadphaseNetworkingTest.cs @@ -2,7 +2,6 @@ using System.Numerics; using System.Threading.Tasks; using NUnit.Framework; -using Robust.Server.GameObjects; using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; @@ -14,8 +13,7 @@ using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Dynamics; using Robust.Shared.Physics.Systems; -using cIPlayerManager = Robust.Client.Player.IPlayerManager; -using sIPlayerManager = Robust.Server.Player.IPlayerManager; +using Robust.Shared.Player; // ReSharper disable AccessToStaticMemberViaDerivedType @@ -42,8 +40,8 @@ public async Task TestBroadphaseNetworking() var cEntMan = client.ResolveDependency(); var netMan = client.ResolveDependency(); var confMan = server.ResolveDependency(); - var cPlayerMan = client.ResolveDependency(); - var sPlayerMan = server.ResolveDependency(); + var cPlayerMan = client.ResolveDependency(); + var sPlayerMan = server.ResolveDependency(); var fixturesSystem = sEntMan.EntitySysManager.GetEntitySystem(); var physicsSystem = sEntMan.EntitySysManager.GetEntitySystem(); @@ -91,7 +89,7 @@ await server.WaitPost(() => // Attach player. var session = sPlayerMan.Sessions.First(); - sEntMan.System().Attach(player, session); + server.PlayerMan.SetAttachedEntity(session, player); sPlayerMan.JoinGame(session); }); @@ -106,7 +104,7 @@ await server.WaitPost(() => // Check player got properly attached await client.WaitPost(() => { - var ent = cEntMan.GetNetEntity(cPlayerMan.LocalPlayer?.ControlledEntity); + var ent = cEntMan.GetNetEntity(cPlayerMan.LocalEntity); Assert.That(ent, Is.EqualTo(playerNet)); }); diff --git a/global.json b/global.json new file mode 100644 index 00000000000..7665c95ab4e --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "7.0.114", + "rollForward": "latestFeature" + } +}