diff --git a/.gitignore b/.gitignore index df2503c..bdf4baa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,108 +1,26 @@ -# ----------------- # -# Godot 4+ specific # -# ----------------- # - -# This is the most important folder to ignore. It contains the project's cache, -# imported asset data, and other generated files. It will be regenerated -# automatically by the editor. Committing it will bloat the repository and -# cause constant merge conflicts. +# Godot/editor cache .godot/ -.claude -.vscode -backup +.godot-home/ +demo/.godot/ -# Ignore user-specific editor settings. -# These files save the state of your editor (window layout, last open scenes, etc.) -# and should not be shared with the team. -.godot/editor/ -editor_layout.cfg -command_system_guide.md -*.md +# SCons and native build output +.sconsign.dblite +*.os +*.o +*.a +*.so +*.dll +*.dylib +*.framework/ +demo/addons/onebit_lua/bin/ +godot-cpp/bin/ +godot-cpp/gen/ -# ------------- # -# C# / .NET # -# ------------- # - -# Ignore the .mono directory, which contains Mono-related build files and cache for Godot 3.x. -# For Godot 4.x with .NET 6+, these files are typically inside .godot/mono -.mono/ - -# Ignore generated build artifacts from dotnet/MSBuild. -bin/ -obj/ - -# Ignore user-specific C# project files created by IDEs like Visual Studio or Rider. -*.csproj.user -*.sln.DotSettings.user - - -# -------------------------- # -# Exported Builds & Templates # -# -------------------------- # - -# Ignore directories where you might store exported builds of your game. -builds/ -export/ -exports/ - -# Ignore the actual exported game files, regardless of where they are. -*.pck -*.exe -*.AppImage -*.html -*.wasm -*.apk -*.aab - -# Ignore export templates, which are large and can be downloaded by any developer. -# You typically don't want these in your repository. -export_templates/ -templates/ -*.zip -*.tpz - - -# ----------------------------------- # -# IDE / Text Editor Specific Settings # -# ----------------------------------- # - -# Visual Studio Code +# Editor/OS noise .vscode/ - -# JetBrains Rider / IntelliJ .idea/ - -# Visual Studio -.vs/ - - -# ---------------- # -# OS Specific # -# ---------------- # - -# macOS .DS_Store -.AppleDouble -.LSOverride - -# Windows Thumbs.db -ehthumbs.db -Desktop.ini - -# ---------------- # -# Other Common # -# ---------------- # - -# Log files -debug_log.txt +# Logs *.log -turn_system_plan.md - -# Asset pack archives (you should unpack them and commit the assets, not the zip) -*.rar -*.7z - - -.CLAUDE.md diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..35ec1da --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "godot-cpp"] + path = godot-cpp + url = https://github.com/godotengine/godot-cpp.git + branch = master diff --git a/.sconsign.dblite b/.sconsign.dblite index 080eafb..9cd0717 100644 Binary files a/.sconsign.dblite and b/.sconsign.dblite differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..080afeb --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# OneBit Lua Runtime + +Native GDExtension prototype for the 1BitRL v2 Lua runtime contract. + +## Current Scope + +This runtime is intentionally small and breaking: + +- `api_version = 2` scripts only. +- No `game` table. +- Explicit `ctx` and `state` handler arguments. +- Lua emits commands and logs; it does not mutate the Godot world directly. +- Queries cross a Godot-owned query host boundary. +- Instruction budget is enabled for handler calls. + +Initial Lua API: + +```lua +sense.is_walkable(x, y) +sense.find_nearest(group) +move.step(dx, dy) +log.say(message, duration) +``` + +## Requirements + +- Godot 4.6. +- `godot-cpp` master, targeting `api_version=4.6`. +- SCons. +- A Lua development package with `pkg-config`. + +This machine currently uses Lua 5.1: + +```sh +pkg-config lua-5.1 --cflags --libs +``` + +Lua 5.1 is explicit for now because it is the installed system package. If the project moves to Lua 5.4, install a Lua 5.4 development package and build with the matching pkg-config name, for example: + +```sh +scons platform=linux target=template_debug api_version=4.6 lua_pkg=lua5.4 +``` + +Script semantics differ between Lua 5.1 and 5.4, so do not change the runtime version silently. + +## Build + +Clone `godot-cpp` as a submodule or checkout at `godot-cpp/`, then run: + +```sh +scons platform=linux target=template_debug api_version=4.6 -j4 +``` + +The demo extension library is written to: + +```text +demo/addons/onebit_lua/bin/ +``` + +## Smoke Test + +Run the demo project headlessly: + +```sh +godot --headless --path demo +``` + +Expected behavior: + +- Loads `OneBitLuaRuntime`. +- Loads a tiny `api_version = 2` script. +- Calls `on_update(ctx, state)`. +- Mutates `state.count`. +- Emits one `move` command. +- Emits one log entry. +- Calls the GDScript query host for `sense.is_walkable` and `sense.find_nearest`. + +## Safe Lua Libraries + +The runtime opens only selected Lua libraries: + +- base +- math +- string +- table + +It does not open: + +- `io` +- `os` +- `package` +- `debug` + +After opening the base library, the runtime removes dangerous dynamic-loading globals: + +- `dofile` +- `load` +- `loadfile` +- `loadstring` +- `module` +- `require` +- `collectgarbage` diff --git a/SConstruct b/SConstruct index 3191f01..aa2d7b9 100644 --- a/SConstruct +++ b/SConstruct @@ -3,7 +3,8 @@ env = SConscript("godot-cpp/SConstruct") env.Append(CPPPATH=["src"]) -env.ParseConfig("pkg-config lua-5.1 --cflags --libs") +lua_pkg = ARGUMENTS.get("lua_pkg", "lua-5.1") +env.ParseConfig("pkg-config {} --cflags --libs".format(lua_pkg)) sources = Glob("src/*.cpp") diff --git a/demo/test_runtime.gd b/demo/test_runtime.gd index 059c9d5..0637bf5 100644 --- a/demo/test_runtime.gd +++ b/demo/test_runtime.gd @@ -36,10 +36,12 @@ return { on_update = function(ctx, state) state.count = (state.count or 0) + 1 + state.world_x = ctx.self.world.x + state.tint_r = ctx.self.tint.r if not ctx.self.is_moving and sense.is_walkable(2, 3) then local nearest = sense.find_nearest("players") move.step(1, 0) - log.say("tick", 1.0) + log.say("tick " .. tostring(state.count) .. " target " .. tostring(nearest.entity_id), 1.0) end end } @@ -52,6 +54,8 @@ return { var ctx := { "self": { "is_moving": false, + "world": Vector2(2.0, 3.0), + "tint": Color(0.25, 0.5, 0.75, 1.0), }, } var state := {} @@ -61,10 +65,78 @@ return { get_tree().quit(1) return - if result.commands.size() != 1 or result.logs.size() != 1 or result.state.get("count", 0) != 1: + if result.commands.size() != 1 or result.logs.size() != 1 or result.state.get("count", 0) != 1 or result.state.get("world_x", 0) != 2 or result.state.get("tint_r", 0) != 0.25 or result.logs[0].message != "tick 1 target 7": push_error("unexpected result: " + str(result)) get_tree().quit(1) return + if not _expect_runtime_error(""" +return { + api_version = 2, + on_update = function(ctx, state) + sense.is_walkable = function() return true end + end +} +""", "read-only"): + get_tree().quit(1) + return + + if not _expect_runtime_error(""" +return { + api_version = 2, + on_update = function(ctx, state) + sense = nil + end +} +""", "globals are read-only"): + get_tree().quit(1) + return + + var mem_rt := OneBitLuaRuntime.new() + mem_rt.set_entity_id(42) + mem_rt.set_query_host(query_host) + mem_rt.set_instruction_budget(1000000) + mem_rt.set_memory_budget(1024 * 1024) + var mem_loaded := mem_rt.load_string(""" +return { + api_version = 2, + on_update = function(ctx, state) + local chunks = {} + for i = 1, 10000 do + chunks[i] = string.rep("x", 4000) .. tostring(i) + end + state.done = true + end +} +""") + if not mem_loaded.ok: + push_error("memory test failed to load: " + str(mem_loaded.error)) + get_tree().quit(1) + return + var mem_result := mem_rt.call_event("on_update", ctx, {}, []) + if mem_result.ok or not str(mem_result.error).contains("memory budget"): + push_error("memory budget test did not fail as expected: " + str(mem_result)) + get_tree().quit(1) + return + print("OneBitLuaRuntime smoke test passed: ", result) get_tree().quit(0) + + +func _expect_runtime_error(source: String, expected_error: String) -> bool: + var rt := OneBitLuaRuntime.new() + rt.set_entity_id(42) + rt.set_query_host(query_host) + var loaded := rt.load_string(source) + if not loaded.ok: + push_error("expected load success, got: " + str(loaded.error)) + return false + + var result := rt.call_event("on_update", {"self": {"is_moving": false}}, {}, []) + if result.ok: + push_error("expected runtime error, got success: " + str(result)) + return false + if not str(result.error).contains(expected_error) or not str(result.error).contains("[string \"\"]"): + push_error("unexpected runtime error: " + str(result.error)) + return false + return true diff --git a/src/onebit_lua_runtime.cpp b/src/onebit_lua_runtime.cpp index 429be15..3617670 100644 --- a/src/onebit_lua_runtime.cpp +++ b/src/onebit_lua_runtime.cpp @@ -3,8 +3,13 @@ #include #include #include +#include #include +#include #include +#include + +#include extern "C" { #include @@ -15,6 +20,8 @@ extern "C" { #define LUA_OK 0 #endif +static const char *ONEBIT_RUNTIME_REGISTRY_KEY = "onebit_lua_runtime"; + using namespace godot; void OneBitLuaRuntime::_bind_methods() { @@ -24,6 +31,7 @@ void OneBitLuaRuntime::_bind_methods() { ClassDB::bind_method(D_METHOD("set_entity_id", "entity_id"), &OneBitLuaRuntime::set_entity_id); ClassDB::bind_method(D_METHOD("set_query_host", "host"), &OneBitLuaRuntime::set_query_host); ClassDB::bind_method(D_METHOD("set_instruction_budget", "budget"), &OneBitLuaRuntime::set_instruction_budget); + ClassDB::bind_method(D_METHOD("set_memory_budget", "bytes"), &OneBitLuaRuntime::set_memory_budget); } OneBitLuaRuntime::OneBitLuaRuntime() { @@ -31,21 +39,69 @@ OneBitLuaRuntime::OneBitLuaRuntime() { } OneBitLuaRuntime::~OneBitLuaRuntime() { - _reset_lua(); + _close_lua(); } -void OneBitLuaRuntime::_reset_lua() { +void OneBitLuaRuntime::_close_lua() { if (lua) { _clear_handlers(); lua_close(lua); lua = nullptr; } +} - lua = luaL_newstate(); +void OneBitLuaRuntime::_reset_lua() { + _close_lua(); + + memory_used_bytes = 0; instruction_budget_exceeded = false; + memory_budget_exceeded = false; + lua = lua_newstate(&OneBitLuaRuntime::_lua_allocator, this); + if (!lua) { + memory_budget_exceeded = true; + return; + } + _open_safe_libraries(); _install_api(); } +void OneBitLuaRuntime::_open_safe_libraries() { + struct SafeLib { + const char *name; + lua_CFunction open; + }; + + const SafeLib safe_libs[] = { + { "", luaopen_base }, + { LUA_MATHLIBNAME, luaopen_math }, + { LUA_STRLIBNAME, luaopen_string }, + { LUA_TABLIBNAME, luaopen_table }, + { nullptr, nullptr }, + }; + + for (const SafeLib *lib = safe_libs; lib->open; lib++) { + lua_pushcfunction(lua, lib->open); + lua_pushstring(lua, lib->name); + lua_call(lua, 1, 0); + } + + const char *blocked_globals[] = { + "dofile", + "load", + "loadfile", + "loadstring", + "module", + "require", + "collectgarbage", + nullptr, + }; + + for (const char **name = blocked_globals; *name; name++) { + lua_pushnil(lua); + lua_setglobal(lua, *name); + } +} + void OneBitLuaRuntime::_clear_handlers() { if (!lua) { handlers.clear(); @@ -60,7 +116,7 @@ void OneBitLuaRuntime::_clear_handlers() { void OneBitLuaRuntime::_install_api() { lua_pushlightuserdata(lua, this); - lua_setglobal(lua, "__onebit_runtime"); + lua_setfield(lua, LUA_REGISTRYINDEX, ONEBIT_RUNTIME_REGISTRY_KEY); lua_newtable(lua); lua_pushlightuserdata(lua, this); @@ -69,19 +125,52 @@ void OneBitLuaRuntime::_install_api() { lua_pushlightuserdata(lua, this); lua_pushcclosure(lua, &OneBitLuaRuntime::_lua_sense_find_nearest, 1); lua_setfield(lua, -2, "find_nearest"); - lua_setglobal(lua, "sense"); + _set_readonly_global("sense"); lua_newtable(lua); lua_pushlightuserdata(lua, this); lua_pushcclosure(lua, &OneBitLuaRuntime::_lua_move_step, 1); lua_setfield(lua, -2, "step"); - lua_setglobal(lua, "move"); + _set_readonly_global("move"); lua_newtable(lua); lua_pushlightuserdata(lua, this); lua_pushcclosure(lua, &OneBitLuaRuntime::_lua_log_say, 1); lua_setfield(lua, -2, "say"); - lua_setglobal(lua, "log"); + _set_readonly_global("log"); +} + +void OneBitLuaRuntime::_set_readonly_global(const char *name) { + int api_table = lua_gettop(lua); + + lua_newtable(lua); + int proxy_table = lua_gettop(lua); + + lua_newtable(lua); + lua_pushvalue(lua, api_table); + lua_setfield(lua, -2, "__index"); + lua_pushcfunction(lua, &OneBitLuaRuntime::_lua_readonly_error); + lua_setfield(lua, -2, "__newindex"); + lua_pushboolean(lua, 0); + lua_setfield(lua, -2, "__metatable"); + lua_setmetatable(lua, proxy_table); + + lua_setglobal(lua, name); + lua_pop(lua, 1); +} + +void OneBitLuaRuntime::_push_script_environment() { + lua_newtable(lua); + int env_table = lua_gettop(lua); + + lua_newtable(lua); + lua_pushvalue(lua, LUA_GLOBALSINDEX); + lua_setfield(lua, -2, "__index"); + lua_pushcfunction(lua, &OneBitLuaRuntime::_lua_global_write_error); + lua_setfield(lua, -2, "__newindex"); + lua_pushboolean(lua, 0); + lua_setfield(lua, -2, "__metatable"); + lua_setmetatable(lua, env_table); } Dictionary OneBitLuaRuntime::_base_result(bool ok, const String &error) const { @@ -99,23 +188,51 @@ Dictionary OneBitLuaRuntime::_error_result(const String &error) const { return _base_result(false, error); } +String OneBitLuaRuntime::_pop_lua_error() { + String error; + if (lua && lua_gettop(lua) > 0 && lua_isstring(lua, -1)) { + error = String(lua_tostring(lua, -1)); + } else { + error = "Unknown Lua error"; + } + if (lua && lua_gettop(lua) > 0) { + lua_pop(lua, 1); + } + return error; +} + Dictionary OneBitLuaRuntime::load_string(const String &source) { + return _load_source(source, ""); +} + +Dictionary OneBitLuaRuntime::_load_source(const String &source, const String &source_name) { _reset_lua(); commands.clear(); logs.clear(); - CharString utf8 = source.utf8(); - if (luaL_loadstring(lua, utf8.get_data()) != LUA_OK) { - String error = String(lua_tostring(lua, -1)); - lua_pop(lua, 1); - return _error_result(error); + if (!lua) { + return _error_result("Failed to create Lua state"); } - if (lua_pcall(lua, 0, 1, 0) != LUA_OK) { - String error = String(lua_tostring(lua, -1)); - lua_pop(lua, 1); + CharString utf8 = source.utf8(); + CharString source_name_utf8 = source_name.utf8(); + if (luaL_loadbuffer(lua, utf8.get_data(), utf8.length(), source_name_utf8.get_data()) != LUA_OK) { + return _error_result(_pop_lua_error()); + } + + _push_script_environment(); + lua_setfenv(lua, -2); + + lua_pushcfunction(lua, &OneBitLuaRuntime::_traceback_handler); + lua_insert(lua, -2); + int error_handler = lua_gettop(lua) - 1; + + if (lua_pcall(lua, 0, 1, error_handler) != LUA_OK) { + String error = _pop_lua_error(); + lua_remove(lua, error_handler); return _error_result(error); } + lua_remove(lua, error_handler); if (!lua_istable(lua, -1)) { lua_pop(lua, 1); @@ -134,7 +251,7 @@ Dictionary OneBitLuaRuntime::load_script(const String &path) { } String source = file->get_as_text(); - return load_string(source); + return _load_source(source, path); } Dictionary OneBitLuaRuntime::_extract_handlers(int table_index, const String &source_name) { @@ -191,16 +308,27 @@ Dictionary OneBitLuaRuntime::call_event(const String &event_name, const Dictiona _push_variant(args[i]); } + int function_index = lua_gettop(lua) - (2 + int(args.size())); + lua_pushcfunction(lua, &OneBitLuaRuntime::_traceback_handler); + lua_insert(lua, function_index); + int error_handler = function_index; + lua_sethook(lua, &OneBitLuaRuntime::_instruction_hook, LUA_MASKCOUNT, int(instruction_budget)); int status = lua_pcall(lua, 2 + int(args.size()), 0, 0); lua_sethook(lua, nullptr, 0, 0); if (status != LUA_OK) { - String error = instruction_budget_exceeded ? String("Lua instruction budget exceeded") : String(lua_tostring(lua, -1)); - lua_pop(lua, 1); + String error = _pop_lua_error(); + if (instruction_budget_exceeded) { + error = "Lua instruction budget exceeded\n" + error; + } else if (memory_budget_exceeded) { + error = "Lua memory budget exceeded\n" + error; + } + lua_remove(lua, error_handler); luaL_unref(lua, LUA_REGISTRYINDEX, state_ref); return _error_result(error); } + lua_remove(lua, error_handler); lua_rawgeti(lua, LUA_REGISTRYINDEX, state_ref); Variant updated_state = _read_lua_value(-1); @@ -226,10 +354,74 @@ void OneBitLuaRuntime::set_instruction_budget(int64_t p_budget) { instruction_budget = p_budget > 0 ? p_budget : 1; } +void OneBitLuaRuntime::set_memory_budget(int64_t p_bytes) { + memory_budget_bytes = p_bytes > 0 ? p_bytes : 1; +} + +void *OneBitLuaRuntime::_lua_allocator(void *ud, void *ptr, size_t osize, size_t nsize) { + (void)osize; + OneBitLuaRuntime *runtime = static_cast(ud); + const size_t header_size = sizeof(size_t); + + if (nsize == 0) { + if (runtime) { + if (ptr) { + void *base_ptr = static_cast(ptr) - header_size; + size_t old_size = *static_cast(base_ptr); + runtime->memory_used_bytes -= int64_t(old_size); + std::free(base_ptr); + } + if (runtime->memory_used_bytes < 0) { + runtime->memory_used_bytes = 0; + } + } else if (ptr) { + std::free(static_cast(ptr) - header_size); + } + return nullptr; + } + + if (!runtime) { + void *base_ptr = ptr ? static_cast(ptr) - header_size : nullptr; + void *new_base = std::realloc(base_ptr, nsize + header_size); + if (!new_base) { + return nullptr; + } + *static_cast(new_base) = nsize; + return static_cast(new_base) + header_size; + } + + size_t old_raw_size = 0; + void *base_ptr = nullptr; + if (ptr) { + base_ptr = static_cast(ptr) - header_size; + old_raw_size = *static_cast(base_ptr); + } + + int64_t old_size = int64_t(old_raw_size); + int64_t new_size = int64_t(nsize); + int64_t delta = new_size - old_size; + if (delta > 0 && runtime->memory_used_bytes + delta > runtime->memory_budget_bytes) { + runtime->memory_budget_exceeded = true; + return nullptr; + } + + void *new_base = std::realloc(base_ptr, nsize + header_size); + if (!new_base) { + return nullptr; + } + *static_cast(new_base) = nsize; + + runtime->memory_used_bytes += delta; + if (runtime->memory_used_bytes < 0) { + runtime->memory_used_bytes = 0; + } + return static_cast(new_base) + header_size; +} + void OneBitLuaRuntime::_instruction_hook(lua_State *L, lua_Debug *debug) { (void)debug; OneBitLuaRuntime *runtime = nullptr; - lua_getglobal(L, "__onebit_runtime"); + lua_getfield(L, LUA_REGISTRYINDEX, ONEBIT_RUNTIME_REGISTRY_KEY); if (lua_islightuserdata(L, -1)) { runtime = static_cast(lua_touserdata(L, -1)); } @@ -241,6 +433,51 @@ void OneBitLuaRuntime::_instruction_hook(lua_State *L, lua_Debug *debug) { luaL_error(L, "Lua instruction budget exceeded"); } +int OneBitLuaRuntime::_lua_readonly_error(lua_State *L) { + const char *key = lua_tostring(L, 2); + if (!key) { + key = ""; + } + return luaL_error(L, "Lua API tables are read-only: cannot assign '%s'", key); +} + +int OneBitLuaRuntime::_lua_global_write_error(lua_State *L) { + const char *key = lua_tostring(L, 2); + if (!key) { + key = ""; + } + return luaL_error(L, "Lua globals are read-only: attempted to assign '%s'", key); +} + +int OneBitLuaRuntime::_traceback_handler(lua_State *L) { + const char *message = lua_tostring(L, 1); + if (!message) { + message = "Unknown Lua error"; + } + + luaL_Buffer buffer; + luaL_buffinit(L, &buffer); + luaL_addstring(&buffer, message); + luaL_addstring(&buffer, "\nstack traceback:"); + + lua_Debug ar; + int level = 1; + while (lua_getstack(L, level, &ar)) { + lua_getinfo(L, "Sln", &ar); + luaL_addstring(&buffer, "\n "); + luaL_addstring(&buffer, ar.short_src ? ar.short_src : ""); + luaL_addstring(&buffer, ":"); + CharString line = String::num_int64(ar.currentline).utf8(); + luaL_addstring(&buffer, line.get_data()); + luaL_addstring(&buffer, ": in "); + luaL_addstring(&buffer, ar.name ? ar.name : "?"); + level++; + } + + luaL_pushresult(&buffer); + return 1; +} + Variant OneBitLuaRuntime::_query(const String &query_name, const Dictionary ¶ms) { if (query_host_id == 0) { return Variant(); @@ -329,6 +566,15 @@ bool OneBitLuaRuntime::_push_variant(const Variant &value) { lua_pushstring(lua, utf8.get_data()); return true; } + case Variant::VECTOR2: { + Vector2 vector = value; + lua_newtable(lua); + lua_pushnumber(lua, vector.x); + lua_setfield(lua, -2, "x"); + lua_pushnumber(lua, vector.y); + lua_setfield(lua, -2, "y"); + return true; + } case Variant::VECTOR2I: { Vector2i vector = value; lua_newtable(lua); @@ -338,6 +584,30 @@ bool OneBitLuaRuntime::_push_variant(const Variant &value) { lua_setfield(lua, -2, "y"); return true; } + case Variant::VECTOR3: { + Vector3 vector = value; + lua_newtable(lua); + lua_pushnumber(lua, vector.x); + lua_setfield(lua, -2, "x"); + lua_pushnumber(lua, vector.y); + lua_setfield(lua, -2, "y"); + lua_pushnumber(lua, vector.z); + lua_setfield(lua, -2, "z"); + return true; + } + case Variant::COLOR: { + Color color = value; + lua_newtable(lua); + lua_pushnumber(lua, color.r); + lua_setfield(lua, -2, "r"); + lua_pushnumber(lua, color.g); + lua_setfield(lua, -2, "g"); + lua_pushnumber(lua, color.b); + lua_setfield(lua, -2, "b"); + lua_pushnumber(lua, color.a); + lua_setfield(lua, -2, "a"); + return true; + } case Variant::DICTIONARY: { Dictionary dict = value; lua_newtable(lua); diff --git a/src/onebit_lua_runtime.h b/src/onebit_lua_runtime.h index bfd11f8..4f0eec6 100644 --- a/src/onebit_lua_runtime.h +++ b/src/onebit_lua_runtime.h @@ -27,7 +27,10 @@ private: int64_t entity_id = 0; uint64_t query_host_id = 0; int64_t instruction_budget = 100000; + int64_t memory_budget_bytes = 16 * 1024 * 1024; + int64_t memory_used_bytes = 0; bool instruction_budget_exceeded = false; + bool memory_budget_exceeded = false; static void _bind_methods(); @@ -35,11 +38,21 @@ private: static int _lua_log_say(lua_State *L); static int _lua_sense_is_walkable(lua_State *L); static int _lua_sense_find_nearest(lua_State *L); + static int _lua_readonly_error(lua_State *L); + static int _lua_global_write_error(lua_State *L); + static int _traceback_handler(lua_State *L); static void _instruction_hook(lua_State *L, lua_Debug *debug); + static void *_lua_allocator(void *ud, void *ptr, size_t osize, size_t nsize); + void _close_lua(); void _reset_lua(); + void _open_safe_libraries(); void _install_api(); + void _set_readonly_global(const char *name); + void _push_script_environment(); void _clear_handlers(); + Dictionary _load_source(const String &source, const String &source_name); + String _pop_lua_error(); Dictionary _base_result(bool ok, const String &error = "") const; Dictionary _error_result(const String &error) const; Variant _query(const String &query_name, const Dictionary ¶ms); @@ -60,6 +73,7 @@ public: void set_entity_id(int64_t p_entity_id); void set_query_host(Object *p_host); void set_instruction_budget(int64_t p_budget); + void set_memory_budget(int64_t p_bytes); }; #endif diff --git a/src/onebit_lua_runtime.os b/src/onebit_lua_runtime.os index 178190f..3ae42f1 100644 Binary files a/src/onebit_lua_runtime.os and b/src/onebit_lua_runtime.os differ diff --git a/src/register_types.cpp b/src/register_types.cpp index a55aca3..5854218 100644 --- a/src/register_types.cpp +++ b/src/register_types.cpp @@ -30,6 +30,7 @@ GDExtensionBool GDE_EXPORT onebit_lua_library_init( godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization); init_obj.register_initializer(initialize_onebit_lua_module); + init_obj.register_terminator(uninitialize_onebit_lua_module); init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE); diff --git a/src/register_types.os b/src/register_types.os index 9160d44..e8b752c 100644 Binary files a/src/register_types.os and b/src/register_types.os differ diff --git a/src/samq2.code-workspace b/src/samq2.code-workspace new file mode 100644 index 0000000..950ad15 --- /dev/null +++ b/src/samq2.code-workspace @@ -0,0 +1,13 @@ +{ + "folders": [ + { + "path": "../../samq2" + }, + { + "path": ".." + } + ], + "settings": { + "git.ignoreLimitWarning": true + } +} \ No newline at end of file