From 561ee9dd64605aaea567c4c2bdf566362bbac891 Mon Sep 17 00:00:00 2001 From: Jan Racek Date: Sat, 28 Mar 2026 20:13:12 +0100 Subject: [PATCH] Camera API, improved cutscene API --- src/cutscene.cpp | 49 ++++++++++++++++++++++++------- src/cutscene.hh | 18 +++++++++++- src/luaapi.cpp | 68 +++++++++++++++++++++++++++++++++++++++++--- src/luaapi.hh | 12 ++++++-- src/scenemanager.cpp | 36 ++++++++++++++++------- src/scenemanager.hh | 6 ++++ 6 files changed, 160 insertions(+), 29 deletions(-) diff --git a/src/cutscene.cpp b/src/cutscene.cpp index 381f3fa..4facced 100644 --- a/src/cutscene.cpp +++ b/src/cutscene.cpp @@ -10,18 +10,21 @@ 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_camera = camera; - m_audio = audio; - m_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 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)) { @@ -106,7 +109,9 @@ bool CutscenePlayer::play(const char* name) { } void CutscenePlayer::stop() { + if (!m_active) return; m_active = nullptr; + fireOnComplete(); } void CutscenePlayer::tick() { @@ -130,10 +135,34 @@ void CutscenePlayer::tick() { m_frame++; if (m_frame > m_active->totalFrames) { - m_active = nullptr; // Cutscene finished + 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) { + printf("Cutscene onComplete error: %s\n", L.optString(-1, "unknown")); + 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: diff --git a/src/cutscene.hh b/src/cutscene.hh index 608e7e7..50c15ae 100644 --- a/src/cutscene.hh +++ b/src/cutscene.hh @@ -9,6 +9,8 @@ #include "gameobject.hh" #include "audiomanager.hh" +#include + namespace psxsplash { class UISystem; // Forward declaration @@ -86,7 +88,8 @@ public: UISystem* uiSystem = nullptr); /// Play cutscene by name. Returns false if not found. - bool play(const char* name); + /// If loop is true, the cutscene replays from the start when it ends. + bool play(const char* name, bool loop = false); /// Stop the current cutscene immediately. void stop(); @@ -94,6 +97,15 @@ public: /// True if a cutscene is currently active. bool isPlaying() const { return m_active != nullptr; } + /// Set a Lua registry reference to call when the cutscene finishes. + /// Pass LUA_NOREF to clear. The callback is called ONCE when the + /// cutscene ends (not on each loop iteration - only when it truly stops). + void setOnCompleteRef(int ref) { m_onCompleteRef = ref; } + int getOnCompleteRef() const { return m_onCompleteRef; } + + /// Set the lua_State for callbacks. Must be called before play(). + void setLuaState(lua_State* L) { m_luaState = L; } + /// Advance one frame. Call once per frame. Does nothing when idle. void tick(); @@ -103,15 +115,19 @@ private: Cutscene* m_active = nullptr; uint16_t m_frame = 0; uint8_t m_nextAudio = 0; + bool m_loop = false; Camera* m_camera = nullptr; AudioManager* m_audio = nullptr; UISystem* m_uiSystem = nullptr; + lua_State* m_luaState = nullptr; + int m_onCompleteRef = LUA_NOREF; psyqo::Trig<> m_trig; void applyTrack(CutsceneTrack& track); void lerpKeyframes(CutsceneKeyframe* kf, uint8_t count, const int16_t initial[3], int16_t out[3]); void lerpAngles(CutsceneKeyframe* kf, uint8_t count, const int16_t initial[3], int16_t out[3]); + void fireOnComplete(); }; } // namespace psxsplash diff --git a/src/luaapi.cpp b/src/luaapi.cpp index 0e1c1bb..9e1b98a 100644 --- a/src/luaapi.cpp +++ b/src/luaapi.cpp @@ -284,7 +284,20 @@ void LuaAPI::RegisterAll(psyqo::Lua& L, SceneManager* scene, CutscenePlayer* cut L.setField(-2, "IsPlaying"); L.setGlobal("Cutscene"); - + + // ======================================================================== + // CONTROLS API + // ======================================================================== + L.newTable(); + + L.push(Controls_SetEnabled); + L.setField(-2, "SetEnabled"); + + L.push(Controls_IsEnabled); + L.setField(-2, "IsEnabled"); + + L.setGlobal("Controls"); + // ======================================================================== // UI API // ======================================================================== @@ -1473,13 +1486,38 @@ void LuaAPI::PersistClear() { int LuaAPI::Cutscene_Play(lua_State* L) { psyqo::Lua lua(L); - + if (!s_cutscenePlayer || !lua.isString(1)) { return 0; } - + const char* name = lua.toString(1); - s_cutscenePlayer->play(name); + bool loop = false; + int onCompleteRef = LUA_NOREF; + + // Optional second argument: options table {loop=bool, onComplete=function} + if (lua.isTable(2)) { + lua.getField(2, "loop"); + if (lua.isBoolean(-1)) loop = lua.toBoolean(-1); + lua.pop(); + + lua.getField(2, "onComplete"); + if (lua.isFunction(-1)) { + onCompleteRef = lua.ref(); // pops and stores in registry + } else { + lua.pop(); + } + } + + // Clear any previous callback before starting a new cutscene + int oldRef = s_cutscenePlayer->getOnCompleteRef(); + if (oldRef != LUA_NOREF) { + luaL_unref(L, LUA_REGISTRYINDEX, oldRef); + } + + s_cutscenePlayer->setLuaState(L); + s_cutscenePlayer->setOnCompleteRef(onCompleteRef); + s_cutscenePlayer->play(name, loop); return 0; } @@ -1503,6 +1541,28 @@ int LuaAPI::Cutscene_IsPlaying(lua_State* L) { return 1; } +// ============================================================================ +// CONTROLS API IMPLEMENTATION +// ============================================================================ + +int LuaAPI::Controls_SetEnabled(lua_State* L) { + psyqo::Lua lua(L); + if (s_sceneManager && lua.isBoolean(1)) { + s_sceneManager->setControlsEnabled(lua.toBoolean(1)); + } + return 0; +} + +int LuaAPI::Controls_IsEnabled(lua_State* L) { + psyqo::Lua lua(L); + if (s_sceneManager) { + lua.push(s_sceneManager->isControlsEnabled()); + } else { + lua.push(false); + } + return 1; +} + // ============================================================================ // UI API IMPLEMENTATION // ============================================================================ diff --git a/src/luaapi.hh b/src/luaapi.hh index 0330910..b4bdce8 100644 --- a/src/luaapi.hh +++ b/src/luaapi.hh @@ -262,14 +262,20 @@ private: // CUTSCENE API - Cutscene playback control // ======================================================================== - // Cutscene.Play(name) -> nil + // Cutscene.Play(name) or Cutscene.Play(name, {loop=bool, onComplete=fn}) static int Cutscene_Play(lua_State* L); - + // Cutscene.Stop() -> nil static int Cutscene_Stop(lua_State* L); - + // Cutscene.IsPlaying() -> boolean static int Cutscene_IsPlaying(lua_State* L); + + // Controls.SetEnabled(bool) - enable/disable all player input + static int Controls_SetEnabled(lua_State* L); + + // Controls.IsEnabled() -> boolean + static int Controls_IsEnabled(lua_State* L); // ======================================================================== // UI API - Canvas and element control diff --git a/src/scenemanager.cpp b/src/scenemanager.cpp index 5000bc5..efd90b9 100644 --- a/src/scenemanager.cpp +++ b/src/scenemanager.cpp @@ -62,6 +62,11 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingSc m_navRegions = sceneSetup.navRegions; // Nav region system (v7+) m_playerNavRegion = m_navRegions.isLoaded() ? m_navRegions.getStartRegion() : NAV_NO_REGION; + // If nav regions are loaded, camera follows the player. Otherwise the + // scene is in "free camera" mode where cutscenes and Lua drive the camera. + m_cameraFollowsPlayer = m_navRegions.isLoaded(); + m_controlsEnabled = true; + // Scene type and render path m_sceneType = sceneSetup.sceneType; @@ -396,13 +401,15 @@ void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) { // Save position BEFORE movement for collision detection psyqo::Vec3 oldPlayerPosition = m_playerPosition; - - m_controls.HandleControls(m_playerPosition, playerRotationX, playerRotationY, playerRotationZ, freecam, m_deltaFrames); - - // Jump input: Cross button triggers jump when grounded - if (m_isGrounded && m_controls.wasButtonPressed(psyqo::AdvancedPad::Button::Cross)) { - m_velocityY = -m_jumpVelocityRaw; // Negative = upward (PSX Y-down) - m_isGrounded = false; + + if (m_controlsEnabled) { + m_controls.HandleControls(m_playerPosition, playerRotationX, playerRotationY, playerRotationZ, freecam, m_deltaFrames); + + // Jump input: Cross button triggers jump when grounded + if (m_isGrounded && m_controls.wasButtonPressed(psyqo::AdvancedPad::Button::Cross)) { + m_velocityY = -m_jumpVelocityRaw; // Negative = upward (PSX Y-down) + m_isGrounded = false; + } } gpu.pumpCallbacks(); @@ -460,10 +467,17 @@ void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) { psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_NAVMESH, navmeshTime); #endif - m_currentCamera.SetPosition(static_cast>(m_playerPosition.x), - static_cast>(m_playerPosition.y), - static_cast>(m_playerPosition.z)); - m_currentCamera.SetRotation(playerRotationX, playerRotationY, playerRotationZ); + // Only snap camera to player when in player-follow mode and no + // cutscene is actively controlling the camera. In free camera mode + // (no nav regions / no PSXPlayer), the camera is driven entirely + // by cutscenes and Lua. After a cutscene ends in free mode, the + // camera stays at the last cutscene position. + if (m_cameraFollowsPlayer && !m_cutscenePlayer.isPlaying()) { + m_currentCamera.SetPosition(static_cast>(m_playerPosition.x), + static_cast>(m_playerPosition.y), + static_cast>(m_playerPosition.z)); + m_currentCamera.SetRotation(playerRotationX, playerRotationY, playerRotationZ); + } // Process pending scene transitions (at end of frame) processPendingSceneLoad(); diff --git a/src/scenemanager.hh b/src/scenemanager.hh index 82d52d4..5930866 100644 --- a/src/scenemanager.hh +++ b/src/scenemanager.hh @@ -81,6 +81,10 @@ class SceneManager { Camera& getCamera() { return m_currentCamera; } Lua& getLua() { return L; } AudioManager& getAudio() { return m_audio; } + + // Controls enable/disable (Lua-driven) + void setControlsEnabled(bool enabled) { m_controlsEnabled = enabled; } + bool isControlsEnabled() const { return m_controlsEnabled; } // Scene loading (for multi-scene support) void requestSceneLoad(int sceneIndex); @@ -162,6 +166,8 @@ class SceneManager { int m_deltaFrames; // Elapsed frame count (1 normally, 2+ if dropped) bool freecam = false; + bool m_controlsEnabled = true; // Lua can disable all player input + bool m_cameraFollowsPlayer = true; // False when scene has no nav regions (freecam/cutscene mode) // Static font pointer (set from main.cpp) static psyqo::Font<>* s_font;