Editor/HumanoidRetargeter/CompiledAssetRecovery.cs
#nullable enable
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Threading;
using HumanoidRetargeter.Target;
using HumanoidRetargeterVrf;
using HumanoidRetargeterVrf.IO;
namespace HumanoidRetargeter.Editor;
/// <summary>Engine-independent bridge to the embedded, extraction-only VRF source.</summary>
internal static class CompiledAssetRecovery
{
internal static bool IsDependency( string path ) => Path.GetExtension( path ).ToLowerInvariant()
is ".vmdl" or ".vmesh" or ".vmat" or ".vphys" or ".vagrp" or ".vanim" or ".vanmgrph";
internal static string Recover( string input, string assetPath, string root, string assetFolder,
IReadOnlyDictionary<string, string> paths, CancellationToken token, SmartPortRig? rig = null )
{
token.ThrowIfCancellationRequested();
using var resource = new Resource { FileName = assetPath };
resource.Read( input );
if ( assetPath.EndsWith( ".vanmgrph", StringComparison.OrdinalIgnoreCase ) )
return resource.DataBlock?.ToString() ?? throw new InvalidDataException( "Missing animgraph data." );
if ( resource.DataBlock is not HumanoidRetargeterVrf.ResourceTypes.Model )
throw new InvalidDataException( "Smart Port recovery requires a model or animgraph." );
using var loader = new Loader( paths, token );
var exporter = rig is null ? null : new SmartPortAnimationExport( rig );
using var content = new ModelExtract( resource, loader ).ToContentFile( exporter is null ? null : exporter.Write );
var text = Encoding.UTF8.GetString( content.Data );
var rebase = new Dictionary<string, string>( StringComparer.OrdinalIgnoreCase );
var modelDirectory = Path.GetDirectoryName( content.FileName )?.Replace( '\\', '/' ) ?? "";
foreach ( var file in content.SubFiles )
{
token.ThrowIfCancellationRequested();
var relative = (modelDirectory + "/" + file.FileName).TrimStart( '/' );
var output = ConfinedPath( root, relative );
if ( !rebase.TryAdd( relative, assetFolder + "/" + relative ) )
throw new InvalidDataException( "Recovery produced duplicate files: " + relative );
Directory.CreateDirectory( Path.GetDirectoryName( output )! );
using var stream = new FileStream( output, FileMode.CreateNew, FileAccess.Write );
stream.Write( file.Extract() );
}
return SmartPortSetup.Rebase( text, rebase );
}
internal static string ConfinedPath( string root, string relative )
{
foreach ( var part in relative.Split( '/', '\\' ) )
if ( part.IndexOfAny( Path.GetInvalidFileNameChars() ) >= 0 )
throw new InvalidDataException( "Invalid recovered filename." );
var fullRoot = Path.GetFullPath( root ).TrimEnd( Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar ) + Path.DirectorySeparatorChar;
var full = Path.GetFullPath( Path.Combine( fullRoot, relative ) );
if ( Path.IsPathRooted( relative ) || !full.StartsWith( fullRoot, StringComparison.OrdinalIgnoreCase ) )
throw new InvalidDataException( "Recovered file escapes its output folder." );
return full;
}
sealed class Loader( IReadOnlyDictionary<string, string> paths, CancellationToken token ) : IFileLoader, IDisposable
{
readonly List<Resource> opened = new();
public Resource LoadFileCompiled( string file ) => LoadFile( file );
public Resource LoadFile( string file )
{
token.ThrowIfCancellationRequested();
file = file.Replace( '\\', '/' );
if ( file.EndsWith( "_c", StringComparison.OrdinalIgnoreCase ) ) file = file[..^2];
if ( !paths.TryGetValue( file, out var physical ) || string.IsNullOrEmpty( physical ) || !File.Exists( physical ) )
throw new FileNotFoundException( "Required Smart Port dependency is unavailable: " + file, file );
var resource = new Resource { FileName = file };
opened.Add( resource );
resource.Read( physical );
return resource;
}
public void Dispose() { foreach ( var resource in opened ) resource.Dispose(); }
}
}