mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-06 13:28:32 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
508 lines
14 KiB
C#
508 lines
14 KiB
C#
using NativeEngine;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.ComponentModel;
|
|
|
|
namespace Editor;
|
|
|
|
/// <summary>
|
|
/// The asset system, provides access to all the assets.
|
|
/// </summary>
|
|
public static partial class AssetSystem
|
|
{
|
|
static Logger log = new Logger( "AssetSystem" );
|
|
|
|
[SkipHotload]
|
|
static Dictionary<ulong, Asset> allAssets = new();
|
|
|
|
[SkipHotload] static ConcurrentDictionary<string, Asset> assetsByPath = new( StringComparer.OrdinalIgnoreCase );
|
|
|
|
/// <summary>
|
|
/// All the assets that are being tracked by the asset system. Does not include deleted assets.
|
|
/// </summary>
|
|
public static IEnumerable<Asset> All => allAssets.Values.Where( x => !x.IsDeleted );
|
|
static bool HasChanges;
|
|
static bool IsInitialized = false;
|
|
|
|
static HashSet<Asset> UpdateQueue = new();
|
|
|
|
/// <summary>
|
|
/// Called after the asset types have been loaded from
|
|
/// </summary>
|
|
internal static void PreInitialize()
|
|
{
|
|
log.Trace( "PreInitialize" );
|
|
|
|
foreach ( var type in AssetType.AssetTypeCache )
|
|
{
|
|
type.Value.Init();
|
|
}
|
|
}
|
|
|
|
internal static void InitializeFromProject( Project project )
|
|
{
|
|
string path = System.IO.Path.Combine( project.GetRootPath(), ".sbox", "cloud.db" );
|
|
CloudDirectory = new CloudAssetDirectory( path );
|
|
|
|
HasChanges = true;
|
|
IsInitialized = true;
|
|
Tick();
|
|
}
|
|
|
|
internal static void Shutdown()
|
|
{
|
|
CloudDirectory?.Dispose();
|
|
CloudDirectory = null;
|
|
}
|
|
|
|
internal static void AssetAdded( IAsset native )
|
|
{
|
|
// log.Trace( $"Asset Added: {native.GetAbsolutePath_Transient()}" );
|
|
|
|
var a = new NativeAsset( native );
|
|
|
|
allAssets[a.AssetId] = a;
|
|
UpdateQueue.Add( a );
|
|
HasChanges = true;
|
|
}
|
|
|
|
|
|
static void UpdateAsset( Asset asset, bool compileImmediately = true )
|
|
{
|
|
asset.UpdateInternals( compileImmediately );
|
|
|
|
if ( string.IsNullOrWhiteSpace( asset.Path ) )
|
|
return;
|
|
|
|
assetsByPath[asset.Path] = asset;
|
|
assetsByPath[asset.AbsolutePath] = asset;
|
|
assetsByPath[asset.RelativePath] = asset;
|
|
assetsByPath[asset.AbsoluteCompiledPath] = asset;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// This is only called on startup. The cache is loaded, so a bunch of assets are known,
|
|
/// then it does a bit of research and sees that the asset is removed, and it can remove it now.
|
|
/// This is the only point where an Asset is actually destroyed, during the mainloop the asset
|
|
/// is just marked as deleted but never destroyed.
|
|
/// </summary>
|
|
internal static void AssetRemoved( uint index )
|
|
{
|
|
var a = allAssets[index];
|
|
Assert.NotNull( a );
|
|
|
|
log.Trace( $"Removed: {a}" );
|
|
|
|
if ( a.Path is not null ) assetsByPath.TryRemove( a.Path, out _ );
|
|
if ( a.AbsolutePath is not null ) assetsByPath.TryRemove( a.AbsolutePath, out _ );
|
|
if ( a.RelativePath is not null ) assetsByPath.TryRemove( a.RelativePath, out _ );
|
|
if ( a.AbsoluteCompiledPath is not null ) assetsByPath.TryRemove( a.AbsoluteCompiledPath, out _ );
|
|
|
|
// Can it ever come back??
|
|
UpdateQueue.Remove( a );
|
|
allAssets.Remove( index );
|
|
a.OnRemoved();
|
|
|
|
}
|
|
|
|
internal static void RecordAssetOpen( uint index )
|
|
{
|
|
var a = allAssets[index];
|
|
Assert.NotNull( a );
|
|
|
|
a.LastOpened = DateTime.Now;
|
|
|
|
}
|
|
|
|
internal static void AssetChanged( uint index )
|
|
{
|
|
var asset = allAssets[index];
|
|
UpdateAsset( asset );
|
|
|
|
if ( UpdateQueue.Add( asset ) )
|
|
{
|
|
NativeAssetProcessor.OnAssetChanged( asset );
|
|
|
|
HasChanges = true;
|
|
}
|
|
}
|
|
|
|
internal static void UpdateAssetAutoTags( uint index )
|
|
{
|
|
allAssets[index].UpdateAutoTags();
|
|
}
|
|
|
|
internal static void AssetScanComplete()
|
|
{
|
|
foreach ( var type in AssetType.AssetTypeCache )
|
|
{
|
|
type.Value.Init();
|
|
}
|
|
|
|
HasChanges = true;
|
|
Tick();
|
|
DeleteOrphans();
|
|
}
|
|
|
|
[EditorEvent.Frame]
|
|
internal static void Tick()
|
|
{
|
|
if ( !HasChanges || !IsInitialized ) return;
|
|
HasChanges = false;
|
|
|
|
// Everything below is safe to do in parallel as long as our dependency info is up to date
|
|
// Which needs to be done on the main thread
|
|
foreach ( var asset in UpdateQueue )
|
|
{
|
|
if ( asset is NativeAsset nativeAsset )
|
|
{
|
|
nativeAsset.native.RequireDependencyInfo_Virtual();
|
|
}
|
|
}
|
|
|
|
Parallel.ForEach( UpdateQueue, ( Asset asset ) => UpdateAsset( asset, false ) );
|
|
|
|
// Thumbnails have their own queue, so do that all in main
|
|
foreach ( var asset in UpdateQueue )
|
|
{
|
|
EditorEvent.RunInterface<IEventListener>( x => x.OnAssetChanged( asset ) );
|
|
|
|
if ( asset.HasCachedThumbnail )
|
|
{
|
|
asset.RebuildThumbnail( true );
|
|
}
|
|
|
|
if ( !asset.IsCompiled && asset.AssetType.IsGameResource )
|
|
{
|
|
asset.Compile( false );
|
|
}
|
|
}
|
|
|
|
UpdateQueue.Clear();
|
|
|
|
EditorEvent.RunInterface<IEventListener>( x => x.OnAssetSystemChanges() );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find an asset by path.
|
|
/// </summary>
|
|
/// <param name="path">The file path to an asset. Can be absolute or relative.</param>
|
|
public static Asset FindByPath( string path )
|
|
{
|
|
if ( string.IsNullOrWhiteSpace( path ) )
|
|
return null;
|
|
|
|
path = path.Replace( '\\', '/' );
|
|
path = path.TrimStart( '/' );
|
|
|
|
if ( assetsByPath.TryGetValue( path, out var asset ) && !asset.IsDeleted )
|
|
{
|
|
return asset;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
internal static Asset Get( IAsset asset )
|
|
{
|
|
return allAssets[asset.GetAssetIndexInt()];
|
|
}
|
|
|
|
internal static Asset Get( uint index )
|
|
{
|
|
return allAssets[index];
|
|
}
|
|
|
|
internal static void RegisterAssetType( int id, IAssetType assetType )
|
|
{
|
|
var color = Color.Parse( assetType.GetColor() );
|
|
|
|
var at = new AssetType
|
|
{
|
|
FriendlyName = assetType.GetFriendlyName(),
|
|
FileExtension = assetType.GetPrimaryExtension(),
|
|
HiddenByDefault = assetType.HideTypeByDefault(),
|
|
PrefersIconThumbnail = assetType.PrefersIconForThumbnail(),
|
|
IconPathSmall = assetType.GetIconSm(),
|
|
IconPathLarge = assetType.GetIconLg(),
|
|
IsSimpleAsset = assetType.IsSimpleAsset(),
|
|
HasDependencies = assetType.HasDependencies(),
|
|
Category = assetType.GetCategory(),
|
|
Color = color ?? Color.Gray
|
|
};
|
|
|
|
at.AllFileExtensions.Add( at.FileExtension );
|
|
|
|
var additionalExtensions = CUtlVectorString.Create( 8, 8 );
|
|
assetType.GetAdditionalExtensions( additionalExtensions );
|
|
|
|
for ( var i = 0; i < additionalExtensions.Count(); ++i )
|
|
{
|
|
at.AllFileExtensions.Add( additionalExtensions.Element( i ) );
|
|
}
|
|
|
|
additionalExtensions.DeleteThis();
|
|
|
|
AssetType.AssetTypeCache[id] = at;
|
|
|
|
at.Init();
|
|
}
|
|
|
|
/// <summary>
|
|
/// If you just created an asset, you probably want to immediately register it
|
|
/// </summary>
|
|
public static Asset RegisterFile( string absoluteFilePath )
|
|
{
|
|
ThreadSafe.AssertIsMainThread();
|
|
|
|
var asset = IAssetSystem.RegisterAssetFile( absoluteFilePath );
|
|
if ( !asset.IsValid ) return null;
|
|
|
|
var ret = Get( asset.GetAssetIndexInt() );
|
|
|
|
// Make sure the ResoureLibrary has proper loaded version, so that properties that target
|
|
// GameResources do not break child assets if child asset was not loaded with Asset.CreateUI beforehand.
|
|
ret.TryLoadGameResource( typeof( GameResource ), out _, true );
|
|
|
|
return ret;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delete orphaned trivial children. These are things that are generated for
|
|
/// usage by an asset, but aren't referenced by anything, so are useless.
|
|
/// </summary>
|
|
public static void DeleteOrphans()
|
|
{
|
|
var orphans = All
|
|
.Where( x => x.IsTrivialChild )
|
|
.Where( x => !x.IsDeleted )
|
|
.Where( x => x.AssetType == AssetType.Texture ) // Note - gib models can be trivial children too, but I don't want to push my luck
|
|
.Where( x => x.GetDependants( false ).Count == 0 )
|
|
.ToArray();
|
|
|
|
foreach ( var o in orphans )
|
|
{
|
|
log.Trace( $"Deleting Orphan \"{o}\"" );
|
|
|
|
o.Delete();
|
|
UpdateQueue.Add( o );
|
|
HasChanges = true;
|
|
}
|
|
}
|
|
|
|
internal static void InitializeCompilerForFilename( IResourceCompilerContext context, string filename )
|
|
{
|
|
var ext = System.IO.Path.GetExtension( filename )[1..].ToLowerInvariant();
|
|
var assetType = AssetType.FromExtension( ext );
|
|
|
|
if ( assetType is null )
|
|
return;
|
|
|
|
if ( assetType.IsGameResource == false )
|
|
return;
|
|
|
|
context.SetCompiler( $"ManagedResourceCompiler" );
|
|
context.SetExtension( ext );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create an empty <see cref="GameResource"/>.
|
|
/// </summary>
|
|
/// <param name="type">Asset type extension for our new <see cref="GameResource"/> instance.</param>
|
|
/// <param name="absoluteFilename">Where to save the new <see cref="GameResource"/> instance. For example from <see cref="FileDialog"/>.</param>
|
|
/// <returns>The new asset, or null if creation failed.</returns>
|
|
public static Asset CreateResource( string type, string absoluteFilename )
|
|
{
|
|
var gameResourceType = EditorTypeLibrary.GetAttributes<AssetTypeAttribute>().Where( x => x.Extension == type ).FirstOrDefault();
|
|
if ( gameResourceType is null )
|
|
{
|
|
Log.Warning( $"Couldn't find matching resource type for extension {type}" );
|
|
return null;
|
|
}
|
|
|
|
var extension = gameResourceType.Extension;
|
|
absoluteFilename = System.IO.Path.ChangeExtension( absoluteFilename, extension );
|
|
|
|
// try to find it first. If we find it, return it.
|
|
var found = AssetSystem.FindByPath( absoluteFilename );
|
|
if ( found is not null ) return found;
|
|
|
|
// create an empty file
|
|
System.IO.File.WriteAllText( absoluteFilename, "{}" );
|
|
|
|
// convert it to an .asset
|
|
var asset = RegisterFile( absoluteFilename );
|
|
if ( asset == null )
|
|
{
|
|
log.Warning( $"Something went wrong when registering {absoluteFilename}" );
|
|
return null;
|
|
}
|
|
|
|
//
|
|
// Load and save it, to create an empty version of it
|
|
//
|
|
if ( asset.TryLoadGameResource( typeof( GameResource ), out var obj, true ) )
|
|
{
|
|
asset.SaveToDisk( obj );
|
|
}
|
|
|
|
return asset;
|
|
}
|
|
|
|
internal static void OnSoundReload( string filename )
|
|
{
|
|
if ( SoundFile.Loaded.TryGetValue( filename, out var soundFile ) )
|
|
{
|
|
soundFile.OnReloadInternal();
|
|
}
|
|
}
|
|
|
|
internal static void OnSoundReloaded( string filename )
|
|
{
|
|
if ( SoundFile.Loaded.TryGetValue( filename, out var soundFile ) )
|
|
{
|
|
soundFile.OnReloadedInternal();
|
|
}
|
|
}
|
|
|
|
internal static void OnDemandRecompile( uint index, string reason )
|
|
{
|
|
// matt: disabling this for now because it's causing re-entrants in CResourceSystem::ForceSynchronizationAndBlockUntilManifestLoaded()
|
|
// which was making the entire resource system shit the bed
|
|
// specifically I was seeing this because Qt events run CHammerEditorSession::RenderView but I imagine it can happen
|
|
// in a shit load of places... layla said they'd got this in-game too but I'm not sure if it's the same thing
|
|
// IToolsDll.Current?.Spin();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Passed parameters for the AssetPicker going from engine to addon code
|
|
/// </summary>
|
|
[EditorBrowsable( EditorBrowsableState.Never )]
|
|
public struct AssetPickerParameters
|
|
{
|
|
public Widget ParentWidget { get; init; }
|
|
public List<AssetType> FilterAssetTypes { get; init; }
|
|
public Action<Asset[]> AssetSelectedCallback { get; init; }
|
|
public int ViewMode { get; init; }
|
|
public Asset InitialSelectedAsset { get; init; }
|
|
public string Title { get; init; }
|
|
public bool ShowCloudAssets { get; init; }
|
|
public string InitialSearchText { get; init; }
|
|
}
|
|
|
|
internal static void PopulateAssetMenu( Native.QMenu qMenu, IAsset asset )
|
|
{
|
|
var menu = new Menu( qMenu );
|
|
EditorEvent.Run( "asset.nativecontextmenu", menu, AssetSystem.Get( asset ) );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called from native to open our managed AssetPicker
|
|
/// </summary>
|
|
internal static void OpenPicker( Native.QWidget parentWidget, CUtlVectorAssetType assetTypes, IAssetPickerListener listener, int viewmode, IAsset selectedAsset, string titleAndSettingsName, bool cloudAllowed, string initialSearchText )
|
|
{
|
|
List<AssetType> filterAssetTypes = new();
|
|
|
|
if ( assetTypes.IsValid )
|
|
{
|
|
for ( int i = 0; i < assetTypes.Count(); i++ )
|
|
{
|
|
filterAssetTypes.Add( AssetType.Find( assetTypes.Element( i ).GetPrimaryExtension() ) );
|
|
}
|
|
}
|
|
|
|
Action<Asset[]> assetsPicked = ( Asset[] assets ) =>
|
|
{
|
|
if ( !listener.IsValid ) return;
|
|
|
|
var picked = AssetPickedWrapper.Create();
|
|
foreach ( var asset in assets )
|
|
{
|
|
if ( asset is NativeAsset nativeAsset )
|
|
{
|
|
picked.AddAsset( nativeAsset.native );
|
|
}
|
|
}
|
|
listener.NotifyAssetPicked( picked );
|
|
picked.DeleteThis();
|
|
};
|
|
|
|
AssetPickerParameters parameters = new()
|
|
{
|
|
ParentWidget = parentWidget.IsValid ? new Widget( parentWidget ) : null,
|
|
InitialSelectedAsset = selectedAsset.IsValid ? AssetSystem.Get( selectedAsset ) : null,
|
|
FilterAssetTypes = filterAssetTypes,
|
|
AssetSelectedCallback = assetsPicked,
|
|
ViewMode = viewmode,
|
|
Title = titleAndSettingsName,
|
|
ShowCloudAssets = true,
|
|
InitialSearchText = initialSearchText
|
|
};
|
|
|
|
EditorEvent.Run( "assetsystem.openpicker", parameters );
|
|
}
|
|
|
|
static ulong _freeIndex = uint.MaxValue;
|
|
|
|
internal static void AddAssetsFromMount( Sandbox.Mounting.BaseGameMount source )
|
|
{
|
|
foreach ( var file in source.Resources )
|
|
{
|
|
var index = _freeIndex++;
|
|
|
|
var assetType = AssetType.ResolveFromPath( file.Path );
|
|
|
|
if ( assetType == null )
|
|
{
|
|
//Log.Warning( $"assetType was null for {file.Path}" );
|
|
continue;
|
|
}
|
|
|
|
var asset = new MountAsset( index, file, source );
|
|
|
|
allAssets[asset.AssetId] = asset;
|
|
UpdateQueue.Add( asset );
|
|
HasChanges = true;
|
|
}
|
|
|
|
Tick();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Callbacks for the asset system. Add this interface to your Widget to get events.
|
|
/// </summary>
|
|
public interface IEventListener
|
|
{
|
|
/// <summary>
|
|
/// An asset has been modified
|
|
/// </summary>
|
|
void OnAssetChanged( Asset asset ) { }
|
|
|
|
/// <summary>
|
|
/// The thumbnail for an asset has been updated
|
|
/// </summary>
|
|
void OnAssetThumbGenerated( Asset asset ) { }
|
|
|
|
/// <summary>
|
|
/// Changes have been detected in the asset system. We won't tell you what, but
|
|
/// you probably need to update the asset list or something.
|
|
/// </summary>
|
|
void OnAssetSystemChanges() { }
|
|
|
|
/// <summary>
|
|
/// Called when a new tag has been added to the asset system.
|
|
/// </summary>
|
|
void OnAssetTagsChanged() { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create an Asset from a serialized property. This is expected to be an embedded asset property.
|
|
/// </summary>
|
|
public static Asset CreateEmbeddedAsset( SerializedProperty target )
|
|
{
|
|
return new EmbeddedAsset( target );
|
|
}
|
|
}
|
|
|