mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-08-01 00:08:05 -04:00
420 lines
12 KiB
C#
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 );
|
|
}
|
|
}
|