Files
secretpsxsplash/src/lua.cpp
2026-03-27 16:39:10 +01:00

473 lines
15 KiB
C++

#include "lua.h"
#include <psyqo-lua/lua.hh>
#include <psyqo/soft-math.hh>
#include <psyqo/trigonometry.hh>
#include <psyqo/xprintf.h>
#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<psxsplash::GameObject>(1);
L.newTable();
L.pushNumber(static_cast<lua_Number>(go->position.x.raw()) / kFixedScale);
L.setField(2, "x");
L.pushNumber(static_cast<lua_Number>(go->position.y.raw()) / kFixedScale);
L.setField(2, "y");
L.pushNumber(static_cast<lua_Number>(go->position.z.raw()) / kFixedScale);
L.setField(2, "z");
return 1;
}
static int gameobjectSetPosition(psyqo::Lua L) {
auto go = L.toUserdata<psxsplash::GameObject>(1);
L.getField(2, "x");
go->position.x = psyqo::FixedPoint<>(static_cast<int32_t>(L.toNumber(3) * kFixedScale), psyqo::FixedPoint<>::RAW);
L.pop();
L.getField(2, "y");
go->position.y = psyqo::FixedPoint<>(static_cast<int32_t>(L.toNumber(3) * kFixedScale), psyqo::FixedPoint<>::RAW);
L.pop();
L.getField(2, "z");
go->position.z = psyqo::FixedPoint<>(static_cast<int32_t>(L.toNumber(3) * kFixedScale), psyqo::FixedPoint<>::RAW);
L.pop();
return 0;
}
static int gamobjectGetActive(psyqo::Lua L) {
auto go = L.toUserdata<psxsplash::GameObject>(1);
L.push(go->isActive());
return 1;
}
static int gamobjectSetActive(psyqo::Lua L) {
auto go = L.toUserdata<psxsplash::GameObject>(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<psxsplash::GameObject>(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<lua_Number>(angle.value) / kAngleScale);
return 1;
}
static int gameobjectSetRotationY(psyqo::Lua L) {
auto go = L.toUserdata<psxsplash::GameObject>(1);
lua_Number piUnits = L.toNumber(2);
psyqo::Angle angle;
angle.value = static_cast<int32_t>(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<uint8_t*>(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();
}
}