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