Files
sbox-public/engine/Sandbox.Engine/Game/Mount/MountUtility.cs
Garry Newman 1dfd1de087 Thumb Textures (#3677)
Adds a universal way to get thumbnail images.

- `thumb:entities/sents/npc/scientist.sent` - resources/assets
- `thumb:mount://ns2/ns2/models/effects/exosuit_part1.model.vmdl` -mount files
- `thumb:facepunch.snowman` - packages

This greatly simplifies UI like the spawnmenu that needs to show thumbnails for these things.

We also add `AssetTypeFlags.IncludeThumbnails`. If this is set then when the package is published, any asset types with this flag will include a "[path].c.png" thumbnail image of it.

Also does Api++ protocol increase.
2025-12-27 17:55:57 +00:00

128 lines
2.8 KiB
C#

namespace Sandbox.Mounting;
public static class MountUtility
{
static readonly Dictionary<string, Texture> _cache = new();
static readonly List<RenderJob> _jobs = new();
static readonly HashSet<RenderJob> _activeJobs = new();
/// <summary>
/// Find a ResourceLoader by its mount path.
/// </summary>
public static ResourceLoader FindLoader( string loaderPath )
{
if ( !loaderPath.StartsWith( "mount://" ) ) return null;
var partIndex = loaderPath.IndexOf( '/', 8 );
if ( partIndex <= 8 ) return null;
var mountName = loaderPath[8..partIndex];
var mount = Directory.Get( mountName );
if ( mount is null ) return null;
return mount.GetByPath( loaderPath );
}
/// <summary>
/// Create a preview texture for the given resource loader.
/// </summary>
public static Texture GetPreviewTexture( string loaderPath )
{
var loader = FindLoader( loaderPath );
if ( loader is null ) return null;
return GetPreviewTexture( loader );
}
/// <summary>
/// Create a preview texture for the given resource loader.
/// </summary>
public static Texture GetPreviewTexture( ResourceLoader loader )
{
Assert.NotNull( loader, "loader was null" );
var path = loader.Path;
// In-memory cache
if ( _cache.TryGetValue( path, out var cached ) )
return cached;
// Disk cache
var cacheKey = $"preview/icons/{path.Md5()}.png";
if ( FileSystem.Cache.TryGet( cacheKey, out var data ) )
{
using var bitmap = Bitmap.CreateFromBytes( data );
var tex = bitmap.ToTexture( true );
_cache[path] = tex;
return tex;
}
// Queue render job
var job = new RenderJob( path, cacheKey );
_jobs.Add( job );
_cache[path] = job.Texture;
return job.Texture;
}
internal static void TickPreviewRenders()
{
_activeJobs.RemoveWhere( x => x.IsFinished );
while ( _jobs.Count > 0 && _activeJobs.Count < 2 )
{
var job = _jobs.OrderBy( x => x.Texture.LastUsed ).First();
_jobs.Remove( job );
_activeJobs.Add( job );
job.Start();
}
}
internal static void FlushCache()
{
_jobs.Clear();
_activeJobs.Clear();
_cache.Clear();
}
}
class RenderJob
{
public string Path { get; }
public Texture Texture { get; }
Task _task;
readonly string _cacheKey;
public bool IsFinished => _task is null || _task.IsCompleted;
public RenderJob( string path, string cacheKey )
{
_cacheKey = cacheKey;
Path = path;
using var bitmap = new Bitmap( 256, 256 );
bitmap.Clear( Color.Transparent );
Texture = bitmap.ToTexture( true );
}
public void Start()
{
_task = Run();
}
public async Task Run()
{
var modelResource = await Model.LoadAsync( Path );
if ( !modelResource.IsValid() ) return;
using var bitmap = new Bitmap( 512, 512 );
SceneUtility.RenderModelBitmap( modelResource, bitmap );
using var resized = bitmap.Resize( 256, 256 );
Texture.Update( resized );
FileSystem.Cache.Set( _cacheKey, resized.ToPng() );
}
}