From 480323f5b91ff81dcdd07c5fb10fff93c122f15a Mon Sep 17 00:00:00 2001 From: Jan Racek Date: Fri, 27 Mar 2026 16:39:10 +0100 Subject: [PATCH] Revamped collision system --- src/collision.cpp | 253 ++++++++++++++++------------------------- src/collision.hh | 32 ++++-- src/loadingscreen.cpp | 13 +++ src/lua.cpp | 42 +++++-- src/lua.h | 39 ++++--- src/scenemanager.cpp | 99 ++++++++++------ src/scenemanager.hh | 7 +- src/splashpack.cpp | 12 +- src/splashpack.hh | 19 +++- src/worldcollision.cpp | 5 - src/worldcollision.hh | 9 +- 11 files changed, 278 insertions(+), 252 deletions(-) diff --git a/src/collision.cpp b/src/collision.cpp index 187e3a0..0ff451b 100644 --- a/src/collision.cpp +++ b/src/collision.cpp @@ -3,17 +3,14 @@ #include -// Helper type alias for brevity using FP = psyqo::FixedPoint<12>; namespace psxsplash { -// Static member initialization psyqo::FixedPoint<12> SpatialGrid::WORLD_MIN = FP(-16); psyqo::FixedPoint<12> SpatialGrid::WORLD_MAX = FP(16); -psyqo::FixedPoint<12> SpatialGrid::CELL_SIZE = FP(4); // (32 / 8) = 4 +psyqo::FixedPoint<12> SpatialGrid::CELL_SIZE = FP(4); -// AABB expand implementation void AABB::expand(const psyqo::Vec3& delta) { psyqo::FixedPoint<12> zero; if (delta.x > zero) max.x = max.x + delta.x; @@ -35,7 +32,6 @@ void SpatialGrid::clear() { } void SpatialGrid::worldToGrid(const psyqo::Vec3& pos, int& gx, int& gy, int& gz) const { - // Clamp position to world bounds auto px = pos.x; auto py = pos.y; auto pz = pos.z; @@ -47,13 +43,10 @@ void SpatialGrid::worldToGrid(const psyqo::Vec3& pos, int& gx, int& gy, int& gz) if (pz < WORLD_MIN) pz = WORLD_MIN; if (pz > WORLD_MAX) pz = WORLD_MAX; - // Convert to grid coordinates (0 to GRID_SIZE-1) - // Using integer division after scaling gx = ((px - WORLD_MIN) / CELL_SIZE).integer(); gy = ((py - WORLD_MIN) / CELL_SIZE).integer(); gz = ((pz - WORLD_MIN) / CELL_SIZE).integer(); - // Clamp to valid range if (gx < 0) gx = 0; if (gx >= GRID_SIZE) gx = GRID_SIZE - 1; if (gy < 0) gy = 0; @@ -69,14 +62,12 @@ int SpatialGrid::getCellIndex(const psyqo::Vec3& pos) const { } void SpatialGrid::insert(uint16_t objectIndex, const AABB& bounds) { - // Get grid range for this AABB int minGx, minGy, minGz; int maxGx, maxGy, maxGz; worldToGrid(bounds.min, minGx, minGy, minGz); worldToGrid(bounds.max, maxGx, maxGy, maxGz); - // Insert into all overlapping cells for (int gz = minGz; gz <= maxGz; gz++) { for (int gy = minGy; gy <= maxGy; gy++) { for (int gx = minGx; gx <= maxGx; gx++) { @@ -86,8 +77,6 @@ void SpatialGrid::insert(uint16_t objectIndex, const AABB& bounds) { if (cell.count < MAX_OBJECTS_PER_CELL) { cell.objectIndices[cell.count++] = objectIndex; } - // If cell is full, object won't be in this cell (may miss collisions) - // This is a tradeoff for memory/performance } } } @@ -96,18 +85,15 @@ void SpatialGrid::insert(uint16_t objectIndex, const AABB& bounds) { int SpatialGrid::queryAABB(const AABB& bounds, uint16_t* output, int maxResults) const { int resultCount = 0; - // Get grid range for query AABB int minGx, minGy, minGz; int maxGx, maxGy, maxGz; worldToGrid(bounds.min, minGx, minGy, minGz); worldToGrid(bounds.max, maxGx, maxGy, maxGz); - // Track which objects we've already added (two 32-bit masks for objects 0-63) - uint32_t addedMaskLow = 0; // Objects 0-31 - uint32_t addedMaskHigh = 0; // Objects 32-63 + uint32_t addedMaskLow = 0; + uint32_t addedMaskHigh = 0; - // Query all overlapping cells for (int gz = minGz; gz <= maxGz; gz++) { for (int gy = minGy; gy <= maxGy; gy++) { for (int gx = minGx; gx <= maxGx; gx++) { @@ -117,7 +103,6 @@ int SpatialGrid::queryAABB(const AABB& bounds, uint16_t* output, int maxResults) for (int i = 0; i < cell.count; i++) { uint16_t objIndex = cell.objectIndices[i]; - // Skip if already added (using bitmask for objects 0-63) if (objIndex < 32) { uint32_t bit = 1U << objIndex; if (addedMaskLow & bit) continue; @@ -149,6 +134,7 @@ void CollisionSystem::init() { void CollisionSystem::reset() { m_colliderCount = 0; + m_triggerBoxCount = 0; m_resultCount = 0; m_triggerPairCount = 0; m_grid.clear(); @@ -166,6 +152,14 @@ void CollisionSystem::registerCollider(uint16_t gameObjectIndex, const AABB& loc data.gameObjectIndex = gameObjectIndex; } +void CollisionSystem::registerTriggerBox(const AABB& bounds, int16_t luaFileIndex) { + if (m_triggerBoxCount >= MAX_TRIGGER_BOXES) return; + + TriggerBoxData& tb = m_triggerBoxes[m_triggerBoxCount++]; + tb.bounds = bounds; + tb.luaFileIndex = luaFileIndex; +} + void CollisionSystem::updateCollider(uint16_t gameObjectIndex, const psyqo::Vec3& position, const psyqo::Matrix33& rotation) { for (int i = 0; i < m_colliderCount; i++) { @@ -177,90 +171,114 @@ void CollisionSystem::updateCollider(uint16_t gameObjectIndex, const psyqo::Vec3 } } -int CollisionSystem::detectCollisions() { +int CollisionSystem::detectCollisions(const AABB& playerAABB, psyqo::Vec3& pushBack) { m_resultCount = 0; + const FP zero(0); + pushBack = psyqo::Vec3{zero, zero, zero}; - // Clear and rebuild spatial grid + // Rebuild spatial grid with all colliders m_grid.clear(); for (int i = 0; i < m_colliderCount; i++) { m_grid.insert(i, m_colliders[i].bounds); } - // Check each collider against potential colliders from grid - for (int i = 0; i < m_colliderCount; i++) { - const CollisionData& colliderA = m_colliders[i]; - - // Skip if no collision type - if (colliderA.type == CollisionType::None) continue; - - // Query spatial grid for nearby objects - uint16_t nearby[32]; - int nearbyCount = m_grid.queryAABB(colliderA.bounds, nearby, 32); - - for (int j = 0; j < nearbyCount; j++) { - int otherIndex = nearby[j]; - - // Skip self - if (otherIndex == i) continue; - - // Skip if already processed (only process pairs once) - if (otherIndex < i) continue; - - const CollisionData& colliderB = m_colliders[otherIndex]; - - // Skip if no collision type - if (colliderB.type == CollisionType::None) continue; - - // Check layer masks - if ((colliderA.layerMask & colliderB.layerMask) == 0) continue; - - // Narrowphase AABB test - psyqo::Vec3 normal; - psyqo::FixedPoint<12> penetration; - - if (testAABB(colliderA.bounds, colliderB.bounds, normal, penetration)) { - // Collision detected - if (m_resultCount < MAX_COLLISION_RESULTS) { - CollisionResult& result = m_results[m_resultCount++]; - result.objectA = colliderA.gameObjectIndex; - result.objectB = colliderB.gameObjectIndex; - result.normal = normal; - result.penetration = penetration; - } - - // Handle triggers - if (colliderA.type == CollisionType::Trigger) { - updateTriggerState(i, otherIndex, true); - } - if (colliderB.type == CollisionType::Trigger) { - updateTriggerState(otherIndex, i, true); - } - } - } - } + // Test player AABB against all colliders for push-back + uint16_t nearby[32]; + int nearbyCount = m_grid.queryAABB(playerAABB, nearby, 32); - // Update trigger pairs that are no longer overlapping - for (int i = 0; i < m_triggerPairCount; i++) { - TriggerPair& pair = m_triggerPairs[i]; - pair.framesSinceContact++; + for (int j = 0; j < nearbyCount; j++) { + int idx = nearby[j]; + const CollisionData& collider = m_colliders[idx]; + if (collider.type == CollisionType::None) continue; - // If no contact for several frames, trigger exit - if (pair.framesSinceContact > 2 && pair.state != 2) { - pair.state = 2; // Exiting + psyqo::Vec3 normal; + psyqo::FixedPoint<12> penetration; + + if (testAABB(playerAABB, collider.bounds, normal, penetration)) { + // Accumulate push-back along the separation normal + pushBack.x = pushBack.x + normal.x * penetration; + pushBack.y = pushBack.y + normal.y * penetration; + pushBack.z = pushBack.z + normal.z * penetration; + + if (m_resultCount < MAX_COLLISION_RESULTS) { + CollisionResult& result = m_results[m_resultCount++]; + result.objectA = 0xFFFF; // player + result.objectB = collider.gameObjectIndex; + result.normal = normal; + result.penetration = penetration; + } } } return m_resultCount; } +void CollisionSystem::detectTriggers(const AABB& playerAABB, SceneManager& scene) { + int writeIndex = 0; + + // Mark all existing pairs as potentially stale + for (int i = 0; i < m_triggerPairCount; i++) { + m_triggerPairs[i].framesSinceContact++; + } + + // Test player against each trigger box + for (int ti = 0; ti < m_triggerBoxCount; ti++) { + const TriggerBoxData& tb = m_triggerBoxes[ti]; + + if (!playerAABB.intersects(tb.bounds)) continue; + + // Find existing pair + bool found = false; + for (int pi = 0; pi < m_triggerPairCount; pi++) { + if (m_triggerPairs[pi].triggerIndex == ti) { + m_triggerPairs[pi].framesSinceContact = 0; + if (m_triggerPairs[pi].state == 0) { + m_triggerPairs[pi].state = 1; // enter -> active + } + found = true; + break; + } + } + + // New pair: enter + if (!found && m_triggerPairCount < MAX_TRIGGER_PAIRS) { + TriggerPair& pair = m_triggerPairs[m_triggerPairCount++]; + pair.triggerIndex = ti; + pair.padding = 0; + pair.framesSinceContact = 0; + pair.state = 0; + pair.padding2 = 0; + } + } + + // Process pairs: fire events and clean up exited pairs + writeIndex = 0; + for (int i = 0; i < m_triggerPairCount; i++) { + TriggerPair& pair = m_triggerPairs[i]; + int16_t luaIdx = m_triggerBoxes[pair.triggerIndex].luaFileIndex; + + if (pair.state == 0) { + // Enter + scene.fireTriggerEnter(luaIdx, pair.triggerIndex); + pair.state = 1; + m_triggerPairs[writeIndex++] = pair; + } else if (pair.framesSinceContact > 2) { + // Exit + scene.fireTriggerExit(luaIdx, pair.triggerIndex); + } else { + // Still inside, keep alive + m_triggerPairs[writeIndex++] = pair; + } + } + m_triggerPairCount = writeIndex; +} + bool CollisionSystem::testAABB(const AABB& a, const AABB& b, psyqo::Vec3& normal, psyqo::FixedPoint<12>& penetration) const { - // Check for overlap on all axes if (a.max.x < b.min.x || a.min.x > b.max.x) return false; if (a.max.y < b.min.y || a.min.y > b.max.y) return false; if (a.max.z < b.min.z || a.min.z > b.max.z) return false; - // Calculate penetration on each axis auto overlapX1 = a.max.x - b.min.x; auto overlapX2 = b.max.x - a.min.x; auto overlapY1 = a.max.y - b.min.y; @@ -268,17 +286,14 @@ bool CollisionSystem::testAABB(const AABB& a, const AABB& b, auto overlapZ1 = a.max.z - b.min.z; auto overlapZ2 = b.max.z - a.min.z; - // Find minimum overlap axis auto minOverlapX = (overlapX1 < overlapX2) ? overlapX1 : overlapX2; auto minOverlapY = (overlapY1 < overlapY2) ? overlapY1 : overlapY2; auto minOverlapZ = (overlapZ1 < overlapZ2) ? overlapZ1 : overlapZ2; - // Constants for normals const FP zero(0); const FP one(1); const FP negOne(-1); - // Determine separation axis (axis with least penetration) if (minOverlapX <= minOverlapY && minOverlapX <= minOverlapZ) { penetration = minOverlapX; normal = psyqo::Vec3{(overlapX1 < overlapX2) ? negOne : one, zero, zero}; @@ -293,31 +308,6 @@ bool CollisionSystem::testAABB(const AABB& a, const AABB& b, return true; } -void CollisionSystem::updateTriggerState(uint16_t triggerIndex, uint16_t otherIndex, bool isOverlapping) { - // Look for existing pair - for (int i = 0; i < m_triggerPairCount; i++) { - TriggerPair& pair = m_triggerPairs[i]; - if (pair.triggerIndex == triggerIndex && pair.otherIndex == otherIndex) { - if (isOverlapping) { - pair.framesSinceContact = 0; - if (pair.state == 0) { - pair.state = 1; // Now staying - } - } - return; - } - } - - // New pair - add it - if (isOverlapping && m_triggerPairCount < MAX_TRIGGERS) { - TriggerPair& pair = m_triggerPairs[m_triggerPairCount++]; - pair.triggerIndex = triggerIndex; - pair.otherIndex = otherIndex; - pair.framesSinceContact = 0; - pair.state = 0; // New (enter event) - } -} - bool CollisionSystem::areColliding(uint16_t indexA, uint16_t indexB) const { for (int i = 0; i < m_resultCount; i++) { if ((m_results[i].objectA == indexA && m_results[i].objectB == indexB) || @@ -332,32 +322,26 @@ bool CollisionSystem::raycast(const psyqo::Vec3& origin, const psyqo::Vec3& dire psyqo::FixedPoint<12> maxDistance, psyqo::Vec3& hitPoint, psyqo::Vec3& hitNormal, uint16_t& hitObjectIndex) const { - // Simple brute-force raycast against all colliders - // TODO: Use spatial grid for optimization - auto closestT = maxDistance; bool hit = false; - // Fixed-point constants const FP zero(0); const FP one(1); const FP negOne(-1); const FP largeVal(1000); const FP negLargeVal(-1000); FP epsilon; - epsilon.value = 4; // ~0.001 in 20.12 fixed point + epsilon.value = 4; for (int i = 0; i < m_colliderCount; i++) { const CollisionData& collider = m_colliders[i]; if (collider.type == CollisionType::None) continue; - // Ray-AABB intersection test (slab method) const AABB& box = collider.bounds; auto tMin = negLargeVal; auto tMax = largeVal; - // X slab if (direction.x != zero) { auto invD = one / direction.x; auto t1 = (box.min.x - origin.x) * invD; @@ -369,7 +353,6 @@ bool CollisionSystem::raycast(const psyqo::Vec3& origin, const psyqo::Vec3& dire continue; } - // Y slab if (direction.y != zero) { auto invD = one / direction.y; auto t1 = (box.min.y - origin.y) * invD; @@ -381,7 +364,6 @@ bool CollisionSystem::raycast(const psyqo::Vec3& origin, const psyqo::Vec3& dire continue; } - // Z slab if (direction.z != zero) { auto invD = one / direction.z; auto t1 = (box.min.z - origin.z) * invD; @@ -402,14 +384,12 @@ bool CollisionSystem::raycast(const psyqo::Vec3& origin, const psyqo::Vec3& dire hitObjectIndex = collider.gameObjectIndex; hit = true; - // Calculate hit point hitPoint = psyqo::Vec3{ origin.x + direction.x * t, origin.y + direction.y * t, origin.z + direction.z * t }; - // Calculate normal (which face was hit) if ((hitPoint.x - box.min.x).abs() < epsilon) hitNormal = psyqo::Vec3{negOne, zero, zero}; else if ((hitPoint.x - box.max.x).abs() < epsilon) hitNormal = psyqo::Vec3{one, zero, zero}; else if ((hitPoint.y - box.min.y).abs() < epsilon) hitNormal = psyqo::Vec3{zero, negOne, zero}; @@ -422,37 +402,4 @@ bool CollisionSystem::raycast(const psyqo::Vec3& origin, const psyqo::Vec3& dire return hit; } -void CollisionSystem::processTriggerEvents(SceneManager& scene) { - // Process trigger pairs and fire Lua events - int writeIndex = 0; - - for (int i = 0; i < m_triggerPairCount; i++) { - TriggerPair& pair = m_triggerPairs[i]; - - // Get game object indices - uint16_t triggerObjIdx = m_colliders[pair.triggerIndex].gameObjectIndex; - uint16_t otherObjIdx = m_colliders[pair.otherIndex].gameObjectIndex; - - switch (pair.state) { - case 0: // Enter - scene.fireTriggerEnter(triggerObjIdx, otherObjIdx); - pair.state = 1; // Move to staying - m_triggerPairs[writeIndex++] = pair; - break; - - case 1: // Staying - scene.fireTriggerStay(triggerObjIdx, otherObjIdx); - m_triggerPairs[writeIndex++] = pair; - break; - - case 2: // Exit - scene.fireTriggerExit(triggerObjIdx, otherObjIdx); - // Don't copy - remove from list - break; - } - } - - m_triggerPairCount = writeIndex; -} - } // namespace psxsplash diff --git a/src/collision.hh b/src/collision.hh index 21126db..6e55fd1 100644 --- a/src/collision.hh +++ b/src/collision.hh @@ -13,8 +13,6 @@ class SceneManager; enum class CollisionType : uint8_t { None = 0, Solid = 1, - Trigger = 2, - Platform = 3 }; using CollisionMask = uint8_t; @@ -23,14 +21,12 @@ struct AABB { psyqo::Vec3 min; psyqo::Vec3 max; - // Check if this AABB intersects another bool intersects(const AABB& other) const { return (min.x <= other.max.x && max.x >= other.min.x) && (min.y <= other.max.y && max.y >= other.min.y) && (min.z <= other.max.z && max.z >= other.min.z); } - // Check if a point is inside this AABB bool contains(const psyqo::Vec3& point) const { return (point.x >= min.x && point.x <= max.x) && (point.y >= min.y && point.y <= max.y) && @@ -72,12 +68,18 @@ struct CollisionResult { psyqo::FixedPoint<12> penetration; }; +struct TriggerBoxData { + AABB bounds; + int16_t luaFileIndex; + uint16_t padding; +}; + struct TriggerPair { uint16_t triggerIndex; - uint16_t otherIndex; - uint8_t framesSinceContact; - uint8_t state; // 0=new, 1=staying, 2=exiting uint16_t padding; + uint8_t framesSinceContact; + uint8_t state; // 0=new(enter), 1=active, 2=exiting + uint16_t padding2; }; class SpatialGrid { @@ -109,7 +111,8 @@ private: class CollisionSystem { public: static constexpr int MAX_COLLIDERS = 64; - static constexpr int MAX_TRIGGERS = 32; + static constexpr int MAX_TRIGGER_BOXES = 32; + static constexpr int MAX_TRIGGER_PAIRS = 32; static constexpr int MAX_COLLISION_RESULTS = 32; CollisionSystem() = default; @@ -122,7 +125,11 @@ public: void updateCollider(uint16_t gameObjectIndex, const psyqo::Vec3& position, const psyqo::Matrix33& rotation); - int detectCollisions(); + void registerTriggerBox(const AABB& bounds, int16_t luaFileIndex); + + int detectCollisions(const AABB& playerAABB, psyqo::Vec3& pushBack); + + void detectTriggers(const AABB& playerAABB, class SceneManager& scene); const CollisionResult* getResults() const { return m_results; } int getResultCount() const { return m_resultCount; } @@ -134,24 +141,25 @@ public: psyqo::Vec3& hitPoint, psyqo::Vec3& hitNormal, uint16_t& hitObjectIndex) const; - void processTriggerEvents(class SceneManager& scene); int getColliderCount() const { return m_colliderCount; } private: CollisionData m_colliders[MAX_COLLIDERS]; int m_colliderCount = 0; + TriggerBoxData m_triggerBoxes[MAX_TRIGGER_BOXES]; + int m_triggerBoxCount = 0; + SpatialGrid m_grid; CollisionResult m_results[MAX_COLLISION_RESULTS]; int m_resultCount = 0; - TriggerPair m_triggerPairs[MAX_TRIGGERS]; + TriggerPair m_triggerPairs[MAX_TRIGGER_PAIRS]; int m_triggerPairCount = 0; bool testAABB(const AABB& a, const AABB& b, psyqo::Vec3& normal, psyqo::FixedPoint<12>& penetration) const; - void updateTriggerState(uint16_t triggerIndex, uint16_t otherIndex, bool isOverlapping); }; } // namespace psxsplash \ No newline at end of file diff --git a/src/loadingscreen.cpp b/src/loadingscreen.cpp index 76dd987..9643051 100644 --- a/src/loadingscreen.cpp +++ b/src/loadingscreen.cpp @@ -1,5 +1,7 @@ #include "loadingscreen.hh" #include "fileloader.hh" +#include +extern "C" int ramsyscall_printf(const char*, ...); #include "renderer.hh" #include @@ -35,16 +37,22 @@ bool LoadingScreen::load(psyqo::GPU& gpu, psyqo::Font<>& systemFont, int sceneIn // Build filename using the active backend's naming convention char filename[32]; FileLoader::BuildLoadingFilename(sceneIndex, filename, sizeof(filename)); + ramsyscall_printf("LoadingScreen: loading '%s'\n", filename); int fileSize = 0; uint8_t* data = FileLoader::Get().LoadFileSync(filename, fileSize); if (!data || fileSize < (int)sizeof(LoaderPackHeader)) { + ramsyscall_printf("LoadingScreen: FAILED to load (data=%p size=%d)\n", data, fileSize); if (data) FileLoader::Get().FreeFile(data); return false; } auto* header = reinterpret_cast(data); + ramsyscall_printf("LoadingScreen: magic='%c%c' ver=%d canvases=%d fonts=%d atlases=%d cluts=%d tableOff=%u\n", + header->magic[0], header->magic[1], header->version, + header->canvasCount, header->fontCount, header->atlasCount, header->clutCount, header->tableOffset); if (header->magic[0] != 'L' || header->magic[1] != 'P') { + ramsyscall_printf("LoadingScreen: bad magic, aborting\n"); FileLoader::Get().FreeFile(data); return false; } @@ -65,12 +73,17 @@ bool LoadingScreen::load(psyqo::GPU& gpu, psyqo::Font<>& systemFont, int sceneIn m_ui.uploadFonts(gpu); // Ensure canvas 0 is visible + ramsyscall_printf("LoadingScreen: canvasCount=%d\n", m_ui.getCanvasCount()); if (m_ui.getCanvasCount() > 0) { m_ui.setCanvasVisible(0, true); + int elemCount = m_ui.getCanvasElementCount(0); + ramsyscall_printf("LoadingScreen: canvas 0 has %d elements\n", elemCount); } // Find the progress bar named "loading" findProgressBar(); + ramsyscall_printf("LoadingScreen: hasProgressBar=%d barXY=(%d,%d) barWH=(%d,%d)\n", + m_hasProgressBar, m_barX, m_barY, m_barW, m_barH); return true; } diff --git a/src/lua.cpp b/src/lua.cpp index 094684a..2d17ecc 100644 --- a/src/lua.cpp +++ b/src/lua.cpp @@ -349,10 +349,9 @@ void psxsplash::Lua::RegisterGameObject(GameObject* go) { // Resolve each event and build the bitmask // Only events that exist in the script get their bit set if (onCreateMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_CREATE; - if (onCollisionMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_COLLISION; + if (onCollideWithPlayerMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_COLLISION; if (onInteractMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_INTERACT; if (onTriggerEnterMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_TRIGGER_ENTER; - if (onTriggerStayMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_TRIGGER_STAY; if (onTriggerExitMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_TRIGGER_EXIT; if (onUpdateMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_UPDATE; if (onDestroyMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_DESTROY; @@ -379,9 +378,9 @@ void psxsplash::Lua::RegisterGameObject(GameObject* go) { } } -void psxsplash::Lua::OnCollision(GameObject* self, GameObject* other) { +void psxsplash::Lua::OnCollideWithPlayer(GameObject* self) { if (!hasEvent(self, EVENT_ON_COLLISION)) return; - onCollisionMethodWrapper.callMethod(*this, self, other); + onCollideWithPlayerMethodWrapper.callMethod(*this, self); } void psxsplash::Lua::OnInteract(GameObject* self) { @@ -394,16 +393,41 @@ void psxsplash::Lua::OnTriggerEnter(GameObject* trigger, GameObject* other) { onTriggerEnterMethodWrapper.callMethod(*this, trigger, other); } -void psxsplash::Lua::OnTriggerStay(GameObject* trigger, GameObject* other) { - if (!hasEvent(trigger, EVENT_ON_TRIGGER_STAY)) return; - onTriggerStayMethodWrapper.callMethod(*this, trigger, other); -} - void psxsplash::Lua::OnTriggerExit(GameObject* trigger, GameObject* other) { if (!hasEvent(trigger, EVENT_ON_TRIGGER_EXIT)) return; onTriggerExitMethodWrapper.callMethod(*this, trigger, other); } +void psxsplash::Lua::OnTriggerEnterScript(int luaFileIndex, int triggerIndex) { + auto L = m_state; + L.rawGetI(LUA_REGISTRYINDEX, m_luascriptsReference); + L.rawGetI(-1, luaFileIndex); + if (!L.isTable(-1)) { L.clearStack(); return; } + L.push("onTriggerEnter", 14); + L.getTable(-2); + if (!L.isFunction(-1)) { L.clearStack(); return; } + L.pushNumber(triggerIndex); + if (L.pcall(1, 0) != LUA_OK) { + printf("Lua error: %s\n", L.toString(-1)); + } + L.clearStack(); +} + +void psxsplash::Lua::OnTriggerExitScript(int luaFileIndex, int triggerIndex) { + auto L = m_state; + L.rawGetI(LUA_REGISTRYINDEX, m_luascriptsReference); + L.rawGetI(-1, luaFileIndex); + if (!L.isTable(-1)) { L.clearStack(); return; } + L.push("onTriggerExit", 13); + L.getTable(-2); + if (!L.isFunction(-1)) { L.clearStack(); return; } + L.pushNumber(triggerIndex); + if (L.pcall(1, 0) != LUA_OK) { + printf("Lua error: %s\n", L.toString(-1)); + } + L.clearStack(); +} + void psxsplash::Lua::OnDestroy(GameObject* go) { if (!hasEvent(go, EVENT_ON_DESTROY)) return; onDestroyMethodWrapper.callMethod(*this, go); diff --git a/src/lua.h b/src/lua.h index f194363..90a8aab 100644 --- a/src/lua.h +++ b/src/lua.h @@ -31,14 +31,13 @@ enum EventMask : uint32_t { EVENT_ON_COLLISION = 1 << 1, EVENT_ON_INTERACT = 1 << 2, EVENT_ON_TRIGGER_ENTER = 1 << 3, - EVENT_ON_TRIGGER_STAY = 1 << 4, - EVENT_ON_TRIGGER_EXIT = 1 << 5, - EVENT_ON_UPDATE = 1 << 6, - EVENT_ON_DESTROY = 1 << 7, - EVENT_ON_ENABLE = 1 << 8, - EVENT_ON_DISABLE = 1 << 9, - EVENT_ON_BUTTON_PRESS = 1 << 10, - EVENT_ON_BUTTON_RELEASE = 1 << 11, + EVENT_ON_TRIGGER_EXIT = 1 << 4, + EVENT_ON_UPDATE = 1 << 5, + EVENT_ON_DESTROY = 1 << 6, + EVENT_ON_ENABLE = 1 << 7, + EVENT_ON_DISABLE = 1 << 8, + EVENT_ON_BUTTON_PRESS = 1 << 9, + EVENT_ON_BUTTON_RELEASE = 1 << 10, }; class Lua { @@ -70,11 +69,12 @@ class Lua { } // Event dispatchers - these check the bitmask before calling Lua - void OnCollision(GameObject* self, GameObject* other); + void OnCollideWithPlayer(GameObject* self); void OnInteract(GameObject* self); void OnTriggerEnter(GameObject* trigger, GameObject* other); - void OnTriggerStay(GameObject* trigger, GameObject* other); void OnTriggerExit(GameObject* trigger, GameObject* other); + void OnTriggerEnterScript(int luaFileIndex, int triggerIndex); + void OnTriggerExitScript(int luaFileIndex, int triggerIndex); void OnUpdate(GameObject* go, int deltaFrames); // Per-object update void OnDestroy(GameObject* go); void OnEnable(GameObject* go); @@ -157,19 +157,18 @@ class Lua { [[no_unique_address]] FunctionWrapper<1, typestring_is("onSceneCreationStart")> onSceneCreationStartFunctionWrapper; [[no_unique_address]] FunctionWrapper<2, typestring_is("onSceneCreationEnd")> onSceneCreationEndFunctionWrapper; - // Object-level events (methodId 100-111, offset to avoid collision with scene events) + // Object-level events (methodId 100+, offset to avoid collision with scene events) [[no_unique_address]] FunctionWrapper<100, typestring_is("onCreate")> onCreateMethodWrapper; - [[no_unique_address]] FunctionWrapper<101, typestring_is("onCollision")> onCollisionMethodWrapper; + [[no_unique_address]] FunctionWrapper<101, typestring_is("onCollideWithPlayer")> onCollideWithPlayerMethodWrapper; [[no_unique_address]] FunctionWrapper<102, typestring_is("onInteract")> onInteractMethodWrapper; [[no_unique_address]] FunctionWrapper<103, typestring_is("onTriggerEnter")> onTriggerEnterMethodWrapper; - [[no_unique_address]] FunctionWrapper<104, typestring_is("onTriggerStay")> onTriggerStayMethodWrapper; - [[no_unique_address]] FunctionWrapper<105, typestring_is("onTriggerExit")> onTriggerExitMethodWrapper; - [[no_unique_address]] FunctionWrapper<106, typestring_is("onUpdate")> onUpdateMethodWrapper; - [[no_unique_address]] FunctionWrapper<107, typestring_is("onDestroy")> onDestroyMethodWrapper; - [[no_unique_address]] FunctionWrapper<108, typestring_is("onEnable")> onEnableMethodWrapper; - [[no_unique_address]] FunctionWrapper<109, typestring_is("onDisable")> onDisableMethodWrapper; - [[no_unique_address]] FunctionWrapper<110, typestring_is("onButtonPress")> onButtonPressMethodWrapper; - [[no_unique_address]] FunctionWrapper<111, typestring_is("onButtonRelease")> onButtonReleaseMethodWrapper; + [[no_unique_address]] FunctionWrapper<104, typestring_is("onTriggerExit")> onTriggerExitMethodWrapper; + [[no_unique_address]] FunctionWrapper<105, typestring_is("onUpdate")> onUpdateMethodWrapper; + [[no_unique_address]] FunctionWrapper<106, typestring_is("onDestroy")> onDestroyMethodWrapper; + [[no_unique_address]] FunctionWrapper<107, typestring_is("onEnable")> onEnableMethodWrapper; + [[no_unique_address]] FunctionWrapper<108, typestring_is("onDisable")> onDisableMethodWrapper; + [[no_unique_address]] FunctionWrapper<109, typestring_is("onButtonPress")> onButtonPressMethodWrapper; + [[no_unique_address]] FunctionWrapper<110, typestring_is("onButtonRelease")> onButtonReleaseMethodWrapper; void PushGameObject(GameObject* go); diff --git a/src/scenemanager.cpp b/src/scenemanager.cpp index 2a96fad..90b90d9 100644 --- a/src/scenemanager.cpp +++ b/src/scenemanager.cpp @@ -178,7 +178,6 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingSc 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; @@ -187,10 +186,8 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingSc bounds.max.y.value = collider->maxY; bounds.max.z.value = collider->maxZ; - // Convert collision type CollisionType type = static_cast(collider->collisionType); - // Register with collision system m_collisionSystem.registerCollider( collider->gameObjectIndex, bounds, @@ -199,6 +196,22 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingSc ); } + // Register trigger boxes from splashpack data + 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); + } + // 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. @@ -289,21 +302,44 @@ void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) { // 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 + // Build player AABB from position + radius/height + AABB playerAABB; + { + psyqo::FixedPoint<12> r; + r.value = m_playerRadius; + psyqo::FixedPoint<12> px = static_cast>(m_playerPosition.x); + psyqo::FixedPoint<12> py = static_cast>(m_playerPosition.y); + psyqo::FixedPoint<12> pz = static_cast>(m_playerPosition.z); + psyqo::FixedPoint<12> h = static_cast>(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); + + // Apply push-back to player position + { + 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; } } - // Process trigger events (enter/stay/exit) - m_collisionSystem.processTriggerEvents(*this); + // 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(); @@ -489,29 +525,14 @@ void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) { 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::fireTriggerEnter(int16_t luaFileIndex, uint16_t triggerIndex) { + if (luaFileIndex < 0) return; + L.OnTriggerEnterScript(luaFileIndex, triggerIndex); } -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); - } +void psxsplash::SceneManager::fireTriggerExit(int16_t luaFileIndex, uint16_t triggerIndex) { + if (luaFileIndex < 0) return; + L.OnTriggerExitScript(luaFileIndex, triggerIndex); } // ============================================================================ @@ -746,8 +767,12 @@ void psxsplash::SceneManager::shrinkBuffer() { if (m_liveDataSize == 0 || m_currentSceneData == nullptr) return; uint8_t* oldBase = m_currentSceneData; - uint8_t* newBase = new uint8_t[m_liveDataSize]; - if (!newBase) return; + // Allocate the shrunk buffer. The volatile cast prevents the compiler + // from assuming operator new never returns NULL (it does with + // -fno-exceptions), which would let it optimize away the null check. + uint8_t* volatile newBaseV = new uint8_t[m_liveDataSize]; + uint8_t* newBase = newBaseV; + if (!newBase) return; // Heap exhausted — keep the full buffer __builtin_memcpy(newBase, oldBase, m_liveDataSize); intptr_t delta = reinterpret_cast(newBase) - reinterpret_cast(oldBase); diff --git a/src/scenemanager.hh b/src/scenemanager.hh index b0fccbb..8c038a4 100644 --- a/src/scenemanager.hh +++ b/src/scenemanager.hh @@ -36,10 +36,9 @@ class SceneManager { static void SetFont(psyqo::Font<>* font) { s_font = font; } static psyqo::Font<>* GetFont() { return s_font; } - // Trigger event callbacks (called by CollisionSystem) - void fireTriggerEnter(uint16_t triggerObjIdx, uint16_t otherObjIdx); - void fireTriggerStay(uint16_t triggerObjIdx, uint16_t otherObjIdx); - void fireTriggerExit(uint16_t triggerObjIdx, uint16_t otherObjIdx); + // Trigger event callbacks (called by CollisionSystem for trigger boxes) + void fireTriggerEnter(int16_t luaFileIndex, uint16_t triggerIndex); + void fireTriggerExit(int16_t luaFileIndex, uint16_t triggerIndex); // Get game object by index (for collision callbacks) GameObject* getGameObject(uint16_t index) { diff --git a/src/splashpack.cpp b/src/splashpack.cpp index d8e81a0..3f44e96 100644 --- a/src/splashpack.cpp +++ b/src/splashpack.cpp @@ -42,7 +42,7 @@ struct SPLASHPACKFileHeader { uint16_t bvhNodeCount; uint16_t bvhTriangleRefCount; uint16_t sceneType; - uint16_t pad0; + uint16_t triggerBoxCount; uint16_t worldCollisionMeshCount; uint16_t worldCollisionTriCount; uint16_t navRegionCount; @@ -93,7 +93,7 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup psyqo::Kernel::assert(data != nullptr, "Splashpack loading data pointer is null"); psxsplash::SPLASHPACKFileHeader *header = reinterpret_cast(data); psyqo::Kernel::assert(__builtin_memcmp(header->magic, "SP", 2) == 0, "Splashpack has incorrect magic"); - psyqo::Kernel::assert(header->version >= 15, "Splashpack version too old (need v15+): re-export from SplashEdit"); + psyqo::Kernel::assert(header->version >= 16, "Splashpack version too old (need v16+): re-export from SplashEdit"); setup.playerStartPosition = header->playerStartPos; setup.playerStartRotation = header->playerStartRot; @@ -137,6 +137,14 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup cursor += sizeof(psxsplash::SPLASHPACKCollider); } + // Read trigger boxes (after colliders) + setup.triggerBoxes.reserve(header->triggerBoxCount); + for (uint16_t i = 0; i < header->triggerBoxCount; i++) { + psxsplash::SPLASHPACKTriggerBox *tb = reinterpret_cast(cursor); + setup.triggerBoxes.push_back(tb); + cursor += sizeof(psxsplash::SPLASHPACKTriggerBox); + } + // BVH data if (header->bvhNodeCount > 0) { BVHNode* bvhNodes = reinterpret_cast(cursor); diff --git a/src/splashpack.hh b/src/splashpack.hh index 0621a89..7ccef56 100644 --- a/src/splashpack.hh +++ b/src/splashpack.hh @@ -19,25 +19,34 @@ namespace psxsplash { /** * Collision data as stored in the binary file (fixed layout for serialization) - * This is the binary-compatible version of CollisionData */ struct SPLASHPACKCollider { // AABB bounds in fixed-point (24 bytes) int32_t minX, minY, minZ; int32_t maxX, maxY, maxZ; // Collision metadata (8 bytes) - uint8_t collisionType; // CollisionType enum - uint8_t layerMask; // Which layers this collides with - uint16_t gameObjectIndex; // Which GameObject this belongs to - uint32_t padding; // Alignment padding + uint8_t collisionType; + uint8_t layerMask; + uint16_t gameObjectIndex; + uint32_t padding; }; static_assert(sizeof(SPLASHPACKCollider) == 32, "SPLASHPACKCollider must be 32 bytes"); +struct SPLASHPACKTriggerBox { + int32_t minX, minY, minZ; + int32_t maxX, maxY, maxZ; + int16_t luaFileIndex; + uint16_t padding; + uint32_t padding2; +}; +static_assert(sizeof(SPLASHPACKTriggerBox) == 32, "SPLASHPACKTriggerBox must be 32 bytes"); + struct SplashpackSceneSetup { int sceneLuaFileIndex; eastl::vector luaFiles; eastl::vector objects; eastl::vector colliders; + eastl::vector triggerBoxes; // New component arrays eastl::vector interactables; diff --git a/src/worldcollision.cpp b/src/worldcollision.cpp index 682bf72..e4d0de6 100644 --- a/src/worldcollision.cpp +++ b/src/worldcollision.cpp @@ -438,9 +438,6 @@ psyqo::Vec3 WorldCollision::moveAndSlide(const psyqo::Vec3& oldPos, const auto& tri = m_triangles[mesh.firstTriangle + ti]; triTests++; - // Skip trigger surfaces - if (tri.flags & SURFACE_TRIGGER) continue; - // Skip floor and ceiling triangles — Y is resolved by nav regions. // In PS1 space (Y-down): floor normals have ny < 0, ceiling ny > 0. // If |ny| > walkable slope threshold, it's a floor/ceiling, not a wall. @@ -511,7 +508,6 @@ bool WorldCollision::groundTrace(const psyqo::Vec3& pos, for (int ti = 0; ti < mesh.triangleCount; ti++) { const auto& tri = m_triangles[mesh.firstTriangle + ti]; - if (tri.flags & SURFACE_TRIGGER) continue; int32_t t = rayVsTriangle(ox, oy, oz, dx, dy, dz, tri); if (t >= 0 && t < bestDist) { @@ -560,7 +556,6 @@ bool WorldCollision::ceilingTrace(const psyqo::Vec3& pos, for (int ti = 0; ti < mesh.triangleCount; ti++) { const auto& tri = m_triangles[mesh.firstTriangle + ti]; - if (tri.flags & SURFACE_TRIGGER) continue; int32_t t = rayVsTriangle(ox, oy, oz, dx, dy, dz, tri); if (t >= 0 && t < bestDist) { diff --git a/src/worldcollision.hh b/src/worldcollision.hh index 9c99abd..85fb554 100644 --- a/src/worldcollision.hh +++ b/src/worldcollision.hh @@ -22,11 +22,10 @@ namespace psxsplash { // Surface flags — packed per-triangle, exported from SplashEdit // ============================================================================ enum SurfaceFlag : uint8_t { - SURFACE_SOLID = 0x01, // Normal solid wall/floor - SURFACE_SLOPE = 0x02, // Steep slope (treated as wall for movement) - SURFACE_STAIRS = 0x04, // Staircase (smooth Y interpolation) - SURFACE_TRIGGER = 0x08, // Non-solid trigger volume - SURFACE_NO_WALK = 0x10, // Marks geometry as non-walkable floor + SURFACE_SOLID = 0x01, + SURFACE_SLOPE = 0x02, + SURFACE_STAIRS = 0x04, + SURFACE_NO_WALK = 0x10, }; // ============================================================================