using System;
using System.Collections.Generic;
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, started, 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. -1 until the first update.
public float StartTime = -1f;
///
/// Absolute user time when playback ends. -1 for infinite (looping) motions.
///
public float EndTime = -1f;
/// 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 = true;
/// Flag set when playback completes. Checked by the queue manager during cleanup.
public bool IsFinished;
/// Flag set on the first update — порт CubismMotionQueueEntry::_started.
public bool IsStarted;
/// Время последнего кадра (состояние), порт CubismMotionQueueEntry::SetState.
public float StateTime;
/// Вес последнего кадра (состояние), порт CubismMotionQueueEntry::SetState.
public float StateWeight;
/// Абсолютное время последней проверки событий UserData (сдвигается каждый кадр).
public float LastCheckEventTime;
/// Длительность фейда-аута, назначенного при вытеснении новым моушеном.
public float FadeOutSeconds;
/// Флаг вытеснения: фейд-аут начат, EndTime будет назначен при следующем обновлении.
public bool IsTriggeredFadeOut;
/// 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;
}
///
/// Порт CubismMotionQueueEntry::SetFadeout: помечает запись для фейда-аута
/// (вызывается очередью, когда стартует новый моушен).
///
public void SetFadeout(float fadeOutSeconds)
{
FadeOutSeconds = fadeOutSeconds;
IsTriggeredFadeOut = true;
}
///
/// Порт CubismMotionQueueEntry::StartFadeout: назначает EndTime, сокращая его,
/// если фейд-аут уже был запущен раньше и раньше должен закончиться.
///
public void StartFadeout(float fadeOutSeconds, float userTimeSeconds)
{
float newEndTimeSeconds = userTimeSeconds + fadeOutSeconds;
IsTriggeredFadeOut = true;
if (EndTime < 0f || newEndTimeSeconds < EndTime)
{
EndTime = newEndTimeSeconds;
}
}
/// Порт CubismMotionQueueEntry::SetState — сохраняет время и вес последнего кадра.
public void SetState(float timeSeconds, float weight)
{
StateTime = timeSeconds;
StateWeight = weight;
}
}
///
/// Manages a queue of instances for a single model.
/// Поддержка вытеснения по приоритету (ReserveMotion), параллельного проигрывания
/// и автосборки завершённых записей. Порт CubismMotionQueueManager из SDK 5-r.5:
///
/// - StartMotion назначает всем прежним записям фейд-аут;
/// - каждый кадр DoUpdateMotion вызывает ,
/// затем проверяет события UserData () и
/// удаляет finished-записи;
/// - колбэком завершения владеет сам моушен: очередь его НЕ дублирует.
///
public class CubismMotionQueueManager
{
private readonly List _entries = new();
private readonly List _firedEventValues = new();
private int _reservedPriority = -1;
///
/// Событие, вызываемое для каждого сработавшего UserData-события за кадр
/// (порт _eventCallback из CubismMotionQueueManager).
///
public event Action EventFired;
///
/// События, собранные за последний кадр DoUpdateMotion по всем записям очереди.
/// Список очищается в начале каждого обновления — читайте его сразу после вызова.
///
public IReadOnlyList FiredEventValues => _firedEventValues;
///
/// 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;
}
///
/// Enqueues a motion for playback. Прежним записям назначается фейд-аут
/// (SetFadeout), после чего они плавно завершаются; сама запись инициализируется
/// при первом кадре .
/// Параметр userTimeSeconds оставлен для совместимости вызовов.
///
/// The motion to play.
/// Whether to auto-remove on finish.
/// Current user time (не используется, порт сигнатуры).
public CubismMotionQueueEntry StartMotion(ACubismMotion motion, bool autoDelete, float userTimeSeconds)
{
if (motion == null)
return null;
// Порт CubismMotionQueueManager::StartMotion: прежние моушены вытесняются фейд-аутом.
foreach (var existing in _entries)
{
if (existing == null) continue;
existing.SetFadeout(existing.Motion?.FadeOutSeconds ?? 0f);
}
var entry = new CubismMotionQueueEntry(motion, autoDelete);
_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 != null && !entry.IsFinished)
return false;
return true;
}
///
/// Per-frame update — порт CubismMotionQueueManager::DoUpdateMotion.
/// Для каждой записи: UpdateParameters (фейд + применение значений + финализация
/// IsFinished самим моушеном), сборUserData-событий за кадр, затем удаление
/// finished-записей и старт отложенного фейда-аута вытесненных.
/// Обход прямой, как в SDK: моушены применяются от старых к новым.
///
public void DoUpdateMotion(CubismNativeModel model, float userTimeSeconds)
{
_firedEventValues.Clear();
int i = 0;
while (i < _entries.Count)
{
CubismMotionQueueEntry entry = _entries[i];
if (entry == null)
{
_entries.RemoveAt(i);
continue;
}
ACubismMotion motion = entry.Motion;
if (motion == null)
{
_entries.RemoveAt(i);
continue;
}
// ------ Применение значений (финализация — внутри моушена/обёртки) ------
motion.UpdateParameters(model, entry, userTimeSeconds);
// ------ Проверка UserData-событий за кадр ------
var fired = motion.GetFiredEvent(
entry.LastCheckEventTime - entry.StartTime,
userTimeSeconds - entry.StartTime);
for (int e = 0; e < fired.Count; e++)
EventFired?.Invoke(motion, fired[e]);
_firedEventValues.AddRange(fired);
entry.LastCheckEventTime = userTimeSeconds;
// ------ Удаление завершённых записей ------
if (entry.IsFinished)
{
_entries.RemoveAt(i);
continue;
}
// Фейд-аут, запланированный при вытеснении, стартует после обновления.
if (entry.IsTriggeredFadeOut)
{
entry.StartFadeout(entry.FadeOutSeconds, userTimeSeconds);
}
i++;
}
}
}
}