Shutdown fixes (#3553)

* Stop generating solutions via -test flag add -generatesolution

* Add TestAppSystem remove Application.InitUnitTest

Avoids some hacks and also makes sure our tests are as close to a real AppSystem as possible.

* Add shutdown unit test

shuts down an re-inits the engine

* Properly dispose native resources hold by managed during shutdown

Should fix a bunch of crashes

* Fix filesystem and networking tests

* StandaloneTest does proper Game Close

* Make sure package tests clean up properly

* Make sure menu scene and resources are released on shutdown

* Report leaked scenes on shutdown

* Ensure DestroyImmediate is not used on scenes

* Fix unmounting in unit tests not clearing native refs

* Force destroy native resource on ResourceLib Clear
This commit is contained in:
Lorenz Junglas
2025-12-08 15:55:11 +01:00
committed by GitHub
parent 86f8de75cc
commit 6808d8768e
51 changed files with 531 additions and 390 deletions

View File

@@ -1,4 +1,5 @@
using System; using Sandbox.Engine;
using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@@ -8,6 +9,17 @@ public static class Launcher
{ {
public static int Main() 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" ) ) if ( !HasCommandLineSwitch( "-project" ) && !HasCommandLineSwitch( "-test" ) )
{ {
// we pass the command line, so we can pass it on to the sbox-launcher (for -game etc) // we pass the command line, so we can pass it on to the sbox-launcher (for -game etc)

View File

@@ -1,6 +1,8 @@
using Sandbox.Diagnostics; using Sandbox.Diagnostics;
using Sandbox.Engine; using Sandbox.Engine;
using Sandbox.Internal; using Sandbox.Internal;
using Sandbox.Network;
using Sandbox.Rendering;
using System; using System;
using System.Globalization; using System.Globalization;
using System.Runtime; using System.Runtime;
@@ -130,15 +132,69 @@ public class AppSystem
public virtual void Shutdown() public virtual void Shutdown()
{ {
// Shut the games down // Make sure game instance is closed
EngineLoop.Exiting(); 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) // Shut the engine down (close window etc)
NativeEngine.EngineGlobal.SourceEngineShutdown( _appSystem, false ); NativeEngine.EngineGlobal.SourceEngineShutdown( _appSystem, false );
// Flush the api (close actvity, update stats etc)
Api.Shutdown();
if ( _appSystem.IsValid ) if ( _appSystem.IsValid )
{ {
_appSystem.Destroy(); _appSystem.Destroy();
@@ -161,11 +217,14 @@ public class AppSystem
Managed.SourceHammer.NativeInterop.Free(); Managed.SourceHammer.NativeInterop.Free();
Managed.SourceModelDoc.NativeInterop.Free(); Managed.SourceModelDoc.NativeInterop.Free();
Managed.SourceAnimgraph.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 commandLine = commandLine.Replace( ".dll", ".exe" ); // uck
_appSystem = CMaterialSystem2AppSystemDict.Create( createInfo.ToMaterialSystem2AppSystemDictCreateInfo() ); _appSystem = CMaterialSystem2AppSystemDict.Create( createInfo.ToMaterialSystem2AppSystemDictCreateInfo() );

View File

@@ -68,7 +68,7 @@ public class StandaloneAppSystem : AppSystem
// Quit next loop after load, if we are testing // Quit next loop after load, if we are testing
else if ( Utility.CommandLine.HasSwitch( "-test-standalone" ) ) else if ( Utility.CommandLine.HasSwitch( "-test-standalone" ) )
{ {
Application.Exit(); Game.Close();
} }
return !wantsToQuit; return !wantsToQuit;

View File

@@ -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, "" );
}
}

View File

@@ -25,12 +25,6 @@ public static class Application
/// </summary> /// </summary>
public static bool IsUnitTest { get; private set; } public static bool IsUnitTest { get; private set; }
/// <summary>
/// True if we're running a live unit test.
/// </summary>
internal static bool IsLiveUnitTest { get; private set; }
/// <summary> /// <summary>
/// True if running without a graphics window, such as in a terminal. /// True if running without a graphics window, such as in a terminal.
/// </summary> /// </summary>
@@ -98,106 +92,7 @@ public static class Application
/// </summary> /// </summary>
public static bool IsVR => VRSystem.IsActive; // garry: I think this is right? But feels like this should be set at startup and never change? 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; internal static void Initialize( bool dedicated, bool headless, bool toolsMode, bool testMode, bool isRetail )
/// <summary>
/// Called from unit test projects to initialize the engine
/// </summary>
public static void InitUnitTest<T>( 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<T>();
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<T>( bool withtools = true, bool withRendering = false )
{
Application.IsLiveUnitTest = true;
InitUnitTest<T>( withtools, withRendering );
}
/// <summary>
/// Called from unit test projects to politely shut down the engine
/// </summary>
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 )
{ {
if ( IsInitialized ) if ( IsInitialized )
throw new InvalidOperationException( "Already Initialized" ); throw new InvalidOperationException( "Already Initialized" );
@@ -213,6 +108,11 @@ public static class Application
IsBenchmark = Environment.GetEnvironmentVariable( "SBOX_MODE" ) == "BENCHMARK"; IsBenchmark = Environment.GetEnvironmentVariable( "SBOX_MODE" ) == "BENCHMARK";
} }
internal static void Shutdown()
{
IsInitialized = false;
}
internal static void TryLoadVersionInfo( string gameFolder ) internal static void TryLoadVersionInfo( string gameFolder )
{ {
Version = "0000000"; Version = "0000000";

View File

@@ -28,7 +28,7 @@ internal static class Bootstrap
/// </summary> /// </summary>
internal static void PreInit( CMaterialSystem2AppSystemDict appDict ) 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 try
{ {
@@ -91,16 +91,6 @@ internal static class Bootstrap
Mounting.Directory.LoadAssemblies(); 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 ) catch ( Exception ex )
{ {
@@ -199,9 +189,12 @@ internal static class Bootstrap
// //
{ {
Screen.UpdateFromEngine(); Screen.UpdateFromEngine();
Material.UI.Init(); Material.UI.InitStatic();
Model.Init(); Gizmo.GizmoDraw.InitStatic();
Texture.InitStaticTextures(); Model.InitStatic();
Texture.InitStatic();
CubemapRendering.InitStatic();
Graphics.InitStatic();
} }
if ( !Application.IsHeadless && !Application.IsStandalone ) if ( !Application.IsHeadless && !Application.IsStandalone )
@@ -294,8 +287,6 @@ internal static class Bootstrap
} }
} }
internal static void InitMinimal( string rootFolder ) internal static void InitMinimal( string rootFolder )
{ {
Environment.CurrentDirectory = rootFolder; Environment.CurrentDirectory = rootFolder;

View File

@@ -384,34 +384,6 @@ internal static class EngineLoop
} }
} }
/// <summary>
/// Called when the application is shutting down
/// </summary>
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();
}
/// <summary> /// <summary>
/// A console command has arrived, or a convar has changed /// A console command has arrived, or a convar has changed
/// </summary> /// </summary>

View File

@@ -124,6 +124,8 @@ internal partial class GlobalContext
oldCts?.Cancel(); oldCts?.Cancel();
oldCts?.Dispose(); oldCts?.Dispose();
ActiveScene = null;
TaskSource = new TaskSource( 1 ); TaskSource = new TaskSource( 1 );
EventSystem?.Dispose(); EventSystem?.Dispose();
@@ -133,6 +135,9 @@ internal partial class GlobalContext
Cookies?.Dispose(); Cookies?.Dispose();
Cookies = null; Cookies = null;
ResourceSystem?.Clear();
ResourceSystem = new ResourceSystem();
} }
string _disabledReason; string _disabledReason;

View File

@@ -15,10 +15,30 @@ public static partial class Gizmo
/// </summary> /// </summary>
public sealed partial class GizmoDraw public sealed partial class GizmoDraw
{ {
static Material LineMaterial = Material.Load( "materials/gizmo/line.vmat" ); static Material LineMaterial;
static Material SolidMaterial = Material.Load( "materials/gizmo/solid.vmat" ); static Material SolidMaterial;
static Material SpriteMaterial = Material.Load( "materials/gizmo/sprite.vmat" ); static Material SpriteMaterial;
static Material GridMaterial = Material.Load( "materials/gizmo/grid.vmat" ); 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() internal GizmoDraw()
{ {

View File

@@ -47,24 +47,6 @@ public static partial class Game
set => GlobalContext.Current.NodeLibrary = value; set => GlobalContext.Current.NodeLibrary = value;
} }
/// <summary>
/// Initialize for a unit test
/// </summary>
internal static void InitUnitTest<T>()
{
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 ) private static void AddNodesFromAssembly( Assembly asm )
{ {
var result = NodeLibrary.AddAssembly( asm ); var result = NodeLibrary.AddAssembly( asm );

View File

@@ -141,21 +141,21 @@ public static partial class Game
return; 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 // Standalone mode and Dedicated Server only: exit whole app
if ( Application.IsStandalone || Application.IsDedicatedServer ) if ( Application.IsStandalone || Application.IsDedicatedServer )
{ {
Application.Exit(); 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();
} }
/// <summary> /// <summary>

View File

@@ -40,7 +40,7 @@ namespace Sandbox
/// </summary> /// </summary>
internal static Material DropShadow { get; set; } internal static Material DropShadow { get; set; }
internal static void Init() internal static void InitStatic()
{ {
Basic = FromShader( "shaders/ui_basic.shader" ); Basic = FromShader( "shaders/ui_basic.shader" );
Box = FromShader( "shaders/ui_cssbox.shader" ); Box = FromShader( "shaders/ui_cssbox.shader" );
@@ -51,6 +51,26 @@ namespace Sandbox
DropShadow = FromShader( "shaders/ui_dropshadow.shader" ); DropShadow = FromShader( "shaders/ui_dropshadow.shader" );
BorderWrap = FromShader( "shaders/ui_borderwrap.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;
}
} }
} }
} }

View File

@@ -41,16 +41,24 @@ public sealed partial class Material : Resource
} }
~Material() ~Material()
{
Dispose();
}
internal void Dispose()
{ {
// kill the native pointer - it does with the native material // kill the native pointer - it does with the native material
// we want to reduce the risk that someone is holding on to it. // we want to reduce the risk that someone is holding on to it.
Attributes.Set( default ); Attributes?.Set( default );
Attributes = null; Attributes = null;
var n = native; if ( !native.IsNull )
native = default; {
var n = native;
native = default;
MainThread.Queue( () => n.DestroyStrongHandle() ); MainThread.Queue( () => n.DestroyStrongHandle() );
}
} }
/// <summary> /// <summary>

View File

@@ -58,11 +58,23 @@ public partial class Model
public static Model Error { get; internal set; } public static Model Error { get; internal set; }
internal static void Init() internal static void InitStatic()
{ {
Cube = Load( "models/dev/box.vmdl" ); Cube = Load( "models/dev/box.vmdl" );
Sphere = Load( "models/dev/sphere.vmdl" ); Sphere = Load( "models/dev/sphere.vmdl" );
Plane = Load( "models/dev/plane.vmdl" ); Plane = Load( "models/dev/plane.vmdl" );
Error = Load( "models/dev/error.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;
}
} }

View File

@@ -28,12 +28,20 @@ public sealed partial class Model : Resource
SetIdFromResourcePath( Name ); SetIdFromResourcePath( Name );
} }
internal void Dispose()
{
if ( !native.IsNull )
{
var n = native;
native = default;
MainThread.Queue( () => n.DestroyStrongHandle() );
}
}
~Model() ~Model()
{ {
var n = native; Dispose();
native = default;
MainThread.Queue( () => n.DestroyStrongHandle() );
} }
/// <summary> /// <summary>

View File

@@ -39,13 +39,20 @@ public abstract partial class Resource : IValid, IJsonConvert, BytePack.ISeriali
/// </summary> /// </summary>
[Hide, JsonIgnore] public virtual bool HasUnsavedChanges => false; [Hide, JsonIgnore] public virtual bool HasUnsavedChanges => false;
~Resource() internal void Destroy()
{ {
// Unregister on main thread // Unregister on main thread
MainThread.Queue( () => { Game.Resources.Unregister( this ); } ); MainThread.Queue( () => { Game.Resources.Unregister( this ); } );
Manifest?.Dispose(); Manifest?.Dispose();
Manifest = default; Manifest = default;
GC.SuppressFinalize( this );
}
~Resource()
{
Destroy();
} }
internal static string FixPath( string filename ) internal static string FixPath( string filename )

View File

@@ -58,13 +58,19 @@ public class ResourceSystem
var toDispose = ResourceIndex.Values.ToArray(); var toDispose = ResourceIndex.Values.ToArray();
ResourceIndex.Clear();
foreach ( var resource in toDispose.OfType<GameResource>() ) foreach ( var resource in toDispose.OfType<GameResource>() )
{ {
resource.DestroyInternal(); resource.DestroyInternal();
} }
foreach ( var resource in toDispose )
{
// Don't wait/rely for finalizer get rid of this immediately
resource.Destroy();
}
ResourceIndex.Clear();
TypeCache.Clear(); TypeCache.Clear();
} }

View File

@@ -67,7 +67,7 @@ public partial class PrefabFile : GameResource
protected override void OnDestroy() protected override void OnDestroy()
{ {
CachedScene?.DestroyImmediate(); CachedScene?.DestroyInternal();
CachedScene = null; CachedScene = null;
Unregister(); Unregister();

View File

@@ -23,7 +23,7 @@ public partial class Texture
/// </summary> /// </summary>
public static Texture Transparent { get; internal set; } 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(); 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(); 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(); 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 ) internal static Texture Create( string name, bool anonymous, TextureBuilder builder, IntPtr data, int dataSize )
{ {
var config = builder._config.GetWithFixes(); var config = builder._config.GetWithFixes();

View File

@@ -52,8 +52,8 @@ public sealed partial class PolygonMesh : IJsonConvert
public int MaterialId { get; set; } public int MaterialId { get; set; }
} }
private static readonly Material DefaultMaterial = Material.Load( "materials/dev/reflectivity_30.vmat" ); private Material DefaultMaterial = Material.Load( "materials/dev/reflectivity_30.vmat" );
private static readonly Vector2 DefaultTextureSize = CalculateTextureSize( DefaultMaterial ); private Vector2 DefaultTextureSize => CalculateTextureSize( DefaultMaterial );
private VertexData<Vector3> Positions { get; init; } private VertexData<Vector3> Positions { get; init; }
private HalfEdgeData<Vector2> TextureCoord { get; init; } private HalfEdgeData<Vector2> TextureCoord { get; init; }

View File

@@ -110,6 +110,7 @@ public partial class GameObject : IValid
/// </summary> /// </summary>
public void DestroyImmediate() public void DestroyImmediate()
{ {
Assert.False( this is Scene, "Don't call DestroyImmediate on a scene." );
ThreadSafe.AssertIsMainThread(); ThreadSafe.AssertIsMainThread();
Term(); Term();

View File

@@ -234,10 +234,10 @@ public partial class SceneNetworkSystem : GameNetworkSystem
LoadingScreen.Title = "Loading Scene"; 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 ) if ( Game.ActiveScene is not null )
{ {
Game.ActiveScene?.DestroyImmediate(); Game.ActiveScene?.Destroy();
Game.ActiveScene = null; Game.ActiveScene = null;
} }
@@ -408,7 +408,7 @@ public partial class SceneNetworkSystem : GameNetworkSystem
if ( Game.ActiveScene is not null ) if ( Game.ActiveScene is not null )
{ {
Game.ActiveScene?.DestroyImmediate(); Game.ActiveScene?.Destroy();
Game.ActiveScene = null; Game.ActiveScene = null;
} }

View File

@@ -100,6 +100,8 @@ public partial class Scene : GameObject
internal virtual void DestroyInternal() internal virtual void DestroyInternal()
{ {
_all.Remove( this );
// Clearing the object index now means we can save time // Clearing the object index now means we can save time
// because we don't have to do it for each object. // 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 // 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() public IDisposable Push()
{ {
ThreadSafe.AssertIsMainThread(); ThreadSafe.AssertIsMainThread();
var old = Game.ActiveScene; var old = Game.ActiveScene;
Game.ActiveScene = this; Game.ActiveScene = this;

View File

@@ -170,9 +170,6 @@ internal static partial class PackageManager
MountedFileSystem.Mount( FileSystem ); MountedFileSystem.Mount( FileSystem );
MountedFileSystem.Mount( AssemblyFileSystem ); 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 // Reload any already resident resources with the ones we've just mounted
NativeEngine.g_pResourceSystem.ReloadSymlinkedResidentResources(); NativeEngine.g_pResourceSystem.ReloadSymlinkedResidentResources();
@@ -199,9 +196,6 @@ internal static partial class PackageManager
AssemblyFileSystem.Dispose(); AssemblyFileSystem.Dispose();
AssemblyFileSystem = null; 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) // 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(); NativeEngine.g_pResourceSystem.ReloadSymlinkedResidentResources();
} }

View File

@@ -20,12 +20,6 @@ internal static partial class PackageManager
/// </summary> /// </summary>
public static event Action<ActivePackage, string> OnPackageInstalledToContext; public static event Action<ActivePackage, string> OnPackageInstalledToContext;
internal static void ResetForUnitTest()
{
ActivePackages = new();
MountedFileSystem = new AggregateFileSystem();
}
static async Task<Package> FetchPackageAsync( string ident, bool localPriority ) static async Task<Package> FetchPackageAsync( string ident, bool localPriority )
{ {
if ( localPriority && Package.TryParseIdent( ident, out var parts ) && !parts.local ) 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 ) private static async Task InstallDependencies( Package package, PackageLoadOptions options )
{ {
HashSet<string> dependancies = new HashSet<string>( StringComparer.OrdinalIgnoreCase ); HashSet<string> dependancies = new HashSet<string>( StringComparer.OrdinalIgnoreCase );

View File

@@ -19,6 +19,31 @@ internal static partial class ConVarSystem
var command = new NativeCommand( value ); var command = new NativeCommand( value );
AddCommand( command ); AddCommand( command );
} }
internal static void ClearNativeCommands()
{
if ( Members.Count == 0 )
return;
System.Collections.Generic.List<string> nativeKeys = null;
foreach ( var (name, command) in Members )
{
if ( command is NativeCommand || command is NativeConVar )
{
nativeKeys ??= new System.Collections.Generic.List<string>();
nativeKeys.Add( name );
}
}
if ( nativeKeys is null )
return;
foreach ( var name in nativeKeys )
{
Members.Remove( name );
}
}
} }

View File

@@ -44,7 +44,7 @@ internal static class EngineFileSystem
/// <summary> /// <summary>
/// Don't try to use the filesystem until you've called this! /// Don't try to use the filesystem until you've called this!
/// </summary> /// </summary>
internal static void Initialize( string rootFolder ) internal static void Initialize( string rootFolder, bool skipBaseFolderInit = false )
{ {
if ( Root != null ) if ( Root != null )
throw new System.Exception( "Filesystem Multi-Initialize" ); throw new System.Exception( "Filesystem Multi-Initialize" );
@@ -52,8 +52,7 @@ internal static class EngineFileSystem
Root = new LocalFileSystem( rootFolder ); Root = new LocalFileSystem( rootFolder );
Temporary = new MemoryFileSystem(); Temporary = new MemoryFileSystem();
if ( Application.IsUnitTest ) if ( skipBaseFolderInit ) return;
return;
if ( Application.IsEditor ) if ( Application.IsEditor )
{ {

View File

@@ -9,7 +9,8 @@ internal partial class NetworkSystem
public void InitializeGameSystem() 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; return;
GameSystem = IGameInstanceDll.Current.CreateGameNetworking( this ); GameSystem = IGameInstanceDll.Current.CreateGameNetworking( this );

View File

@@ -11,7 +11,7 @@ namespace Sandbox;
/// </summary> /// </summary>
internal class ServerPackages internal class ServerPackages
{ {
public static ServerPackages Current { get; private set; } public static ServerPackages Current { get; private set; } = new();
internal record struct ServerPackageInfo(); internal record struct ServerPackageInfo();
internal StringTable StringTable; internal StringTable StringTable;
@@ -86,7 +86,7 @@ internal class ServerPackages
internal ServerPackages() internal ServerPackages()
{ {
Assert.IsNull( Current ); // WTF???
Current = this; Current = this;
StringTable = new StringTable( "ServerPackages", true ); StringTable = new StringTable( "ServerPackages", true );

View File

@@ -1,4 +1,5 @@
using NativeEngine; using NativeEngine;
using Sandbox.VR;
namespace Sandbox; namespace Sandbox;
@@ -25,6 +26,11 @@ public class ComputeShader
ComputeMaterial = material; ComputeMaterial = material;
} }
internal void Dispose()
{
ComputeMaterial.Dispose();
}
/// <summary> /// <summary>
/// Dispatch this compute shader using explicit thread counts. /// Dispatch this compute shader using explicit thread counts.
/// </summary> /// </summary>

View File

@@ -1,5 +1,3 @@
using NativeEngine;
using System.Linq;
using Sandbox.Rendering; using Sandbox.Rendering;
namespace Sandbox; namespace Sandbox;
@@ -10,7 +8,18 @@ namespace Sandbox;
/// </summary> /// </summary>
internal static class CubemapRendering 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;
}
/// <summary> /// <summary>
/// Specifies the quality level for GGX filtering of environment maps. /// Specifies the quality level for GGX filtering of environment maps.

View File

@@ -4,7 +4,18 @@ namespace Sandbox;
public static partial class Graphics 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;
}
/// <summary> /// <summary>
/// Which method to use when downsampling a texture /// Which method to use when downsampling a texture

View File

@@ -46,7 +46,7 @@ internal class BloomDownsampleLayer : ProceduralRenderLayer
} }
internal class QuarterDepthDownsampleLayer : ProceduralRenderLayer internal class QuarterDepthDownsampleLayer : ProceduralRenderLayer
{ {
private static Material DepthResolve = Material.Create( "depthresolve", "shaders/depthresolve.shader" ); private Material DepthResolve;
private bool MSAAInput; private bool MSAAInput;
public QuarterDepthDownsampleLayer() public QuarterDepthDownsampleLayer()
@@ -55,6 +55,7 @@ internal class QuarterDepthDownsampleLayer : ProceduralRenderLayer
Flags |= LayerFlags.NeverRemove | LayerFlags.DoesntModifyColorBuffers; Flags |= LayerFlags.NeverRemove | LayerFlags.DoesntModifyColorBuffers;
ClearFlags = ClearFlags.Depth | ClearFlags.Stencil; ClearFlags = ClearFlags.Depth | ClearFlags.Stencil;
LayerType = SceneLayerType.Opaque; LayerType = SceneLayerType.Opaque;
DepthResolve = Material.Create( "depthresolve", "shaders/depthresolve.shader" );
} }
public void Setup( ISceneView view, RenderViewport viewport, SceneViewRenderTargetHandle rtDepth, bool msaaInput, RenderTarget rtOutDepth ) public void Setup( ISceneView view, RenderViewport viewport, SceneViewRenderTargetHandle rtDepth, bool msaaInput, RenderTarget rtOutDepth )

View File

@@ -43,4 +43,10 @@ internal partial class RenderPipeline
// Return to pool // Return to pool
Pool.Enqueue( renderPipeline ); Pool.Enqueue( renderPipeline );
} }
internal static void ClearPool()
{
Pool.Clear();
ActivePipelines.Clear();
}
} }

View File

@@ -95,4 +95,13 @@ public static partial class TextRendering
//Log.Info( $"TextManager: {total} ({deleted} deleted)" ); //Log.Info( $"TextManager: {total} ({deleted} deleted)" );
} }
internal static void ClearCache()
{
foreach ( var item in Dictionary )
{
item.Value.Dispose();
}
Dictionary.Clear();
}
} }

View File

@@ -10,3 +10,4 @@ using System.Runtime.CompilerServices;
[assembly: TasksPersistOnContextReset] [assembly: TasksPersistOnContextReset]
[assembly: InternalsVisibleTo( "Sandbox.Tools" )] [assembly: InternalsVisibleTo( "Sandbox.Tools" )]
[assembly: InternalsVisibleTo( "Sandbox.AppSystem" )] [assembly: InternalsVisibleTo( "Sandbox.AppSystem" )]
[assembly: InternalsVisibleTo( "Sandbox.Test" )]

View File

@@ -110,8 +110,8 @@ internal partial class GameInstance : IGameInstance
if ( activePackage != null && !Application.IsStandalone ) if ( activePackage != null && !Application.IsStandalone )
{ {
Game.Language.Shutdown(); Game.Language?.Shutdown();
FileSystem.Mounted.UnMount( activePackage.FileSystem ); FileSystem.Mounted?.UnMount( activePackage.FileSystem );
activePackage = null; 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 // Is this the right place for it? Map packages are marked with "game" so they never get unmounted
PackageManager.UnmountTagged( "game" ); PackageManager.UnmountTagged( "game" );
GameInstanceDll.Current.OnGameInstanceClosed( this ); GameInstanceDll.Current.Shutdown( this );
Game.Shutdown(); Game.Shutdown();

View File

@@ -66,7 +66,7 @@ internal partial class GameInstanceDll : Engine.IGameInstanceDll
} }
} }
Assert.IsNull( PackageLoader ); PackageLoader?.Dispose();
PackageLoader = new PackageLoader( "GameMenu", typeof( GameInstanceDll ).Assembly ); PackageLoader = new PackageLoader( "GameMenu", typeof( GameInstanceDll ).Assembly );
PackageLoader.HotloadWatch( Game.GameAssembly ); // Sandbox.Game is per instance PackageLoader.HotloadWatch( Game.GameAssembly ); // Sandbox.Game is per instance
PackageLoader.OnAfterHotload = OnAfterHotload; 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(); 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();
} }
/// <summary> /// <summary>
@@ -700,7 +703,7 @@ internal partial class GameInstanceDll : Engine.IGameInstanceDll
/// Called when the game menu is closed /// Called when the game menu is closed
/// </summary> /// </summary>
/// <param name="instance"></param> /// <param name="instance"></param>
public void OnGameInstanceClosed( IGameInstance instance ) public void Shutdown( IGameInstance instance )
{ {
NativeErrorReporter.Breadcrumb( true, "game", "Closed game instance" ); NativeErrorReporter.Breadcrumb( true, "game", "Closed game instance" );
NativeErrorReporter.SetTag( "game", null ); NativeErrorReporter.SetTag( "game", null );

View File

@@ -211,8 +211,44 @@ internal sealed class MenuDll : IMenuDll
public void Exiting() public void Exiting()
{ {
using var scope = PushScope(); using ( PushScope() )
Game.Cookies?.Save(); {
// 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() void LoadResources()

View File

@@ -2,123 +2,78 @@
using Sandbox.ActionGraphs; using Sandbox.ActionGraphs;
using Sandbox.Engine; using Sandbox.Engine;
using Sandbox.Internal; using Sandbox.Internal;
using Sandbox.Tasks;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
namespace ActionGraphs; namespace ActionGraphs;
[TestClass] [TestClass]
public class LiveGamePackage 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();
}
/// <summary> /// <summary>
/// Asserts that all the ActionGraphs referenced by a given scene in a downloaded /// Asserts that all the ActionGraphs referenced by a given scene in a downloaded
/// package have no errors. /// package have no errors.
/// </summary> /// </summary>
[TestMethod] [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 "d174cab5-7a05-476c-a545-4db2fd685032", // Prefab references game object from other scene
"e9ac7c29-ff9f-4c3c-8d9d-7228c4711248", // Inventory method changed parameter types "e9ac7c29-ff9f-4c3c-8d9d-7228c4711248", // Inventory method changed parameter types
"462927b9-1f01-4ba8-9f6b-2e1e6a5934e4" // 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<Guid>( ignoreGuids.Select( Guid.Parse ) ); var ignoreGuidSet = new HashSet<Guid>( 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<SceneFile>( 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; foreach ( var message in graph.Messages )
enroller.OnAssemblyAdded = ( a ) =>
{ {
Game.TypeLibrary.AddAssembly( a.Assembly, true ); Console.WriteLine( $" {message}" );
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 );
} }
Assert.IsNotNull( activePackage ); if ( !ignoreGuidSet.Contains( graph.Guid ) )
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<SceneFile>( 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 )
{ {
Console.WriteLine( $"{graph.Guid}: {graph.Title} {(ignoreGuidSet.Contains( graph.Guid ) ? "(IGNORED)" : "")}" ); anyErrors |= graph.HasErrors();
foreach ( var message in graph.Messages )
{
Console.WriteLine( $" {message}" );
}
if ( !ignoreGuidSet.Contains( graph.Guid ) )
{
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();
} }
} }

View File

@@ -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();
}
}

View File

@@ -18,7 +18,7 @@ public partial class FileSystem
System.IO.Directory.CreateDirectory( ".source2/TestFolder" ); System.IO.Directory.CreateDirectory( ".source2/TestFolder" );
Sandbox.EngineFileSystem.Shutdown(); Sandbox.EngineFileSystem.Shutdown();
Sandbox.EngineFileSystem.Initialize( ".source2/TestFolder" ); Sandbox.EngineFileSystem.Initialize( ".source2/TestFolder", true );
Sandbox.EngineFileSystem.Root.WriteAllText( "root_text_file.txt", "Hello" ); Sandbox.EngineFileSystem.Root.WriteAllText( "root_text_file.txt", "Hello" );

View File

@@ -5,12 +5,6 @@ namespace Packages;
[TestClass] [TestClass]
public class PackageDownload public class PackageDownload
{ {
[TestInitialize]
public void TestInitialize()
{
PackageManager.ResetForUnitTest();
}
[TestMethod] [TestMethod]
[DataRow( "facepunch.sandbox" )] [DataRow( "facepunch.sandbox" )]
[DataRow( "garry.grassworld" )] [DataRow( "garry.grassworld" )]

View File

@@ -10,7 +10,6 @@ public partial class PackageLoader
public void TestInitialize() public void TestInitialize()
{ {
Project.Clear(); Project.Clear();
PackageManager.ResetForUnitTest();
AssetDownloadCache.Initialize( $"{Environment.CurrentDirectory}/.source2/package_manager_folder" ); AssetDownloadCache.Initialize( $"{Environment.CurrentDirectory}/.source2/package_manager_folder" );
} }
@@ -94,7 +93,7 @@ public partial class PackageLoader
var addonName = "garry.grassworld"; var addonName = "garry.grassworld";
var addonClass = "GrassSpawner"; var addonClass = "GrassSpawner";
var (library, _, enroller) = Preamble(); var (library, packageLoader, enroller) = Preamble();
Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount, "No package files mounted" ); Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount, "No package files mounted" );
@@ -115,13 +114,16 @@ public partial class PackageLoader
PackageManager.UnmountTagged( "client" ); PackageManager.UnmountTagged( "client" );
enroller.Dispose();
packageLoader.Dispose();
Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount, "Unmounted everything" ); Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount, "Unmounted everything" );
} }
//[TestMethod] //[TestMethod]
public async Task LoadPackageWithAddonWithLibrary( string packageName ) 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" ); Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount, "No package files mounted" );
@@ -138,6 +140,9 @@ public partial class PackageLoader
PackageManager.UnmountTagged( "client" ); PackageManager.UnmountTagged( "client" );
enroller.Dispose();
packageLoader.Dispose();
Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount, "Unmounted everything" ); Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount, "Unmounted everything" );
} }
@@ -150,7 +155,7 @@ public partial class PackageLoader
[TestMethod] [TestMethod]
public async Task LoadRuntimeGamePackage() public async Task LoadRuntimeGamePackage()
{ {
var (library, _, enroller) = Preamble(); var (library, packageLoader, enroller) = Preamble();
Project.AddFromFileBuiltIn( "addons/base" ); Project.AddFromFileBuiltIn( "addons/base" );
var project = Project.AddFromFile( "unittest/addons/spacewars" ); var project = Project.AddFromFile( "unittest/addons/spacewars" );
@@ -170,8 +175,11 @@ public partial class PackageLoader
var gameClass = library.GetType( "SpaceWarsGameManager" ); var gameClass = library.GetType( "SpaceWarsGameManager" );
Assert.IsNotNull( gameClass, "Found game class" ); Assert.IsNotNull( gameClass, "Found game class" );
// cleanup enroller.Dispose();
PackageManager.UnmountTagged( "client" ); packageLoader.Dispose();
PackageManager.UnmountAll();
Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount );
} }
/// <summary> /// <summary>
@@ -203,6 +211,12 @@ public partial class PackageLoader
Assert.IsInstanceOfType<InvalidOperationException>( exception ); Assert.IsInstanceOfType<InvalidOperationException>( exception );
Assert.IsTrue( exception.Message.Contains( "Disabled during static constructors." ) ); 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 ) //Assert.IsTrue( Project.CompileAsync )
// cleanup
PackageManager.UnmountTagged( "client" );
packageLoader.Dispose(); packageLoader.Dispose();
PackageManager.UnmountAll();
Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount );
} }
/// <summary> /// <summary>

View File

@@ -10,7 +10,7 @@ public class PackageManagement
[TestInitialize] [TestInitialize]
public void TestInitialize() public void TestInitialize()
{ {
PackageManager.ResetForUnitTest(); PackageManager.UnmountAll();
var dir = $"{Environment.CurrentDirectory}/.source2/package_manager_folder"; var dir = $"{Environment.CurrentDirectory}/.source2/package_manager_folder";
@@ -33,12 +33,6 @@ public class PackageManagement
AssetDownloadCache.Initialize( dir ); AssetDownloadCache.Initialize( dir );
} }
[TestCleanup]
public void TestCleanup()
{
}
/// <summary> /// <summary>
/// Should throw an exception on invalid/missing package /// Should throw an exception on invalid/missing package
/// </summary> /// </summary>
@@ -179,9 +173,14 @@ public class PackageManagement
[TestMethod] [TestMethod]
public async Task DownloadPackagesWithMatchingFiles() public async Task DownloadPackagesWithMatchingFiles()
{ {
var pm = PackageManager.ActivePackages;
var a = PackageManager.InstallAsync( new PackageLoadOptions( "titanovsky.ufrts_archery2_3", "fff" ) ); var a = PackageManager.InstallAsync( new PackageLoadOptions( "titanovsky.ufrts_archery2_3", "fff" ) );
var b = PackageManager.InstallAsync( new PackageLoadOptions( "titanovsky.ufrts_crates2", "fff" ) ); var b = PackageManager.InstallAsync( new PackageLoadOptions( "titanovsky.ufrts_crates2", "fff" ) );
await Task.WhenAll( a, b ); await Task.WhenAll( a, b );
PackageManager.UnmountAll();
Assert.AreEqual( 0, PackageManager.MountedFileSystem.FileCount );
} }
} }

View File

@@ -7,21 +7,18 @@ global using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
[TestClass] [TestClass]
public class TestInit public class TestInit
{ {
public static Sandbox.AppSystem TestAppSystem;
[AssemblyInitialize] [AssemblyInitialize]
public static void ClassInitialize( TestContext context ) public static void ClassInitialize( TestContext context )
{ {
#if LIVE_UNIT_TEST TestAppSystem = new TestAppSystem();
Sandbox.Application.InitLiveUnitTest<TestInit>(); TestAppSystem.Init();
#else
Sandbox.Application.InitUnitTest<TestInit>();
#endif
} }
[AssemblyCleanup] [AssemblyCleanup]
public static void AssemblyCleanup() public static void AssemblyCleanup()
{ {
Sandbox.Application.ShutdownUnitTest(); TestAppSystem.Shutdown();
} }
} }

View File

@@ -50,6 +50,7 @@
<ProjectReference Include="..\Sandbox.Engine\Sandbox.Engine.csproj" /> <ProjectReference Include="..\Sandbox.Engine\Sandbox.Engine.csproj" />
<ProjectReference Include="..\Sandbox.Tools\Sandbox.Tools.csproj" /> <ProjectReference Include="..\Sandbox.Tools\Sandbox.Tools.csproj" />
<ProjectReference Include="..\Sandbox.Menu\Sandbox.Menu.csproj" /> <ProjectReference Include="..\Sandbox.Menu\Sandbox.Menu.csproj" />
<ProjectReference Include="..\Sandbox.AppSystem\Sandbox.AppSystem.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -5,7 +5,7 @@ namespace GameObjects.Components;
[TestClass] [TestClass]
public class ModelPhysicsTests 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] [TestMethod]
public void ComponentCreation() public void ComponentCreation()

View File

@@ -13,7 +13,28 @@ internal class BuildAddons( string name ) : Step( name )
string rootDir = Directory.GetCurrentDirectory(); string rootDir = Directory.GetCurrentDirectory();
string gameDir = Path.Combine( rootDir, "game" ); 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( bool addonsSuccess = Utility.RunDotnetCommand(
gameDir, gameDir,
@@ -26,7 +47,7 @@ internal class BuildAddons( string name ) : Step( name )
return ExitCode.Failure; return ExitCode.Failure;
} }
Log.Info( "Step 2: Building Menu" ); Log.Info( "Step 3: Building Menu" );
string menuBuildPath = Path.Combine( gameDir, "bin", "managed", "MenuBuild.exe" ); string menuBuildPath = Path.Combine( gameDir, "bin", "managed", "MenuBuild.exe" );
if ( !File.Exists( menuBuildPath ) ) if ( !File.Exists( menuBuildPath ) )

View File

@@ -72,27 +72,6 @@ internal class Test( string name ) : Step( name )
return ExitCode.Failure; 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!" ); Log.Info( "All tests completed successfully!" );
return ExitCode.Success; return ExitCode.Success;
} }

View File

@@ -48,6 +48,9 @@ public partial class MenuSystem : IMenuSystem
Dev?.Delete(); Dev?.Delete();
Dev = null; Dev = null;
// Null so GC can have it's way
Instance = null;
} }
Package oldGamePackage; Package oldGamePackage;