namespace Cthangover.Live2D.Cubism.Framework { /// /// Represents one enqueued motion in the playback queue. /// Holds timing data (start time, end time, fade-in start time), /// lifecycle flags (available, finished, auto-delete), and a /// reference to the motion itself. /// /// Created by /// and consumed frame-by-frame in . /// public class CubismMotionQueueEntry { /// Absolute user time when motion playback begins. public float StartTime; /// /// Absolute user time when playback ends. -1 for infinite (looping) motions. /// public float EndTime; /// Absolute user time when fade-in begins (may differ from StartTime for offset motions). public float FadeInStartTime; /// Whether this entry should be processed during updates. public bool IsAvailable; /// Flag set when playback completes. Checked by the queue manager during cleanup. public bool IsFinished; /// The motion instance driving parameter updates. public ACubismMotion Motion; /// If true, the entry is automatically removed from the queue when finished. public bool AutoDelete; /// Arbitrary user data attached to this queue entry. public object CustomData; /// /// Creates a new queue entry for the given motion. /// AutoDelete controls whether the entry is removed on finish. /// public CubismMotionQueueEntry(ACubismMotion motion, bool autoDelete) { Motion = motion; AutoDelete = autoDelete; IsAvailable = true; } } /// /// Manages a queue of instances for a single model. /// Supports priority reservation (higher-priority motions preempt lower ones), /// concurrent playback of multiple motions, and automatic cleanup of finished entries. /// /// Two instances are used by : /// one for character motions (animations) and one for expressions. /// Each is updated independently in the per-frame loop, allowing motions /// and expressions to blend independently with their own fade weights. /// /// The update loop iterates entries in reverse for safe removal during /// traversal. Finished auto-delete entries are removed immediately; /// non-auto-delete entries remain in the queue with their IsFinished flag set. /// public class CubismMotionQueueManager { private readonly System.Collections.Generic.List _entries = new(); private int _reservedPriority = -1; /// /// Sets the reserved priority threshold. /// Motions with priority below this are rejected by . /// public void SetReservePriority(int priority) => _reservedPriority = priority; /// /// Attempts to reserve the given priority level. /// Returns true if the priority is accepted (higher than current reserved), /// false if a higher-priority motion is already reserved. /// public bool ReserveMotion(int priority) { if (_reservedPriority >= 0 && _reservedPriority > priority) return false; _reservedPriority = priority; return true; } /// /// Creates and enqueues a motion for playback. /// Instantiates a , calls /// on it, and /// adds it to the queue. Returns the entry for optional tracking. /// /// The motion to play. /// Whether to auto-remove on finish. /// Current user time for start time calculation. public CubismMotionQueueEntry StartMotion(ACubismMotion motion, bool autoDelete, float userTimeSeconds) { if (motion == null) return null; var entry = new CubismMotionQueueEntry(motion, autoDelete); motion.SetupMotionQueueEntry(entry, userTimeSeconds); _entries.Add(entry); return entry; } /// /// Stops all motions and clears the queue. Resets priority reservation. /// public void StopAllMotions() { _entries.Clear(); _reservedPriority = -1; } /// /// Returns true if all entries in the queue are finished. /// public bool IsFinished() { foreach (var entry in _entries) if (!entry.IsFinished) return false; return true; } /// /// Per-frame update for all queued motions. /// Iterates in reverse for safe removal. For each active entry: /// computes fade weight, invokes , /// and checks for completion. Finished auto-delete entries are removed. /// public void DoUpdateMotion(CubismNativeModel model, float userTimeSeconds) { for (int i = _entries.Count - 1; i >= 0; i--) { var entry = _entries[i]; if (!entry.IsAvailable || entry.IsFinished) { if (entry.AutoDelete) _entries.RemoveAt(i); continue; } var weight = entry.Motion.UpdateFadeWeight(entry, userTimeSeconds); entry.Motion.DoUpdateParameters(model, userTimeSeconds, weight, entry); if (entry.Motion.GetDuration() > 0f) { if (userTimeSeconds >= entry.EndTime && !entry.Motion.IsLoop) { entry.IsFinished = true; entry.Motion.OnFinishedMotion?.Invoke(entry.Motion); } } } } } }