Code/WeaponImporter/CharacterOverlay.cs
using System.Text.Json;
namespace WeaponImporter;
/// <summary>A replacement character animation for one action.</summary>
public sealed class OverlayInfo
{
public string Role;
/// <summary>Model that owns the sequence ("" = the character's own model). Must share the character's skeleton.</summary>
public string Model;
public string Sequence;
/// <summary>Fade in/out time in seconds.</summary>
public float Blend = 0.2f;
}
/// <summary>
/// Plays a replacement character sequence for an action on a hidden model with the same
/// skeleton and blends it over the upper body (spine and everything above), so the weapon and
/// the hands follow it while the animgraph keeps driving the legs.
/// </summary>
internal sealed class CharacterOverlay
{
private SceneModel _model;
private Model _source;
private string _sequence;
private readonly Dictionary<string, int> _boneIndex = new( StringComparer.Ordinal );
public bool Active { get; private set; }
public float Weight { get; private set; }
public float Duration { get; private set; }
/// <summary>Starts the overlay; returns false when the model or sequence is unusable.</summary>
public bool Begin( SkinnedModelRenderer body, OverlayInfo info, out string error )
{
error = null;
Active = false;
Weight = 0f;
if ( info == null || string.IsNullOrEmpty( info.Sequence ) || body?.SceneModel == null )
return false;
var model = string.IsNullOrEmpty( info.Model ) ? body.Model : Sandbox.Model.Load( info.Model );
if ( model == null || model.IsError )
{
error = $"model '{info.Model}' could not be loaded";
return false;
}
if ( !model.AnimationNames.Contains( info.Sequence ) )
{
error = $"model '{model.Name}' has no sequence '{info.Sequence}'";
return false;
}
if ( _model == null || !_model.IsValid() || _source != model )
{
Dispose();
_model = new SceneModel( body.SceneModel.World, model, body.SceneModel.Transform )
{
UseAnimGraph = false,
RenderingEnabled = false,
};
_source = model;
_boneIndex.Clear();
for ( int i = 0; i < model.BoneCount; i++ )
_boneIndex[model.GetBoneName( i )] = i;
}
_sequence = info.Sequence;
_model.CurrentSequence.Name = _sequence;
_model.Update( 0f );
Duration = _model.CurrentSequence.Duration;
Active = true;
return true;
}
/// <summary>Poses the overlay at <paramref name="seconds"/> into the action with the given blend weight.</summary>
public void Update( SkinnedModelRenderer body, float seconds, float weight )
{
if ( !Active || _model == null || !_model.IsValid() )
return;
_model.Transform = body.SceneModel.Transform;
_model.CurrentSequence.Name = _sequence;
_model.CurrentSequence.Time = Math.Clamp( seconds, 0f, MathF.Max( 0f, Duration ) );
_model.Update( 0f );
Weight = Math.Clamp( weight, 0f, 1f );
}
/// <summary>World transform of a bone in the overlay pose.</summary>
public bool TryGetWorld( string bone, out Transform world )
{
world = global::Transform.Zero;
if ( !Active || _model == null || !_boneIndex.TryGetValue( bone, out var index ) )
return false;
world = _model.GetBoneWorldTransform( index );
return true;
}
public void End()
{
Active = false;
Weight = 0f;
}
public void Dispose()
{
if ( _model != null && _model.IsValid() )
_model.Delete();
_model = null;
_source = null;
Active = false;
}
/// <summary>Parses <c>{"reload":{"model":"","sequence":"reload_rifle","blend":0.2}}</c>.</summary>
public static bool TryParse( string json, Dictionary<string, OverlayInfo> into, out string error )
{
into.Clear();
error = null;
if ( string.IsNullOrWhiteSpace( json ) )
return true;
try
{
using var doc = JsonDocument.Parse( json );
if ( doc.RootElement.ValueKind != JsonValueKind.Object )
{
error = "expected an object";
return false;
}
foreach ( var entry in doc.RootElement.EnumerateObject() )
{
if ( entry.Value.ValueKind != JsonValueKind.Object )
continue;
var info = new OverlayInfo { Role = entry.Name.ToLowerInvariant() };
if ( entry.Value.TryGetProperty( "model", out var m ) && m.ValueKind == JsonValueKind.String )
info.Model = m.GetString();
if ( entry.Value.TryGetProperty( "sequence", out var s ) && s.ValueKind == JsonValueKind.String )
info.Sequence = s.GetString();
if ( entry.Value.TryGetProperty( "blend", out var b ) && b.ValueKind == JsonValueKind.Number )
info.Blend = MathF.Max( 0f, b.GetSingle() );
if ( !string.IsNullOrEmpty( info.Sequence ) )
into[info.Role] = info;
}
return true;
}
catch ( JsonException e )
{
error = e.Message;
return false;
}
}
}