Files
sbox-public/engine/Sandbox.GameInstance/GameInstanceDll.Network.cs
2026-07-14 18:34:23 +02:00

420 lines
12 KiB
C#

using Sandbox.Network;
using Sandbox.Tasks;
using System;
using System.Collections.Generic;
using System.IO;
namespace Sandbox;
internal partial class GameInstanceDll
{
readonly StringTable CodeArchiveTable = new( "CodeArchive", true );
internal readonly ServerPackages ServerPackages = new();
/// <summary>
/// The config table is used to send config (like physics config, input config) to the client.
/// This isn't always needed, because the config is loaded from the package. But if we're operating
/// without a package, it is needed.
/// </summary>
readonly StringTable ConfigTable = new( "Config", true );
/// <summary>
/// Hold and network any small files such as StyleSheets and compiled prefab assets.
/// </summary>
readonly SmallNetworkFiles NetworkedSmallFiles = new( "SmallFiles" );
/// <summary>
/// Hold and network any files from Project Settings (.config files.)
/// </summary>
readonly SmallNetworkFiles NetworkedConfigFiles = new( "ConfigFiles" );
/// <summary>
/// Hold and network any localization files.
/// </summary>
readonly SmallNetworkFiles NetworkedLangFiles = new( "LangFiles" );
/// <summary>
/// Hold and network any small files such as StyleSheets and compiled prefab assets.
/// </summary>
readonly LargeNetworkFiles NetworkedLargeFiles = new( "LargeFiles" );
/// <summary>
/// Hold and network any small files such as StyleSheets and compiled prefab assets.
/// </summary>
readonly ReplicatedConvars ReplicatedConvars = new( "ReplicatedConvars" );
private List<FileWatch> FileWatchers { get; set; } = new();
private bool DidMountNetworkedFiles { get; set; }
public GameNetworkSystem CreateGameNetworking( NetworkSystem system )
{
var instance = new SceneNetworkSystem( TypeLibrary, system );
NetworkedLargeFiles.NetworkInitialize( instance );
Platform.Chat.NetworkInitialize( instance );
if ( Networking.IsHost )
{
AddFilesToNetwork( NetworkedConfigFiles, EngineFileSystem.ProjectSettings, [".config"] );
AddFilesToNetwork( NetworkedLangFiles, Game.Language.FileSystem, [".json"] );
BuildNetworkedFiles();
}
else if ( !DidMountNetworkedFiles )
{
EngineFileSystem.ProjectSettings.Mount( NetworkedConfigFiles.Files );
Game.Language.FileSystem.Mount( NetworkedLangFiles.Files );
Game.Language.Refresh();
FileSystem.Mounted.Mount( NetworkedLargeFiles.Files );
FileSystem.Mounted.Mount( NetworkedSmallFiles.Files );
NetworkedSmallFiles.Refresh();
NetworkedConfigFiles.Refresh();
NetworkedLangFiles.Refresh();
ResourceLoader.LoadAllGameResource( FileSystem.Mounted, reloadExisting: true );
FontManager.Instance.LoadAll( FileSystem.Mounted );
DidMountNetworkedFiles = true;
}
return instance;
}
public async Task<GameNetworkSystem> CreateGameNetworkingAsync( NetworkSystem system )
{
var instance = new SceneNetworkSystem( TypeLibrary, system );
NetworkedLargeFiles.NetworkInitialize( instance );
Platform.Chat.NetworkInitialize( instance );
if ( Networking.IsHost )
{
AddFilesToNetwork( NetworkedConfigFiles, EngineFileSystem.ProjectSettings, [".config"] );
AddFilesToNetwork( NetworkedLangFiles, Game.Language.FileSystem, [".json"] );
BuildNetworkedFiles();
}
else if ( !DidMountNetworkedFiles )
{
EngineFileSystem.ProjectSettings.Mount( NetworkedConfigFiles.Files );
Game.Language.FileSystem.Mount( NetworkedLangFiles.Files );
Game.Language.Refresh();
FileSystem.Mounted.Mount( NetworkedLargeFiles.Files );
FileSystem.Mounted.Mount( NetworkedSmallFiles.Files );
NetworkedSmallFiles.Refresh();
NetworkedConfigFiles.Refresh();
NetworkedLangFiles.Refresh();
LoadingScreen.Title = "Loading Resources";
await ResourceLoader.LoadAllGameResourceAsync( FileSystem.Mounted, reloadExisting: true );
FontManager.Instance.LoadAll( FileSystem.Mounted );
DidMountNetworkedFiles = true;
}
return instance;
}
void AddFilesToNetwork( SmallNetworkFiles target, BaseFileSystem fs, HashSet<string> validExtensions )
{
var files = fs.FindFile( "/", "*", true );
foreach ( var fileName in files )
{
var extension = Path.GetExtension( fileName );
if ( !validExtensions.Contains( extension ) )
continue;
var text = fs.ReadAllBytes( fileName );
target.AddFile( fs, fileName, text.ToArray() );
}
var watcher = fs.Watch();
watcher.OnChanges += w =>
{
foreach ( var fileName in w.Changes )
{
var extension = Path.GetExtension( fileName );
if ( !validExtensions.Contains( extension ) )
continue;
if ( fs.FileExists( fileName ) )
{
var text = fs.ReadAllBytes( fileName );
target.AddFile( fs, fileName, text.ToArray() );
}
else
{
target.RemoveFile( fileName );
}
}
};
FileWatchers.Add( watcher );
}
/// <summary>
/// This is used to compile code archives that come in from the network.
/// </summary>
CompileGroup compileGroup;
public void InstallNetworkTables( NetworkSystem system )
{
system.InstallTable( CodeArchiveTable );
system.InstallTable( ServerPackages.StringTable );
system.InstallTable( NetworkedSmallFiles.StringTable );
system.InstallTable( NetworkedConfigFiles.StringTable );
system.InstallTable( NetworkedLangFiles.StringTable );
system.InstallTable( NetworkedLargeFiles.StringTable );
system.InstallTable( ReplicatedConvars.StringTable );
CodeArchiveTable.OnChangeOrAdd = ( entry ) =>
{
var codeArchive = new CodeArchive( entry.Data );
var compiler = compileGroup.GetOrCreateCompiler( codeArchive.CompilerName );
compiler.UpdateFromArchive( codeArchive );
};
CodeArchiveTable.PostNetworkUpdate = () => FinishLoadingCodeArchives();
//
// Config
//
system.InstallTable( ConfigTable );
ConfigTable.PostNetworkUpdate = UpdateConfigFromNetworkTable;
}
bool FinishLoadingCodeArchives()
{
if ( !compileGroup.NeedsBuild )
{
FinishLoadingAssemblies();
return true;
}
// We need to build it syncronously because we don't want other
// network shit coming in, that was created using the new assemblies
// and us not being able to understand because we don't have the
// new code compiled and loaded yet!
SyncContext.RunBlocking( compileGroup.BuildAsync() );
if ( !compileGroup.BuildResult.Success )
return false;
//
// Get the new assemblies and update them
//
foreach ( var assm in compileGroup.BuildResult.Output )
{
using var stream = new MemoryStream( assm.AssemblyData );
AssemblyEnroller.LoadAssemblyFromStream( assm.Compiler.AssemblyName, stream );
}
//
// Do the hotload and stuff
//
FinishLoadingAssemblies();
return true;
}
public async Task<bool> LoadNetworkTables( NetworkSystem system )
{
compileGroup = new CompileGroup( "server" );
// Don't hotload while we're downloading stuff!
using var pauseAsmLoadScope = PauseLoadingAssemblies();
// Any assemblies come our way?
foreach ( var entry in CodeArchiveTable.Entries )
{
var codeArchive = new CodeArchive( entry.Value.Data );
var compiler = compileGroup.GetOrCreateCompiler( codeArchive.CompilerName );
compiler.UpdateFromArchive( codeArchive );
}
// We might have loaded new assemblies, here's a safe time to
// hotload before we start downloading again.
if ( !FinishLoadingCodeArchives() )
{
Disconnect( "Failed to compile code archives. Check log for details." );
return false;
}
// Load configs from network tables
UpdateConfigFromNetworkTable();
await ServerPackages.InstallAll();
// Prevent a blank title between the last package install and the download queue start.
if ( string.IsNullOrWhiteSpace( LoadingScreen.Title ) )
LoadingScreen.Title = "Loading..";
await NetworkedLargeFiles.RunDownloadQueue( system, default );
return true;
}
static string[] _interestingExtensions = new[] { "_c", ".scss", ".ttf" };
static string[] _engineAssets = new[] { "vtex_c", "vmat_c", "vsnd_c", "vmdl_c", "vpk", "vanmgrph_c", "shader_c" }; // anything the native engine loads from disk has to be a LARGE download
List<string> _netIncludePaths = new(); // wildcard-supported paths we also want to include content of
// Small files only live in an in-memory filesystem, which native loaders can't read - engine assets must be a real file on disk.
internal static bool ShouldUseLargeDownload( string filename, long size )
=> size >= 1024 * 64 || _engineAssets.Any( x => filename.EndsWith( x ) );
bool ShouldNetworkFile( string filename )
{
filename = filename.NormalizeFilename();
if ( !AssetDownloadCache.IsLegalDownload( filename ) )
return false;
if ( _netIncludePaths.Any( x => filename.WildcardMatch( x ) ) )
return true;
return _interestingExtensions.Any( x => filename.EndsWith( x ) );
}
void UpdateNetworkFile( BaseFileSystem fs, string filename )
{
// ignore code junk
if ( filename.Contains( "\\code\\obj\\", System.StringComparison.OrdinalIgnoreCase ) )
return;
if ( filename.EndsWith( "vmap" ) ) filename = Path.ChangeExtension( filename, ".vpk" );
else if ( !ShouldNetworkFile( filename ) )
{
return;
}
if ( !fs.FileExists( filename ) )
return;
var fullPath = fs.GetFullPath( filename );
var size = fs.FileSize( filename );
if ( !ShouldUseLargeDownload( filename, size ) )
{
var bytes = fs.ReadAllBytes( filename );
var wasAdded = NetworkedSmallFiles.AddFile( fs, filename, bytes.ToArray() );
if ( wasAdded )
{
if ( AssetDownloadCache.DebugNetworkFiles )
Log.Info( $"Adding Small File {filename} ({size.FormatBytes()})" );
}
else
{
Log.Warning( $"File '{filename}' ('{fullPath}') doesn't exist - skipping" );
}
}
else
{
var wasAdded = NetworkedLargeFiles.AddFile( filename );
if ( wasAdded )
{
if ( AssetDownloadCache.DebugNetworkFiles )
Log.Info( $"Adding LARGE File {filename} ({size.FormatBytes()})" );
}
else
{
Log.Warning( $"File '{filename}' ('{fullPath}') doesn't exist - skipping" );
}
}
}
/// <summary>
/// Go through our mounted files and make them available to joining clients for download
/// </summary>
void BuildNetworkedFiles()
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var gameInstance = IGameInstance.Current as GameInstance;
if ( gameInstance is null )
{
Log.Warning( "Game Instance was null when building network files" );
return;
}
// No network files needed for package based games
if ( gameInstance.IsRemote ) return;
if ( AssetDownloadCache.DebugNetworkFiles )
Log.Info( "Building network files.." );
// include anything on resource paths
var project = Project.Current;
if ( project is not null && !string.IsNullOrWhiteSpace( project.Config.Resources ) )
{
var resourcePaths = project.Config.Resources.Split( "\n", StringSplitOptions.RemoveEmptyEntries )
.Select( x => x.Trim() )
.Where( x => !x.StartsWith( "//" ) )
.Select( x => x.NormalizeFilename( true, false ) );
_netIncludePaths.AddRange( resourcePaths );
}
var fs = gameInstance.GameFileSystem;
var files = fs.FindFile( "/", "*", true );
foreach ( var file in files )
{
UpdateNetworkFile( fs, file );
}
var watcher = fs.Watch();
watcher.OnChanges += w =>
{
foreach ( var fileName in w.Changes )
{
UpdateNetworkFile( fs, fileName );
}
};
FileWatchers.Add( watcher );
NetworkTransientGeneratedFiles( project );
if ( AssetDownloadCache.DebugNetworkFiles )
Log.Info( $"..done in {sw.Elapsed.TotalSeconds:0.00}s" );
}
/// <summary>
/// Make runtime-generated assets in the project's .sbox/transient/ folder available to joining clients
/// This is necessary for connected clients to see things like TextureGenerators.
/// </summary>
void NetworkTransientGeneratedFiles( Project project )
{
if ( project is null )
return;
var transientFolder = Path.Combine( project.GetRootPath(), ".sbox", "transient" );
if ( !Directory.Exists( transientFolder ) )
return;
var transientFs = new LocalFileSystem( transientFolder );
foreach ( var file in transientFs.FindFile( "/", "*", true ) )
{
UpdateNetworkFile( transientFs, file );
}
var watcher = transientFs.Watch();
watcher.OnChanges += w =>
{
foreach ( var fileName in w.Changes )
{
UpdateNetworkFile( transientFs, fileName );
}
};
FileWatchers.Add( watcher );
}
}