Files
sbox-public/engine/Sandbox.Engine/Systems/Networking/Tables/ServerPackages.cs
Lorenz Junglas 6808d8768e 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
2025-12-08 15:55:11 +01:00

187 lines
4.3 KiB
C#

using Sandbox.Internal;
using Sandbox.Menu;
using Sandbox.Network;
using System.Threading;
namespace Sandbox;
/// <summary>
/// Manages the network string table "ServerPackages", which contains a list of packages that the client should
/// have installed. During join the client will install these packages before loading in.
/// </summary>
internal class ServerPackages
{
public static ServerPackages Current { get; private set; } = new();
internal record struct ServerPackageInfo();
internal StringTable StringTable;
internal class PackageDownload
{
public string ident;
public Package package;
public bool IsDownloading;
public bool IsMounted;
public bool IsErrored;
public PackageManager.ActivePackage activePackage;
internal async ValueTask<BaseFileSystem> DownloadAndMount( CancellationToken token )
{
// Already downloaded
if ( activePackage != null )
{
return activePackage.FileSystem;
}
// Downloading right now in another task
if ( IsDownloading )
{
while ( IsDownloading ) await Task.Delay( 20 );
return activePackage?.FileSystem;
}
IsDownloading = true;
try
{
package = await Package.Fetch( ident, false );
if ( package == null )
{
Log.Warning( $"Package not found: {ident}" );
return null;
}
var o = new PackageLoadOptions
{
PackageIdent = ident,
ContextTag = "game",
Loading = new UpdateLoadingScreen(),
AllowLocalPackages = true,
CancellationToken = token
};
activePackage = await PackageManager.InstallAsync( o );
// Success
IsMounted = true;
o.Loading.Dispose();
return activePackage.FileSystem;
}
catch ( System.Exception e )
{
Log.Warning( e, e.Message );
Log.Warning( e, e.StackTrace );
IsErrored = true;
return null;
}
finally
{
IsDownloading = false;
}
}
}
static CaseInsensitiveDictionary<PackageDownload> Downloads;
internal ServerPackages()
{
// WTF???
Current = this;
StringTable = new StringTable( "ServerPackages", true );
StringTable.OnChangeOrAdd += ( e ) => _ = ClientInstallPackage( e );
Clear();
}
internal void Clear()
{
StringTable.Reset();
Downloads = new();
}
internal async Task InstallAll()
{
// Conna: let's make a copy here incase the entries table is modified during
// the installation. This could happen if a new package is added from
// the host while we're installing.
var entries = StringTable.Entries.ToDictionary();
Log.Info( $"Installing {entries.Count} server packages.." );
var sw = System.Diagnostics.Stopwatch.StartNew();
await entries.ForEachTaskAsync( async p =>
{
await ClientInstallPackage( p.Value );
} );
Log.Info( $"Installation Complete ({sw.Elapsed.TotalSeconds:0.00}s)" );
}
internal async Task ClientInstallPackage( StringTable.Entry entry )
{
string ident = entry.Name;
if ( ident.StartsWith( "local." ) )
return;
Log.Info( $"Installing server package: {ident}" );
ServerPackageInfo packageInfo = entry.Read<ServerPackageInfo>();
await DownloadAndMount( ident );
}
internal void AddRequirement( Package package, ServerPackageInfo info = default )
{
AddRequirement( package.GetIdent( false, true ), info );
}
internal void AddRequirement( string packageIdent, ServerPackageInfo info = default )
{
StringTable.Set( packageIdent, info );
}
internal async ValueTask<BaseFileSystem> DownloadAndMount( string packageIdent, CancellationToken token = default )
{
ThreadSafe.AssertIsMainThread();
if ( Networking.IsHost )
{
AddRequirement( packageIdent, new ServerPackageInfo() );
}
if ( Downloads.TryGetValue( packageIdent, out var dl ) )
return await dl.DownloadAndMount( token );
dl = new();
dl.ident = packageIdent;
Downloads[packageIdent] = dl;
return await dl.DownloadAndMount( token );
}
internal PackageDownload Get( string packageIdent )
{
if ( !Downloads.TryGetValue( packageIdent, out var dl ) )
return null;
return dl;
}
}
internal class UpdateLoadingScreen : ILoadingInterface
{
public void Dispose()
{
LoadingScreen.Title = "";
LoadingScreen.Subtitle = "";
}
public void LoadingProgress( LoadingProgress progress )
{
LoadingScreen.Title = $"{progress.Title}";
LoadingScreen.Subtitle = $"{progress.Percent:n0}% • {progress.Mbps:n0}mbps • {progress.CalculateETA().ToRemainingTimeString()}";
}
}