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;