Editor/WeaponImporter/Tool/Engine/StockAnimations.cs
using System.IO.Compression;
using Editor;
using Sandbox;
using WeaponImporter.Core.Setup;
namespace WeaponImporter.Tool;
/// <summary>
/// The optional stock third-person animations (item use, throws, dual pistols, melee stances
/// and attacks, fists, hold poses), retargeted onto the s&box human. They are not part of
/// the library: the Download button fetches them from the GitHub release as a zip holding the
/// compiled model and unpacks it into the project's Assets, where the importer finds it.
/// </summary>
public static class StockAnimations
{
public const string Version = "1";
public const string Url = "https://github.com/zeljkovranjes/sbox-weapon-importer/releases/download/stock-animations-v" + Version + "/weapon_importer_stock_animations.zip";
/// <summary>Project-relative folder the zip unpacks into.</summary>
public const string Folder = "weapon_importer/stock";
/// <summary>The model holding every stock sequence.</summary>
public const string ModelPath = StockThirdPerson.Model;
private static IReadOnlyList<string> _sequences;
private static string _sequencesFor;
/// <summary>Whether the stock animations are in this project (the compiled model, or its source).</summary>
public static bool Installed
{
get
{
var root = AssetCompiler.AssetsRoot;
if ( root.Length == 0 )
return false;
var path = AssetCompiler.Absolute( ModelPath );
return System.IO.File.Exists( path + "_c" ) || System.IO.File.Exists( path );
}
}
/// <summary>The stock model's sequences, or none when not installed.</summary>
public static IReadOnlyList<string> Sequences
{
get
{
if ( !Installed )
return Array.Empty<string>();
var root = AssetCompiler.AssetsRoot;
if ( _sequences is { Count: > 0 } && _sequencesFor == root )
return _sequences;
var model = Model.Load( ModelPath );
if ( model is null || model.IsError )
return Array.Empty<string>();
_sequences = model.AnimationNames.Where( n => n.StartsWith( "wi_", StringComparison.Ordinal ) ).ToList();
_sequencesFor = root;
return _sequences;
}
}
/// <summary>Downloads the zip from the release and unpacks it into Assets. Returns an error, or null.</summary>
public static async Task<string> InstallAsync( IProgress<string> progress = null, CancellationToken cancel = default )
{
var root = AssetCompiler.AssetsRoot;
if ( root.Length == 0 )
return "No project is open.";
progress?.Report( "Downloading stock animations" );
byte[] bytes;
try
{
bytes = await Http.RequestBytesAsync( Url, cancellationToken: cancel );
}
catch ( Exception e ) when ( e is not OperationCanceledException )
{
return $"Download failed: {e.Message}";
}
cancel.ThrowIfCancellationRequested();
progress?.Report( "Unpacking stock animations" );
var written = new List<string>();
try
{
using var zip = new ZipArchive( new System.IO.MemoryStream( bytes ), ZipArchiveMode.Read );
var rootFull = System.IO.Path.GetFullPath( root );
var allowed = System.IO.Path.GetFullPath( System.IO.Path.Combine( rootFull, Folder ) ) + System.IO.Path.DirectorySeparatorChar;
foreach ( var entry in zip.Entries )
{
if ( entry.FullName.EndsWith( "/" ) )
continue;
// Only inside Assets/weapon_importer/stock: a zip can't write anywhere else.
var target = System.IO.Path.GetFullPath( System.IO.Path.Combine( rootFull, entry.FullName ) );
if ( !target.StartsWith( allowed, StringComparison.OrdinalIgnoreCase ) )
continue;
System.IO.Directory.CreateDirectory( System.IO.Path.GetDirectoryName( target ) );
using var input = entry.Open();
using var output = System.IO.File.Create( target );
await input.CopyToAsync( output, cancel );
written.Add( target );
}
}
catch ( Exception e ) when ( e is System.IO.InvalidDataException or System.IO.IOException )
{
return $"The download is not a valid stock animation zip: {e.Message}";
}
var compiled = AssetCompiler.Absolute( ModelPath ) + "_c";
if ( !written.Any( w => string.Equals( w, compiled, StringComparison.OrdinalIgnoreCase ) ) )
return "The zip holds no stock model.";
await EditorThread.SwitchToMainThread();
foreach ( var file in written )
AssetSystem.RegisterFile( file );
_sequences = null;
// The model is ready once it loads with its sequences.
var model = await AssetCompiler.LoadModelAsync( ModelPath, new[] { "wi_hold_object" } );
if ( model is null || model.IsError || Sequences.Count == 0 )
return "The stock animations were unpacked but the model did not load.";
progress?.Report( $"Stock animations installed ({Sequences.Count} animations)" );
return null;
}
}