702 lines
26 KiB
C++
702 lines
26 KiB
C++
#include "scenemanager.hh"
|
|
|
|
#include <utility>
|
|
|
|
#include "collision.hh"
|
|
#include "profiler.hh"
|
|
#include "renderer.hh"
|
|
#include "splashpack.hh"
|
|
#include "luaapi.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) {
|
|
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);
|
|
|
|
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);
|
|
}
|
|
|
|
// 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);
|
|
|
|
// 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);
|
|
}
|
|
|
|
m_playerPosition = sceneSetup.playerStartPosition;
|
|
|
|
playerRotationX = 0.0_pi;
|
|
playerRotationY = 0.0_pi;
|
|
playerRotationZ = 0.0_pi;
|
|
|
|
m_playerHeight = sceneSetup.playerHeight;
|
|
|
|
// Load movement parameters from splashpack (v8+)
|
|
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; // fallback to default
|
|
m_jumpVelocityRaw = (int32_t)sceneSetup.jumpVelocity.value;
|
|
int32_t gravityRaw = (int32_t)sceneSetup.gravity.value;
|
|
m_gravityPerFrame = gravityRaw / 30; // Convert per-second² to per-frame velocity change
|
|
if (m_gravityPerFrame == 0 && gravityRaw > 0) m_gravityPerFrame = 1; // Ensure nonzero
|
|
m_velocityY = 0;
|
|
m_isGrounded = true;
|
|
m_lastFrameTime = 0;
|
|
m_deltaFrames = 1;
|
|
|
|
// Initialize collision system
|
|
m_collisionSystem.init();
|
|
|
|
// Register colliders from splashpack data
|
|
for (size_t i = 0; i < sceneSetup.colliders.size(); i++) {
|
|
SPLASHPACKCollider* collider = sceneSetup.colliders[i];
|
|
if (collider == nullptr) continue;
|
|
|
|
// Convert fixed-point values from binary format to AABB
|
|
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;
|
|
|
|
// Convert collision type
|
|
CollisionType type = static_cast<CollisionType>(collider->collisionType);
|
|
|
|
// Register with collision system
|
|
m_collisionSystem.registerCollider(
|
|
collider->gameObjectIndex,
|
|
bounds,
|
|
type,
|
|
collider->layerMask
|
|
);
|
|
}
|
|
|
|
// Load Lua files - order is important here. We need
|
|
// to load the Lua files before we register the game objects,
|
|
// as the game objects may reference Lua files by index.
|
|
for (int i = 0; i < m_luaFiles.size(); i++) {
|
|
auto luaFile = m_luaFiles[i];
|
|
L.LoadLuaFile(luaFile->luaCode, luaFile->length, i);
|
|
}
|
|
|
|
L.RegisterSceneScripts(sceneSetup.sceneLuaFileIndex);
|
|
|
|
L.OnSceneCreationStart();
|
|
|
|
// Register game objects
|
|
for (auto object : m_gameObjects) {
|
|
L.RegisterGameObject(object);
|
|
}
|
|
|
|
m_controls.Init();
|
|
Renderer::GetInstance().SetCamera(m_currentCamera);
|
|
|
|
L.OnSceneCreationEnd();
|
|
}
|
|
|
|
void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) {
|
|
LuaAPI::IncrementFrameCount();
|
|
|
|
// Tick cutscene player (advance frame and apply tracks before rendering)
|
|
m_cutscenePlayer.tick();
|
|
|
|
// Delta-time measurement: count elapsed frames based on gpu timer
|
|
// PS1 NTSC frame = ~33333 microseconds (30fps vsync)
|
|
{
|
|
uint32_t now = gpu.now();
|
|
if (m_lastFrameTime != 0) {
|
|
uint32_t elapsed = now - m_lastFrameTime;
|
|
// 33333us per frame at 30fps. If >50000us, we dropped a frame.
|
|
m_deltaFrames = (elapsed > 50000) ? 2 : 1;
|
|
if (elapsed > 83000) m_deltaFrames = 3; // Two frames dropped
|
|
}
|
|
m_lastFrameTime = now;
|
|
}
|
|
|
|
uint32_t renderingStart = gpu.now();
|
|
auto& renderer = psxsplash::Renderer::GetInstance();
|
|
// Dispatch render path based on scene type.
|
|
// Interior scenes (type 1) use room/portal occlusion; exterior scenes use BVH culling.
|
|
if (m_sceneType == 1 && m_roomCount > 0 && m_rooms != nullptr) {
|
|
// Determine which room the camera is in for portal culling.
|
|
// During cutscene playback the camera may be elsewhere from the player,
|
|
// so use the actual camera position to find the room.
|
|
int camRoom = -1;
|
|
if (m_navRegions.isLoaded()) {
|
|
if (m_cutscenePlayer.isPlaying()) {
|
|
// Camera-driven: look up nav region from camera world position
|
|
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) {
|
|
// Normal gameplay: use cached player nav 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
|
|
|
|
// Collision detection
|
|
uint32_t collisionStart = gpu.now();
|
|
int collisionCount = m_collisionSystem.detectCollisions();
|
|
|
|
// Process solid collisions - call OnCollision on BOTH objects
|
|
const CollisionResult* results = m_collisionSystem.getResults();
|
|
for (int i = 0; i < collisionCount; i++) {
|
|
auto* objA = getGameObject(results[i].objectA);
|
|
auto* objB = getGameObject(results[i].objectB);
|
|
if (objA && objB) {
|
|
L.OnCollision(objA, objB);
|
|
L.OnCollision(objB, objA); // Call on both objects
|
|
}
|
|
}
|
|
|
|
// Process trigger events (enter/stay/exit)
|
|
m_collisionSystem.processTriggerEvents(*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) {
|
|
// Priority: WorldCollision + NavRegions (v7) > NavGrid (v5) > Legacy Navmesh
|
|
if (m_worldCollision.isLoaded()) {
|
|
// Move-and-slide against world geometry (XZ walls only)
|
|
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();
|
|
}
|
|
|
|
// Trigger event callbacks
|
|
void psxsplash::SceneManager::fireTriggerEnter(uint16_t triggerObjIdx, uint16_t otherObjIdx) {
|
|
auto* trigger = getGameObject(triggerObjIdx);
|
|
auto* other = getGameObject(otherObjIdx);
|
|
if (trigger && other) {
|
|
L.OnTriggerEnter(trigger, other);
|
|
}
|
|
}
|
|
|
|
void psxsplash::SceneManager::fireTriggerStay(uint16_t triggerObjIdx, uint16_t otherObjIdx) {
|
|
auto* trigger = getGameObject(triggerObjIdx);
|
|
auto* other = getGameObject(otherObjIdx);
|
|
if (trigger && other) {
|
|
L.OnTriggerStay(trigger, other);
|
|
}
|
|
}
|
|
|
|
void psxsplash::SceneManager::fireTriggerExit(uint16_t triggerObjIdx, uint16_t otherObjIdx) {
|
|
auto* trigger = getGameObject(triggerObjIdx);
|
|
auto* other = getGameObject(otherObjIdx);
|
|
if (trigger && other) {
|
|
L.OnTriggerExit(trigger, other);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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 (PCdrv multi-scene)
|
|
// ============================================================================
|
|
|
|
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;
|
|
|
|
// Build filename: scene_N.splashpack
|
|
char filename[32];
|
|
snprintf(filename, sizeof(filename), "scene_%d.splashpack", targetIndex);
|
|
|
|
// 1. 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();
|
|
|
|
// 2. Free old splashpack data BEFORE loading the new one.
|
|
// This avoids having both scene buffers in the heap simultaneously,
|
|
// which is the primary source of fragmentation that prevents
|
|
// the Lua compiler from finding large contiguous blocks.
|
|
if (m_currentSceneData) {
|
|
SceneLoader::FreeFile(m_currentSceneData);
|
|
m_currentSceneData = nullptr;
|
|
}
|
|
|
|
// 3. Allocate new scene data (heap is now maximally consolidated)
|
|
int fileSize = 0;
|
|
uint8_t* newData = SceneLoader::LoadFile(filename, fileSize);
|
|
if (!newData) return;
|
|
|
|
m_currentSceneData = newData;
|
|
m_currentSceneIndex = targetIndex;
|
|
|
|
// 4. Initialize with new data (creates fresh Lua VM inside)
|
|
InitializeScene(newData);
|
|
}
|
|
|
|
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. Free vector BACKING STORAGE (not just contents).
|
|
// clear() only sets size=0 but keeps the allocated capacity.
|
|
// swap-with-empty releases the heap blocks so they can be coalesced.
|
|
{ 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;
|
|
}
|
|
|
|
// ============================================================================
|
|
// OBJECT NAME LOOKUP
|
|
// ============================================================================
|
|
|
|
// Inline streq (no libc on bare-metal PS1)
|
|
static bool name_eq(const char* a, const char* b) {
|
|
while (*a && *b) { if (*a++ != *b++) return false; }
|
|
return *a == *b;
|
|
}
|
|
|
|
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] && name_eq(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] && name_eq(m_audioClipNames[i], name)) {
|
|
return static_cast<int>(i);
|
|
}
|
|
}
|
|
return -1;
|
|
}
|