using Sandbox.Internal; using Sandbox.Network; using System.Threading; namespace Sandbox; internal class LargeNetworkFiles { public StringTable StringTable { get; init; } record struct LargeFileInfo( long Size, ulong CRC ); RedirectFileSystem RedirectFileSystem { get; set; } HashSet downloadQueue = new(); public LargeNetworkFiles( string name ) { StringTable = new( name, true ); StringTable.OnChangeOrAdd += OnTableEntryUpdated; StringTable.OnRemoved += OnTableEntryRemoved; StringTable.OnSnapshot += OnTableSnapshot; } /// /// Reset the string table. /// public void Reset() { StringTable.Reset(); RedirectFileSystem?.Dispose(); RedirectFileSystem = AssetDownloadCache.CreateRedirectFileSystem(); } /// /// Add all files from the network. /// public void Refresh() { foreach ( var (_, entry) in StringTable.Entries ) { AddFileToFileSystem( entry.Name, entry.Read() ); } } /// /// Add a file to be networked. /// public bool AddFile( string fileName ) { if ( !EngineFileSystem.Mounted.FileExists( fileName ) ) return false; var crc = EngineFileSystem.Mounted.GetCrc( fileName ); var size = EngineFileSystem.Mounted.FileSize( fileName ); var normalizedFileName = NormalizeFileName( fileName ); StringTable.Set( normalizedFileName, new LargeFileInfo( size, crc ) ); return true; } /// /// Remove a networked file. /// public void RemoveFile( string fileName ) { var normalizedFileName = NormalizeFileName( fileName ); StringTable.Remove( normalizedFileName ); } string NormalizeFileName( string fileName ) { return BaseFileSystem.NormalizeFilename( fileName ).TrimStart( '/' ); } void OnTableEntryUpdated( StringTable.Entry entry ) { AddFileToFileSystem( entry.Name, entry.Read() ); } void OnTableEntryRemoved( StringTable.Entry entry ) { } void OnTableSnapshot() { Log.Info( "Checking for network files.." ); var sw = System.Diagnostics.Stopwatch.StartNew(); Refresh(); Log.Info( $"..done in {sw.Elapsed.TotalSeconds:0.00}s" ); } void AddFileToFileSystem( string fileName, LargeFileInfo contents ) { // Can we find this file somewhere, or do we need to download it? if ( EngineFileSystem.Mounted.FileExists( fileName ) ) { var size = EngineFileSystem.Mounted.FileSize( fileName ); if ( size == contents.Size ) { var crc = EngineFileSystem.Mounted.GetCrc( fileName ); if ( crc == contents.CRC ) { if ( AssetDownloadCache.DebugNetworkFiles ) { Log.Info( $"Skipping downloading {fileName} - we already have it" ); } return; } } } if ( !AssetDownloadCache.IsLegalDownload( fileName ) ) return; if ( AssetDownloadCache.TryMount( RedirectFileSystem, fileName, contents.CRC ) ) return; if ( AssetDownloadCache.DebugNetworkFiles ) { Log.Info( $"Queued Network File: {fileName} / {contents.Size} / {contents.CRC}" ); } downloadQueue.Add( fileName ); } public async Task RunDownloadQueue( NetworkSystem system, CancellationToken token ) { if ( RedirectFileSystem is null ) return; Assert.NotNull( Connection.Host ); Log.Info( $"Downloading {downloadQueue.Count} files.." ); var currentCount = 0; var sw = System.Diagnostics.Stopwatch.StartNew(); foreach ( var file in downloadQueue ) { if ( !StringTable.Entries.TryGetValue( file, out var entry ) ) continue; var info = entry.Read(); if ( AssetDownloadCache.DebugNetworkFiles ) { Log.Info( $"Download file {file}" ); } system.UpdateLoading( $"Download file ({currentCount + 1}/{downloadQueue.Count}) {file}" ); if ( RedirectFileSystem.FileExists( file.NormalizeFilename( true ) ) ) { currentCount++; continue; } token.ThrowIfCancellationRequested(); if ( Connection.Host is null ) { throw new TaskCanceledException( "Connection became null" ); } // download the file var response = await Connection.Host.SendRequest( new RequestFile { filename = file } ); token.ThrowIfCancellationRequested(); if ( response is not byte[] data ) { Log.Warning( $"Unknown response {response}!" ); currentCount++; continue; } var fn = AssetDownloadCache.StoreFile( file, info.CRC, data ); if ( fn is not null ) { RedirectFileSystem.AddAbsFile( file, fn ); } currentCount++; } downloadQueue.Clear(); Log.Info( $"Download Complete ({downloadQueue.Count()} files total) ({sw.Elapsed.TotalSeconds:0.00}s)" ); } internal void NetworkInitialize( GameNetworkSystem instance ) { instance.AddHandler( OnRequestNetworkFile ); } async Task OnRequestNetworkFile( RequestFile file, Connection connection, Guid msgGuid ) { Log.Info( $"Client requested file {file.filename} ({msgGuid})" ); await Task.Delay( 100 ); var contents = await EngineFileSystem.Mounted.ReadAllBytesAsync( file.filename ); Log.Info( $"Read {contents.Length.FormatBytes()}" ); connection.SendResponse( msgGuid, contents ); } [Expose] public record struct RequestFile( string filename ); }