Camera API, improved cutscene API

This commit is contained in:
Jan Racek
2026-03-28 20:13:12 +01:00
parent 68cf8a7460
commit 561ee9dd64
6 changed files with 160 additions and 29 deletions

View File

@@ -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:

View File

@@ -9,6 +9,8 @@
#include "gameobject.hh"
#include "audiomanager.hh"
#include <psyqo-lua/lua.hh>
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

View File

@@ -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
// ============================================================================

View File

@@ -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

View File

@@ -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<psyqo::FixedPoint<12>>(m_playerPosition.x),
static_cast<psyqo::FixedPoint<12>>(m_playerPosition.y),
static_cast<psyqo::FixedPoint<12>>(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<psyqo::FixedPoint<12>>(m_playerPosition.x),
static_cast<psyqo::FixedPoint<12>>(m_playerPosition.y),
static_cast<psyqo::FixedPoint<12>>(m_playerPosition.z));
m_currentCamera.SetRotation(playerRotationX, playerRotationY, playerRotationZ);
}
// Process pending scene transitions (at end of frame)
processPendingSceneLoad();

View File

@@ -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;