473 lines
15 KiB
C++
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();
|
|
}
|
|
}
|