optimizations + bugfixes

* Bombs explode on damage/explosion
* Orbits spell fixed
* Performance: implemented shared component indexes,
compiled Lua caching, and updates only for changed projectile fields.
Also removed accumulating hidden entity slots.
This commit is contained in:
Gaël committed 2026-10-20 11:02:18 +02:00
1 parent 87bff164f1
commit a34f42d53e
17 files changed
+727 -131

No files matched your search

+109 -4
View File
@@ -86,10 +86,19 @@ double num(lua_State* L, int i, double def = 0) { return lua_isnumber(L, i) ? lu
std::string str(lua_State* L, int i) { return lua_isstring(L, i) ? lua_tostring(L, i) : ""; }
// ---- file loading ----
static size_t script_cache_revision = 0;
bool load_from_wak(LuaHost& h, lua_State* L, const std::string& path, std::string* err) {
static std::unordered_map<std::string, std::vector<uint8_t>> cache; // the scripts of the entities run again and again: read the archive once
if (LuaHost::scripts_changed) { cache.clear(); LuaHost::scripts_changed = false; } // (a mod replaced a file)
static std::unordered_map<std::string, std::vector<uint8_t>> compiled;
if (LuaHost::scripts_changed) { cache.clear(); compiled.clear(); ++script_cache_revision; LuaHost::scripts_changed = false; } // (a mod replaced a file)
if (const auto chunk = compiled.find(path); chunk != compiled.end()) {
// Load a fresh closure so each entity still receives its own environment.
// Parsing identical source for every per-frame projectile script is costly.
if (luaL_loadbuffer(L, (const char*)chunk->second.data(), chunk->second.size(), ("@" + path).c_str()) == 0) return true;
lua_pop(L, 1);
compiled.erase(chunk);
}
auto it = cache.find(path);
if (it == cache.end()) {
std::vector<uint8_t> raw;
@@ -117,6 +126,14 @@ bool load_from_wak(LuaHost& h, lua_State* L, const std::string& path, std::strin
lua_pop(L, 1);
return false;
}
std::vector<uint8_t> bytecode;
const auto writer = [](lua_State*, const void* bytes, size_t size, void* output) -> int {
auto& out = *static_cast<std::vector<uint8_t>*>(output);
const auto* begin = static_cast<const uint8_t*>(bytes);
out.insert(out.end(), begin, begin + size);
return 0;
};
if (lua_dump(L, writer, &bytecode) == 0 && !bytecode.empty()) compiled.emplace(path, std::move(bytecode));
return true;
}
@@ -461,6 +478,21 @@ LuaHost::LuaHost(const Wak& wak, GameRandom& rng) : wak_(wak), rng_(rng) {
L_ = luaL_newstate();
luaL_openlibs(L_);
register_api();
// Retain function identities for validated native script paths. Lua overrides
// (including overrides local to an entity's VM) must keep running through Lua.
auto capture = [&](const char* prefix) {
lua_pushnil(L_);
while (lua_next(L_, -2)) {
if (lua_type(L_, -2) == LUA_TSTRING && lua_isfunction(L_, -1)) {
const std::string key = std::string(prefix) + lua_tostring(L_, -2);
lua_pushvalue(L_, -1);
original_api_[key] = luaL_ref(L_, LUA_REGISTRYINDEX);
}
lua_pop(L_, 1);
}
};
lua_pushvalue(L_, LUA_GLOBALSINDEX); capture(""); lua_pop(L_, 1);
lua_getglobal(L_, "math"); capture("math."); lua_pop(L_, 1);
// Spawn functions the engine registers for every biome before its script runs (the script may override colours).
// Colours as RGBA hex, the table common to all biomes of the reference data (noita-tools maps.json).
static const struct { uint32_t rgba; const char* fn; } kDefaults[] = {
@@ -554,7 +586,7 @@ void LuaHost::register_api() {
register_host_natives(*this);
}
void LuaHost::register_native(const char* name, NativeFn fn) { reg(L_, *this, name, fn); }
void LuaHost::register_native(const char* name, NativeFn fn) { ++execution_revision_; reg(L_, *this, name, fn); }
// Pushes the current environment table (the globals when env is empty), made on first use: a table whose metatable sends missing names to the globals
static void push_env_table(lua_State* L, const std::string& key) {
@@ -580,7 +612,12 @@ static void get_fn(lua_State* L, const std::string& env, const std::string& name
}
void LuaHost::drop_env(const std::string& key) {
++execution_revision_;
if (key.empty()) return;
if (const auto files = compiled_files_.find(key); files != compiled_files_.end()) {
for (const auto& [file, ref] : files->second) luaL_unref(L_, LUA_REGISTRYINDEX, ref);
compiled_files_.erase(files);
}
lua_getfield(L_, LUA_REGISTRYINDEX, "ng_envs");
if (lua_istable(L_, -1)) { lua_pushnil(L_); lua_setfield(L_, -2, key.c_str()); }
lua_pop(L_, 1);
@@ -593,7 +630,50 @@ bool LuaHost::has_function(const std::string& name, bool own) {
return f;
}
bool LuaHost::has_cached_file(const std::string& path) const {
if (scripts_changed || compiled_revision_ != script_cache_revision) return false;
const auto files = compiled_files_.find(env);
return files != compiled_files_.end() && files->second.count(path) != 0;
}
bool LuaHost::uses_original_api(std::initializer_list<const char*> names) {
const int top = lua_gettop(L_);
push_env_table(L_, env);
const int environment = lua_gettop(L_);
auto finish = [&](bool result) { lua_settop(L_, top); return result; };
if (!env.empty()) {
if (!lua_getmetatable(L_, environment)) return finish(false);
lua_pushliteral(L_, "__index"); lua_rawget(L_, -2);
lua_pushvalue(L_, LUA_GLOBALSINDEX);
const bool standardLookup = lua_rawequal(L_, -1, -2) != 0;
lua_pop(L_, 3);
if (!standardLookup) return finish(false);
}
auto global = [&](const std::string& name) {
lua_pushlstring(L_, name.data(), name.size()); lua_rawget(L_, environment);
if (lua_isnil(L_, -1) && !env.empty()) { lua_pop(L_, 1); lua_pushlstring(L_, name.data(), name.size()); lua_rawget(L_, LUA_GLOBALSINDEX); }
};
for (const char* name : names) {
const auto original = original_api_.find(name);
if (original == original_api_.end()) return finish(false);
const char* dot = std::strchr(name, '.');
if (dot) {
global(std::string(name, dot));
if (!lua_istable(L_, -1)) return finish(false);
lua_pushstring(L_, dot + 1); lua_rawget(L_, -2); lua_remove(L_, -2);
} else global(name);
lua_rawgeti(L_, LUA_REGISTRYINDEX, original->second);
const bool same = lua_rawequal(L_, -1, -2) != 0;
lua_pop(L_, 2);
if (!same) return finish(false);
}
global("NULL_ENTITY");
const bool zero = lua_isnumber(L_, -1) && lua_tonumber(L_, -1) == 0;
return finish(zero);
}
bool LuaHost::call_function(const std::string& name, const std::vector<LVal>& args, std::string* err) {
++execution_revision_;
get_fn(L_, env, name);
if (!lua_isfunction(L_, -1)) {
lua_pop(L_, 1);
@@ -612,6 +692,7 @@ bool LuaHost::call_function(const std::string& name, const std::vector<LVal>& ar
}
bool LuaHost::call_function_numbers(const std::string& name, const std::vector<LVal>& args, int nresults, std::vector<double>& results, std::string* err) {
++execution_revision_;
results.assign(nresults, 0.0);
get_fn(L_, env, name); // (not a function: the pcall fails, as the engine's does)
for (const LVal& a : args) {
@@ -713,12 +794,35 @@ bool LuaHost::gen_set(int cid, const std::string& key, const std::string& value)
bool LuaHost::load_once(const std::string& path) { return loaded_once_.insert(path).second; }
bool LuaHost::run_file(const std::string& path, std::string* err) {
if (!load_from_wak(*this, L_, path, err)) return false;
if (!env.empty()) { push_env_table(L_, env); lua_setfenv(L_, -2); }
++execution_revision_;
if (scripts_changed) { // refresh source/bytecode before consulting this state's closures
if (!load_from_wak(*this, L_, path, err)) return false;
lua_pop(L_, 1);
}
if (compiled_revision_ != script_cache_revision) {
for (const auto& [key, files] : compiled_files_) for (const auto& [file, ref] : files) luaL_unref(L_, LUA_REGISTRYINDEX, ref);
compiled_files_.clear();
compiled_revision_ = script_cache_revision;
}
auto& files = compiled_files_[env];
const auto cached = files.find(path);
if (cached != files.end()) lua_rawgeti(L_, LUA_REGISTRYINDEX, cached->second);
else {
if (!load_from_wak(*this, L_, path, err)) return false;
lua_pushvalue(L_, -1);
files.emplace(path, luaL_ref(L_, LUA_REGISTRYINDEX));
}
// Separate closures for separate environments; dropping a VM releases its chunks too.
// Reusing a chunk also lets LuaJIT retain its hot counters and compiled traces.
push_env_table(L_, env);
lua_getfenv(L_, -2);
if (lua_rawequal(L_, -1, -2)) lua_pop(L_, 2);
else { lua_pop(L_, 1); lua_setfenv(L_, -2); }
return pcall_traced(L_, 0, 0, err);
}
bool LuaHost::run_string(const std::string& code, const std::string& chunk_name, std::string* err) {
++execution_revision_;
if (luaL_loadbuffer(L_, code.data(), code.size(), chunk_name.c_str()) != 0) {
if (err) *err = lua_tostring(L_, -1);
lua_pop(L_, 1);
@@ -728,6 +832,7 @@ bool LuaHost::run_string(const std::string& code, const std::string& chunk_name,
}
bool LuaHost::call_global(const std::string& name, const std::vector<double>& args, std::string* err) {
++execution_revision_;
lua_getglobal(L_, name.c_str());
if (!lua_isfunction(L_, -1)) {
lua_pop(L_, 1);