more fixes

This commit is contained in:
Jan Racek
2026-03-28 01:32:07 +01:00
parent bfab154547
commit b01b72751a
6 changed files with 158 additions and 144 deletions

View File

@@ -7,48 +7,60 @@ namespace psxsplash {
/** /**
* Interactable component - enables player interaction with objects. * Interactable component - enables player interaction with objects.
* *
* When the player is within interaction radius and presses the interact button, * When the player is within interaction radius and presses the interact button,
* the onInteract Lua event fires on the associated GameObject. * the onInteract Lua event fires on the associated GameObject.
*/ */
struct Interactable { struct Interactable {
// Interaction radius squared (fixed-point 12-bit, pre-squared for fast distance checks) // Interaction radius squared (fixed-point 12-bit, pre-squared for fast distance checks)
psyqo::FixedPoint<12> radiusSquared; psyqo::FixedPoint<12> radiusSquared;
// Interaction point offset from object center // Button index that triggers interaction (0-15, matches psyqo::AdvancedPad::Button)
psyqo::FixedPoint<12> offsetX;
psyqo::FixedPoint<12> offsetY;
psyqo::FixedPoint<12> offsetZ;
// Button index that triggers interaction (0-15)
uint8_t interactButton; uint8_t interactButton;
// Configuration flags // Configuration flags
uint8_t flags; // bit 0: isRepeatable, bit 1: showPrompt, bit 2: requireLineOfSight uint8_t flags; // bit 0: isRepeatable, bit 1: showPrompt, bit 2: requireLineOfSight
// Cooldown between interactions (in frames) // Cooldown between interactions (in frames)
uint16_t cooldownFrames; uint16_t cooldownFrames;
// Runtime state // Runtime state
uint16_t currentCooldown; // Frames remaining until can interact again uint16_t currentCooldown; // Frames remaining until can interact again
uint16_t gameObjectIndex; // Index of associated GameObject uint16_t gameObjectIndex; // Index of associated GameObject
// Prompt canvas name (null-terminated, max 15 chars + null)
char promptCanvasName[16];
// Flag accessors // Flag accessors
bool isRepeatable() const { return flags & 0x01; } bool isRepeatable() const { return flags & 0x01; }
bool showPrompt() const { return flags & 0x02; } bool showPrompt() const { return flags & 0x02; }
bool requireLineOfSight() const { return flags & 0x04; } bool requireLineOfSight() const { return flags & 0x04; }
// Check if ready to interact // Check if ready to interact
bool canInteract() const { return currentCooldown == 0; } bool canInteract() const {
// Non-repeatable interactions: once cooldown was set, it stays permanently
if (!isRepeatable() && cooldownFrames > 0 && currentCooldown == 0) {
// Check if we already triggered once (cooldownFrames acts as sentinel)
// We use a special value to mark "already used"
}
return currentCooldown == 0;
}
// Called when interaction happens // Called when interaction happens
void triggerCooldown() { currentCooldown = cooldownFrames; } void triggerCooldown() {
if (isRepeatable()) {
currentCooldown = cooldownFrames;
} else {
// Non-repeatable: set to max to permanently disable
currentCooldown = 0xFFFF;
}
}
// Called each frame to decrement cooldown // Called each frame to decrement cooldown
void update() { void update() {
if (currentCooldown > 0) currentCooldown--; if (currentCooldown > 0 && currentCooldown != 0xFFFF) currentCooldown--;
} }
}; };
static_assert(sizeof(Interactable) == 24, "Interactable is not 24 bytes"); static_assert(sizeof(Interactable) == 28, "Interactable must be 28 bytes");
} // namespace psxsplash } // namespace psxsplash

View File

@@ -1263,9 +1263,9 @@ int LuaAPI::Audio_StopAll(lua_State* L) {
int LuaAPI::Debug_Log(lua_State* L) { int LuaAPI::Debug_Log(lua_State* L) {
psyqo::Lua lua(L); psyqo::Lua lua(L);
if (lua.isString(1)) {
// Debug.Log is a no-op on PS1 to avoid printf overhead. printf("%s\n", lua.toString(1));
// Messages are only visible in emulator TTY builds. }
return 0; return 0;
} }

View File

@@ -11,6 +11,7 @@
#include "loadingscreen.hh" #include "loadingscreen.hh"
#include <psyqo/primitives/misc.hh> #include <psyqo/primitives/misc.hh>
#include <psyqo/trigonometry.hh>
#include "lua.h" #include "lua.h"
@@ -23,6 +24,13 @@ using namespace psxsplash;
// Static member definition // Static member definition
psyqo::Font<>* psxsplash::SceneManager::s_font = nullptr; psyqo::Font<>* psxsplash::SceneManager::s_font = nullptr;
// Default player collision radius: ~0.5 world units at GTE 100 -> 20 in 20.12
static constexpr int32_t PLAYER_RADIUS = 20;
// Interaction system state
static psyqo::Trig<> s_interactTrig;
static int s_activePromptCanvas = -1; // Currently shown prompt canvas index (-1 = none)
void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingScreen* loading) { void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingScreen* loading) {
auto& gpu = Renderer::GetInstance().getGPU(); auto& gpu = Renderer::GetInstance().getGPU();
@@ -47,7 +55,6 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingSc
m_gameObjects = std::move(sceneSetup.objects); m_gameObjects = std::move(sceneSetup.objects);
m_objectNames = std::move(sceneSetup.objectNames); m_objectNames = std::move(sceneSetup.objectNames);
m_bvh = sceneSetup.bvh; // Copy BVH for frustum culling 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_navRegions = sceneSetup.navRegions; // Nav region system (v7+)
m_playerNavRegion = m_navRegions.isLoaded() ? m_navRegions.getStartRegion() : NAV_NO_REGION; m_playerNavRegion = m_navRegions.isLoaded() ? m_navRegions.getStartRegion() : NAV_NO_REGION;
@@ -406,91 +413,41 @@ void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) {
#endif #endif
uint32_t navmeshStart = gpu.now(); uint32_t navmeshStart = gpu.now();
if (!freecam) { if (!freecam && m_navRegions.isLoaded()) {
if (m_worldCollision.isLoaded()) { // Apply gravity
psyqo::Vec3 slid = m_worldCollision.moveAndSlide( for (int f = 0; f < m_deltaFrames; f++) {
oldPlayerPosition, m_playerPosition, m_playerRadius, 0xFF); m_velocityY += m_gravityPerFrame;
}
// Apply vertical velocity to position
int32_t posYDelta = (m_velocityY * m_deltaFrames) / 30;
m_playerPosition.y.value += posYDelta;
m_playerPosition.x = slid.x; // Resolve position via nav regions
m_playerPosition.z = slid.z; 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);
// Apply gravity: velocity changes each frame if (m_playerNavRegion != NAV_NO_REGION) {
for (int f = 0; f < m_deltaFrames; f++) { m_playerPosition.x.value = px;
m_velocityY += m_gravityPerFrame; m_playerPosition.z.value = pz;
}
// 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 int32_t cameraAtFloor = floorY - m_playerHeight.raw();
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_playerPosition.y.value >= cameraAtFloor) {
if (m_playerNavRegion != NAV_NO_REGION) { m_playerPosition.y.value = cameraAtFloor;
m_playerPosition.x.value = px; m_velocityY = 0;
m_playerPosition.z.value = pz; m_isGrounded = true;
// 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 { } else {
// Ground trace fallback (no nav regions) m_isGrounded = false;
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;
}
} }
} else {
m_playerPosition = oldPlayerPosition;
m_playerNavRegion = prevRegion;
m_velocityY = 0;
m_isGrounded = true;
} }
} }
@@ -527,48 +484,88 @@ void psxsplash::SceneManager::fireTriggerExit(int16_t luaFileIndex, uint16_t tri
// ============================================================================ // ============================================================================
void psxsplash::SceneManager::updateInteractionSystem() { void psxsplash::SceneManager::updateInteractionSystem() {
// Get interact button state - Cross button by default // Tick cooldowns for all interactables
auto interactButton = psyqo::AdvancedPad::Button::Cross; for (auto* interactable : m_interactables) {
bool buttonPressed = m_controls.wasButtonPressed(interactButton); if (interactable) interactable->update();
}
if (!buttonPressed) return; // Early out if no interaction attempt
// Player position for distance checks
// Player position for distance check
psyqo::FixedPoint<12> playerX = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.x); 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> playerY = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.y);
psyqo::FixedPoint<12> playerZ = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.z); psyqo::FixedPoint<12> playerZ = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.z);
// Find closest interactable in range // Player forward direction from Y rotation (for line-of-sight checks)
Interactable* closest = nullptr; psyqo::FixedPoint<12> forwardX = s_interactTrig.sin(playerRotationY);
psyqo::FixedPoint<12> forwardZ = s_interactTrig.cos(playerRotationY);
// First pass: find which interactable is closest and in range (for prompt display)
Interactable* inRange = nullptr;
psyqo::FixedPoint<12> closestDistSq; psyqo::FixedPoint<12> closestDistSq;
closestDistSq.value = 0x7FFFFFFF; // Max positive value closestDistSq.value = 0x7FFFFFFF;
for (auto* interactable : m_interactables) { for (auto* interactable : m_interactables) {
if (!interactable || !interactable->canInteract()) continue; if (!interactable) continue;
// Check if object is active
auto* go = getGameObject(interactable->gameObjectIndex); auto* go = getGameObject(interactable->gameObjectIndex);
if (!go || !go->isActive()) continue; if (!go || !go->isActive()) continue;
// Calculate distance squared // Distance check
psyqo::FixedPoint<12> dx = playerX - interactable->offsetX - go->position.x; psyqo::FixedPoint<12> dx = playerX - go->position.x;
psyqo::FixedPoint<12> dy = playerY - interactable->offsetY - go->position.y; psyqo::FixedPoint<12> dy = playerY - go->position.y;
psyqo::FixedPoint<12> dz = playerZ - interactable->offsetZ - go->position.z; psyqo::FixedPoint<12> dz = playerZ - go->position.z;
psyqo::FixedPoint<12> distSq = dx * dx + dy * dy + dz * dz; psyqo::FixedPoint<12> distSq = dx * dx + dy * dy + dz * dz;
// Check if in range and closer than current closest if (distSq > interactable->radiusSquared) continue;
if (distSq <= interactable->radiusSquared && distSq < closestDistSq) {
closest = interactable; // Line-of-sight check: dot product of forward vector and direction to object
if (interactable->requireLineOfSight()) {
// dot = forwardX * dx + forwardZ * dz (XZ plane only)
// Negative dot means object is behind the player
psyqo::FixedPoint<12> dot = forwardX * dx + forwardZ * dz;
// Object must be in front of the player (dot < 0 in the coordinate system
// because dx points FROM player TO object, and forward points where player faces)
// Actually: dx = playerX - objX, so it points FROM object TO player.
// We want the object in front, so we need -dx direction to align with forward.
// dot(forward, objDir) where objDir = obj - player = -dx, -dz
psyqo::FixedPoint<12> facingDot = -(forwardX * dx + forwardZ * dz);
if (facingDot.value <= 0) continue; // Object is behind the player
}
if (distSq < closestDistSq) {
inRange = interactable;
closestDistSq = distSq; closestDistSq = distSq;
} }
} }
// Interact with closest // Prompt canvas management: show only when in range AND can interact
if (closest != nullptr) { int newPromptCanvas = -1;
triggerInteraction(getGameObject(closest->gameObjectIndex)); if (inRange && inRange->canInteract() && inRange->showPrompt() && inRange->promptCanvasName[0] != '\0') {
closest->triggerCooldown(); newPromptCanvas = m_uiSystem.findCanvas(inRange->promptCanvasName);
} }
if (newPromptCanvas != s_activePromptCanvas) {
// Hide old prompt
if (s_activePromptCanvas >= 0) {
m_uiSystem.setCanvasVisible(s_activePromptCanvas, false);
}
// Show new prompt
if (newPromptCanvas >= 0) {
m_uiSystem.setCanvasVisible(newPromptCanvas, true);
}
s_activePromptCanvas = newPromptCanvas;
}
// Check if the closest in-range interactable can be activated
if (!inRange || !inRange->canInteract()) return;
// Check if the correct button for this interactable was pressed
auto button = static_cast<psyqo::AdvancedPad::Button>(
static_cast<uint16_t>(inRange->interactButton));
if (!m_controls.wasButtonPressed(button)) return;
// Trigger the interaction
triggerInteraction(getGameObject(inRange->gameObjectIndex));
inRange->triggerCooldown();
} }
void psxsplash::SceneManager::triggerInteraction(GameObject* interactable) { void psxsplash::SceneManager::triggerInteraction(GameObject* interactable) {
@@ -728,8 +725,9 @@ void psxsplash::SceneManager::clearScene() {
m_audio.reset(); // Free SPU RAM and stop all voices m_audio.reset(); // Free SPU RAM and stop all voices
m_collisionSystem.init(); // Re-init collision system m_collisionSystem.init(); // Re-init collision system
m_cutsceneCount = 0; m_cutsceneCount = 0;
s_activePromptCanvas = -1; // Reset prompt tracking
m_cutscenePlayer.init(nullptr, 0, nullptr, nullptr); // Reset cutscene player m_cutscenePlayer.init(nullptr, 0, nullptr, nullptr); // Reset cutscene player
// BVH, WorldCollision, and NavRegions will be overwritten by next load // BVH and NavRegions will be overwritten by next load
// Reset UI system (disconnect from renderer before splashpack data disappears) // Reset UI system (disconnect from renderer before splashpack data disappears)
Renderer::GetInstance().SetUISystem(nullptr); Renderer::GetInstance().SetUISystem(nullptr);
@@ -774,7 +772,6 @@ void psxsplash::SceneManager::shrinkBuffer() {
for (auto& inter : m_interactables) inter = reloc(inter); for (auto& inter : m_interactables) inter = reloc(inter);
m_bvh.relocate(delta); m_bvh.relocate(delta);
m_worldCollision.relocate(delta);
m_navRegions.relocate(delta); m_navRegions.relocate(delta);
m_rooms = reloc(m_rooms); m_rooms = reloc(m_rooms);

View File

@@ -13,7 +13,6 @@
#include "gameobject.hh" #include "gameobject.hh"
#include "lua.h" #include "lua.h"
#include "splashpack.hh" #include "splashpack.hh"
#include "worldcollision.hh"
#include "navregion.hh" #include "navregion.hh"
#include "audiomanager.hh" #include "audiomanager.hh"
#include "interactable.hh" #include "interactable.hh"
@@ -104,7 +103,6 @@ class SceneManager {
psxsplash::SplashPackLoader m_loader; psxsplash::SplashPackLoader m_loader;
CollisionSystem m_collisionSystem; CollisionSystem m_collisionSystem;
BVHManager m_bvh; // Spatial acceleration for frustum culling BVHManager m_bvh; // Spatial acceleration for frustum culling
WorldCollision m_worldCollision; // Triangle-level world collision (v7+)
NavRegionSystem m_navRegions; // Convex region navigation (v7+) NavRegionSystem m_navRegions; // Convex region navigation (v7+)
uint16_t m_playerNavRegion = NAV_NO_REGION; // Current nav region for player uint16_t m_playerNavRegion = NAV_NO_REGION; // Current nav region for player
@@ -153,11 +151,11 @@ class SceneManager {
psyqo::FixedPoint<12, uint16_t> m_playerHeight; psyqo::FixedPoint<12, uint16_t> m_playerHeight;
int32_t m_playerRadius; // Collision radius in fp12 (replaces hardcoded PLAYER_RADIUS) int32_t m_playerRadius;
int32_t m_velocityY; // Vertical velocity in fp12 per second (negative = up) int32_t m_velocityY;
int32_t m_gravityPerFrame; // Gravity velocity change per frame (fp12) int32_t m_gravityPerFrame;
int32_t m_jumpVelocityRaw; // Initial jump velocity in fp12 per second int32_t m_jumpVelocityRaw;
bool m_isGrounded; // On the ground (can jump) bool m_isGrounded;
// Frame timing // Frame timing
uint32_t m_lastFrameTime; // gpu.now() timestamp of previous frame uint32_t m_lastFrameTime; // gpu.now() timestamp of previous frame

View File

@@ -13,7 +13,6 @@
#include "lua.h" #include "lua.h"
#include "mesh.hh" #include "mesh.hh"
#include "streq.hh" #include "streq.hh"
#include "worldcollision.hh"
#include "navregion.hh" #include "navregion.hh"
#include "renderer.hh" #include "renderer.hh"
@@ -151,10 +150,20 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
cursor += sizeof(psxsplash::Interactable); cursor += sizeof(psxsplash::Interactable);
} }
// Skip over legacy world collision data if present in older binaries
if (header->worldCollisionMeshCount > 0) { if (header->worldCollisionMeshCount > 0) {
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor); uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3); cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
cursor = const_cast<uint8_t*>(setup.worldCollision.initializeFromData(cursor)); // CollisionDataHeader: 20 bytes
const uint16_t meshCount = *reinterpret_cast<const uint16_t*>(cursor);
const uint16_t triCount = *reinterpret_cast<const uint16_t*>(cursor + 2);
const uint16_t chunkW = *reinterpret_cast<const uint16_t*>(cursor + 4);
const uint16_t chunkH = *reinterpret_cast<const uint16_t*>(cursor + 6);
cursor += 20; // CollisionDataHeader
cursor += meshCount * 32; // CollisionMeshHeader (32 bytes each)
cursor += triCount * 52; // CollisionTri (52 bytes each)
if (chunkW > 0 && chunkH > 0)
cursor += chunkW * chunkH * 4; // CollisionChunk (4 bytes each)
} }
if (header->navRegionCount > 0) { if (header->navRegionCount > 0) {

View File

@@ -8,7 +8,6 @@
#include "collision.hh" #include "collision.hh"
#include "gameobject.hh" #include "gameobject.hh"
#include "lua.h" #include "lua.h"
#include "worldcollision.hh"
#include "navregion.hh" #include "navregion.hh"
#include "audiomanager.hh" #include "audiomanager.hh"
#include "interactable.hh" #include "interactable.hh"
@@ -66,7 +65,6 @@ struct SplashpackSceneSetup {
eastl::vector<const char*> audioClipNames; eastl::vector<const char*> audioClipNames;
BVHManager bvh; // Spatial acceleration structure for culling BVHManager bvh; // Spatial acceleration structure for culling
WorldCollision worldCollision;
NavRegionSystem navRegions; NavRegionSystem navRegions;
psyqo::GTE::PackedVec3 playerStartPosition; psyqo::GTE::PackedVec3 playerStartPosition;
psyqo::GTE::PackedVec3 playerStartRotation; psyqo::GTE::PackedVec3 playerStartRotation;