#include "lua.h" #include #include #include #include #include "gameobject.hh" constexpr const char GAMEOBJECT_SCRIPT[] = R"( return function(metatable) local get_position = metatable.get_position local set_position = metatable.set_position local get_active = metatable.get_active local set_active = metatable.set_active local get_rotationY = metatable.get_rotationY local set_rotationY = metatable.set_rotationY metatable.get_position = nil metatable.set_position = nil metatable.get_active = nil metatable.set_active = nil metatable.get_rotationY = nil metatable.set_rotationY = nil function metatable.__index(self, key) local raw = rawget(self, key) if raw ~= nil then return raw end if key == "position" then return get_position(self.__cpp_ptr) elseif key == "active" then return get_active(self.__cpp_ptr) elseif key == "rotationY" then return get_rotationY(self.__cpp_ptr) end return nil end function metatable.__newindex(self, key, value) if key == "position" then set_position(self.__cpp_ptr, value) return elseif key == "active" then set_active(self.__cpp_ptr, value) return elseif key == "rotationY" then set_rotationY(self.__cpp_ptr, value) return end rawset(self, key, value) end end )"; // Lua helpers static constexpr lua_Number kFixedScale = 4096; static int gameobjectGetPosition(psyqo::Lua L) { auto go = L.toUserdata(1); L.newTable(); L.pushNumber(static_cast(go->position.x.raw()) / kFixedScale); L.setField(2, "x"); L.pushNumber(static_cast(go->position.y.raw()) / kFixedScale); L.setField(2, "y"); L.pushNumber(static_cast(go->position.z.raw()) / kFixedScale); L.setField(2, "z"); return 1; } static int gameobjectSetPosition(psyqo::Lua L) { auto go = L.toUserdata(1); L.getField(2, "x"); go->position.x = psyqo::FixedPoint<>(static_cast(L.toNumber(3) * kFixedScale), psyqo::FixedPoint<>::RAW); L.pop(); L.getField(2, "y"); go->position.y = psyqo::FixedPoint<>(static_cast(L.toNumber(3) * kFixedScale), psyqo::FixedPoint<>::RAW); L.pop(); L.getField(2, "z"); go->position.z = psyqo::FixedPoint<>(static_cast(L.toNumber(3) * kFixedScale), psyqo::FixedPoint<>::RAW); L.pop(); return 0; } static int gamobjectGetActive(psyqo::Lua L) { auto go = L.toUserdata(1); L.push(go->isActive()); return 1; } static int gamobjectSetActive(psyqo::Lua L) { auto go = L.toUserdata(1); bool active = L.toBoolean(2); go->setActive(active); return 0; } // Angle constants: psyqo::Angle is FixedPoint<10>, so 1.0_pi = raw 1024 static constexpr lua_Number kAngleScale = 1024; static psyqo::Trig<> s_trig; // Fast integer atan2 approximation → psyqo::Angle (pi-fraction units) // Uses linear approximation in first octant then folds to full circle. // Max error ~4° (acceptable for PS1 game objects). static psyqo::Angle fastAtan2(int32_t sinVal, int32_t cosVal) { psyqo::Angle result; if (cosVal == 0 && sinVal == 0) { result.value = 0; return result; } int32_t abs_s = sinVal < 0 ? -sinVal : sinVal; int32_t abs_c = cosVal < 0 ? -cosVal : cosVal; int32_t minV = abs_s < abs_c ? abs_s : abs_c; int32_t maxV = abs_s > abs_c ? abs_s : abs_c; // Compute angle in first octant [0, π/4 = 256 Angle units] // angle = (minV/maxV) * 256, using only 32-bit math. // Max minV for normalized sin/cos ≈ 4096, so minV * 256 ≈ 1M — fits int32. int32_t angle = (minV * 256) / maxV; // Past 45°: use complement if (abs_s > abs_c) angle = 512 - angle; // π/2 - angle // Quadrant 2/3: cos < 0 if (cosVal < 0) angle = 1024 - angle; // π - angle // Quadrant 3/4: sin < 0 if (sinVal < 0) angle = -angle; result.value = angle; return result; } static int gameobjectGetRotationY(psyqo::Lua L) { auto go = L.toUserdata(1); // Y rotation matrix: vs[0].x = cos(θ), vs[0].z = sin(θ) int32_t sinRaw = go->rotation.vs[0].z.raw(); int32_t cosRaw = go->rotation.vs[0].x.raw(); psyqo::Angle angle = fastAtan2(sinRaw, cosRaw); // Return in pi-units: 0.5 = π/2 = 90° L.pushNumber(static_cast(angle.value) / kAngleScale); return 1; } static int gameobjectSetRotationY(psyqo::Lua L) { auto go = L.toUserdata(1); lua_Number piUnits = L.toNumber(2); psyqo::Angle angle; angle.value = static_cast(piUnits * kAngleScale); go->rotation = psyqo::SoftMath::generateRotationMatrix33(angle, psyqo::SoftMath::Axis::Y, s_trig); return 0; } void psxsplash::Lua::Init() { auto L = m_state; // Load and run the game objects script if (L.loadBuffer(GAMEOBJECT_SCRIPT, "buffer:gameObjects") == 0) { if (L.pcall(0, 1) == 0) { // This will be our metatable L.newTable(); L.push(gameobjectGetPosition); L.setField(-2, "get_position"); L.push(gameobjectSetPosition); L.setField(-2, "set_position"); L.push(gamobjectGetActive); L.setField(-2, "get_active"); L.push(gamobjectSetActive); L.setField(-2, "set_active"); L.push(gameobjectGetRotationY); L.setField(-2, "get_rotationY"); L.push(gameobjectSetRotationY); L.setField(-2, "set_rotationY"); L.copy(-1); m_metatableReference = L.ref(); if (L.pcall(1, 0) == 0) { // success } else { printf("Error registering Lua script: %s\n", L.optString(-1, "Unknown error")); L.clearStack(); return; } } else { // Print Lua error if script execution fails printf("Error executing Lua script: %s\n", L.optString(-1, "Unknown error")); L.clearStack(); return; } } else { // Print Lua error if script loading fails printf("Error loading Lua script: %s\n", L.optString(-1, "Unknown error")); L.clearStack(); return; } L.newTable(); m_luascriptsReference = L.ref(); } void psxsplash::Lua::Shutdown() { // Close the Lua VM if it's still open. // Safe to call multiple times or on an already-closed VM. if (m_state.getState()) { m_state.close(); } m_metatableReference = LUA_NOREF; m_luascriptsReference = LUA_NOREF; m_luaSceneScriptsReference = LUA_NOREF; } void psxsplash::Lua::Reset() { // Nuclear reset: destroy the entire Lua VM and create a fresh one. Shutdown(); m_state = psyqo::Lua(); // fresh state (luaL_newstate + openlibs) Init(); } void psxsplash::Lua::LoadLuaFile(const char* code, size_t len, int index) { auto L = m_state; char filename[32]; snprintf(filename, sizeof(filename), "lua_asset:%d", index); if (L.loadBuffer(code, len, filename) != LUA_OK) { printf("Lua error: %s\n", L.toString(-1)); L.pop(); return; } // (1) script func L.rawGetI(LUA_REGISTRYINDEX, m_luascriptsReference); // (1) script func (2) scripts table L.newTable(); // (1) script func (2) scripts table (3) env {} // Give the environment a metatable that falls back to _G // so scripts can see Entity, Debug, Input, etc. L.newTable(); // (1) script func (2) scripts table (3) env {} (4) mt {} L.pushGlobalTable(); // (1) script func (2) scripts table (3) env {} (4) mt {} (5) _G L.setField(-2, "__index"); // (1) script func (2) scripts table (3) env {} (4) mt { __index = _G } L.setMetatable(-2); // (1) script func (2) scripts table (3) env { mt } L.pushNumber(index); // (1) script func (2) scripts table (3) env (4) index L.copy(-2); // (1) script func (2) scripts table (3) env (4) index (5) env L.setTable(-4); // (1) script func (2) scripts table (3) env lua_setupvalue(L.getState(), -3, 1); // (1) script func (2) scripts table L.pop(); // (1) script func if (L.pcall(0, 0)) { printf("Lua error: %s\n", L.toString(-1)); L.pop(); } } void psxsplash::Lua::RegisterSceneScripts(int index) { if (index < 0) return; auto L = m_state; L.newTable(); // (1) {} L.copy(1); // (1) {} (2) {} m_luaSceneScriptsReference = L.ref(); // (1) {} L.rawGetI(LUA_REGISTRYINDEX, m_luascriptsReference); // (1) {} (2) scripts table L.pushNumber(index); // (1) {} (2) script environments table (3) index L.getTable(-2); // (1) {} (2) script environments table (3) script environment table for the scene if (!L.isTable(-1)) { // Scene Lua file index is invalid or script not loaded printf("Warning: scene Lua file index %d not found\n", index); L.pop(3); return; } onSceneCreationStartFunctionWrapper.resolveGlobal(L); onSceneCreationEndFunctionWrapper.resolveGlobal(L); L.pop(3); // empty stack } // We're going to store the Lua table for the object at the address of the object, // and the table for its methods at the address of the object + 1 byte. void psxsplash::Lua::RegisterGameObject(GameObject* go) { uint8_t* ptr = reinterpret_cast(go); auto L = m_state; L.push(ptr); // (1) go L.newTable(); // (1) go (2) {} L.push(ptr); // (1) go (2) {} (3) go L.setField(-2, "__cpp_ptr"); // (1) go (2) { __cpp_ptr = go } L.rawGetI(LUA_REGISTRYINDEX, m_metatableReference); // (1) go (2) { __cpp_ptr = go } (3) metatable if (L.isTable(-1)) { L.setMetatable(-2); } else { printf("Warning: metatableForAllGameObjects not found\n"); L.pop(); } // (1) go (2) { __cpp_ptr = go + metatable } L.rawSet(LUA_REGISTRYINDEX); // empty stack L.newTable(); // (1) {} L.push(ptr + 1); // (1) {} (2) go + 1 L.copy(1); // (1) {} (2) go + 1 (3) {} L.rawSet(LUA_REGISTRYINDEX); // (1) {} // Initialize event mask for this object uint32_t eventMask = EVENT_NONE; if (go->luaFileIndex != -1) { L.rawGetI(LUA_REGISTRYINDEX, m_luascriptsReference); // (1) {} (2) script environments table L.rawGetI(-1, go->luaFileIndex); // (1) {} (2) script environments table (3) script environment table for this object // Guard: if the script file failed to load (e.g. compilation error), // the environment will be nil — skip event resolution. if (!L.isTable(-1)) { L.pop(2); } else { // 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 (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 (onTriggerExitMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_TRIGGER_EXIT; if (onUpdateMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_UPDATE; if (onDestroyMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_DESTROY; if (onEnableMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_ENABLE; if (onDisableMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_DISABLE; if (onButtonPressMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_BUTTON_PRESS; if (onButtonReleaseMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_BUTTON_RELEASE; L.pop(2); // (1) {} } } // Store the event mask directly in the GameObject go->eventMask = eventMask; L.pop(); // empty stack // Fire onCreate event if this object handles it if (eventMask & EVENT_ON_CREATE) { onCreateMethodWrapper.callMethod(*this, go); } } void psxsplash::Lua::OnCollideWithPlayer(GameObject* self) { if (!hasEvent(self, EVENT_ON_COLLISION)) return; onCollideWithPlayerMethodWrapper.callMethod(*this, self); } void psxsplash::Lua::OnInteract(GameObject* self) { if (!hasEvent(self, EVENT_ON_INTERACT)) return; onInteractMethodWrapper.callMethod(*this, self); } void psxsplash::Lua::OnTriggerEnter(GameObject* trigger, GameObject* other) { if (!hasEvent(trigger, EVENT_ON_TRIGGER_ENTER)) return; onTriggerEnterMethodWrapper.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); // Clear the event mask when object is destroyed go->eventMask = EVENT_NONE; } void psxsplash::Lua::OnEnable(GameObject* go) { if (!hasEvent(go, EVENT_ON_ENABLE)) return; onEnableMethodWrapper.callMethod(*this, go); } void psxsplash::Lua::OnDisable(GameObject* go) { if (!hasEvent(go, EVENT_ON_DISABLE)) return; onDisableMethodWrapper.callMethod(*this, go); } void psxsplash::Lua::OnButtonPress(GameObject* go, int button) { if (!hasEvent(go, EVENT_ON_BUTTON_PRESS)) return; onButtonPressMethodWrapper.callMethod(*this, go, button); } void psxsplash::Lua::OnButtonRelease(GameObject* go, int button) { if (!hasEvent(go, EVENT_ON_BUTTON_RELEASE)) return; onButtonReleaseMethodWrapper.callMethod(*this, go, button); } void psxsplash::Lua::OnUpdate(GameObject* go, int deltaFrames) { if (!hasEvent(go, EVENT_ON_UPDATE)) return; onUpdateMethodWrapper.callMethod(*this, go, deltaFrames); } void psxsplash::Lua::PushGameObject(GameObject* go) { auto L = m_state; L.push(go); L.rawGet(LUA_REGISTRYINDEX); if (!L.isTable(-1)) { printf("Warning: GameObject not found in Lua registry\n"); L.pop(); } }