mirror of
https://github.com/Facepunch/sbox-public.git
synced 2025-12-23 22:48:07 -05:00
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:
@@ -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)
|
||||
|
||||
@@ -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() );
|
||||
|
||||
@@ -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;
|
||||
|
||||
34
engine/Sandbox.AppSystem/TestAppSystem.cs
Normal file
34
engine/Sandbox.AppSystem/TestAppSystem.cs
Normal 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, "" );
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,6 @@ public static class Application
|
||||
/// </summary>
|
||||
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>
|
||||
/// True if running without a graphics window, such as in a terminal.
|
||||
/// </summary>
|
||||
@@ -98,106 +92,7 @@ public static class Application
|
||||
/// </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?
|
||||
|
||||
static CMaterialSystem2AppSystemDict AppSystem;
|
||||
|
||||
/// <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 )
|
||||
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";
|
||||
|
||||
@@ -28,7 +28,7 @@ internal static class Bootstrap
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
@@ -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>
|
||||
/// A console command has arrived, or a convar has changed
|
||||
/// </summary>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15,10 +15,30 @@ public static partial class Gizmo
|
||||
/// </summary>
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -47,24 +47,6 @@ public static partial class Game
|
||||
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 )
|
||||
{
|
||||
var result = NodeLibrary.AddAssembly( asm );
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace Sandbox
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -39,13 +39,20 @@ public abstract partial class Resource : IValid, IJsonConvert, BytePack.ISeriali
|
||||
/// </summary>
|
||||
[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 )
|
||||
|
||||
@@ -58,13 +58,19 @@ public class ResourceSystem
|
||||
|
||||
var toDispose = ResourceIndex.Values.ToArray();
|
||||
|
||||
ResourceIndex.Clear();
|
||||
|
||||
foreach ( var resource in toDispose.OfType<GameResource>() )
|
||||
{
|
||||
resource.DestroyInternal();
|
||||
}
|
||||
|
||||
foreach ( var resource in toDispose )
|
||||
{
|
||||
// Don't wait/rely for finalizer get rid of this immediately
|
||||
resource.Destroy();
|
||||
}
|
||||
|
||||
ResourceIndex.Clear();
|
||||
|
||||
TypeCache.Clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ public partial class PrefabFile : GameResource
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
CachedScene?.DestroyImmediate();
|
||||
CachedScene?.DestroyInternal();
|
||||
CachedScene = null;
|
||||
|
||||
Unregister();
|
||||
|
||||
@@ -23,7 +23,7 @@ public partial class Texture
|
||||
/// </summary>
|
||||
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();
|
||||
|
||||
@@ -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<Vector3> Positions { get; init; }
|
||||
private HalfEdgeData<Vector2> TextureCoord { get; init; }
|
||||
|
||||
@@ -110,6 +110,7 @@ public partial class GameObject : IValid
|
||||
/// </summary>
|
||||
public void DestroyImmediate()
|
||||
{
|
||||
Assert.False( this is Scene, "Don't call DestroyImmediate on a scene." );
|
||||
ThreadSafe.AssertIsMainThread();
|
||||
|
||||
Term();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -20,12 +20,6 @@ internal static partial class PackageManager
|
||||
/// </summary>
|
||||
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 )
|
||||
{
|
||||
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<string> dependancies = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
|
||||
|
||||
@@ -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<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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ internal static class EngineFileSystem
|
||||
/// <summary>
|
||||
/// Don't try to use the filesystem until you've called this!
|
||||
/// </summary>
|
||||
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 )
|
||||
{
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Sandbox;
|
||||
/// </summary>
|
||||
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 );
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatch this compute shader using explicit thread counts.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using NativeEngine;
|
||||
using System.Linq;
|
||||
using Sandbox.Rendering;
|
||||
|
||||
namespace Sandbox;
|
||||
@@ -10,7 +8,18 @@ namespace Sandbox;
|
||||
/// </summary>
|
||||
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>
|
||||
/// Specifies the quality level for GGX filtering of environment maps.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Which method to use when downsampling a texture
|
||||
|
||||
@@ -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 )
|
||||
|
||||
@@ -43,4 +43,10 @@ internal partial class RenderPipeline
|
||||
// Return to pool
|
||||
Pool.Enqueue( renderPipeline );
|
||||
}
|
||||
|
||||
internal static void ClearPool()
|
||||
{
|
||||
Pool.Clear();
|
||||
ActivePipelines.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,3 +10,4 @@ using System.Runtime.CompilerServices;
|
||||
[assembly: TasksPersistOnContextReset]
|
||||
[assembly: InternalsVisibleTo( "Sandbox.Tools" )]
|
||||
[assembly: InternalsVisibleTo( "Sandbox.AppSystem" )]
|
||||
[assembly: InternalsVisibleTo( "Sandbox.Test" )]
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -700,7 +703,7 @@ internal partial class GameInstanceDll : Engine.IGameInstanceDll
|
||||
/// Called when the game menu is closed
|
||||
/// </summary>
|
||||
/// <param name="instance"></param>
|
||||
public void OnGameInstanceClosed( IGameInstance instance )
|
||||
public void Shutdown( IGameInstance instance )
|
||||
{
|
||||
NativeErrorReporter.Breadcrumb( true, "game", "Closed game instance" );
|
||||
NativeErrorReporter.SetTag( "game", null );
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts that all the ActionGraphs referenced by a given scene in a downloaded
|
||||
/// package have no errors.
|
||||
/// </summary>
|
||||
[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<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;
|
||||
|
||||
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<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 )
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
24
engine/Sandbox.Test/Engine/Shutdown.cs
Normal file
24
engine/Sandbox.Test/Engine/Shutdown.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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" );
|
||||
|
||||
|
||||
@@ -5,12 +5,6 @@ namespace Packages;
|
||||
[TestClass]
|
||||
public class PackageDownload
|
||||
{
|
||||
[TestInitialize]
|
||||
public void TestInitialize()
|
||||
{
|
||||
PackageManager.ResetForUnitTest();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow( "facepunch.sandbox" )]
|
||||
[DataRow( "garry.grassworld" )]
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -203,6 +211,12 @@ public partial class PackageLoader
|
||||
Assert.IsInstanceOfType<InvalidOperationException>( 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 );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Should throw an exception on invalid/missing package
|
||||
/// </summary>
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TestInit>();
|
||||
#else
|
||||
Sandbox.Application.InitUnitTest<TestInit>();
|
||||
#endif
|
||||
|
||||
TestAppSystem = new TestAppSystem();
|
||||
TestAppSystem.Init();
|
||||
}
|
||||
|
||||
[AssemblyCleanup]
|
||||
public static void AssemblyCleanup()
|
||||
{
|
||||
Sandbox.Application.ShutdownUnitTest();
|
||||
|
||||
TestAppSystem.Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
<ProjectReference Include="..\Sandbox.Engine\Sandbox.Engine.csproj" />
|
||||
<ProjectReference Include="..\Sandbox.Tools\Sandbox.Tools.csproj" />
|
||||
<ProjectReference Include="..\Sandbox.Menu\Sandbox.Menu.csproj" />
|
||||
<ProjectReference Include="..\Sandbox.AppSystem\Sandbox.AppSystem.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 ) )
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user