831 lines
30 KiB
C++
831 lines
30 KiB
C++
#include "scenemanager.hh"
|
|
|
|
#include <utility>
|
|
|
|
#include "collision.hh"
|
|
#include "profiler.hh"
|
|
#include "renderer.hh"
|
|
#include "splashpack.hh"
|
|
#include "streq.hh"
|
|
#include "luaapi.hh"
|
|
#include "loadingscreen.hh"
|
|
|
|
#include <psyqo/primitives/misc.hh>
|
|
|
|
#include "lua.h"
|
|
|
|
using namespace psyqo::trig_literals;
|
|
|
|
using namespace psyqo::fixed_point_literals;
|
|
|
|
using namespace psxsplash;
|
|
|
|
// Static member definition
|
|
psyqo::Font<>* psxsplash::SceneManager::s_font = nullptr;
|
|
|
|
void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingScreen* loading) {
|
|
auto& gpu = Renderer::GetInstance().getGPU();
|
|
|
|
L.Reset();
|
|
|
|
// Initialize audio system
|
|
m_audio.init();
|
|
|
|
// Register the Lua API
|
|
LuaAPI::RegisterAll(L.getState(), this, &m_cutscenePlayer, &m_uiSystem);
|
|
|
|
#ifdef PSXSPLASH_PROFILER
|
|
debug::Profiler::getInstance().initialize();
|
|
#endif
|
|
|
|
SplashpackSceneSetup sceneSetup;
|
|
m_loader.LoadSplashpack(splashpackData, sceneSetup);
|
|
|
|
if (loading && loading->isActive()) loading->updateProgress(gpu, 40);
|
|
|
|
m_luaFiles = std::move(sceneSetup.luaFiles);
|
|
m_gameObjects = std::move(sceneSetup.objects);
|
|
m_objectNames = std::move(sceneSetup.objectNames);
|
|
m_bvh = sceneSetup.bvh; // Copy BVH for frustum culling
|
|
m_worldCollision = sceneSetup.worldCollision; // World collision soup (v7+)
|
|
m_navRegions = sceneSetup.navRegions; // Nav region system (v7+)
|
|
m_playerNavRegion = m_navRegions.isLoaded() ? m_navRegions.getStartRegion() : NAV_NO_REGION;
|
|
|
|
// Scene type and render path
|
|
m_sceneType = sceneSetup.sceneType;
|
|
|
|
// Room/portal data for interior scenes (v11+)
|
|
m_rooms = sceneSetup.rooms;
|
|
m_roomCount = sceneSetup.roomCount;
|
|
m_portals = sceneSetup.portals;
|
|
m_portalCount = sceneSetup.portalCount;
|
|
m_roomTriRefs = sceneSetup.roomTriRefs;
|
|
m_roomTriRefCount = sceneSetup.roomTriRefCount;
|
|
|
|
// Configure fog from splashpack data (v11+)
|
|
if (sceneSetup.fogEnabled) {
|
|
psxsplash::FogConfig fogCfg;
|
|
fogCfg.enabled = true;
|
|
fogCfg.color = {.r = sceneSetup.fogR, .g = sceneSetup.fogG, .b = sceneSetup.fogB};
|
|
fogCfg.density = sceneSetup.fogDensity;
|
|
Renderer::GetInstance().SetFog(fogCfg);
|
|
} else {
|
|
psxsplash::FogConfig fogCfg;
|
|
fogCfg.enabled = false;
|
|
Renderer::GetInstance().SetFog(fogCfg);
|
|
}
|
|
// Copy component arrays
|
|
m_interactables = std::move(sceneSetup.interactables);
|
|
|
|
// Load audio clips into SPU RAM
|
|
m_audioClipNames = std::move(sceneSetup.audioClipNames);
|
|
for (size_t i = 0; i < sceneSetup.audioClips.size(); i++) {
|
|
auto& clip = sceneSetup.audioClips[i];
|
|
m_audio.loadClip((int)i, clip.adpcmData, clip.sizeBytes, clip.sampleRate, clip.loop);
|
|
}
|
|
|
|
if (loading && loading->isActive()) loading->updateProgress(gpu, 55);
|
|
|
|
// Copy cutscene data into scene manager storage (sceneSetup is stack-local)
|
|
m_cutsceneCount = sceneSetup.cutsceneCount;
|
|
for (int i = 0; i < m_cutsceneCount; i++) {
|
|
m_cutscenes[i] = sceneSetup.loadedCutscenes[i];
|
|
}
|
|
|
|
// Initialize cutscene player (v12+)
|
|
m_cutscenePlayer.init(
|
|
m_cutsceneCount > 0 ? m_cutscenes : nullptr,
|
|
m_cutsceneCount,
|
|
&m_currentCamera,
|
|
&m_audio,
|
|
&m_uiSystem
|
|
);
|
|
|
|
// Initialize UI system (v13+)
|
|
if (sceneSetup.uiCanvasCount > 0 && sceneSetup.uiTableOffset != 0 && s_font != nullptr) {
|
|
m_uiSystem.init(*s_font);
|
|
m_uiSystem.loadFromSplashpack(splashpackData, sceneSetup.uiCanvasCount,
|
|
sceneSetup.uiFontCount, sceneSetup.uiTableOffset);
|
|
m_uiSystem.uploadFonts(Renderer::GetInstance().getGPU());
|
|
Renderer::GetInstance().SetUISystem(&m_uiSystem);
|
|
|
|
if (loading && loading->isActive()) loading->updateProgress(gpu, 70);
|
|
|
|
// Resolve UI track handles: the splashpack loader stored raw name pointers
|
|
// in CutsceneTrack.target for UI tracks. Now that UISystem is loaded, resolve
|
|
// those names to canvas indices / element handles.
|
|
for (int ci = 0; ci < m_cutsceneCount; ci++) {
|
|
for (uint8_t ti = 0; ti < m_cutscenes[ci].trackCount; ti++) {
|
|
auto& track = m_cutscenes[ci].tracks[ti];
|
|
bool isUI = static_cast<uint8_t>(track.trackType) >= 5;
|
|
if (!isUI || track.target == nullptr) continue;
|
|
|
|
const char* nameStr = reinterpret_cast<const char*>(track.target);
|
|
track.target = nullptr; // Clear the temporary name pointer
|
|
|
|
if (track.trackType == TrackType::UICanvasVisible) {
|
|
// Name is just the canvas name
|
|
track.uiHandle = static_cast<int16_t>(m_uiSystem.findCanvas(nameStr));
|
|
} else {
|
|
// Name is "canvasName/elementName" — find the '/' separator
|
|
const char* sep = nameStr;
|
|
while (*sep && *sep != '/') sep++;
|
|
if (*sep == '/') {
|
|
// Temporarily null-terminate the canvas portion
|
|
// (nameStr points into splashpack data, which is mutable)
|
|
char* mutableSep = const_cast<char*>(sep);
|
|
*mutableSep = '\0';
|
|
int canvasIdx = m_uiSystem.findCanvas(nameStr);
|
|
*mutableSep = '/'; // Restore the separator
|
|
if (canvasIdx >= 0) {
|
|
track.uiHandle = static_cast<int16_t>(
|
|
m_uiSystem.findElement(canvasIdx, sep + 1));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
Renderer::GetInstance().SetUISystem(nullptr);
|
|
}
|
|
|
|
#ifdef PSXSPLASH_MEMOVERLAY
|
|
if (s_font != nullptr) {
|
|
m_memOverlay.init(s_font);
|
|
Renderer::GetInstance().SetMemOverlay(&m_memOverlay);
|
|
}
|
|
#endif
|
|
|
|
m_playerPosition = sceneSetup.playerStartPosition;
|
|
|
|
playerRotationX = 0.0_pi;
|
|
playerRotationY = 0.0_pi;
|
|
playerRotationZ = 0.0_pi;
|
|
|
|
m_playerHeight = sceneSetup.playerHeight;
|
|
|
|
m_controls.setMoveSpeed(sceneSetup.moveSpeed);
|
|
m_controls.setSprintSpeed(sceneSetup.sprintSpeed);
|
|
m_playerRadius = (int32_t)sceneSetup.playerRadius.value;
|
|
if (m_playerRadius == 0) m_playerRadius = PLAYER_RADIUS;
|
|
m_jumpVelocityRaw = (int32_t)sceneSetup.jumpVelocity.value;
|
|
int32_t gravityRaw = (int32_t)sceneSetup.gravity.value;
|
|
m_gravityPerFrame = gravityRaw / 30;
|
|
if (m_gravityPerFrame == 0 && gravityRaw > 0) m_gravityPerFrame = 1;
|
|
m_velocityY = 0;
|
|
m_isGrounded = true;
|
|
m_lastFrameTime = 0;
|
|
m_deltaFrames = 1;
|
|
|
|
m_collisionSystem.init();
|
|
|
|
for (size_t i = 0; i < sceneSetup.colliders.size(); i++) {
|
|
SPLASHPACKCollider* collider = sceneSetup.colliders[i];
|
|
if (collider == nullptr) continue;
|
|
|
|
AABB bounds;
|
|
bounds.min.x.value = collider->minX;
|
|
bounds.min.y.value = collider->minY;
|
|
bounds.min.z.value = collider->minZ;
|
|
bounds.max.x.value = collider->maxX;
|
|
bounds.max.y.value = collider->maxY;
|
|
bounds.max.z.value = collider->maxZ;
|
|
|
|
CollisionType type = static_cast<CollisionType>(collider->collisionType);
|
|
|
|
m_collisionSystem.registerCollider(
|
|
collider->gameObjectIndex,
|
|
bounds,
|
|
type,
|
|
collider->layerMask
|
|
);
|
|
}
|
|
|
|
for (size_t i = 0; i < sceneSetup.triggerBoxes.size(); i++) {
|
|
SPLASHPACKTriggerBox* tb = sceneSetup.triggerBoxes[i];
|
|
if (tb == nullptr) continue;
|
|
|
|
AABB bounds;
|
|
bounds.min.x.value = tb->minX;
|
|
bounds.min.y.value = tb->minY;
|
|
bounds.min.z.value = tb->minZ;
|
|
bounds.max.x.value = tb->maxX;
|
|
bounds.max.y.value = tb->maxY;
|
|
bounds.max.z.value = tb->maxZ;
|
|
|
|
m_collisionSystem.registerTriggerBox(bounds, tb->luaFileIndex);
|
|
}
|
|
|
|
|
|
for (int i = 0; i < m_luaFiles.size(); i++) {
|
|
auto luaFile = m_luaFiles[i];
|
|
L.LoadLuaFile(luaFile->luaCode, luaFile->length, i);
|
|
}
|
|
|
|
if (loading && loading->isActive()) loading->updateProgress(gpu, 85);
|
|
|
|
L.RegisterSceneScripts(sceneSetup.sceneLuaFileIndex);
|
|
|
|
L.OnSceneCreationStart();
|
|
|
|
for (auto object : m_gameObjects) {
|
|
L.RegisterGameObject(object);
|
|
}
|
|
|
|
m_controls.forceAnalogMode();
|
|
m_controls.Init();
|
|
Renderer::GetInstance().SetCamera(m_currentCamera);
|
|
|
|
L.OnSceneCreationEnd();
|
|
|
|
if (loading && loading->isActive()) loading->updateProgress(gpu, 95);
|
|
|
|
m_liveDataSize = sceneSetup.liveDataSize;
|
|
shrinkBuffer();
|
|
|
|
if (loading && loading->isActive()) loading->updateProgress(gpu, 100);
|
|
}
|
|
|
|
void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) {
|
|
LuaAPI::IncrementFrameCount();
|
|
|
|
m_cutscenePlayer.tick();
|
|
|
|
{
|
|
uint32_t now = gpu.now();
|
|
if (m_lastFrameTime != 0) {
|
|
uint32_t elapsed = now - m_lastFrameTime;
|
|
m_deltaFrames = (elapsed > 50000) ? 2 : 1;
|
|
if (elapsed > 83000) m_deltaFrames = 3;
|
|
}
|
|
m_lastFrameTime = now;
|
|
}
|
|
|
|
uint32_t renderingStart = gpu.now();
|
|
auto& renderer = psxsplash::Renderer::GetInstance();
|
|
if (m_sceneType == 1 && m_roomCount > 0 && m_rooms != nullptr) {
|
|
|
|
int camRoom = -1;
|
|
if (m_navRegions.isLoaded()) {
|
|
if (m_cutscenePlayer.isPlaying()) {
|
|
auto& camPos = m_currentCamera.GetPosition();
|
|
uint16_t camRegion = m_navRegions.findRegion(camPos.x.value, camPos.z.value);
|
|
if (camRegion != NAV_NO_REGION) {
|
|
uint8_t ri = m_navRegions.getRoomIndex(camRegion);
|
|
if (ri != 0xFF) camRoom = (int)ri;
|
|
}
|
|
} else if (m_playerNavRegion != NAV_NO_REGION) {
|
|
uint8_t ri = m_navRegions.getRoomIndex(m_playerNavRegion);
|
|
if (ri != 0xFF) camRoom = (int)ri;
|
|
}
|
|
}
|
|
renderer.RenderWithRooms(m_gameObjects, m_rooms, m_roomCount,
|
|
m_portals, m_portalCount, m_roomTriRefs, camRoom);
|
|
} else {
|
|
renderer.RenderWithBVH(m_gameObjects, m_bvh);
|
|
}
|
|
gpu.pumpCallbacks();
|
|
uint32_t renderingEnd = gpu.now();
|
|
uint32_t renderingTime = renderingEnd - renderingStart;
|
|
|
|
#ifdef PSXSPLASH_PROFILER
|
|
psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_RENDERING, renderingTime);
|
|
#endif
|
|
|
|
uint32_t collisionStart = gpu.now();
|
|
|
|
AABB playerAABB;
|
|
{
|
|
psyqo::FixedPoint<12> r;
|
|
r.value = m_playerRadius;
|
|
psyqo::FixedPoint<12> px = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.x);
|
|
psyqo::FixedPoint<12> py = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.y);
|
|
psyqo::FixedPoint<12> pz = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.z);
|
|
psyqo::FixedPoint<12> h = static_cast<psyqo::FixedPoint<12>>(m_playerHeight);
|
|
playerAABB.min = psyqo::Vec3{px - r, py - h, pz - r};
|
|
playerAABB.max = psyqo::Vec3{px + r, py, pz + r};
|
|
}
|
|
|
|
psyqo::Vec3 pushBack;
|
|
int collisionCount = m_collisionSystem.detectCollisions(playerAABB, pushBack);
|
|
|
|
{
|
|
psyqo::FixedPoint<12> zero;
|
|
if (pushBack.x != zero || pushBack.z != zero) {
|
|
m_playerPosition.x = m_playerPosition.x + pushBack.x;
|
|
m_playerPosition.z = m_playerPosition.z + pushBack.z;
|
|
}
|
|
}
|
|
|
|
// Fire onCollideWithPlayer Lua events on collided objects
|
|
const CollisionResult* results = m_collisionSystem.getResults();
|
|
for (int i = 0; i < collisionCount; i++) {
|
|
if (results[i].objectA != 0xFFFF) continue;
|
|
auto* obj = getGameObject(results[i].objectB);
|
|
if (obj) {
|
|
L.OnCollideWithPlayer(obj);
|
|
}
|
|
}
|
|
|
|
// Process trigger boxes (enter/exit)
|
|
m_collisionSystem.detectTriggers(playerAABB, *this);
|
|
|
|
gpu.pumpCallbacks();
|
|
uint32_t collisionEnd = gpu.now();
|
|
|
|
uint32_t luaStart = gpu.now();
|
|
// Lua update tick - call onUpdate for all registered objects with onUpdate handler
|
|
for (auto* go : m_gameObjects) {
|
|
if (go && go->isActive()) {
|
|
L.OnUpdate(go, m_deltaFrames);
|
|
}
|
|
}
|
|
gpu.pumpCallbacks();
|
|
uint32_t luaEnd = gpu.now();
|
|
uint32_t luaTime = luaEnd - luaStart;
|
|
#ifdef PSXSPLASH_PROFILER
|
|
psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_LUA, luaTime);
|
|
#endif
|
|
|
|
// Update game systems
|
|
processEnableDisableEvents();
|
|
|
|
|
|
uint32_t controlsStart = gpu.now();
|
|
|
|
// Update button state tracking first
|
|
m_controls.UpdateButtonStates();
|
|
|
|
// Update interaction system (checks for interact button press)
|
|
updateInteractionSystem();
|
|
|
|
// Dispatch button events to all objects
|
|
uint16_t pressed = m_controls.getButtonsPressed();
|
|
uint16_t released = m_controls.getButtonsReleased();
|
|
|
|
if (pressed || released) {
|
|
// Only iterate objects if there are button events
|
|
for (auto* go : m_gameObjects) {
|
|
if (!go || !go->isActive()) continue;
|
|
|
|
if (pressed) {
|
|
// Dispatch press events for each pressed button
|
|
for (int btn = 0; btn < 16; btn++) {
|
|
if (pressed & (1 << btn)) {
|
|
L.OnButtonPress(go, btn);
|
|
}
|
|
}
|
|
}
|
|
if (released) {
|
|
// Dispatch release events for each released button
|
|
for (int btn = 0; btn < 16; btn++) {
|
|
if (released & (1 << btn)) {
|
|
L.OnButtonRelease(go, btn);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
gpu.pumpCallbacks();
|
|
uint32_t controlsEnd = gpu.now();
|
|
uint32_t controlsTime = controlsEnd - controlsStart;
|
|
#ifdef PSXSPLASH_PROFILER
|
|
psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_CONTROLS, controlsTime);
|
|
#endif
|
|
|
|
uint32_t navmeshStart = gpu.now();
|
|
if (!freecam) {
|
|
if (m_worldCollision.isLoaded()) {
|
|
psyqo::Vec3 slid = m_worldCollision.moveAndSlide(
|
|
oldPlayerPosition, m_playerPosition, m_playerRadius, 0xFF);
|
|
|
|
|
|
m_playerPosition.x = slid.x;
|
|
m_playerPosition.z = slid.z;
|
|
|
|
// Apply gravity: velocity changes each frame
|
|
for (int f = 0; f < m_deltaFrames; f++) {
|
|
m_velocityY += m_gravityPerFrame;
|
|
}
|
|
|
|
// Apply vertical velocity to position
|
|
// velocityY is in fp12 per-second; convert per-frame: pos += vel / 30
|
|
int32_t posYDelta = (m_velocityY * m_deltaFrames) / 30;
|
|
m_playerPosition.y.value += posYDelta;
|
|
|
|
// Resolve floor Y from nav regions if available
|
|
if (m_navRegions.isLoaded()) {
|
|
uint16_t prevRegion = m_playerNavRegion;
|
|
int32_t px = m_playerPosition.x.value;
|
|
int32_t pz = m_playerPosition.z.value;
|
|
int32_t floorY = m_navRegions.resolvePosition(
|
|
px, pz, m_playerNavRegion);
|
|
|
|
|
|
if (m_playerNavRegion != NAV_NO_REGION) {
|
|
m_playerPosition.x.value = px;
|
|
m_playerPosition.z.value = pz;
|
|
|
|
// Ground (feet) position in PSX coords:
|
|
// Camera is at position.y, feet are at position.y + playerHeight
|
|
// (Y-down: larger Y = lower)
|
|
int32_t cameraAtFloor = floorY - m_playerHeight.raw();
|
|
|
|
if (m_playerPosition.y.value >= cameraAtFloor) {
|
|
// Player is at or below floor — snap to ground
|
|
m_playerPosition.y.value = cameraAtFloor;
|
|
m_velocityY = 0;
|
|
m_isGrounded = true;
|
|
} else {
|
|
// Player is above floor (jumping/airborne)
|
|
m_isGrounded = false;
|
|
}
|
|
} else {
|
|
// Off all nav regions — revert to old position
|
|
m_playerPosition = oldPlayerPosition;
|
|
m_playerNavRegion = prevRegion;
|
|
m_velocityY = 0;
|
|
m_isGrounded = true;
|
|
}
|
|
} else {
|
|
// Ground trace fallback (no nav regions)
|
|
int32_t groundY;
|
|
int32_t groundNormalY;
|
|
uint8_t surfFlags;
|
|
if (m_worldCollision.groundTrace(m_playerPosition,
|
|
4096 * 4, // max 4 units down
|
|
groundY, groundNormalY, surfFlags, 0xFF)) {
|
|
int32_t cameraAtFloor = groundY - m_playerHeight.raw();
|
|
if (m_playerPosition.y.value >= cameraAtFloor) {
|
|
m_playerPosition.y.value = cameraAtFloor;
|
|
m_velocityY = 0;
|
|
m_isGrounded = true;
|
|
} else {
|
|
m_isGrounded = false;
|
|
}
|
|
} else {
|
|
m_playerPosition = oldPlayerPosition;
|
|
m_velocityY = 0;
|
|
m_isGrounded = true;
|
|
}
|
|
}
|
|
|
|
// Ceiling check: if jumping upward, check for ceiling collision
|
|
if (m_velocityY < 0 && m_worldCollision.isLoaded()) {
|
|
int32_t ceilingY;
|
|
if (m_worldCollision.ceilingTrace(m_playerPosition,
|
|
m_playerHeight.raw(), ceilingY, 0xFF)) {
|
|
// Hit a ceiling — stop upward velocity
|
|
m_velocityY = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
gpu.pumpCallbacks();
|
|
uint32_t navmeshEnd = gpu.now();
|
|
uint32_t navmeshTime = navmeshEnd - navmeshStart;
|
|
#ifdef PSXSPLASH_PROFILER
|
|
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);
|
|
|
|
// Process pending scene transitions (at end of frame)
|
|
processPendingSceneLoad();
|
|
}
|
|
|
|
void psxsplash::SceneManager::fireTriggerEnter(int16_t luaFileIndex, uint16_t triggerIndex) {
|
|
if (luaFileIndex < 0) return;
|
|
L.OnTriggerEnterScript(luaFileIndex, triggerIndex);
|
|
}
|
|
|
|
void psxsplash::SceneManager::fireTriggerExit(int16_t luaFileIndex, uint16_t triggerIndex) {
|
|
if (luaFileIndex < 0) return;
|
|
L.OnTriggerExitScript(luaFileIndex, triggerIndex);
|
|
}
|
|
|
|
// ============================================================================
|
|
// INTERACTION SYSTEM
|
|
// ============================================================================
|
|
|
|
void psxsplash::SceneManager::updateInteractionSystem() {
|
|
// Get interact button state - Cross button by default
|
|
auto interactButton = psyqo::AdvancedPad::Button::Cross;
|
|
bool buttonPressed = m_controls.wasButtonPressed(interactButton);
|
|
|
|
if (!buttonPressed) return; // Early out if no interaction attempt
|
|
|
|
// Player position for distance check
|
|
psyqo::FixedPoint<12> playerX = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.x);
|
|
psyqo::FixedPoint<12> playerY = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.y);
|
|
psyqo::FixedPoint<12> playerZ = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.z);
|
|
|
|
// Find closest interactable in range
|
|
Interactable* closest = nullptr;
|
|
psyqo::FixedPoint<12> closestDistSq;
|
|
closestDistSq.value = 0x7FFFFFFF; // Max positive value
|
|
|
|
for (auto* interactable : m_interactables) {
|
|
if (!interactable || !interactable->canInteract()) continue;
|
|
|
|
// Check if object is active
|
|
auto* go = getGameObject(interactable->gameObjectIndex);
|
|
if (!go || !go->isActive()) continue;
|
|
|
|
// Calculate distance squared
|
|
psyqo::FixedPoint<12> dx = playerX - interactable->offsetX - go->position.x;
|
|
psyqo::FixedPoint<12> dy = playerY - interactable->offsetY - go->position.y;
|
|
psyqo::FixedPoint<12> dz = playerZ - interactable->offsetZ - go->position.z;
|
|
|
|
psyqo::FixedPoint<12> distSq = dx * dx + dy * dy + dz * dz;
|
|
|
|
// Check if in range and closer than current closest
|
|
if (distSq <= interactable->radiusSquared && distSq < closestDistSq) {
|
|
closest = interactable;
|
|
closestDistSq = distSq;
|
|
}
|
|
}
|
|
|
|
// Interact with closest
|
|
if (closest != nullptr) {
|
|
triggerInteraction(getGameObject(closest->gameObjectIndex));
|
|
closest->triggerCooldown();
|
|
}
|
|
}
|
|
|
|
void psxsplash::SceneManager::triggerInteraction(GameObject* interactable) {
|
|
if (!interactable) return;
|
|
L.OnInteract(interactable);
|
|
}
|
|
|
|
// ============================================================================
|
|
// ENABLE/DISABLE SYSTEM
|
|
// ============================================================================
|
|
|
|
void psxsplash::SceneManager::setObjectActive(GameObject* go, bool active) {
|
|
if (!go) return;
|
|
|
|
bool wasActive = go->isActive();
|
|
if (wasActive == active) return; // No change
|
|
|
|
go->setActive(active);
|
|
|
|
// Fire appropriate event
|
|
if (active) {
|
|
L.OnEnable(go);
|
|
} else {
|
|
L.OnDisable(go);
|
|
}
|
|
}
|
|
|
|
void psxsplash::SceneManager::processEnableDisableEvents() {
|
|
// Process any pending enable/disable flags (for batched operations)
|
|
for (auto* go : m_gameObjects) {
|
|
if (!go) continue;
|
|
|
|
if (go->isPendingEnable()) {
|
|
go->setPendingEnable(false);
|
|
if (!go->isActive()) {
|
|
go->setActive(true);
|
|
L.OnEnable(go);
|
|
}
|
|
}
|
|
|
|
if (go->isPendingDisable()) {
|
|
go->setPendingDisable(false);
|
|
if (go->isActive()) {
|
|
go->setActive(false);
|
|
L.OnDisable(go);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SCENE LOADING
|
|
// ============================================================================
|
|
|
|
void psxsplash::SceneManager::requestSceneLoad(int sceneIndex) {
|
|
if (sceneIndex == m_currentSceneIndex) return;
|
|
m_pendingSceneIndex = sceneIndex;
|
|
}
|
|
|
|
void psxsplash::SceneManager::processPendingSceneLoad() {
|
|
if (m_pendingSceneIndex < 0) return;
|
|
|
|
int targetIndex = m_pendingSceneIndex;
|
|
m_pendingSceneIndex = -1;
|
|
|
|
auto& gpu = Renderer::GetInstance().getGPU();
|
|
loadScene(gpu, targetIndex, /*isFirstScene=*/false);
|
|
}
|
|
|
|
void psxsplash::SceneManager::loadScene(psyqo::GPU& gpu, int sceneIndex, bool isFirstScene) {
|
|
// Build filename using the active backend's naming convention
|
|
char filename[32];
|
|
FileLoader::BuildSceneFilename(sceneIndex, filename, sizeof(filename));
|
|
|
|
psyqo::Prim::FastFill ff(psyqo::Color{.r = 0, .g = 0, .b = 0});
|
|
ff.rect = psyqo::Rect{0, 0, 320, 240};
|
|
gpu.sendPrimitive(ff);
|
|
ff.rect = psyqo::Rect{0, 256, 320, 240};
|
|
gpu.sendPrimitive(ff);
|
|
gpu.pumpCallbacks();
|
|
|
|
LoadingScreen loading;
|
|
if (s_font) {
|
|
if (loading.load(gpu, *s_font, sceneIndex)) {
|
|
loading.renderInitialAndFree(gpu);
|
|
}
|
|
}
|
|
|
|
if (!isFirstScene) {
|
|
// Tear down EVERYTHING in the current scene first —
|
|
// Lua VM, vector backing storage, audio. This returns as much
|
|
// heap memory as possible before any new allocation.
|
|
clearScene();
|
|
|
|
// Free old splashpack data BEFORE loading the new one.
|
|
// This avoids having both scene buffers in the heap simultaneously.
|
|
if (m_currentSceneData) {
|
|
FileLoader::Get().FreeFile(m_currentSceneData);
|
|
m_currentSceneData = nullptr;
|
|
}
|
|
}
|
|
|
|
if (loading.isActive()) loading.updateProgress(gpu, 20);
|
|
|
|
// Load scene data — use progress-aware variant so the loading bar
|
|
// animates during the (potentially slow) CD-ROM read.
|
|
int fileSize = 0;
|
|
uint8_t* newData = nullptr;
|
|
|
|
if (loading.isActive()) {
|
|
struct Ctx { LoadingScreen* ls; psyqo::GPU* gpu; };
|
|
Ctx ctx{&loading, &gpu};
|
|
FileLoader::LoadProgressInfo progress{
|
|
[](uint8_t pct, void* ud) {
|
|
auto* c = static_cast<Ctx*>(ud);
|
|
c->ls->updateProgress(*c->gpu, pct);
|
|
},
|
|
&ctx, 20, 30
|
|
};
|
|
newData = FileLoader::Get().LoadFileSyncWithProgress(
|
|
filename, fileSize, &progress);
|
|
} else {
|
|
newData = FileLoader::Get().LoadFileSync(filename, fileSize);
|
|
}
|
|
|
|
if (!newData && isFirstScene) {
|
|
// Fallback: try legacy name for backwards compatibility (PCdrv only)
|
|
newData = FileLoader::Get().LoadFileSync("output.bin", fileSize);
|
|
}
|
|
|
|
if (!newData) {
|
|
return;
|
|
}
|
|
|
|
if (loading.isActive()) loading.updateProgress(gpu, 30);
|
|
|
|
m_currentSceneData = newData;
|
|
m_currentSceneIndex = sceneIndex;
|
|
|
|
// Initialize with new data (creates fresh Lua VM inside)
|
|
InitializeScene(newData, loading.isActive() ? &loading : nullptr);
|
|
}
|
|
|
|
void psxsplash::SceneManager::clearScene() {
|
|
// 1. Shut down the Lua VM first — frees ALL Lua-allocated memory
|
|
// (bytecode, strings, tables, registry) in one shot via lua_close.
|
|
L.Shutdown();
|
|
|
|
// 2. Clear all vectors to free their heap storage (game objects, Lua files, names, etc)
|
|
{ eastl::vector<GameObject*> tmp; tmp.swap(m_gameObjects); }
|
|
{ eastl::vector<LuaFile*> tmp; tmp.swap(m_luaFiles); }
|
|
{ eastl::vector<const char*> tmp; tmp.swap(m_objectNames); }
|
|
{ eastl::vector<const char*> tmp; tmp.swap(m_audioClipNames); }
|
|
{ eastl::vector<Interactable*> tmp; tmp.swap(m_interactables); }
|
|
|
|
// 3. Reset hardware / subsystems
|
|
m_audio.reset(); // Free SPU RAM and stop all voices
|
|
m_collisionSystem.init(); // Re-init collision system
|
|
m_cutsceneCount = 0;
|
|
m_cutscenePlayer.init(nullptr, 0, nullptr, nullptr); // Reset cutscene player
|
|
// BVH, WorldCollision, and NavRegions will be overwritten by next load
|
|
|
|
// Reset UI system (disconnect from renderer before splashpack data disappears)
|
|
Renderer::GetInstance().SetUISystem(nullptr);
|
|
|
|
// Reset room/portal pointers (they point into splashpack data which is being freed)
|
|
m_rooms = nullptr;
|
|
m_roomCount = 0;
|
|
m_portals = nullptr;
|
|
m_portalCount = 0;
|
|
m_roomTriRefs = nullptr;
|
|
m_roomTriRefCount = 0;
|
|
m_sceneType = 0;
|
|
}
|
|
|
|
void psxsplash::SceneManager::shrinkBuffer() {
|
|
if (m_liveDataSize == 0 || m_currentSceneData == nullptr) return;
|
|
|
|
uint8_t* oldBase = m_currentSceneData;
|
|
|
|
uint8_t* volatile newBaseV = new uint8_t[m_liveDataSize];
|
|
uint8_t* newBase = newBaseV;
|
|
if (!newBase) return;
|
|
__builtin_memcpy(newBase, oldBase, m_liveDataSize);
|
|
|
|
intptr_t delta = reinterpret_cast<intptr_t>(newBase) - reinterpret_cast<intptr_t>(oldBase);
|
|
|
|
auto reloc = [delta](auto* ptr) -> decltype(ptr) {
|
|
if (!ptr) return ptr;
|
|
return reinterpret_cast<decltype(ptr)>(reinterpret_cast<intptr_t>(ptr) + delta);
|
|
};
|
|
|
|
for (auto& go : m_gameObjects) {
|
|
go = reloc(go);
|
|
go->polygons = reloc(go->polygons);
|
|
}
|
|
for (auto& lf : m_luaFiles) {
|
|
lf = reloc(lf);
|
|
lf->luaCode = reloc(lf->luaCode);
|
|
}
|
|
for (auto& name : m_objectNames) name = reloc(name);
|
|
for (auto& name : m_audioClipNames) name = reloc(name);
|
|
for (auto& inter : m_interactables) inter = reloc(inter);
|
|
|
|
m_bvh.relocate(delta);
|
|
m_worldCollision.relocate(delta);
|
|
m_navRegions.relocate(delta);
|
|
|
|
m_rooms = reloc(m_rooms);
|
|
m_portals = reloc(m_portals);
|
|
m_roomTriRefs = reloc(m_roomTriRefs);
|
|
|
|
for (int ci = 0; ci < m_cutsceneCount; ci++) {
|
|
auto& cs = m_cutscenes[ci];
|
|
cs.name = reloc(cs.name);
|
|
cs.audioEvents = reloc(cs.audioEvents);
|
|
for (uint8_t ti = 0; ti < cs.trackCount; ti++) {
|
|
auto& track = cs.tracks[ti];
|
|
track.keyframes = reloc(track.keyframes);
|
|
if (track.target) track.target = reloc(track.target);
|
|
}
|
|
}
|
|
|
|
m_uiSystem.relocate(delta);
|
|
|
|
if (!m_gameObjects.empty()) {
|
|
L.RelocateGameObjects(
|
|
reinterpret_cast<GameObject**>(m_gameObjects.data()),
|
|
m_gameObjects.size(), delta);
|
|
}
|
|
|
|
FileLoader::Get().FreeFile(oldBase);
|
|
m_currentSceneData = newBase;
|
|
}
|
|
|
|
// ============================================================================
|
|
// OBJECT NAME LOOKUP
|
|
// ============================================================================
|
|
|
|
|
|
psxsplash::GameObject* psxsplash::SceneManager::findObjectByName(const char* name) const {
|
|
if (!name || m_objectNames.empty()) return nullptr;
|
|
for (size_t i = 0; i < m_objectNames.size() && i < m_gameObjects.size(); i++) {
|
|
if (m_objectNames[i] && streq(m_objectNames[i], name)) {
|
|
return m_gameObjects[i];
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
int psxsplash::SceneManager::findAudioClipByName(const char* name) const {
|
|
if (!name || m_audioClipNames.empty()) return -1;
|
|
for (size_t i = 0; i < m_audioClipNames.size(); i++) {
|
|
if (m_audioClipNames[i] && streq(m_audioClipNames[i], name)) {
|
|
return static_cast<int>(i);
|
|
}
|
|
}
|
|
return -1;
|
|
}
|