mirror of
https://github.com/Facepunch/sbox-public.git
synced 2025-12-23 22:48:07 -05:00
* 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
911 lines
24 KiB
C#
911 lines
24 KiB
C#
using Microsoft.CodeAnalysis.CSharp;
|
|
using Sandbox.ActionGraphs;
|
|
using Sandbox.Audio;
|
|
using Sandbox.Diagnostics;
|
|
using Sandbox.Internal;
|
|
using Sandbox.UI;
|
|
using Sandbox.Utility;
|
|
using Sandbox.VR;
|
|
using System;
|
|
using System.IO;
|
|
|
|
namespace Sandbox;
|
|
|
|
internal partial class GameInstanceDll : Engine.IGameInstanceDll
|
|
{
|
|
public static GameInstanceDll Current { get; private set; }
|
|
|
|
public static GameInstance gameInstance;
|
|
|
|
public static PackageLoader PackageLoader { get; set; }
|
|
PackageLoader.Enroller AssemblyEnroller { get; set; }
|
|
|
|
private bool _isAssemblyLoadingPaused;
|
|
|
|
public void Bootstrap()
|
|
{
|
|
Current = this;
|
|
|
|
GlobalContext.Current.Reset();
|
|
GlobalContext.Current.LocalAssembly = GetType().Assembly;
|
|
|
|
Game.InitHost();
|
|
|
|
SetupInputContext();
|
|
|
|
JsonUpgrader.UpdateUpgraders( TypeLibrary );
|
|
|
|
Sandbox.Generator.Processor.DefaultPackageAssetResolver = ResolvePackageAsset;
|
|
|
|
PackageManager.OnPackageInstalledToContext += OnPackageInstalled;
|
|
|
|
//
|
|
// Files accessible by game instance
|
|
//
|
|
GlobalContext.Current.FileMount = new AggregateFileSystem();
|
|
{
|
|
if ( Application.IsEditor )
|
|
{
|
|
// If we're in the editor, also mount the cloud folder, which is where we
|
|
// download resources and assets from sbox.game to.
|
|
FileSystem.Mounted.Mount( EngineFileSystem.LibraryContent );
|
|
}
|
|
|
|
if ( Application.IsStandalone )
|
|
{
|
|
// In standalone, we don't ship code - only assets
|
|
FileSystem.Mounted.CreateAndMount( EngineFileSystem.Addons, $"/base/assets" );
|
|
FileSystem.Mounted.CreateAndMount( EngineFileSystem.Root, "/core/" );
|
|
}
|
|
else
|
|
{
|
|
FileSystem.Mounted.CreateAndMount( EngineFileSystem.Addons, "/base/assets/" );
|
|
FileSystem.Mounted.CreateAndMount( EngineFileSystem.Addons, "/base/code/" );
|
|
FileSystem.Mounted.CreateAndMount( EngineFileSystem.Root, "/core/" );
|
|
FileSystem.Mounted.CreateAndMount( EngineFileSystem.Addons, "/citizen/assets/" );
|
|
}
|
|
}
|
|
|
|
PackageLoader?.Dispose();
|
|
PackageLoader = new PackageLoader( "GameMenu", typeof( GameInstanceDll ).Assembly );
|
|
PackageLoader.HotloadWatch( Game.GameAssembly ); // Sandbox.Game is per instance
|
|
PackageLoader.OnAfterHotload = OnAfterHotload;
|
|
|
|
{
|
|
ConVarSystem.AddAssembly( GetType().Assembly, "game" );
|
|
ConVarSystem.AddAssembly( Game.GameAssembly, "game" );
|
|
}
|
|
}
|
|
|
|
public Task Initialize()
|
|
{
|
|
ResetEnvironment();
|
|
Networking.StartThread();
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public void Exiting()
|
|
{
|
|
Networking.Disconnect();
|
|
Networking.StopThread();
|
|
|
|
Event.Run( "app.exit" );
|
|
Game.Cookies?.Save();
|
|
}
|
|
|
|
static int Counter;
|
|
|
|
void OnAfterHotload()
|
|
{
|
|
GlobalContext.Current.OnHotload();
|
|
Game.ActiveScene?.OnHotload();
|
|
Event.Run( "hotloaded" );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called from the code generator. The game package should already contain this package's content
|
|
/// so we just need to work out where it's meant to point to
|
|
/// </summary>
|
|
private string ResolvePackageAsset( string packageName )
|
|
{
|
|
Package package = Package.FetchAsync( packageName, false ).Result;
|
|
return package?.PrimaryAsset;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This should reset our environment to a clean state.
|
|
/// </summary>
|
|
public void ResetEnvironment()
|
|
{
|
|
Log.Trace( "Game Menu - ResetEnvironment" );
|
|
|
|
// Use a new package loader for every game if we're not in editor
|
|
// The editor is only going to load 1 game and ToolsDll has a reference to it
|
|
if ( !PackageLoader.ToolsMode )
|
|
{
|
|
PackageLoader?.Dispose();
|
|
PackageLoader = null;
|
|
|
|
PackageLoader = new PackageLoader( "GameMenu", typeof( GameInstanceDll ).Assembly );
|
|
PackageLoader.HotloadWatch( Game.GameAssembly ); // Sandbox.Game is per instance
|
|
PackageLoader.OnAfterHotload = OnAfterHotload;
|
|
}
|
|
|
|
if ( DidMountNetworkedFiles )
|
|
{
|
|
EngineFileSystem.Mounted.UnMount( NetworkedSmallFiles.Files );
|
|
EngineFileSystem.ProjectSettings.UnMount( NetworkedConfigFiles.Files );
|
|
DidMountNetworkedFiles = false;
|
|
}
|
|
|
|
FontManager.Instance.Reset();
|
|
FontManager.Instance.LoadAll( FileSystem.Mounted );
|
|
|
|
AssemblyEnroller?.Dispose();
|
|
AssemblyEnroller = null;
|
|
|
|
CodeArchiveTable.Reset();
|
|
NetworkedSmallFiles.Reset();
|
|
NetworkedConfigFiles.Reset();
|
|
NetworkedLargeFiles.Reset();
|
|
ReplicatedConvars.Reset();
|
|
ServerPackages.Clear();
|
|
|
|
FileWatchers.ForEach( w => w.Dispose() );
|
|
FileWatchers.Clear();
|
|
|
|
Screen.UpdateFromEngine();
|
|
|
|
Game.InitTypeLibrary();
|
|
|
|
UserPermission.Load();
|
|
|
|
Input.ReadConfig( null );
|
|
StyleSheet.InitStyleSheets();
|
|
Networking.Reset();
|
|
Connection.Reset();
|
|
GlobalContext.Current.Reset();
|
|
NativeResourceCache.Clear();
|
|
Speech.Recognition.Reset();
|
|
Json.Initialize();
|
|
VRSystem.Reset();
|
|
|
|
if ( !Application.IsEditor )
|
|
{
|
|
Mixer.ResetToDefault();
|
|
}
|
|
|
|
Sound.Clear();
|
|
Application.ClearGame();
|
|
|
|
ReflectionQueryCache.ClearTypeCache();
|
|
|
|
EngineFileSystem.ProjectSettings?.Dispose();
|
|
EngineFileSystem.ProjectSettings = null;
|
|
|
|
ProjectSettings.ClearCache();
|
|
ErrorReporter.ResetCounters();
|
|
|
|
AssemblyEnroller = PackageLoader.CreateEnroller( $"gamedll{Counter++}" );
|
|
|
|
AssemblyEnroller.OnAssemblyAdded += ( a ) =>
|
|
{
|
|
Assert.NotNull( a.Assembly );
|
|
|
|
if ( a.IsEditorAssembly )
|
|
return;
|
|
|
|
Game.TypeLibrary.AddAssembly( a.Assembly, true );
|
|
Game.NodeLibrary.AddAssembly( a.Assembly );
|
|
ConVarSystem.AddAssembly( a.Assembly, "game" );
|
|
Cloud.UpdateTypes( a.Assembly );
|
|
Json.Initialize();
|
|
JsonUpgrader.UpdateUpgraders( TypeLibrary );
|
|
|
|
if ( !a.IsEditorAssembly && a.CodeArchiveBytes is not null )
|
|
{
|
|
AddArchiveToCodeArchiveTable( a );
|
|
}
|
|
|
|
ReplicatedConvars.OnAssembliesLoaded();
|
|
};
|
|
|
|
AssemblyEnroller.OnAssemblyRemoved += ( a ) =>
|
|
{
|
|
if ( a.IsEditorAssembly )
|
|
return;
|
|
|
|
Assert.NotNull( a.Assembly );
|
|
|
|
Game.NodeLibrary.RemoveAssembly( a.Assembly );
|
|
Game.TypeLibrary.RemoveAssembly( a.Assembly );
|
|
ConVarSystem.RemoveAssembly( a.Assembly );
|
|
JsonUpgrader.UpdateUpgraders( TypeLibrary );
|
|
|
|
CodeArchiveTable.Remove( a.Name );
|
|
};
|
|
|
|
AssemblyEnroller.OnAssemblyFastHotload += ( a ) =>
|
|
{
|
|
if ( !a.IsEditorAssembly && a.CodeArchiveBytes is not null )
|
|
{
|
|
AddArchiveToCodeArchiveTable( a );
|
|
}
|
|
};
|
|
|
|
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>
|
|
/// This method grabs the currently loaded assembly's code archive, strips serverside code and spits it back out into the
|
|
/// CodeArchiveTable.
|
|
/// </summary>
|
|
/// <param name="a"></param>
|
|
private void AddArchiveToCodeArchiveTable( LoadedAssembly a )
|
|
{
|
|
if ( a?.CodeArchiveBytes is null )
|
|
return;
|
|
|
|
//
|
|
// If we're not a dedicated server OR this isn't a game assembly,
|
|
// just use the original archive bytes
|
|
//
|
|
if ( !Application.IsDedicatedServer || !a.IsGame )
|
|
{
|
|
CodeArchiveTable.Set( a.Name, a.CodeArchiveBytes );
|
|
return;
|
|
}
|
|
|
|
//
|
|
// Find matching compiler
|
|
//
|
|
var compiler = Project.All
|
|
.Select( x => x.Compiler )
|
|
.FirstOrDefault( x => x is not null && x.AssemblyName.Equals( a.Name ) );
|
|
|
|
if ( compiler?.Output?.Archive is null )
|
|
{
|
|
CodeArchiveTable.Set( a.Name, a.CodeArchiveBytes );
|
|
return;
|
|
}
|
|
|
|
var config = compiler.GetConfiguration();
|
|
|
|
//
|
|
// Strip the SERVER define from the archive's DefineConstants
|
|
//
|
|
var parts = config.DefineConstants.Split( ';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries ).ToList();
|
|
parts.RemoveAll( x => x.Equals( "SERVER", StringComparison.OrdinalIgnoreCase ) );
|
|
|
|
var newConfig = config with { DefineConstants = string.Join( ";", parts ) };
|
|
|
|
var strippedArchive = new CodeArchive
|
|
{
|
|
CompilerName = compiler.Name,
|
|
Configuration = newConfig
|
|
};
|
|
|
|
foreach ( var tree in compiler.Output.Archive.SyntaxTrees )
|
|
{
|
|
var text = tree.GetText().ToString();
|
|
var newTree = CSharpSyntaxTree.ParseText( text, path: tree.FilePath, encoding: System.Text.Encoding.UTF8, options: newConfig.GetParseOptions() );
|
|
|
|
//
|
|
// Make sure we strip any disabled text trivia from the tree
|
|
//
|
|
newTree = Compiler.StripDisabledTextTrivia( newTree );
|
|
strippedArchive.SyntaxTrees.Add( newTree );
|
|
}
|
|
|
|
//
|
|
// Need to maintain all the extra stuff in the original archive
|
|
//
|
|
foreach ( var f in compiler.Output.Archive.AdditionalFiles )
|
|
{
|
|
strippedArchive.AdditionalFiles.Add( f );
|
|
}
|
|
|
|
foreach ( var kv in compiler.Output.Archive.FileMap )
|
|
{
|
|
strippedArchive.FileMap[kv.Key] = kv.Value;
|
|
}
|
|
|
|
foreach ( var r in compiler.Output.Archive.References )
|
|
{
|
|
strippedArchive.References.Add( r );
|
|
}
|
|
|
|
var bytes = strippedArchive.Serialize();
|
|
CodeArchiveTable.Set( a.Name, bytes );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Don't attempt to load new assemblies during <see cref="Tick"/> inside this scope.
|
|
/// </summary>
|
|
private DisposeAction PauseLoadingAssemblies()
|
|
{
|
|
if ( _isAssemblyLoadingPaused )
|
|
{
|
|
// Don't unpause after this scope if we're already paused
|
|
|
|
return default;
|
|
}
|
|
|
|
_isAssemblyLoadingPaused = true;
|
|
|
|
return new DisposeAction( () =>
|
|
{
|
|
_isAssemblyLoadingPaused = false;
|
|
} );
|
|
}
|
|
|
|
public void FinishLoadingAssemblies()
|
|
{
|
|
PackageLoader.Tick();
|
|
|
|
// we send table updates right after packageloader has run
|
|
// so that any further messages will be read with the same
|
|
// assemblies
|
|
Networking.System?.SendTableUpdates();
|
|
}
|
|
|
|
public void CloseGame()
|
|
{
|
|
if ( gameInstance is null ) return;
|
|
|
|
ConVarSystem.SaveAll();
|
|
|
|
// Scope disconnect so we can shutdown game before disconnect and stop game objects from sending network destroy,
|
|
// orphaned action should take care of it.
|
|
using ( Networking.DisconnectScope() )
|
|
{
|
|
gameInstance.Shutdown();
|
|
gameInstance = null;
|
|
IGameInstance.Current = null;
|
|
}
|
|
|
|
Application.ClearGame();
|
|
|
|
LoadingScreen.IsVisible = false;
|
|
LoadingScreen.Media = null;
|
|
|
|
Sound.StopAll( 0.2f );
|
|
|
|
ResetEnvironment();
|
|
Mounting.MountUtility.TickPreviewRenders();
|
|
}
|
|
|
|
internal Input.Context _perFrameInput = Input.Context.Create( "ClientPerFrame" );
|
|
|
|
public void Tick()
|
|
{
|
|
var scene = Game.ActiveScene;
|
|
|
|
using var sceneScope = scene?.Push();
|
|
|
|
if ( scene is not null )
|
|
{
|
|
// Update the time now that we're in the scene scope
|
|
scene.UpdateTime( RealTime.Delta );
|
|
|
|
// If we're a client then advance server time
|
|
scene.SyncServerTime();
|
|
|
|
// push this time scope now that the scene has updated time
|
|
Time.Update( scene.TimeNow, scene.TimeDelta );
|
|
}
|
|
|
|
if ( gameInstance?.WantsToQuit ?? false )
|
|
{
|
|
CloseGame();
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// Update input
|
|
//
|
|
{
|
|
_perFrameInput.Flip();
|
|
_perFrameInput.Push();
|
|
Input.Process();
|
|
}
|
|
|
|
//
|
|
// Recieve incoming network messages, send heartbeat and other outgoing messages
|
|
//
|
|
Networking.PreFrameTick();
|
|
|
|
//
|
|
// We may have got new assemblies in the network update,
|
|
// so finish loading them now before running updates
|
|
//
|
|
if ( !_isAssemblyLoadingPaused )
|
|
{
|
|
FinishLoadingAssemblies();
|
|
}
|
|
|
|
//
|
|
// Run the actual game scene tick
|
|
//
|
|
using ( Performance.Scope( "GameFrame" ) )
|
|
{
|
|
RunGameFrame( scene );
|
|
}
|
|
|
|
Networking.PostFrameTick();
|
|
|
|
Connection.ClearUpdateContextInput();
|
|
}
|
|
|
|
if ( !Application.IsDedicatedServer )
|
|
{
|
|
ActionGraphDebugger.Tick();
|
|
RichPresenceSystem.Tick();
|
|
Services.Achievements.Tick();
|
|
}
|
|
|
|
// Advance per frame scene metrics
|
|
TickSceneStats( scene );
|
|
|
|
Analytics.Tick();
|
|
|
|
// Run any pending queue'd mainthread tasks here
|
|
// so they're in the same scene scope
|
|
MainThread.RunQueues();
|
|
}
|
|
|
|
private void RunGameFrame( Scene activeScene )
|
|
{
|
|
if ( !Game.IsPlaying ) return;
|
|
if ( activeScene is null ) return;
|
|
if ( Networking.IsConnecting ) return;
|
|
|
|
LoadingScreen.IsVisible = activeScene.IsLoading;
|
|
|
|
activeScene.GameTick( 0 ); // we already advanced time
|
|
|
|
// Run any pending queue'd mainthread tasks here
|
|
// so they're in the same scene scope
|
|
MainThread.RunQueues();
|
|
}
|
|
|
|
public void SimulateUI()
|
|
{
|
|
bool mouseIsAllowed = true;
|
|
if ( IMenuDll.Current is not null )
|
|
{
|
|
mouseIsAllowed = !IMenuDll.Current.HasOverlayMouseInput();
|
|
}
|
|
|
|
using ( Game.ActiveScene?.Push() )
|
|
{
|
|
Game.Language?.Tick();
|
|
GlobalContext.Current.UISystem.Simulate( mouseIsAllowed );
|
|
|
|
Game.ActiveScene?.ProcessDeletes();
|
|
}
|
|
}
|
|
|
|
public void ClosePopups( object panelClickedOn )
|
|
{
|
|
BasePopup.CloseAll( panelClickedOn as Panel );
|
|
}
|
|
|
|
public void Disconnect()
|
|
{
|
|
Game.Close();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the game asynchronously
|
|
/// </summary>
|
|
public async Task LoadGamePackageAsync( string ident, GameLoadingFlags flags, CancellationToken ct )
|
|
{
|
|
try
|
|
{
|
|
ThreadSafe.AssertIsMainThread();
|
|
await LoadGamePackageAsyncInternal( ident, flags, ct );
|
|
}
|
|
catch ( System.Exception e )
|
|
{
|
|
LoadingScreen.IsVisible = false;
|
|
LoadingScreen.Media = null;
|
|
|
|
using ( IMenuDll.Current?.PushScope() )
|
|
{
|
|
IMenuSystem.Current?.Popup( "error", "Loading Error", $"There was an error when loading this game. {e.Message}" );
|
|
}
|
|
|
|
Log.Warning( e, e.Message );
|
|
}
|
|
}
|
|
|
|
public async Task LoadGamePackageAsyncInternal( string ident, GameLoadingFlags flags, CancellationToken ct )
|
|
{
|
|
//
|
|
// We might not need to reload if this is the same package.
|
|
// Bit of extra dancing here because we want #local to be treated as released
|
|
//
|
|
if ( gameInstance is not null && !flags.Contains( GameLoadingFlags.Reload ) )
|
|
{
|
|
Package.TryParseIdent( ident, out var iparts );
|
|
Package.TryParseIdent( gameInstance?.Ident, out var oparts );
|
|
|
|
// No need to recreate - this is the same package (and the same version)
|
|
if ( iparts.package == oparts.package && iparts.org == oparts.org && iparts.version == oparts.version )
|
|
return;
|
|
}
|
|
|
|
gameInstance?.Shutdown();
|
|
|
|
//
|
|
// If this isn't part of a remote connection, leave any active network session
|
|
//
|
|
if ( !flags.Contains( GameLoadingFlags.Remote ) )
|
|
{
|
|
Networking.Disconnect();
|
|
}
|
|
|
|
Application.ClearGame();
|
|
|
|
if ( !Application.IsDedicatedServer && !Application.IsStandalone )
|
|
{
|
|
// Get the stats ready
|
|
var s = Sandbox.Services.Stats.GetGlobalStats( ident );
|
|
if ( !s.IsRefreshing )
|
|
{
|
|
_ = s.Refresh();
|
|
}
|
|
|
|
// get the local stats too
|
|
var playerStats = Sandbox.Services.Stats.GetLocalPlayerStats( ident );
|
|
if ( !playerStats.IsRefreshing )
|
|
{
|
|
_ = playerStats.Refresh();
|
|
}
|
|
|
|
await Task.Delay( 10 );
|
|
LoadingScreen.Title ??= "Loading..";
|
|
}
|
|
|
|
GameInstance newInstance = default;
|
|
|
|
try
|
|
{
|
|
if ( Application.IsStandalone )
|
|
{
|
|
newInstance = new StandaloneGameInstance( ident, flags );
|
|
}
|
|
else
|
|
{
|
|
newInstance = new GameInstance( ident, flags );
|
|
}
|
|
|
|
ResetEnvironment();
|
|
|
|
NativeErrorReporter.Breadcrumb( true, "game", $"Loading game package {ident}" );
|
|
NativeErrorReporter.SetTag( "game", ident );
|
|
NativeErrorReporter.SetTag( "map", LaunchArguments.Map );
|
|
|
|
if ( !Application.IsDedicatedServer && !Application.IsStandalone )
|
|
{
|
|
await Task.Delay( 10 );
|
|
}
|
|
|
|
if ( !await newInstance.LoadAsync( AssemblyEnroller, ct ) )
|
|
{
|
|
ResetEnvironment();
|
|
newInstance.Close();
|
|
newInstance.Shutdown();
|
|
newInstance = default;
|
|
|
|
throw new System.Exception( "Loading failed." );
|
|
}
|
|
|
|
if ( ct.IsCancellationRequested )
|
|
return;
|
|
|
|
await Task.Delay( 10 );
|
|
GC.Collect( GC.MaxGeneration, GCCollectionMode.Optimized, false, false );
|
|
await Task.Delay( 10 );
|
|
|
|
if ( Package.TryParseIdent( ident, out var parsed ) )
|
|
{
|
|
EngineFileSystem.Data.CreateDirectory( parsed.org );
|
|
|
|
var package = parsed.local ? $"{parsed.package}#local" : parsed.package;
|
|
|
|
GlobalContext.Current.FileOrg = EngineFileSystem.Data.CreateSubSystem( parsed.org );
|
|
GlobalContext.Current.FileOrg.CreateDirectory( package );
|
|
|
|
GlobalContext.Current.FileData = FileSystem.OrganizationData.CreateSubSystem( package );
|
|
}
|
|
else
|
|
{
|
|
EngineFileSystem.Data.CreateDirectory( ".local" );
|
|
|
|
GlobalContext.Current.FileOrg = EngineFileSystem.Data.CreateSubSystem( ".local" );
|
|
GlobalContext.Current.FileOrg.CreateDirectory( ident );
|
|
|
|
GlobalContext.Current.FileData = FileSystem.OrganizationData.CreateSubSystem( ident );
|
|
}
|
|
|
|
Game.Cookies = new CookieContainer( "cookies", false, GlobalContext.Current.FileData );
|
|
|
|
IGameInstance.Current = newInstance;
|
|
gameInstance = newInstance;
|
|
newInstance = default;
|
|
|
|
//
|
|
// Boot up
|
|
//
|
|
if ( flags.Contains( GameLoadingFlags.Host ) )
|
|
{
|
|
if ( !gameInstance.OpenStartupScene() )
|
|
{
|
|
ResetEnvironment();
|
|
LoadingScreen.IsVisible = false;
|
|
LoadingScreen.Media = null;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ( Application.IsEditor )
|
|
{
|
|
Game.CheatsEnabled = true; // auto-enable cheats in the editor
|
|
}
|
|
|
|
//
|
|
// If we're the game, start it straight away.
|
|
// In editor we're going to start in editor mode
|
|
//
|
|
if ( !Application.IsEditor )
|
|
{
|
|
Game.IsPlaying = true;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
// Loading failed
|
|
if ( newInstance is not null )
|
|
{
|
|
newInstance.Close();
|
|
newInstance.Shutdown();
|
|
newInstance = default;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnPackageInstalled( PackageManager.ActivePackage package, string context )
|
|
{
|
|
Log.Trace( $"OnPackageInstalled: {package.Package.FullIdent} {context}" );
|
|
|
|
// only load if a game context (tools can install packages)
|
|
if ( context != "game" ) return;
|
|
|
|
// Load all the GameResources and fonts in the package
|
|
if ( package.FileSystem is not null )
|
|
{
|
|
ResourceLoader.LoadAllGameResource( package.FileSystem );
|
|
FontManager.Instance.LoadAll( package.FileSystem );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the game menu is closed
|
|
/// </summary>
|
|
/// <param name="instance"></param>
|
|
public void Shutdown( IGameInstance instance )
|
|
{
|
|
NativeErrorReporter.Breadcrumb( true, "game", "Closed game instance" );
|
|
NativeErrorReporter.SetTag( "game", null );
|
|
NativeErrorReporter.SetTag( "map", null );
|
|
|
|
if ( gameInstance == instance ) gameInstance = null;
|
|
if ( IGameInstance.Current == instance ) IGameInstance.Current = null;
|
|
}
|
|
|
|
public void OnRender( SwapChainHandle_t swapChain )
|
|
{
|
|
Game.Render( swapChain );
|
|
}
|
|
|
|
/// <summary>
|
|
/// The play button was pressed in the editor
|
|
/// </summary>
|
|
public void EditorPlay()
|
|
{
|
|
if ( gameInstance is null )
|
|
{
|
|
Log.Warning( "Tried to editor play but we don't have a game instance" );
|
|
return;
|
|
}
|
|
|
|
Game.IsPlaying = true;
|
|
|
|
if ( !gameInstance.OpenStartupScene() )
|
|
{
|
|
Log.Warning( "There was a problem opening the StartupScene" );
|
|
return;
|
|
}
|
|
}
|
|
|
|
public TypeLibrary TypeLibrary => Sandbox.Internal.GlobalGameNamespace.TypeLibrary;
|
|
|
|
/// <summary>
|
|
/// Pushes the game scope. This will push the active scene and the right time.
|
|
/// </summary>
|
|
public IDisposable PushScope()
|
|
{
|
|
return Game.ActiveScene?.Push();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called per frame to add scene's stats to our analytics
|
|
/// </summary>
|
|
void TickSceneStats( Scene scene )
|
|
{
|
|
var sceneValid = scene.IsValid();
|
|
Api.Performance.CollectStat( "GameObjectCount", sceneValid ? scene.Directory.GameObjectCount : 0 );
|
|
Api.Performance.CollectStat( "ComponentCount", sceneValid ? scene.Directory.ComponentCount : 0 );
|
|
Api.Performance.CollectStat( "RootGameObjects", sceneValid ? scene.Children.Count : 0 );
|
|
Api.Performance.CollectStat( "CameraCount", sceneValid ? scene.GetAllComponents<CameraComponent>().Count() : 0 );
|
|
Api.Performance.CollectStat( "ColliderCount", sceneValid ? scene.PhysicsWorld.Bodies.Count() : 0 );
|
|
Api.Performance.CollectStat( "DynamicBodyCount", sceneValid ? scene.PhysicsWorld.Bodies.Where( x => x.BodyType == PhysicsBodyType.Dynamic ).Count() : 0 );
|
|
Api.Performance.CollectStat( "KeyframeBodyCount", sceneValid ? scene.PhysicsWorld.Bodies.Where( x => x.BodyType == PhysicsBodyType.Keyframed ).Count() : 0 );
|
|
Api.Performance.CollectStat( "StaticBodyCount", sceneValid ? scene.PhysicsWorld.Bodies.Where( x => x.BodyType == PhysicsBodyType.Static ).Count() : 0 );
|
|
Api.Performance.CollectStat( "Particles", sceneValid ? scene.GetAllComponents<ParticleEffect>().Sum( x => x.Particles.Count ) : 0 );
|
|
|
|
Api.Performance.CollectStat( "GameObjectsDestroyed", SceneMetrics.GameObjectsDestroyed );
|
|
Api.Performance.CollectStat( "ParticlesCreated", SceneMetrics.ParticlesCreated );
|
|
Api.Performance.CollectStat( "ParticlesDestroyed", SceneMetrics.ParticlesDestroyed );
|
|
Api.Performance.CollectStat( "GameObjectsCreated", SceneMetrics.GameObjectsCreated );
|
|
Api.Performance.CollectStat( "GameObjectsDestroyed", SceneMetrics.GameObjectsDestroyed );
|
|
Api.Performance.CollectStat( "ComponentsCreated", SceneMetrics.ComponentsCreated );
|
|
Api.Performance.CollectStat( "ComponentsDestroyed", SceneMetrics.ComponentsDestroyed );
|
|
Api.Performance.CollectStat( "RayTrace", SceneMetrics.RayTrace );
|
|
Api.Performance.CollectStat( "RayTraceAll", SceneMetrics.RayTraceAll );
|
|
|
|
SceneMetrics.Flip();
|
|
}
|
|
|
|
void IGameInstanceDll.ResetSceneListenerMetrics()
|
|
{
|
|
Game.ActiveScene.ResetListenerMetrics();
|
|
}
|
|
|
|
object IGameInstanceDll.GetSceneListenerMetrics()
|
|
{
|
|
return Game.ActiveScene.GetListenerMetrics();
|
|
}
|
|
|
|
[ConCmd( "game", ConVarFlags.Protected, Help = "Play a game" )]
|
|
public static async Task StartGame( string gameIdent, string mapIdent = null )
|
|
{
|
|
// We don't want to open games in the editor
|
|
if ( Application.IsEditor )
|
|
return;
|
|
|
|
// We can load and run projects if we're a Dedicated Server.
|
|
if ( Application.IsDedicatedServer && gameIdent.ToLower().Contains( ".sbproj" ) )
|
|
{
|
|
await Project.InitializeBuiltIn( false );
|
|
|
|
var project = Project.AddFromFile( gameIdent );
|
|
|
|
NativeEngine.FullFileSystem.AddProjectPath( gameIdent, project.GetAssetsPath().ToLowerInvariant() );
|
|
|
|
var libraries = Path.Combine( project.RootDirectory.FullName, "Libraries" );
|
|
|
|
// We should iterate all available libraries and add their projects
|
|
foreach ( var folder in Directory.EnumerateDirectories( libraries ) )
|
|
{
|
|
var configs = Directory.EnumerateFiles( folder, "*.sbproj" ).ToArray();
|
|
if ( configs.Length != 1 ) continue;
|
|
Project.AddFromFile( configs[0] );
|
|
}
|
|
|
|
// We need to reload the project since we may have had a bunch of libraries added
|
|
project.Load();
|
|
|
|
await Project.CompileAsync();
|
|
|
|
if ( !project.Active )
|
|
{
|
|
Log.Error( $"Unable to load {gameIdent}" );
|
|
return;
|
|
}
|
|
|
|
gameIdent = project.Package.FullIdent;
|
|
}
|
|
|
|
Log.Info( $"Loading game '{gameIdent}'" );
|
|
|
|
LaunchArguments.Map = mapIdent;
|
|
|
|
if ( LaunchArguments.Map is not null )
|
|
{
|
|
Log.Info( $" with map: '{LaunchArguments.Map}'" );
|
|
}
|
|
|
|
await IGameInstanceDll.Current.LoadGamePackageAsync( gameIdent, GameLoadingFlags.Host, default );
|
|
Log.Info( $"Load Complete" );
|
|
}
|
|
|
|
public static void Create()
|
|
{
|
|
IGameInstanceDll.Current = new GameInstanceDll();
|
|
|
|
// PreJIT the methods in these dlls to avoid doing it during the game
|
|
{
|
|
var e = new Api.Events.EventRecord( "PreJIT.Game" );
|
|
|
|
using ( e.ScopeTimer( "Sandbox.GameInstance" ) )
|
|
{
|
|
Sandbox.ReflectionUtility.PreJIT( typeof( GameInstanceDll ).Assembly );
|
|
}
|
|
|
|
using ( e.ScopeTimer( "Sandbox.System" ) )
|
|
{
|
|
Sandbox.ReflectionUtility.PreJIT( typeof( Vector3 ).Assembly );
|
|
}
|
|
|
|
using ( e.ScopeTimer( "Sandbox.Engine" ) )
|
|
{
|
|
Sandbox.ReflectionUtility.PreJIT( typeof( Bootstrap ).Assembly );
|
|
}
|
|
|
|
e.Submit();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to get the replicated var value from the host
|
|
/// </summary>
|
|
public bool TryGetReplicatedVarValue( string name, out string value )
|
|
{
|
|
value = default;
|
|
|
|
if ( !Networking.IsActive ) return false;
|
|
if ( Networking.IsHost ) return false;
|
|
|
|
return ReplicatedConvars.TryGetValue( name, out value );
|
|
}
|
|
|
|
public InputContext InputContext { get; private set; }
|
|
|
|
internal void SetupInputContext()
|
|
{
|
|
var uiSystem = new UISystem();
|
|
|
|
var input = new InputContext();
|
|
input.Name = GetType().Name;
|
|
input.TargetUISystem = uiSystem;
|
|
input.OnGameMouseWheel += Sandbox.Input.AddMouseWheel;
|
|
input.OnMouseMotion += Sandbox.Input.AddMouseMovement;
|
|
input.OnGameButton += Input.OnButton;
|
|
|
|
InputContext = input;
|
|
|
|
GlobalContext.Current.UISystem = uiSystem;
|
|
GlobalContext.Current.InputContext = input;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load the assemblies from this package into the current game instance
|
|
/// </summary>
|
|
public Task LoadPackageAssembliesAsync( Package package )
|
|
{
|
|
AssemblyEnroller.LoadPackage( package.FullIdent, true );
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|