mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-08-01 08:18:20 -04:00
- Lets you create addons that target a game (your addon project needs ParentGame set, do this in the wizard/project settings) - Code archives can load again if no precompiled dll is found
246 lines
7.6 KiB
C#
246 lines
7.6 KiB
C#
using Sandbox.Engine;
|
|
using System.Diagnostics;
|
|
using System.Threading;
|
|
|
|
namespace Sandbox;
|
|
|
|
internal static class ResourceLoader
|
|
{
|
|
// Native resource extensions (with _c suffix) not covered by AssetTypeAttribute.
|
|
private static readonly HashSet<string> NativeExtensions = new( StringComparer.OrdinalIgnoreCase )
|
|
{
|
|
".vmat_c", ".vmdl_c", ".vtex_c", ".shader_c", ".vanmgrph_c"
|
|
};
|
|
|
|
/// Registers resource paths into PathIndex without loading them, for any file whose
|
|
/// extension is in <paramref name="extensions"/>. Called during LoadAllGameResource.
|
|
private static void RegisterPaths( ReadOnlySpan<string> files, IReadOnlySet<string> extensions )
|
|
{
|
|
foreach ( var file in files )
|
|
{
|
|
if ( !extensions.Contains( System.IO.Path.GetExtension( file ) ) )
|
|
continue;
|
|
|
|
// RegisterPath calls FixPath internally, which strips the _c suffix.
|
|
Game.Resources.RegisterPath( file );
|
|
}
|
|
}
|
|
|
|
internal static void LoadAllGameResource( BaseFileSystem fileSystem, bool reloadExisting = false, Package sourcePackage = null )
|
|
{
|
|
var sw = Stopwatch.StartNew();
|
|
var types = Game.TypeLibrary.GetAttributes<AssetTypeAttribute>().DistinctBy( x => x.Extension )
|
|
.ToDictionary( x => $".{x.Extension}_c", x => x, StringComparer.OrdinalIgnoreCase );
|
|
|
|
var allFiles = fileSystem.FindFile( "/", "*", true ).ToArray();
|
|
|
|
// Union GameResource extensions with native-only ones so PathIndex covers everything.
|
|
var allExtensions = new HashSet<string>( types.Keys, StringComparer.OrdinalIgnoreCase );
|
|
allExtensions.UnionWith( NativeExtensions );
|
|
|
|
RegisterPaths( allFiles, allExtensions );
|
|
|
|
var allResources = new List<GameResource>();
|
|
|
|
foreach ( var file in allFiles )
|
|
{
|
|
var extension = System.IO.Path.GetExtension( file );
|
|
|
|
if ( !types.TryGetValue( extension, out var type ) )
|
|
continue;
|
|
|
|
// Skip resources that are already fully loaded - this allows calling this method
|
|
// multiple times (e.g. once per package) without redundant work.
|
|
if ( !reloadExisting && ResourceLibrary.TryGet<GameResource>( file.Trim( '/' ), out var existing ) && !existing.IsPromise )
|
|
continue;
|
|
|
|
try
|
|
{
|
|
var se = Game.Resources.LoadGameResource( type, file, fileSystem, true, sourcePackage );
|
|
if ( se != null ) allResources.Add( se );
|
|
}
|
|
catch ( Exception ex )
|
|
{
|
|
Log.Warning( ex, $"Exception when trying to load {file}" );
|
|
}
|
|
}
|
|
|
|
//
|
|
// When we're loading a bunch of GameResource we defer their PostLoad until everything is loaded.
|
|
// Everyone is gonna wanna do Resource.Get<>() within their PostLoad and not care about load order.
|
|
// This keeps things intuitive for end users.
|
|
//
|
|
foreach ( var resource in allResources )
|
|
{
|
|
resource.PostLoadInternal();
|
|
}
|
|
|
|
foreach ( var type in types )
|
|
{
|
|
AddWatcherForType( type.Value );
|
|
}
|
|
|
|
// TODO: Check for edited but not saved OR recompiled assets and load in their values on server/client
|
|
// like editing an asset while the gamemode is running would?
|
|
}
|
|
|
|
internal static async Task LoadAllGameResourceAsync( BaseFileSystem fileSystem, CancellationToken ct = default, bool reloadExisting = false, Package sourcePackage = null )
|
|
{
|
|
var sw = Stopwatch.StartNew();
|
|
var types = Game.TypeLibrary.GetAttributes<AssetTypeAttribute>().DistinctBy( x => x.Extension )
|
|
.ToDictionary( x => $".{x.Extension}_c", x => x, StringComparer.OrdinalIgnoreCase );
|
|
|
|
var allExtensions = new HashSet<string>( types.Keys, StringComparer.OrdinalIgnoreCase );
|
|
allExtensions.UnionWith( NativeExtensions );
|
|
|
|
var allFiles = new List<string>();
|
|
foreach ( var file in fileSystem.FindFile( "/", "*", true ) )
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
allFiles.Add( file );
|
|
if ( allExtensions.Contains( System.IO.Path.GetExtension( file ) ) )
|
|
Game.Resources.RegisterPath( file );
|
|
if ( sw.ElapsedMilliseconds > 8 ) { LoadingScreen.Subtitle = System.IO.Path.GetFileName( file ); await Task.Yield(); sw.Restart(); }
|
|
}
|
|
|
|
var allResources = new List<GameResource>();
|
|
|
|
foreach ( var file in allFiles )
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
var extension = System.IO.Path.GetExtension( file );
|
|
if ( !types.TryGetValue( extension, out var type ) ) continue;
|
|
|
|
// Skip resources that are already fully loaded - this allows calling this method
|
|
// multiple times (e.g. once per package) without redundant work.
|
|
if ( !reloadExisting && ResourceLibrary.TryGet<GameResource>( file.Trim( '/' ), out var existing ) && !existing.IsPromise )
|
|
continue;
|
|
|
|
try
|
|
{
|
|
var se = Game.Resources.LoadGameResource( type, file, fileSystem, true, sourcePackage );
|
|
if ( se != null ) allResources.Add( se );
|
|
}
|
|
catch ( Exception ex )
|
|
{
|
|
Log.Warning( ex, $"Exception when trying to load {file}" );
|
|
}
|
|
|
|
if ( sw.ElapsedMilliseconds > 8 ) { LoadingScreen.Subtitle = System.IO.Path.GetFileName( file ); await Task.Yield(); sw.Restart(); }
|
|
}
|
|
|
|
foreach ( var resource in allResources )
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
resource.PostLoadInternal();
|
|
if ( sw.ElapsedMilliseconds > 8 ) { LoadingScreen.Subtitle = System.IO.Path.GetFileName( resource.ResourcePath ); await Task.Yield(); sw.Restart(); }
|
|
}
|
|
|
|
LoadingScreen.Subtitle = null;
|
|
|
|
foreach ( var type in types )
|
|
AddWatcherForType( type.Value );
|
|
}
|
|
|
|
|
|
static Dictionary<string, FileWatch> Watchers = new();
|
|
|
|
static void AddWatcherForType( AssetTypeAttribute type )
|
|
{
|
|
if ( string.IsNullOrEmpty( type.Extension ) )
|
|
return;
|
|
|
|
// Watcher already set up for this type - no need to allocate another one.
|
|
if ( Watchers.ContainsKey( type.TargetType.AssemblyQualifiedName ) )
|
|
return;
|
|
|
|
var watcher = EngineFileSystem.Mounted.Watch( $"*.{type.Extension}_c" );
|
|
watcher.OnChanges += ( w ) => OnAssetFilesChanged( w, type );
|
|
|
|
Watchers[type.TargetType.AssemblyQualifiedName] = watcher;
|
|
}
|
|
|
|
private static void OnAssetFilesChanged( FileWatch watch, AssetTypeAttribute type )
|
|
{
|
|
foreach ( var change in watch.Changes )
|
|
{
|
|
OnAssetFileChanged( change, type );
|
|
}
|
|
}
|
|
|
|
static void OnAssetFileChanged( string file, AssetTypeAttribute type )
|
|
{
|
|
var fs = EngineFileSystem.Mounted;
|
|
|
|
if ( !file.EndsWith( "_c" ) )
|
|
file += "_c";
|
|
|
|
//
|
|
// Asset doesn't exist, maybe just added?
|
|
//
|
|
if ( !ResourceLibrary.TryGet<GameResource>( file.Trim( '/' ), out var asset ) || asset.IsPromise )
|
|
{
|
|
// file wasn't found, so I don't know what was happening.
|
|
if ( !fs.FileExists( file ) )
|
|
return;
|
|
|
|
Log.Info( $"Detected Added File {file}" );
|
|
Game.Resources.LoadGameResource( type, file, fs );
|
|
return;
|
|
}
|
|
|
|
//
|
|
// File was removed, tell the asset system it died
|
|
//
|
|
if ( !fs.FileExists( file ) )
|
|
{
|
|
Log.Info( $"Detected Asset File Deleted {file}" );
|
|
|
|
// Removes from ResourceLibrary
|
|
asset.DestroyInternal();
|
|
return;
|
|
}
|
|
|
|
Span<byte> data = fs.ReadAllBytes( file );
|
|
|
|
if ( data.Length <= 3 )
|
|
{
|
|
Log.Warning( $"Couldn't load json data from {file}" );
|
|
return;
|
|
}
|
|
|
|
bool hasCompiledChanges = asset.TryLoadFromData( data );
|
|
bool externalChanges = false;
|
|
if ( hasCompiledChanges )
|
|
{
|
|
// check for source file changes
|
|
if ( fs.FileExists( asset.ResourcePath ) )
|
|
{
|
|
var jsonBlob = fs.ReadAllText( asset.ResourcePath );
|
|
if ( string.IsNullOrEmpty( jsonBlob ) ) return;
|
|
|
|
var sourceHash = jsonBlob.FastHash();
|
|
if ( sourceHash != asset.LastSavedSourceHash && asset.LastSavedSourceHash != 0 )
|
|
{
|
|
IToolsDll.Current?.RunEvent<ResourceLibrary.IEventListener>( i => i.OnExternalChanges( asset ) );
|
|
externalChanges = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
asset.PostReloadInternal();
|
|
|
|
if ( externalChanges )
|
|
{
|
|
IToolsDll.Current?.RunEvent<ResourceLibrary.IEventListener>( i => i.OnExternalChangesPostLoad( asset ) );
|
|
}
|
|
}
|
|
|
|
internal static void Clear()
|
|
{
|
|
// Dispose of watchers too
|
|
foreach ( var watcher in Watchers ) watcher.Value.Dispose();
|
|
Watchers.Clear();
|
|
}
|
|
}
|