#include "cutscene.hh" #include #include #include #include "streq.hh" #include "uisystem.hh" namespace psxsplash { void CutscenePlayer::init(Cutscene* cutscenes, int count, Camera* camera, AudioManager* audio, UISystem* uiSystem) { m_cutscenes = cutscenes; m_count = count; m_active = nullptr; m_frame = 0; m_nextAudio = 0; m_loop = false; m_camera = camera; m_audio = audio; m_uiSystem = uiSystem; m_onCompleteRef = LUA_NOREF; } bool CutscenePlayer::play(const char* name, bool loop) { if (!name || !m_cutscenes) return false; m_loop = loop; for (int i = 0; i < m_count; i++) { if (m_cutscenes[i].name && streq(m_cutscenes[i].name, name)) { m_active = &m_cutscenes[i]; m_frame = 0; m_nextAudio = 0; // Capture initial state for pre-first-keyframe blending for (uint8_t ti = 0; ti < m_active->trackCount; ti++) { CutsceneTrack& track = m_active->tracks[ti]; track.initialValues[0] = track.initialValues[1] = track.initialValues[2] = 0; switch (track.trackType) { case TrackType::CameraPosition: if (m_camera) { auto& pos = m_camera->GetPosition(); track.initialValues[0] = (int16_t)pos.x.value; track.initialValues[1] = (int16_t)pos.y.value; track.initialValues[2] = (int16_t)pos.z.value; } break; case TrackType::CameraRotation: if (m_camera) { track.initialValues[0] = m_camera->GetAngleX(); track.initialValues[1] = m_camera->GetAngleY(); track.initialValues[2] = m_camera->GetAngleZ(); } break; case TrackType::ObjectPosition: if (track.target) { track.initialValues[0] = (int16_t)track.target->position.x.value; track.initialValues[1] = (int16_t)track.target->position.y.value; track.initialValues[2] = (int16_t)track.target->position.z.value; } break; case TrackType::ObjectRotation: // Initial rotation angles: 0,0,0 (no way to extract Euler from matrix) break; case TrackType::ObjectActive: if (track.target) { track.initialValues[0] = track.target->isActive() ? 1 : 0; } break; case TrackType::UICanvasVisible: if (m_uiSystem) { track.initialValues[0] = m_uiSystem->isCanvasVisible(track.uiHandle) ? 1 : 0; } break; case TrackType::UIElementVisible: if (m_uiSystem) { track.initialValues[0] = m_uiSystem->isElementVisible(track.uiHandle) ? 1 : 0; } break; case TrackType::UIProgress: if (m_uiSystem) { track.initialValues[0] = m_uiSystem->getProgress(track.uiHandle); } break; case TrackType::UIPosition: if (m_uiSystem) { int16_t px, py; m_uiSystem->getPosition(track.uiHandle, px, py); track.initialValues[0] = px; track.initialValues[1] = py; } break; case TrackType::UIColor: if (m_uiSystem) { uint8_t cr, cg, cb; m_uiSystem->getColor(track.uiHandle, cr, cg, cb); track.initialValues[0] = cr; track.initialValues[1] = cg; track.initialValues[2] = cb; } break; } } return true; } } return false; } void CutscenePlayer::stop() { if (!m_active) return; m_active = nullptr; fireOnComplete(); } void CutscenePlayer::tick() { if (!m_active) return; for (uint8_t i = 0; i < m_active->trackCount; i++) { applyTrack(m_active->tracks[i]); } while (m_nextAudio < m_active->audioEventCount) { CutsceneAudioEvent& evt = m_active->audioEvents[m_nextAudio]; if (evt.frame <= m_frame) { if (m_audio) { m_audio->play(evt.clipIndex, evt.volume, evt.pan); } m_nextAudio++; } else { break; } } m_frame++; if (m_frame > m_active->totalFrames) { if (m_loop) { // Restart from the beginning m_frame = 0; m_nextAudio = 0; } else { m_active = nullptr; // Cutscene finished fireOnComplete(); } } } void CutscenePlayer::fireOnComplete() { if (m_onCompleteRef == LUA_NOREF || !m_luaState) return; psyqo::Lua L(m_luaState); L.rawGetI(LUA_REGISTRYINDEX, m_onCompleteRef); if (L.isFunction(-1)) { if (L.pcall(0, 0) != LUA_OK) { L.pop(); } } else { L.pop(); } // Unreference the callback (one-shot) luaL_unref(m_luaState, LUA_REGISTRYINDEX, m_onCompleteRef); m_onCompleteRef = LUA_NOREF; } static int32_t applyCurve(int32_t t, InterpMode mode) { switch (mode) { default: case InterpMode::Linear: return t; case InterpMode::Step: return 0; case InterpMode::EaseIn: return (int32_t)((int64_t)t * t >> 12); case InterpMode::EaseOut: return (int32_t)(((int64_t)t * (8192 - t)) >> 12); case InterpMode::EaseInOut: { int64_t t2 = (int64_t)t * t; int64_t t3 = t2 * t; return (int32_t)((3 * t2 - 2 * (t3 >> 12)) >> 12); } } } static bool findKfPair(CutsceneKeyframe* kf, uint8_t count, uint16_t frame, uint8_t& a, uint8_t& b, int32_t& t, int16_t out[3]) { if (count == 0) { out[0] = out[1] = out[2] = 0; return false; } if (frame <= kf[0].getFrame() || count == 1) { out[0] = kf[0].values[0]; out[1] = kf[0].values[1]; out[2] = kf[0].values[2]; return false; } if (frame >= kf[count - 1].getFrame()) { out[0] = kf[count - 1].values[0]; out[1] = kf[count - 1].values[1]; out[2] = kf[count - 1].values[2]; return false; } b = 1; while (b < count && kf[b].getFrame() <= frame) b++; a = b - 1; uint16_t span = kf[b].getFrame() - kf[a].getFrame(); if (span == 0) { out[0] = kf[a].values[0]; out[1] = kf[a].values[1]; out[2] = kf[a].values[2]; return false; } uint32_t num = (uint32_t)(frame - kf[a].getFrame()) << 12; int32_t rawT = (int32_t)(num / span); t = applyCurve(rawT, kf[b].getInterp()); return true; } void CutscenePlayer::lerpKeyframes(CutsceneKeyframe* kf, uint8_t count, const int16_t initial[3], int16_t out[3]) { uint8_t a, b; int32_t t; if (!findKfPair(kf, count, m_frame, a, b, t, out)) { if (count > 0 && kf[0].getFrame() > 0 && m_frame < kf[0].getFrame()) { uint16_t span = kf[0].getFrame(); uint32_t num = (uint32_t)m_frame << 12; int32_t rawT = (int32_t)(num / span); int32_t ct = applyCurve(rawT, kf[0].getInterp()); for (int i = 0; i < 3; i++) { int32_t delta = (int32_t)kf[0].values[i] - (int32_t)initial[i]; out[i] = (int16_t)((int32_t)initial[i] + ((delta * ct) >> 12)); } } return; } for (int i = 0; i < 3; i++) { int32_t delta = (int32_t)kf[b].values[i] - (int32_t)kf[a].values[i]; out[i] = (int16_t)((int32_t)kf[a].values[i] + ((delta * t) >> 12)); } } static constexpr int32_t ANGLE_FULL_CIRCLE = 2048; static constexpr int32_t ANGLE_HALF_CIRCLE = 1024; void CutscenePlayer::lerpAngles(CutsceneKeyframe* kf, uint8_t count, const int16_t initial[3], int16_t out[3]) { uint8_t a, b; int32_t t; if (!findKfPair(kf, count, m_frame, a, b, t, out)) { if (count > 0 && kf[0].getFrame() > 0 && m_frame < kf[0].getFrame()) { uint16_t span = kf[0].getFrame(); uint32_t num = (uint32_t)m_frame << 12; int32_t rawT = (int32_t)(num / span); int32_t ct = applyCurve(rawT, kf[0].getInterp()); for (int i = 0; i < 3; i++) { int32_t from = (int32_t)initial[i]; int32_t to = (int32_t)kf[0].values[i]; int32_t delta = to - from; delta = ((delta + ANGLE_HALF_CIRCLE) % ANGLE_FULL_CIRCLE + ANGLE_FULL_CIRCLE) % ANGLE_FULL_CIRCLE - ANGLE_HALF_CIRCLE; out[i] = (int16_t)(from + ((delta * ct) >> 12)); } } return; } for (int i = 0; i < 3; i++) { int32_t from = (int32_t)kf[a].values[i]; int32_t to = (int32_t)kf[b].values[i]; int32_t delta = to - from; delta = ((delta + ANGLE_HALF_CIRCLE) % ANGLE_FULL_CIRCLE + ANGLE_FULL_CIRCLE) % ANGLE_FULL_CIRCLE - ANGLE_HALF_CIRCLE; out[i] = (int16_t)(from + ((delta * t) >> 12)); } } void CutscenePlayer::applyTrack(CutsceneTrack& track) { if (track.keyframeCount == 0 || !track.keyframes) return; int16_t out[3]; switch (track.trackType) { case TrackType::CameraPosition: { if (!m_camera) return; lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out); psyqo::FixedPoint<12> x, y, z; x.value = (int32_t)out[0]; y.value = (int32_t)out[1]; z.value = (int32_t)out[2]; m_camera->SetPosition(x, y, z); break; } case TrackType::CameraRotation: { if (!m_camera) return; lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out); psyqo::Angle rx, ry, rz; rx.value = (int32_t)out[0]; ry.value = (int32_t)out[1]; rz.value = (int32_t)out[2]; m_camera->SetRotation(rx, ry, rz); break; } case TrackType::ObjectPosition: { if (!track.target) return; lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out); track.target->position.x.value = (int32_t)out[0]; track.target->position.y.value = (int32_t)out[1]; track.target->position.z.value = (int32_t)out[2]; break; } case TrackType::ObjectRotation: { if (!track.target) return; lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out); psyqo::Angle rx, ry, rz; rx.value = (int32_t)out[0]; ry.value = (int32_t)out[1]; rz.value = (int32_t)out[2]; auto matY = psyqo::SoftMath::generateRotationMatrix33(ry, psyqo::SoftMath::Axis::Y, m_trig); auto matX = psyqo::SoftMath::generateRotationMatrix33(rx, psyqo::SoftMath::Axis::X, m_trig); auto matZ = psyqo::SoftMath::generateRotationMatrix33(rz, psyqo::SoftMath::Axis::Z, m_trig); auto temp = psyqo::SoftMath::multiplyMatrix33(matY, matX); track.target->rotation = psyqo::SoftMath::multiplyMatrix33(temp, matZ); break; } case TrackType::ObjectActive: { if (!track.target) return; CutsceneKeyframe* kf = track.keyframes; uint8_t count = track.keyframeCount; int16_t activeVal = (count > 0 && m_frame < kf[0].getFrame()) ? track.initialValues[0] : kf[0].values[0]; for (uint8_t i = 0; i < count; i++) { if (kf[i].getFrame() <= m_frame) { activeVal = kf[i].values[0]; } else { break; } } track.target->setActive(activeVal != 0); break; } // ── UI track types ── case TrackType::UICanvasVisible: { if (!m_uiSystem) return; CutsceneKeyframe* kf = track.keyframes; uint8_t count = track.keyframeCount; int16_t val = (count > 0 && m_frame < kf[0].getFrame()) ? track.initialValues[0] : kf[0].values[0]; for (uint8_t i = 0; i < count; i++) { if (kf[i].getFrame() <= m_frame) val = kf[i].values[0]; else break; } m_uiSystem->setCanvasVisible(track.uiHandle, val != 0); break; } case TrackType::UIElementVisible: { if (!m_uiSystem) return; CutsceneKeyframe* kf = track.keyframes; uint8_t count = track.keyframeCount; int16_t val = (count > 0 && m_frame < kf[0].getFrame()) ? track.initialValues[0] : kf[0].values[0]; for (uint8_t i = 0; i < count; i++) { if (kf[i].getFrame() <= m_frame) val = kf[i].values[0]; else break; } m_uiSystem->setElementVisible(track.uiHandle, val != 0); break; } case TrackType::UIProgress: { if (!m_uiSystem) return; lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out); int16_t v = out[0]; if (v < 0) v = 0; if (v > 100) v = 100; m_uiSystem->setProgress(track.uiHandle, (uint8_t)v); break; } case TrackType::UIPosition: { if (!m_uiSystem) return; lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out); m_uiSystem->setPosition(track.uiHandle, out[0], out[1]); break; } case TrackType::UIColor: { if (!m_uiSystem) return; lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out); uint8_t cr = (out[0] < 0) ? 0 : ((out[0] > 255) ? 255 : (uint8_t)out[0]); uint8_t cg = (out[1] < 0) ? 0 : ((out[1] > 255) ? 255 : (uint8_t)out[1]); uint8_t cb = (out[2] < 0) ? 0 : ((out[2] > 255) ? 255 : (uint8_t)out[2]); m_uiSystem->setColor(track.uiHandle, cr, cg, cb); break; } } } } // namespace psxsplash