Files
secretpsxsplash/src/scenemanager.cpp
2026-03-25 12:25:29 +01:00

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