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