403 lines
15 KiB
C++
403 lines
15 KiB
C++
#include "cutscene.hh"
|
|
|
|
#include <psyqo/fixed-point.hh>
|
|
#include <psyqo/soft-math.hh>
|
|
#include <psyqo/trigonometry.hh>
|
|
#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
|