diff --git a/engine/Launcher/SboxDev/Launcher.cs b/engine/Launcher/SboxDev/Launcher.cs index 5573ae09..db82c385 100644 --- a/engine/Launcher/SboxDev/Launcher.cs +++ b/engine/Launcher/SboxDev/Launcher.cs @@ -1,4 +1,5 @@ -using System; +using Sandbox.Engine; +using System; using System.Diagnostics; using System.Linq; @@ -8,6 +9,17 @@ public static class Launcher { public static int Main() { + if ( HasCommandLineSwitch( "-generatesolution" ) ) + { + NetCore.InitializeInterop( Environment.CurrentDirectory ); + Bootstrap.InitMinimal( Environment.CurrentDirectory ); + Project.InitializeBuiltIn( false ).GetAwaiter().GetResult(); + Project.GenerateSolution().GetAwaiter().GetResult(); + Managed.SandboxEngine.NativeInterop.Free(); + EngineFileSystem.Shutdown(); + return 0; + } + if ( !HasCommandLineSwitch( "-project" ) && !HasCommandLineSwitch( "-test" ) ) { // we pass the command line, so we can pass it on to the sbox-launcher (for -game etc) diff --git a/engine/Sandbox.AppSystem/AppSystem.cs b/engine/Sandbox.AppSystem/AppSystem.cs index 2a01a9cd..647aa508 100644 --- a/engine/Sandbox.AppSystem/AppSystem.cs +++ b/engine/Sandbox.AppSystem/AppSystem.cs @@ -1,6 +1,8 @@ using Sandbox.Diagnostics; using Sandbox.Engine; using Sandbox.Internal; +using Sandbox.Network; +using Sandbox.Rendering; using System; using System.Globalization; using System.Runtime; @@ -130,15 +132,69 @@ public class AppSystem public virtual void Shutdown() { - // Shut the games down - EngineLoop.Exiting(); + // Make sure game instance is closed + IGameInstanceDll.Current?.CloseGame(); + + // Send shutdown event, should allow us to track successful shutdown vs crash + { + var analytic = new Api.Events.EventRecord( "Exit" ); + analytic.SetValue( "uptime", RealTime.Now ); + // We could record a bunch of stats during the session and + // submit them here. I'm thinking things like num games played + // menus visited, time in menus, time in game, files downloaded. + // Things to give us a whole session picture. + analytic.Submit(); + } + + ConVarSystem.SaveAll(); + + IToolsDll.Current?.Exiting(); + IMenuDll.Current?.Exiting(); + IGameInstanceDll.Current?.Exiting(); + + SoundFile.Shutdown(); + SoundHandle.Shutdown(); + DedicatedServer.Shutdown(); + + // Flush API + Api.Shutdown(); + + ConVarSystem.ClearNativeCommands(); + + // Whatever package still exists needs to fuck off + PackageManager.UnmountAll(); + + // Clear static resources + Texture.DisposeStatic(); + Model.DisposeStatic(); + Material.UI.DisposeStatic(); + Gizmo.GizmoDraw.DisposeStatic(); + CubemapRendering.DisposeStatic(); + Graphics.DisposeStatic(); + + TextRendering.ClearCache(); + + NativeResourceCache.Clear(); + + // Renderpipeline may hold onto native resources, clear them out + RenderPipeline.ClearPool(); + + // Run GC and finalizers to clear any resources held by managed + GC.Collect(); + GC.WaitForPendingFinalizers(); + + // Run the queue one more time, since some finalizers queue tasks + MainThread.RunQueues(); + + // print each scene that is leaked + foreach ( var leakedScene in Scene.All ) + { + log.Warning( $"Leaked scene {leakedScene.Id} during shutdown." ); + } // Shut the engine down (close window etc) NativeEngine.EngineGlobal.SourceEngineShutdown( _appSystem, false ); - // Flush the api (close actvity, update stats etc) - Api.Shutdown(); - if ( _appSystem.IsValid ) { _appSystem.Destroy(); @@ -161,11 +217,14 @@ public class AppSystem Managed.SourceHammer.NativeInterop.Free(); Managed.SourceModelDoc.NativeInterop.Free(); Managed.SourceAnimgraph.NativeInterop.Free(); + + EngineFileSystem.Shutdown(); + Application.Shutdown(); } - protected void InitGame( AppSystemCreateInfo createInfo ) + protected void InitGame( AppSystemCreateInfo createInfo, string commandLine = null ) { - var commandLine = System.Environment.CommandLine; + commandLine ??= System.Environment.CommandLine; commandLine = commandLine.Replace( ".dll", ".exe" ); // uck _appSystem = CMaterialSystem2AppSystemDict.Create( createInfo.ToMaterialSystem2AppSystemDictCreateInfo() ); diff --git a/engine/Sandbox.AppSystem/StandaloneAppSystem.cs b/engine/Sandbox.AppSystem/StandaloneAppSystem.cs index 229c7b54..346a8000 100644 --- a/engine/Sandbox.AppSystem/StandaloneAppSystem.cs +++ b/engine/Sandbox.AppSystem/StandaloneAppSystem.cs @@ -68,7 +68,7 @@ public class StandaloneAppSystem : AppSystem // Quit next loop after load, if we are testing else if ( Utility.CommandLine.HasSwitch( "-test-standalone" ) ) { - Application.Exit(); + Game.Close(); } return !wantsToQuit; diff --git a/engine/Sandbox.AppSystem/TestAppSystem.cs b/engine/Sandbox.AppSystem/TestAppSystem.cs new file mode 100644 index 00000000..5ed8762c --- /dev/null +++ b/engine/Sandbox.AppSystem/TestAppSystem.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime; + +namespace Sandbox; + +public class TestAppSystem : AppSystem +{ + public override void Init() + { + GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency; + var GameFolder = System.Environment.GetEnvironmentVariable( "FACEPUNCH_ENGINE", EnvironmentVariableTarget.Process ); + if ( GameFolder is null ) throw new Exception( "FACEPUNCH_ENGINE not found" ); + + NetCore.InitializeInterop( GameFolder ); + + var nativeDllPath = $"{GameFolder}\\bin\\win64\\"; + // + // Put our native dll path first so that when looking up native dlls we'll + // always use the ones from our folder first + // + var path = System.Environment.GetEnvironmentVariable( "PATH" ); + path = $"{nativeDllPath};{path}"; + System.Environment.SetEnvironmentVariable( "PATH", path ); + + CreateGame(); + + var createInfo = new AppSystemCreateInfo() + { + Flags = AppSystemFlags.IsGameApp | AppSystemFlags.IsUnitTest + }; + + InitGame( createInfo, "" ); + } +} diff --git a/engine/Sandbox.Engine/Application.cs b/engine/Sandbox.Engine/Application.cs index 1f84ae2b..2ac18b7d 100644 --- a/engine/Sandbox.Engine/Application.cs +++ b/engine/Sandbox.Engine/Application.cs @@ -25,12 +25,6 @@ public static class Application /// public static bool IsUnitTest { get; private set; } - - /// - /// True if we're running a live unit test. - /// - internal static bool IsLiveUnitTest { get; private set; } - /// /// True if running without a graphics window, such as in a terminal. /// @@ -98,106 +92,7 @@ public static class Application /// public static bool IsVR => VRSystem.IsActive; // garry: I think this is right? But feels like this should be set at startup and never change? - static CMaterialSystem2AppSystemDict AppSystem; - - /// - /// Called from unit test projects to initialize the engine - /// - public static void InitUnitTest( bool withtools = true, bool withRendering = false ) - { - if ( IsInitialized ) - throw new InvalidOperationException( "Already Initialized" ); - - SyncContext.Init(); - SyncContext.Reset(); - - ThreadSafe.MarkMainThread(); - - var callingAssembly = Assembly.GetCallingAssembly(); - var GameFolder = System.Environment.GetEnvironmentVariable( "FACEPUNCH_ENGINE", IsLiveUnitTest ? EnvironmentVariableTarget.User : EnvironmentVariableTarget.Process ); - if ( GameFolder is null ) throw new Exception( "FACEPUNCH_ENGINE not found" ); - - var nativeDllPath = $"{GameFolder}\\bin\\win64\\"; - - var native = NativeLibrary.Load( $"{nativeDllPath}engine2.dll" ); - - // - // Put our native dll path first so that when looking up native dlls we'll - // always use the ones from our folder first - // - var path = System.Environment.GetEnvironmentVariable( "PATH" ); - path = $"{nativeDllPath};{path}"; - System.Environment.SetEnvironmentVariable( "PATH", path ); - - Api.Init(); - EngineFileSystem.Initialize( GameFolder ); - Application.InitializeGame( false, false, false, true, false ); - NetCore.InitializeInterop( GameFolder ); - - Game.InitUnitTest(); - - AppSystem = CMaterialSystem2AppSystemDict.Create( new NativeEngine.MaterialSystem2AppSystemDictCreateInfo() - { - iFlags = NativeEngine.MaterialSystem2AppSystemDictFlags.IsGameApp - } ); - - AppSystem.SuppressCOMInitialization(); - AppSystem.SuppressStartupManifestLoad( true ); - AppSystem.SetModGameSubdir( "core" ); - AppSystem.SetInTestMode(); - - if ( withRendering ) - { - AppSystem.SetDefaultRenderSystemOption( "-vulkan" ); - } - - if ( !NativeEngine.EngineGlobal.SourceEnginePreInit( "", AppSystem ) ) - { - throw new System.Exception( "SourceEnginePreInit failed" ); - } - - AppSystem.InitFinishSetupMaterialSystem(); - - AppSystem.AddSystem( "engine2", "SceneSystem_002" ); - AppSystem.AddSystem( "engine2", "SceneUtils_001" ); - AppSystem.AddSystem( "engine2", "WorldRendererMgr001" ); - - NativeLibrary.Free( native ); - - if ( withtools ) - { - var sandboxGame = Assembly.Load( "Sandbox.Tools" ); - sandboxGame.GetType( "Editor.AssemblyInitialize", true, true ) - .GetMethod( "InitializeUnitTest", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic ) - .Invoke( null, new[] { callingAssembly } ); - } - } - - internal static void InitLiveUnitTest( bool withtools = true, bool withRendering = false ) - { - Application.IsLiveUnitTest = true; - InitUnitTest( withtools, withRendering ); - } - - /// - /// Called from unit test projects to politely shut down the engine - /// - public static void ShutdownUnitTest() - { - if ( !IsUnitTest ) - { - throw new InvalidOperationException( "Not running a unit test" ); - } - - if ( AppSystem.IsValid ) - { - NativeEngine.EngineGlobal.SourceEngineShutdown( AppSystem, false ); - AppSystem.Destroy(); - AppSystem = default; - } - } - - internal static void InitializeGame( bool dedicated, bool headless, bool toolsMode, bool testMode, bool isRetail ) + internal static void Initialize( bool dedicated, bool headless, bool toolsMode, bool testMode, bool isRetail ) { if ( IsInitialized ) throw new InvalidOperationException( "Already Initialized" ); @@ -213,6 +108,11 @@ public static class Application IsBenchmark = Environment.GetEnvironmentVariable( "SBOX_MODE" ) == "BENCHMARK"; } + internal static void Shutdown() + { + IsInitialized = false; + } + internal static void TryLoadVersionInfo( string gameFolder ) { Version = "0000000"; diff --git a/engine/Sandbox.Engine/Core/Bootstrap.cs b/engine/Sandbox.Engine/Core/Bootstrap.cs index 46390590..71a5bb22 100644 --- a/engine/Sandbox.Engine/Core/Bootstrap.cs +++ b/engine/Sandbox.Engine/Core/Bootstrap.cs @@ -28,7 +28,7 @@ internal static class Bootstrap /// internal static void PreInit( CMaterialSystem2AppSystemDict appDict ) { - Application.InitializeGame( appDict.IsDedicatedServer(), appDict.IsConsoleApp(), appDict.IsInToolsMode(), appDict.IsInTestMode(), EngineGlobal.IsRetail() ); + Application.Initialize( appDict.IsDedicatedServer(), appDict.IsConsoleApp(), appDict.IsInToolsMode(), appDict.IsInTestMode(), EngineGlobal.IsRetail() ); try { @@ -91,16 +91,6 @@ internal static class Bootstrap Mounting.Directory.LoadAssemblies(); } - - // - // In testmode (-test) we want to build the .sln files now because we'll be closed - // down after this call. - // - if ( Application.IsUnitTest ) - { - SyncContext.RunBlocking( Project.InitializeBuiltIn() ); - SyncContext.RunBlocking( Project.GenerateSolution() ); - } } catch ( Exception ex ) { @@ -199,9 +189,12 @@ internal static class Bootstrap // { Screen.UpdateFromEngine(); - Material.UI.Init(); - Model.Init(); - Texture.InitStaticTextures(); + Material.UI.InitStatic(); + Gizmo.GizmoDraw.InitStatic(); + Model.InitStatic(); + Texture.InitStatic(); + CubemapRendering.InitStatic(); + Graphics.InitStatic(); } if ( !Application.IsHeadless && !Application.IsStandalone ) @@ -294,8 +287,6 @@ internal static class Bootstrap } } - - internal static void InitMinimal( string rootFolder ) { Environment.CurrentDirectory = rootFolder; diff --git a/engine/Sandbox.Engine/Core/EngineLoop.cs b/engine/Sandbox.Engine/Core/EngineLoop.cs index f532b90e..05358b04 100644 --- a/engine/Sandbox.Engine/Core/EngineLoop.cs +++ b/engine/Sandbox.Engine/Core/EngineLoop.cs @@ -384,34 +384,6 @@ internal static class EngineLoop } } - /// - /// Called when the application is shutting down - /// - internal static void Exiting() - { - // Send shutdown event, should allow us to track successful shutdown vs crash - { - var analytic = new Api.Events.EventRecord( "Exit" ); - analytic.SetValue( "uptime", RealTime.Now ); - // We could record a bunch of stats during the session and - // submit them here. I'm thinking things like num games played - // menus visited, time in menus, time in game, files downloaded. - // Things to give us a whole session picture. - analytic.Submit(); - } - - ConVarSystem.SaveAll(); - - IToolsDll.Current?.Exiting(); - IMenuDll.Current?.Exiting(); - IGameInstanceDll.Current?.Exiting(); - - SoundFile.Shutdown(); - SoundHandle.Shutdown(); - - DedicatedServer.Shutdown(); - } - /// /// A console command has arrived, or a convar has changed /// diff --git a/engine/Sandbox.Engine/Core/GlobalContext/GlobalContext.cs b/engine/Sandbox.Engine/Core/GlobalContext/GlobalContext.cs index 2253fbc7..63ff52ad 100644 --- a/engine/Sandbox.Engine/Core/GlobalContext/GlobalContext.cs +++ b/engine/Sandbox.Engine/Core/GlobalContext/GlobalContext.cs @@ -124,6 +124,8 @@ internal partial class GlobalContext oldCts?.Cancel(); oldCts?.Dispose(); + ActiveScene = null; + TaskSource = new TaskSource( 1 ); EventSystem?.Dispose(); @@ -133,6 +135,9 @@ internal partial class GlobalContext Cookies?.Dispose(); Cookies = null; + + ResourceSystem?.Clear(); + ResourceSystem = new ResourceSystem(); } string _disabledReason; diff --git a/engine/Sandbox.Engine/Editor/Gizmos/Draw/Draw.cs b/engine/Sandbox.Engine/Editor/Gizmos/Draw/Draw.cs index d41f675c..d877a58e 100644 --- a/engine/Sandbox.Engine/Editor/Gizmos/Draw/Draw.cs +++ b/engine/Sandbox.Engine/Editor/Gizmos/Draw/Draw.cs @@ -15,10 +15,30 @@ public static partial class Gizmo /// public sealed partial class GizmoDraw { - static Material LineMaterial = Material.Load( "materials/gizmo/line.vmat" ); - static Material SolidMaterial = Material.Load( "materials/gizmo/solid.vmat" ); - static Material SpriteMaterial = Material.Load( "materials/gizmo/sprite.vmat" ); - static Material GridMaterial = Material.Load( "materials/gizmo/grid.vmat" ); + static Material LineMaterial; + static Material SolidMaterial; + static Material SpriteMaterial; + static Material GridMaterial; + + internal static void InitStatic() + { + LineMaterial = Material.Load( "materials/gizmo/line.vmat" ); + SolidMaterial = Material.Load( "materials/gizmo/solid.vmat" ); + SpriteMaterial = Material.Load( "materials/gizmo/sprite.vmat" ); + GridMaterial = Material.Load( "materials/gizmo/grid.vmat" ); + } + + internal static void DisposeStatic() + { + LineMaterial?.Dispose(); + LineMaterial = null; + SolidMaterial?.Dispose(); + SolidMaterial = null; + SpriteMaterial?.Dispose(); + SpriteMaterial = null; + GridMaterial?.Dispose(); + GridMaterial = null; + } internal GizmoDraw() { diff --git a/engine/Sandbox.Engine/Game/Game/Game.Host.cs b/engine/Sandbox.Engine/Game/Game/Game.Host.cs index abf2fd08..abe799da 100644 --- a/engine/Sandbox.Engine/Game/Game/Game.Host.cs +++ b/engine/Sandbox.Engine/Game/Game/Game.Host.cs @@ -47,24 +47,6 @@ public static partial class Game set => GlobalContext.Current.NodeLibrary = value; } - - /// - /// Initialize for a unit test - /// - internal static void InitUnitTest() - { - GlobalContext.Current.Reset(); - GlobalContext.Current.LocalAssembly = typeof( T ).Assembly; - GlobalContext.Current.UISystem = new UISystem(); - GlobalContext.Current.InputContext = new InputContext(); - GlobalContext.Current.InputContext.TargetUISystem = GlobalContext.Current.UISystem; - - Game.InitHost(); - - TypeLibrary.AddAssembly( typeof( T ).Assembly, false ); - } - - private static void AddNodesFromAssembly( Assembly asm ) { var result = NodeLibrary.AddAssembly( asm ); diff --git a/engine/Sandbox.Engine/Game/Game/Game.cs b/engine/Sandbox.Engine/Game/Game/Game.cs index cc427abb..ad5606a7 100644 --- a/engine/Sandbox.Engine/Game/Game/Game.cs +++ b/engine/Sandbox.Engine/Game/Game/Game.cs @@ -141,21 +141,21 @@ public static partial class Game return; } + // Might want to queue this up. do it the next frame? + // Be aware that this could be called from the GameDll or the MenuDll + // So anything here needs to be safe to call from either + + if ( IGameInstance.Current is not null ) + { + IGameInstance.Current.Close(); + LaunchArguments.Reset(); + } + // Standalone mode and Dedicated Server only: exit whole app if ( Application.IsStandalone || Application.IsDedicatedServer ) { Application.Exit(); } - - // Might want to queue this up. do it the next frame? - // Be aware that this could be called from the GameDll or the MenuDll - // So anything here needs to be safe to call from either - - if ( IGameInstance.Current is null ) - return; - - IGameInstance.Current.Close(); - LaunchArguments.Reset(); } /// diff --git a/engine/Sandbox.Engine/Resources/Material/Material.UI.cs b/engine/Sandbox.Engine/Resources/Material/Material.UI.cs index a0729e3c..5a1423fa 100644 --- a/engine/Sandbox.Engine/Resources/Material/Material.UI.cs +++ b/engine/Sandbox.Engine/Resources/Material/Material.UI.cs @@ -40,7 +40,7 @@ namespace Sandbox /// internal static Material DropShadow { get; set; } - internal static void Init() + internal static void InitStatic() { Basic = FromShader( "shaders/ui_basic.shader" ); Box = FromShader( "shaders/ui_cssbox.shader" ); @@ -51,6 +51,26 @@ namespace Sandbox DropShadow = FromShader( "shaders/ui_dropshadow.shader" ); BorderWrap = FromShader( "shaders/ui_borderwrap.shader" ); } + + internal static void DisposeStatic() + { + Basic?.Dispose(); + Basic = null; + Box?.Dispose(); + Box = null; + BoxShadow?.Dispose(); + BoxShadow = null; + Text?.Dispose(); + Text = null; + BackdropFilter?.Dispose(); + BackdropFilter = null; + Filter?.Dispose(); + Filter = null; + DropShadow?.Dispose(); + DropShadow = null; + BorderWrap?.Dispose(); + BorderWrap = null; + } } } } diff --git a/engine/Sandbox.Engine/Resources/Material/Material.cs b/engine/Sandbox.Engine/Resources/Material/Material.cs index 7f2a7a31..96758f4e 100644 --- a/engine/Sandbox.Engine/Resources/Material/Material.cs +++ b/engine/Sandbox.Engine/Resources/Material/Material.cs @@ -41,16 +41,24 @@ public sealed partial class Material : Resource } ~Material() + { + Dispose(); + } + + internal void Dispose() { // kill the native pointer - it does with the native material // we want to reduce the risk that someone is holding on to it. - Attributes.Set( default ); + Attributes?.Set( default ); Attributes = null; - var n = native; - native = default; + if ( !native.IsNull ) + { + var n = native; + native = default; - MainThread.Queue( () => n.DestroyStrongHandle() ); + MainThread.Queue( () => n.DestroyStrongHandle() ); + } } /// diff --git a/engine/Sandbox.Engine/Resources/Model/Model.Static.cs b/engine/Sandbox.Engine/Resources/Model/Model.Static.cs index 089bec51..7b668454 100644 --- a/engine/Sandbox.Engine/Resources/Model/Model.Static.cs +++ b/engine/Sandbox.Engine/Resources/Model/Model.Static.cs @@ -58,11 +58,23 @@ public partial class Model public static Model Error { get; internal set; } - internal static void Init() + internal static void InitStatic() { Cube = Load( "models/dev/box.vmdl" ); Sphere = Load( "models/dev/sphere.vmdl" ); Plane = Load( "models/dev/plane.vmdl" ); Error = Load( "models/dev/error.vmdl" ); } + + internal static void DisposeStatic() + { + Cube?.Dispose(); + Cube = null; + Sphere?.Dispose(); + Sphere = null; + Plane?.Dispose(); + Plane = null; + Error?.Dispose(); + Error = null; + } } diff --git a/engine/Sandbox.Engine/Resources/Model/Model.cs b/engine/Sandbox.Engine/Resources/Model/Model.cs index dec734f7..bd03f8bf 100644 --- a/engine/Sandbox.Engine/Resources/Model/Model.cs +++ b/engine/Sandbox.Engine/Resources/Model/Model.cs @@ -28,12 +28,20 @@ public sealed partial class Model : Resource SetIdFromResourcePath( Name ); } + internal void Dispose() + { + if ( !native.IsNull ) + { + var n = native; + native = default; + + MainThread.Queue( () => n.DestroyStrongHandle() ); + } + } + ~Model() { - var n = native; - native = default; - - MainThread.Queue( () => n.DestroyStrongHandle() ); + Dispose(); } /// diff --git a/engine/Sandbox.Engine/Resources/Resource.cs b/engine/Sandbox.Engine/Resources/Resource.cs index 6ffd6750..5d9220fa 100644 --- a/engine/Sandbox.Engine/Resources/Resource.cs +++ b/engine/Sandbox.Engine/Resources/Resource.cs @@ -39,13 +39,20 @@ public abstract partial class Resource : IValid, IJsonConvert, BytePack.ISeriali /// [Hide, JsonIgnore] public virtual bool HasUnsavedChanges => false; - ~Resource() + internal void Destroy() { // Unregister on main thread MainThread.Queue( () => { Game.Resources.Unregister( this ); } ); Manifest?.Dispose(); Manifest = default; + + GC.SuppressFinalize( this ); + } + + ~Resource() + { + Destroy(); } internal static string FixPath( string filename ) diff --git a/engine/Sandbox.Engine/Resources/ResourceLibrary.cs b/engine/Sandbox.Engine/Resources/ResourceLibrary.cs index 306482c0..4f3b2345 100644 --- a/engine/Sandbox.Engine/Resources/ResourceLibrary.cs +++ b/engine/Sandbox.Engine/Resources/ResourceLibrary.cs @@ -58,13 +58,19 @@ public class ResourceSystem var toDispose = ResourceIndex.Values.ToArray(); - ResourceIndex.Clear(); - foreach ( var resource in toDispose.OfType() ) { resource.DestroyInternal(); } + foreach ( var resource in toDispose ) + { + // Don't wait/rely for finalizer get rid of this immediately + resource.Destroy(); + } + + ResourceIndex.Clear(); + TypeCache.Clear(); } diff --git a/engine/Sandbox.Engine/Resources/Scene/PrefabFile.cs b/engine/Sandbox.Engine/Resources/Scene/PrefabFile.cs index 9117a398..a432488b 100644 --- a/engine/Sandbox.Engine/Resources/Scene/PrefabFile.cs +++ b/engine/Sandbox.Engine/Resources/Scene/PrefabFile.cs @@ -67,7 +67,7 @@ public partial class PrefabFile : GameResource protected override void OnDestroy() { - CachedScene?.DestroyImmediate(); + CachedScene?.DestroyInternal(); CachedScene = null; Unregister(); diff --git a/engine/Sandbox.Engine/Resources/Textures/Texture.Static.cs b/engine/Sandbox.Engine/Resources/Textures/Texture.Static.cs index ededb4db..045dd5c6 100644 --- a/engine/Sandbox.Engine/Resources/Textures/Texture.Static.cs +++ b/engine/Sandbox.Engine/Resources/Textures/Texture.Static.cs @@ -23,7 +23,7 @@ public partial class Texture /// public static Texture Transparent { get; internal set; } - internal static void InitStaticTextures() + internal static void InitStatic() { Invalid = Create( 1, 1 ).WithData( new byte[4] { 255, 0, 255, 255 } ).Finish(); White = Create( 1, 1 ).WithData( new byte[4] { 255, 255, 255, 255 } ).Finish(); @@ -31,6 +31,18 @@ public partial class Texture Black = Create( 1, 1 ).WithData( new byte[4] { 0, 0, 0, 255 } ).Finish(); } + internal static void DisposeStatic() + { + Invalid?.Dispose(); + Invalid = default; + White?.Dispose(); + White = default; + Transparent?.Dispose(); + Transparent = default; + Black?.Dispose(); + Black = default; + } + internal static Texture Create( string name, bool anonymous, TextureBuilder builder, IntPtr data, int dataSize ) { var config = builder._config.GetWithFixes(); diff --git a/engine/Sandbox.Engine/Scene/Components/Mesh/PolygonMesh.cs b/engine/Sandbox.Engine/Scene/Components/Mesh/PolygonMesh.cs index 0991fdb1..7a0aea7a 100644 --- a/engine/Sandbox.Engine/Scene/Components/Mesh/PolygonMesh.cs +++ b/engine/Sandbox.Engine/Scene/Components/Mesh/PolygonMesh.cs @@ -52,8 +52,8 @@ public sealed partial class PolygonMesh : IJsonConvert public int MaterialId { get; set; } } - private static readonly Material DefaultMaterial = Material.Load( "materials/dev/reflectivity_30.vmat" ); - private static readonly Vector2 DefaultTextureSize = CalculateTextureSize( DefaultMaterial ); + private Material DefaultMaterial = Material.Load( "materials/dev/reflectivity_30.vmat" ); + private Vector2 DefaultTextureSize => CalculateTextureSize( DefaultMaterial ); private VertexData Positions { get; init; } private HalfEdgeData TextureCoord { get; init; } diff --git a/engine/Sandbox.Engine/Scene/GameObject/GameObject.Destroy.cs b/engine/Sandbox.Engine/Scene/GameObject/GameObject.Destroy.cs index 5aaedd9c..e7f99177 100644 --- a/engine/Sandbox.Engine/Scene/GameObject/GameObject.Destroy.cs +++ b/engine/Sandbox.Engine/Scene/GameObject/GameObject.Destroy.cs @@ -110,6 +110,7 @@ public partial class GameObject : IValid /// public void DestroyImmediate() { + Assert.False( this is Scene, "Don't call DestroyImmediate on a scene." ); ThreadSafe.AssertIsMainThread(); Term(); diff --git a/engine/Sandbox.Engine/Scene/Networking/SceneNetworkSystem.cs b/engine/Sandbox.Engine/Scene/Networking/SceneNetworkSystem.cs index 7f100ff7..395b98d9 100644 --- a/engine/Sandbox.Engine/Scene/Networking/SceneNetworkSystem.cs +++ b/engine/Sandbox.Engine/Scene/Networking/SceneNetworkSystem.cs @@ -234,10 +234,10 @@ public partial class SceneNetworkSystem : GameNetworkSystem LoadingScreen.Title = "Loading Scene"; } - // Go ahead and destroy the scene immediately (if it exists.) + // Go ahead and destroy the scene if ( Game.ActiveScene is not null ) { - Game.ActiveScene?.DestroyImmediate(); + Game.ActiveScene?.Destroy(); Game.ActiveScene = null; } @@ -408,7 +408,7 @@ public partial class SceneNetworkSystem : GameNetworkSystem if ( Game.ActiveScene is not null ) { - Game.ActiveScene?.DestroyImmediate(); + Game.ActiveScene?.Destroy(); Game.ActiveScene = null; } diff --git a/engine/Sandbox.Engine/Scene/Scene/Scene.cs b/engine/Sandbox.Engine/Scene/Scene/Scene.cs index 71774b1d..650eaa26 100644 --- a/engine/Sandbox.Engine/Scene/Scene/Scene.cs +++ b/engine/Sandbox.Engine/Scene/Scene/Scene.cs @@ -100,6 +100,8 @@ public partial class Scene : GameObject internal virtual void DestroyInternal() { + _all.Remove( this ); + // Clearing the object index now means we can save time // because we don't have to do it for each object. // Note that we can't do this in Clear because we don't want to @@ -154,7 +156,6 @@ public partial class Scene : GameObject public IDisposable Push() { ThreadSafe.AssertIsMainThread(); - var old = Game.ActiveScene; Game.ActiveScene = this; diff --git a/engine/Sandbox.Engine/Services/Packages/PackageManager/PackageManager.ActivePackage.cs b/engine/Sandbox.Engine/Services/Packages/PackageManager/PackageManager.ActivePackage.cs index 76215431..1cb97e1c 100644 --- a/engine/Sandbox.Engine/Services/Packages/PackageManager/PackageManager.ActivePackage.cs +++ b/engine/Sandbox.Engine/Services/Packages/PackageManager/PackageManager.ActivePackage.cs @@ -170,9 +170,6 @@ internal static partial class PackageManager MountedFileSystem.Mount( FileSystem ); MountedFileSystem.Mount( AssemblyFileSystem ); - if ( Application.IsUnitTest ) // todo: fully init the engine for unit test - return; - // Reload any already resident resources with the ones we've just mounted NativeEngine.g_pResourceSystem.ReloadSymlinkedResidentResources(); @@ -199,9 +196,6 @@ internal static partial class PackageManager AssemblyFileSystem.Dispose(); AssemblyFileSystem = null; - if ( Application.IsUnitTest ) // todo: fully init the engine for unit test - return; - // Reload any resident resources that were just unmounted (they shouldn't be used & will appear as an error, or a local variant) NativeEngine.g_pResourceSystem.ReloadSymlinkedResidentResources(); } diff --git a/engine/Sandbox.Engine/Services/Packages/PackageManager/PackageManager.cs b/engine/Sandbox.Engine/Services/Packages/PackageManager/PackageManager.cs index c7b2a75d..bafa1f8b 100644 --- a/engine/Sandbox.Engine/Services/Packages/PackageManager/PackageManager.cs +++ b/engine/Sandbox.Engine/Services/Packages/PackageManager/PackageManager.cs @@ -20,12 +20,6 @@ internal static partial class PackageManager /// public static event Action OnPackageInstalledToContext; - internal static void ResetForUnitTest() - { - ActivePackages = new(); - MountedFileSystem = new AggregateFileSystem(); - } - static async Task FetchPackageAsync( string ident, bool localPriority ) { if ( localPriority && Package.TryParseIdent( ident, out var parts ) && !parts.local ) @@ -131,6 +125,15 @@ internal static partial class PackageManager } } + internal static void UnmountAll() + { + foreach ( var item in ActivePackages.ToArray() ) + { + item.Delete(); + ActivePackages.Remove( item ); + } + } + private static async Task InstallDependencies( Package package, PackageLoadOptions options ) { HashSet dependancies = new HashSet( StringComparer.OrdinalIgnoreCase ); diff --git a/engine/Sandbox.Engine/Systems/Console/ConVarSystem.Native.cs b/engine/Sandbox.Engine/Systems/Console/ConVarSystem.Native.cs index 8a6ecdc9..b3d79e03 100644 --- a/engine/Sandbox.Engine/Systems/Console/ConVarSystem.Native.cs +++ b/engine/Sandbox.Engine/Systems/Console/ConVarSystem.Native.cs @@ -19,6 +19,31 @@ internal static partial class ConVarSystem var command = new NativeCommand( value ); AddCommand( command ); } + + internal static void ClearNativeCommands() + { + if ( Members.Count == 0 ) + return; + + System.Collections.Generic.List nativeKeys = null; + + foreach ( var (name, command) in Members ) + { + if ( command is NativeCommand || command is NativeConVar ) + { + nativeKeys ??= new System.Collections.Generic.List(); + nativeKeys.Add( name ); + } + } + + if ( nativeKeys is null ) + return; + + foreach ( var name in nativeKeys ) + { + Members.Remove( name ); + } + } } diff --git a/engine/Sandbox.Engine/Systems/Filesystem/EngineFileSystem.cs b/engine/Sandbox.Engine/Systems/Filesystem/EngineFileSystem.cs index 9a6b67f2..3f775a4f 100644 --- a/engine/Sandbox.Engine/Systems/Filesystem/EngineFileSystem.cs +++ b/engine/Sandbox.Engine/Systems/Filesystem/EngineFileSystem.cs @@ -44,7 +44,7 @@ internal static class EngineFileSystem /// /// Don't try to use the filesystem until you've called this! /// - internal static void Initialize( string rootFolder ) + internal static void Initialize( string rootFolder, bool skipBaseFolderInit = false ) { if ( Root != null ) throw new System.Exception( "Filesystem Multi-Initialize" ); @@ -52,8 +52,7 @@ internal static class EngineFileSystem Root = new LocalFileSystem( rootFolder ); Temporary = new MemoryFileSystem(); - if ( Application.IsUnitTest ) - return; + if ( skipBaseFolderInit ) return; if ( Application.IsEditor ) { diff --git a/engine/Sandbox.Engine/Systems/Networking/System/NetworkSystem.Game.cs b/engine/Sandbox.Engine/Systems/Networking/System/NetworkSystem.Game.cs index 78c1bc43..f4119699 100644 --- a/engine/Sandbox.Engine/Systems/Networking/System/NetworkSystem.Game.cs +++ b/engine/Sandbox.Engine/Systems/Networking/System/NetworkSystem.Game.cs @@ -9,7 +9,8 @@ internal partial class NetworkSystem public void InitializeGameSystem() { - if ( IGameInstanceDll.Current is null ) + // If we are unit testing we dont want to do any of this for now, this only works with a gamepackage loaded + if ( IGameInstanceDll.Current is null || Application.IsUnitTest ) return; GameSystem = IGameInstanceDll.Current.CreateGameNetworking( this ); diff --git a/engine/Sandbox.Engine/Systems/Networking/Tables/ServerPackages.cs b/engine/Sandbox.Engine/Systems/Networking/Tables/ServerPackages.cs index 04c2812a..b19e803d 100644 --- a/engine/Sandbox.Engine/Systems/Networking/Tables/ServerPackages.cs +++ b/engine/Sandbox.Engine/Systems/Networking/Tables/ServerPackages.cs @@ -11,7 +11,7 @@ namespace Sandbox; /// internal class ServerPackages { - public static ServerPackages Current { get; private set; } + public static ServerPackages Current { get; private set; } = new(); internal record struct ServerPackageInfo(); internal StringTable StringTable; @@ -86,7 +86,7 @@ internal class ServerPackages internal ServerPackages() { - Assert.IsNull( Current ); + // WTF??? Current = this; StringTable = new StringTable( "ServerPackages", true ); diff --git a/engine/Sandbox.Engine/Systems/Render/ComputeShader.cs b/engine/Sandbox.Engine/Systems/Render/ComputeShader.cs index 9aaaa45e..65ff3a43 100644 --- a/engine/Sandbox.Engine/Systems/Render/ComputeShader.cs +++ b/engine/Sandbox.Engine/Systems/Render/ComputeShader.cs @@ -1,4 +1,5 @@ using NativeEngine; +using Sandbox.VR; namespace Sandbox; @@ -25,6 +26,11 @@ public class ComputeShader ComputeMaterial = material; } + internal void Dispose() + { + ComputeMaterial.Dispose(); + } + /// /// Dispatch this compute shader using explicit thread counts. /// diff --git a/engine/Sandbox.Engine/Systems/Render/CubemapRendering.cs b/engine/Sandbox.Engine/Systems/Render/CubemapRendering.cs index ba302ddf..66f399df 100644 --- a/engine/Sandbox.Engine/Systems/Render/CubemapRendering.cs +++ b/engine/Sandbox.Engine/Systems/Render/CubemapRendering.cs @@ -1,5 +1,3 @@ -using NativeEngine; -using System.Linq; using Sandbox.Rendering; namespace Sandbox; @@ -10,7 +8,18 @@ namespace Sandbox; /// internal static class CubemapRendering { - static ComputeShader EnvmapFilter = new( "envmap_filtering_cs" ); + static ComputeShader EnvmapFilter; + + internal static void InitStatic() + { + EnvmapFilter = new( "envmap_filtering_cs" ); + } + + internal static void DisposeStatic() + { + EnvmapFilter?.Dispose(); + EnvmapFilter = null; + } /// /// Specifies the quality level for GGX filtering of environment maps. diff --git a/engine/Sandbox.Engine/Systems/Render/Graphics.MipmapGen.cs b/engine/Sandbox.Engine/Systems/Render/Graphics.MipmapGen.cs index 5de282b9..41d8a1ac 100644 --- a/engine/Sandbox.Engine/Systems/Render/Graphics.MipmapGen.cs +++ b/engine/Sandbox.Engine/Systems/Render/Graphics.MipmapGen.cs @@ -4,7 +4,18 @@ namespace Sandbox; public static partial class Graphics { - internal static ComputeShader MipMapGeneratorShader = new ComputeShader( "downsample_cs" ); + internal static ComputeShader MipMapGeneratorShader; + + internal static void InitStatic() + { + MipMapGeneratorShader = new ComputeShader( "downsample_cs" ); + } + + internal static void DisposeStatic() + { + MipMapGeneratorShader?.Dispose(); + MipMapGeneratorShader = null; + } /// /// Which method to use when downsampling a texture diff --git a/engine/Sandbox.Engine/Systems/Render/RenderPipeline/BloomLayer.cs b/engine/Sandbox.Engine/Systems/Render/RenderPipeline/BloomLayer.cs index b2c7b4d9..b19594a2 100644 --- a/engine/Sandbox.Engine/Systems/Render/RenderPipeline/BloomLayer.cs +++ b/engine/Sandbox.Engine/Systems/Render/RenderPipeline/BloomLayer.cs @@ -46,7 +46,7 @@ internal class BloomDownsampleLayer : ProceduralRenderLayer } internal class QuarterDepthDownsampleLayer : ProceduralRenderLayer { - private static Material DepthResolve = Material.Create( "depthresolve", "shaders/depthresolve.shader" ); + private Material DepthResolve; private bool MSAAInput; public QuarterDepthDownsampleLayer() @@ -55,6 +55,7 @@ internal class QuarterDepthDownsampleLayer : ProceduralRenderLayer Flags |= LayerFlags.NeverRemove | LayerFlags.DoesntModifyColorBuffers; ClearFlags = ClearFlags.Depth | ClearFlags.Stencil; LayerType = SceneLayerType.Opaque; + DepthResolve = Material.Create( "depthresolve", "shaders/depthresolve.shader" ); } public void Setup( ISceneView view, RenderViewport viewport, SceneViewRenderTargetHandle rtDepth, bool msaaInput, RenderTarget rtOutDepth ) diff --git a/engine/Sandbox.Engine/Systems/Render/RenderPipeline/RenderPipeline.Static.cs b/engine/Sandbox.Engine/Systems/Render/RenderPipeline/RenderPipeline.Static.cs index 6e4835ff..13e57614 100644 --- a/engine/Sandbox.Engine/Systems/Render/RenderPipeline/RenderPipeline.Static.cs +++ b/engine/Sandbox.Engine/Systems/Render/RenderPipeline/RenderPipeline.Static.cs @@ -43,4 +43,10 @@ internal partial class RenderPipeline // Return to pool Pool.Enqueue( renderPipeline ); } + + internal static void ClearPool() + { + Pool.Clear(); + ActivePipelines.Clear(); + } } diff --git a/engine/Sandbox.Engine/Systems/Render/TextRendering/TextRendering.cs b/engine/Sandbox.Engine/Systems/Render/TextRendering/TextRendering.cs index 36ec15c3..9ccd4d99 100644 --- a/engine/Sandbox.Engine/Systems/Render/TextRendering/TextRendering.cs +++ b/engine/Sandbox.Engine/Systems/Render/TextRendering/TextRendering.cs @@ -95,4 +95,13 @@ public static partial class TextRendering //Log.Info( $"TextManager: {total} ({deleted} deleted)" ); } + + internal static void ClearCache() + { + foreach ( var item in Dictionary ) + { + item.Value.Dispose(); + } + Dictionary.Clear(); + } } diff --git a/engine/Sandbox.GameInstance/Assembly.cs b/engine/Sandbox.GameInstance/Assembly.cs index c5ddf524..bdc776c1 100644 --- a/engine/Sandbox.GameInstance/Assembly.cs +++ b/engine/Sandbox.GameInstance/Assembly.cs @@ -10,3 +10,4 @@ using System.Runtime.CompilerServices; [assembly: TasksPersistOnContextReset] [assembly: InternalsVisibleTo( "Sandbox.Tools" )] [assembly: InternalsVisibleTo( "Sandbox.AppSystem" )] +[assembly: InternalsVisibleTo( "Sandbox.Test" )] diff --git a/engine/Sandbox.GameInstance/GameInstance.cs b/engine/Sandbox.GameInstance/GameInstance.cs index 10958240..fd354eb0 100644 --- a/engine/Sandbox.GameInstance/GameInstance.cs +++ b/engine/Sandbox.GameInstance/GameInstance.cs @@ -110,8 +110,8 @@ internal partial class GameInstance : IGameInstance if ( activePackage != null && !Application.IsStandalone ) { - Game.Language.Shutdown(); - FileSystem.Mounted.UnMount( activePackage.FileSystem ); + Game.Language?.Shutdown(); + FileSystem.Mounted?.UnMount( activePackage.FileSystem ); activePackage = null; } @@ -121,7 +121,7 @@ internal partial class GameInstance : IGameInstance // Is this the right place for it? Map packages are marked with "game" so they never get unmounted PackageManager.UnmountTagged( "game" ); - GameInstanceDll.Current.OnGameInstanceClosed( this ); + GameInstanceDll.Current.Shutdown( this ); Game.Shutdown(); diff --git a/engine/Sandbox.GameInstance/GameInstanceDll.cs b/engine/Sandbox.GameInstance/GameInstanceDll.cs index 1067d80a..7032a7d7 100644 --- a/engine/Sandbox.GameInstance/GameInstanceDll.cs +++ b/engine/Sandbox.GameInstance/GameInstanceDll.cs @@ -66,7 +66,7 @@ internal partial class GameInstanceDll : Engine.IGameInstanceDll } } - Assert.IsNull( PackageLoader ); + PackageLoader?.Dispose(); PackageLoader = new PackageLoader( "GameMenu", typeof( GameInstanceDll ).Assembly ); PackageLoader.HotloadWatch( Game.GameAssembly ); // Sandbox.Game is per instance PackageLoader.OnAfterHotload = OnAfterHotload; @@ -234,11 +234,14 @@ internal partial class GameInstanceDll : Engine.IGameInstanceDll } }; - // Clear resource library so resources don't leak between games, - // then let IMenuDll reload whatever resources it needs - - Game.Resources.Clear(); IMenuDll.Current?.Reset(); + + // Run GC and finalizers to clear any native resources held + GC.Collect(); + GC.WaitForPendingFinalizers(); + + // Run the queue one more time, since some finalizers queue tasks + MainThread.RunQueues(); } /// @@ -700,7 +703,7 @@ internal partial class GameInstanceDll : Engine.IGameInstanceDll /// Called when the game menu is closed /// /// - public void OnGameInstanceClosed( IGameInstance instance ) + public void Shutdown( IGameInstance instance ) { NativeErrorReporter.Breadcrumb( true, "game", "Closed game instance" ); NativeErrorReporter.SetTag( "game", null ); diff --git a/engine/Sandbox.Menu/MenuDll.cs b/engine/Sandbox.Menu/MenuDll.cs index 8aa38dcf..36ac02da 100644 --- a/engine/Sandbox.Menu/MenuDll.cs +++ b/engine/Sandbox.Menu/MenuDll.cs @@ -211,8 +211,44 @@ internal sealed class MenuDll : IMenuDll public void Exiting() { - using var scope = PushScope(); - Game.Cookies?.Save(); + using ( PushScope() ) + { + // Shutdown menu system + IMenuSystem.Current?.Shutdown(); + IMenuSystem.Current = null; + + // Unregister messaging + Sandbox.Services.Messaging.OnMessage -= OnMessageFromBackend; + + // Save and dispose cookies + Game.Cookies?.Save(); + Game.Cookies = null; + + // Cleanup scene + MenuScene.Scene?.Destroy(); + MenuScene.Scene = null; + + // Dispose package loader and enroller + Enroller?.Dispose(); + Enroller = null; + + Loader?.Dispose(); + Loader = null; + + // Shutdown Steamworks interfaces + if ( !Application.IsEditor ) + { + Steamworks.SteamClient.Cleanup(); + } + + // Expire async context to prevent lingering tasks + AsyncContext.Expire( null ); + + // Clear global context + GlobalContext.Current.Reset(); + + IMenuDll.Current = null; + } } void LoadResources() diff --git a/engine/Sandbox.Test/ActionGraphs/LiveGamePackage.cs b/engine/Sandbox.Test/ActionGraphs/LiveGamePackage.cs index 3be16b02..32b5a2f3 100644 --- a/engine/Sandbox.Test/ActionGraphs/LiveGamePackage.cs +++ b/engine/Sandbox.Test/ActionGraphs/LiveGamePackage.cs @@ -2,123 +2,78 @@ using Sandbox.ActionGraphs; using Sandbox.Engine; using Sandbox.Internal; +using Sandbox.Tasks; using System; using System.Collections.Generic; +using System.Threading; namespace ActionGraphs; [TestClass] public class LiveGamePackage { - [TestInitialize] - public void TestInitialize() - { - Project.Clear(); - PackageManager.ResetForUnitTest(); - AssetDownloadCache.Initialize( $"{Environment.CurrentDirectory}/.source2/package_manager_folder" ); - } - - [TestCleanup] - public void TestCleanup() - { - Game.NodeLibrary = null; - GlobalContext.Current.FileMount = null; - - Game.Shutdown(); - } - /// /// Asserts that all the ActionGraphs referenced by a given scene in a downloaded /// package have no errors. /// [TestMethod] - [DataRow( "fish.sauna", 76972L, "scenes/finland.scene", 140, + [DataRow( "fish.sauna", 76972L, "scenes/finland.scene", 14, "d174cab5-7a05-476c-a545-4db2fd685032", // Prefab references game object from other scene "e9ac7c29-ff9f-4c3c-8d9d-7228c4711248", // Inventory method changed parameter types "462927b9-1f01-4ba8-9f6b-2e1e6a5934e4" // Inventory method changed parameter types )] - public async Task AssertNoGraphErrorsInScene( string packageName, long? version, string scenePath, int graphCount, params string[] ignoreGuids ) + public void AssertNoGraphErrorsInScene( string packageName, long? version, string scenePath, int graphCount, params string[] ignoreGuids ) { + AssetDownloadCache.Initialize( $"{Environment.CurrentDirectory}/.source2/package_manager_folder" ); + + PackageManager.UnmountAll(); + // Let's make sure we have base content mounted + IGameInstanceDll.Current?.Bootstrap(); + var ignoreGuidSet = new HashSet( ignoreGuids.Select( Guid.Parse ) ); - using ( var packageLoader = new Sandbox.PackageLoader( "Test", GetType().Assembly ) ) + var packageIdent = version is { } v ? $"{packageName}#{v}" : packageName; + + // Use the production loading logic - run blocking to ensure it completes + var loadTask = GameInstanceDll.Current.LoadGamePackageAsync( packageIdent, GameLoadingFlags.Host, CancellationToken.None ); + SyncContext.RunBlocking( loadTask ); + + Assert.IsNotNull( GameInstanceDll.gameInstance, "Game instance should be loaded" ); + Assert.AreNotEqual( 0, PackageManager.MountedFileSystem.FileCount, "We have package files mounted" ); + Assert.AreNotEqual( 0, GlobalGameNamespace.TypeLibrary.Types.Count, "Library has classes" ); + + var sceneFile = ResourceLibrary.Get( scenePath ); + Assert.IsNotNull( sceneFile, "Target scene exists" ); + + ActionGraphDebugger.Enabled = true; + + Game.ActiveScene = new Scene(); + Game.ActiveScene.LoadFromFile( sceneFile.ResourcePath ); + + var graphs = ActionGraphDebugger.GetAllGraphs(); + Assert.AreEqual( graphCount, graphs.Count, "Scene has expected graph count" ); + + var anyErrors = false; + + foreach ( var graph in graphs ) { - using var enroller = packageLoader.CreateEnroller( "test-enroller" ); + Console.WriteLine( $"{graph.Guid}: {graph.Title} {(ignoreGuidSet.Contains( graph.Guid ) ? "(IGNORED)" : "")}" ); - GlobalContext.Current.FileMount = PackageManager.MountedFileSystem; - - enroller.OnAssemblyAdded = ( a ) => + foreach ( var message in graph.Messages ) { - Game.TypeLibrary.AddAssembly( a.Assembly, true ); - Game.NodeLibrary.AddAssembly( a.Assembly ); - }; - - Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount, "No package files mounted" ); - - var downloadOptions = new PackageLoadOptions - { - PackageIdent = version is { } v ? $"{packageName}#{v}" : packageName, - ContextTag = "client", - AllowLocalPackages = false - }; - - var activePackage = await PackageManager.InstallAsync( downloadOptions ); - - if ( version is not null ) - { - Assert.AreEqual( version.Value, activePackage.Package.Revision.VersionId ); + Console.WriteLine( $" {message}" ); } - Assert.IsNotNull( activePackage ); - Assert.AreNotEqual( 0, PackageManager.MountedFileSystem.FileCount, "We have package files mounted" ); - - // Load the assemblies into the context - enroller.LoadPackage( packageName ); - - Assert.AreNotEqual( 0, GlobalGameNamespace.TypeLibrary.Types.Count, "Library has classes" ); - - JsonUpgrader.UpdateUpgraders( GlobalGameNamespace.TypeLibrary ); - - ResourceLoader.LoadAllGameResource( PackageManager.MountedFileSystem ); - - var sceneFile = ResourceLibrary.Get( scenePath ); - - Assert.IsNotNull( sceneFile, "Target scene exists" ); - - ActionGraphDebugger.Enabled = true; - - var anyErrors = false; - - Game.ActiveScene = new Scene(); - Game.ActiveScene.LoadFromFile( sceneFile.ResourcePath ); - - var graphs = ActionGraphDebugger.GetAllGraphs(); - - Assert.AreEqual( graphCount, graphs.Count, "Scene has expected graph count" ); - - foreach ( var graph in graphs ) + if ( !ignoreGuidSet.Contains( graph.Guid ) ) { - Console.WriteLine( $"{graph.Guid}: {graph.Title} {(ignoreGuidSet.Contains( graph.Guid ) ? "(IGNORED)" : "")}" ); - - foreach ( var message in graph.Messages ) - { - Console.WriteLine( $" {message}" ); - } - - if ( !ignoreGuidSet.Contains( graph.Guid ) ) - { - - anyErrors |= graph.HasErrors(); - } + anyErrors |= graph.HasErrors(); } - - ActionGraphDebugger.Enabled = false; - - PackageManager.UnmountTagged( "client" ); - - Assert.IsFalse( anyErrors ); } - Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount, "Unmounted everything" ); + ActionGraphDebugger.Enabled = false; + + Assert.IsFalse( anyErrors, "No unexpected graph errors" ); + + GameInstanceDll.Current?.CloseGame(); } } diff --git a/engine/Sandbox.Test/Engine/Shutdown.cs b/engine/Sandbox.Test/Engine/Shutdown.cs new file mode 100644 index 00000000..14be2b4f --- /dev/null +++ b/engine/Sandbox.Test/Engine/Shutdown.cs @@ -0,0 +1,24 @@ +namespace Misc; + +[TestClass] +public class Shutdown +{ + [TestMethod] + public void Single() + { + // We already initialized the app for testing, so we can directly shutdown + TestInit.TestAppSystem.Shutdown(); + + // We need to re-init because other tests still need it + TestInit.TestAppSystem.Init(); + } + + [TestMethod] + public void Multiple() + { + TestInit.TestAppSystem.Shutdown(); + TestInit.TestAppSystem.Init(); + TestInit.TestAppSystem.Shutdown(); + TestInit.TestAppSystem.Init(); + } +} diff --git a/engine/Sandbox.Test/Filesystem/FileSystem.cs b/engine/Sandbox.Test/Filesystem/FileSystem.cs index ae380357..665f0e09 100644 --- a/engine/Sandbox.Test/Filesystem/FileSystem.cs +++ b/engine/Sandbox.Test/Filesystem/FileSystem.cs @@ -18,7 +18,7 @@ public partial class FileSystem System.IO.Directory.CreateDirectory( ".source2/TestFolder" ); Sandbox.EngineFileSystem.Shutdown(); - Sandbox.EngineFileSystem.Initialize( ".source2/TestFolder" ); + Sandbox.EngineFileSystem.Initialize( ".source2/TestFolder", true ); Sandbox.EngineFileSystem.Root.WriteAllText( "root_text_file.txt", "Hello" ); diff --git a/engine/Sandbox.Test/Package/PackageDownload.cs b/engine/Sandbox.Test/Package/PackageDownload.cs index ffa77c60..90d7d56f 100644 --- a/engine/Sandbox.Test/Package/PackageDownload.cs +++ b/engine/Sandbox.Test/Package/PackageDownload.cs @@ -5,12 +5,6 @@ namespace Packages; [TestClass] public class PackageDownload { - [TestInitialize] - public void TestInitialize() - { - PackageManager.ResetForUnitTest(); - } - [TestMethod] [DataRow( "facepunch.sandbox" )] [DataRow( "garry.grassworld" )] diff --git a/engine/Sandbox.Test/Package/PackageLoader.cs b/engine/Sandbox.Test/Package/PackageLoader.cs index f1a3b983..133ce74b 100644 --- a/engine/Sandbox.Test/Package/PackageLoader.cs +++ b/engine/Sandbox.Test/Package/PackageLoader.cs @@ -10,7 +10,6 @@ public partial class PackageLoader public void TestInitialize() { Project.Clear(); - PackageManager.ResetForUnitTest(); AssetDownloadCache.Initialize( $"{Environment.CurrentDirectory}/.source2/package_manager_folder" ); } @@ -94,7 +93,7 @@ public partial class PackageLoader var addonName = "garry.grassworld"; var addonClass = "GrassSpawner"; - var (library, _, enroller) = Preamble(); + var (library, packageLoader, enroller) = Preamble(); Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount, "No package files mounted" ); @@ -115,13 +114,16 @@ public partial class PackageLoader PackageManager.UnmountTagged( "client" ); + enroller.Dispose(); + packageLoader.Dispose(); + Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount, "Unmounted everything" ); } //[TestMethod] public async Task LoadPackageWithAddonWithLibrary( string packageName ) { - var (library, _, enroller) = Preamble(); + var (library, packageLoader, enroller) = Preamble(); Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount, "No package files mounted" ); @@ -138,6 +140,9 @@ public partial class PackageLoader PackageManager.UnmountTagged( "client" ); + enroller.Dispose(); + packageLoader.Dispose(); + Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount, "Unmounted everything" ); } @@ -150,7 +155,7 @@ public partial class PackageLoader [TestMethod] public async Task LoadRuntimeGamePackage() { - var (library, _, enroller) = Preamble(); + var (library, packageLoader, enroller) = Preamble(); Project.AddFromFileBuiltIn( "addons/base" ); var project = Project.AddFromFile( "unittest/addons/spacewars" ); @@ -170,8 +175,11 @@ public partial class PackageLoader var gameClass = library.GetType( "SpaceWarsGameManager" ); Assert.IsNotNull( gameClass, "Found game class" ); - // cleanup - PackageManager.UnmountTagged( "client" ); + enroller.Dispose(); + packageLoader.Dispose(); + + PackageManager.UnmountAll(); + Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount ); } /// @@ -203,6 +211,12 @@ public partial class PackageLoader Assert.IsInstanceOfType( exception ); Assert.IsTrue( exception.Message.Contains( "Disabled during static constructors." ) ); + + enroller.Dispose(); + packageLoader.Dispose(); + + PackageManager.UnmountAll(); + Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount ); } // @@ -328,10 +342,10 @@ public partial class PackageLoader //Assert.IsTrue( Project.CompileAsync ) - // cleanup - PackageManager.UnmountTagged( "client" ); - packageLoader.Dispose(); + + PackageManager.UnmountAll(); + Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount ); } /// diff --git a/engine/Sandbox.Test/Package/PackageManager.cs b/engine/Sandbox.Test/Package/PackageManager.cs index 9abffd96..200e80ea 100644 --- a/engine/Sandbox.Test/Package/PackageManager.cs +++ b/engine/Sandbox.Test/Package/PackageManager.cs @@ -10,7 +10,7 @@ public class PackageManagement [TestInitialize] public void TestInitialize() { - PackageManager.ResetForUnitTest(); + PackageManager.UnmountAll(); var dir = $"{Environment.CurrentDirectory}/.source2/package_manager_folder"; @@ -33,12 +33,6 @@ public class PackageManagement AssetDownloadCache.Initialize( dir ); } - [TestCleanup] - public void TestCleanup() - { - - } - /// /// Should throw an exception on invalid/missing package /// @@ -179,9 +173,14 @@ public class PackageManagement [TestMethod] public async Task DownloadPackagesWithMatchingFiles() { + var pm = PackageManager.ActivePackages; + var a = PackageManager.InstallAsync( new PackageLoadOptions( "titanovsky.ufrts_archery2_3", "fff" ) ); var b = PackageManager.InstallAsync( new PackageLoadOptions( "titanovsky.ufrts_crates2", "fff" ) ); await Task.WhenAll( a, b ); + + PackageManager.UnmountAll(); + Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount ); } } diff --git a/engine/Sandbox.Test/Sandbox.Test.cs b/engine/Sandbox.Test/Sandbox.Test.cs index 3aed44f2..5d6f6761 100644 --- a/engine/Sandbox.Test/Sandbox.Test.cs +++ b/engine/Sandbox.Test/Sandbox.Test.cs @@ -7,21 +7,18 @@ global using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; [TestClass] public class TestInit { + public static Sandbox.AppSystem TestAppSystem; + [AssemblyInitialize] public static void ClassInitialize( TestContext context ) { -#if LIVE_UNIT_TEST - Sandbox.Application.InitLiveUnitTest(); -#else - Sandbox.Application.InitUnitTest(); -#endif - + TestAppSystem = new TestAppSystem(); + TestAppSystem.Init(); } [AssemblyCleanup] public static void AssemblyCleanup() { - Sandbox.Application.ShutdownUnitTest(); - + TestAppSystem.Shutdown(); } } diff --git a/engine/Sandbox.Test/Sandbox.Test.csproj b/engine/Sandbox.Test/Sandbox.Test.csproj index 0af367ec..63995ea4 100644 --- a/engine/Sandbox.Test/Sandbox.Test.csproj +++ b/engine/Sandbox.Test/Sandbox.Test.csproj @@ -50,6 +50,7 @@ + diff --git a/engine/Sandbox.Test/Scene/Components/ModelPhysicsTests.cs b/engine/Sandbox.Test/Scene/Components/ModelPhysicsTests.cs index 667986aa..7447e3b3 100644 --- a/engine/Sandbox.Test/Scene/Components/ModelPhysicsTests.cs +++ b/engine/Sandbox.Test/Scene/Components/ModelPhysicsTests.cs @@ -5,7 +5,7 @@ namespace GameObjects.Components; [TestClass] public class ModelPhysicsTests { - private static readonly Model CitizenModel = Model.Load( "models/citizen/citizen.vmdl" ); + private static Model CitizenModel => Model.Load( "models/citizen/citizen.vmdl" ); [TestMethod] public void ComponentCreation() diff --git a/engine/Tools/SboxBuild/Steps/BuildAddons.cs b/engine/Tools/SboxBuild/Steps/BuildAddons.cs index bb5f00ee..f8ca882d 100644 --- a/engine/Tools/SboxBuild/Steps/BuildAddons.cs +++ b/engine/Tools/SboxBuild/Steps/BuildAddons.cs @@ -13,7 +13,28 @@ internal class BuildAddons( string name ) : Step( name ) string rootDir = Directory.GetCurrentDirectory(); string gameDir = Path.Combine( rootDir, "game" ); - Log.Info( "Step 1: Building Addons" ); + Log.Info( "Step 1: Generate solution" ); + + string sboxDevPath = Path.Combine( gameDir, "sbox-dev.exe" ); + if ( !File.Exists( sboxDevPath ) ) + { + Log.Error( $"Error: sbox-dev.exe not found at {sboxDevPath}" ); + return ExitCode.Failure; + } + + bool gameTestSuccess = Utility.RunProcess( + sboxDevPath, + "-generatesolution", + gameDir + ); + + if ( !gameTestSuccess ) + { + Log.Error( "Solution generation failed!" ); + return ExitCode.Failure; + } + + Log.Info( "Step 2: Building Addons" ); bool addonsSuccess = Utility.RunDotnetCommand( gameDir, @@ -26,7 +47,7 @@ internal class BuildAddons( string name ) : Step( name ) return ExitCode.Failure; } - Log.Info( "Step 2: Building Menu" ); + Log.Info( "Step 3: Building Menu" ); string menuBuildPath = Path.Combine( gameDir, "bin", "managed", "MenuBuild.exe" ); if ( !File.Exists( menuBuildPath ) ) diff --git a/engine/Tools/SboxBuild/Steps/Test.cs b/engine/Tools/SboxBuild/Steps/Test.cs index 11e3ecd8..e2a7e5a7 100644 --- a/engine/Tools/SboxBuild/Steps/Test.cs +++ b/engine/Tools/SboxBuild/Steps/Test.cs @@ -72,27 +72,6 @@ internal class Test( string name ) : Step( name ) return ExitCode.Failure; } - Log.Info( "Step 3: Testing Game" ); - - string sboxDevPath = Path.Combine( gameDir, "sbox-dev.exe" ); - if ( !File.Exists( sboxDevPath ) ) - { - Log.Error( $"Error: sbox-dev.exe not found at {sboxDevPath}" ); - return ExitCode.Failure; - } - - bool gameTestSuccess = Utility.RunProcess( - sboxDevPath, - "-test", - gameDir - ); - - if ( !gameTestSuccess ) - { - Log.Error( "Game tests failed!" ); - return ExitCode.Failure; - } - Log.Info( "All tests completed successfully!" ); return ExitCode.Success; } diff --git a/game/addons/menu/Code/MenuSystem.cs b/game/addons/menu/Code/MenuSystem.cs index ce63a96d..7fc0bdd4 100644 --- a/game/addons/menu/Code/MenuSystem.cs +++ b/game/addons/menu/Code/MenuSystem.cs @@ -48,6 +48,9 @@ public partial class MenuSystem : IMenuSystem Dev?.Delete(); Dev = null; + + // Null so GC can have it's way + Instance = null; } Package oldGamePackage;