using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using LibMMD.Motion;
using LibMMD.Reader;
using UnityEngine;
namespace LibMMD.Unity3D.Animation
{
///
/// Model-independent, parsed VMD data. It can be loaded and cached before a model exists,
/// then bound to any compatible MMD model by MmdGameObject.LoadMotion.
///
public sealed class MmdMotionAsset
{
internal MmdMotionAsset(string sourcePath, MmdMotion motion)
{
SourcePath = sourcePath;
Motion = motion ?? throw new ArgumentNullException(nameof(motion));
}
public string SourcePath { get; }
public string Name => Motion.Name;
public int LengthInFrames => Motion.Length;
public double LengthInSeconds => Motion.Length / 30.0;
public IEnumerable BoneNames => Motion.BoneMotions.Keys;
public IEnumerable MorphNames => Motion.MorphMotions.Keys;
public MmdMotion Motion { get; }
}
/// Parses VMD files without requiring a GameObject or a loaded model.
public static class MmdMotionLoader
{
private static readonly object CacheLock = new object();
private static string _cacheKey;
private static MmdMotionAsset _cachedAsset;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetForPlaySession()
{
lock (CacheLock)
{
_cacheKey = null;
_cachedAsset = null;
}
}
public static MmdMotionAsset Load(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentException("A VMD path is required.", nameof(path));
}
var key = BuildKey(path);
if (key == null) throw new FileNotFoundException("VMD file was not found.", path);
lock (CacheLock)
{
if (key == _cacheKey && _cachedAsset != null) return _cachedAsset;
}
// Parse outside the lock: simultaneous callers may do duplicate work, but a slow
// filesystem read must never block a cache hit on another thread.
var asset = new MmdMotionAsset(path, new VmdReader().Read(path));
var keyAfterRead = BuildKey(path);
if (keyAfterRead == null || keyAfterRead != key)
{
// Never publish data read while the file was being replaced. A following call
// will parse the new version instead of receiving a stale cache entry.
return asset;
}
lock (CacheLock)
{
_cacheKey = key;
_cachedAsset = asset;
return asset;
}
}
public static Task LoadAsync(string path)
{
return Task.Run(() => Load(path));
}
internal static string BuildKey(string path)
{
if (string.IsNullOrWhiteSpace(path)) return null;
try
{
var info = new FileInfo(path);
return info.Exists
? $"{info.FullName}|{info.Length}|{info.LastWriteTimeUtc.Ticks}"
: null;
}
catch (System.Exception)
{
// Freshness cannot be proven, so caching is disabled for this load.
return null;
}
}
}
}