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; } } } }