#include #include #include "lua/lua_host.h" #include "core/xml.h" #include #include #include #include #include #include #include #include extern "C" { #include "lauxlib.h" #include "lua.h" #include "lualib.h" } namespace ng { namespace { constexpr const char* kApiNames[] = { #include "lua/api_names.inc" }; LuaHost& host_of(lua_State* L) { return *static_cast(lua_touserdata(L, lua_upvalueindex(1))); } using Native = int (*)(LuaHost&, lua_State*); int thunk(lua_State* L) { auto fn = reinterpret_cast(lua_touserdata(L, lua_upvalueindex(2))); return fn(host_of(L), L); } void reg(lua_State* L, LuaHost& h, const char* name, Native fn) { lua_pushlightuserdata(L, &h); lua_pushlightuserdata(L, reinterpret_cast(fn)); lua_pushcclosure(L, thunk, 2); lua_setglobal(L, name); } // Documented-but-unimplemented engine function: count the call, return nothing. int stub_thunk(lua_State* L) { host_of(L).note_missing(lua_tostring(L, lua_upvalueindex(2))); return 0; } } // namespace std::vector LuaHost::stub_names() { std::vector r; for (const char* n : kApiNames) { lua_getglobal(L_, n); if (lua_tocfunction(L_, -1) == stub_thunk) r.push_back(n); lua_pop(L_, 1); } return r; } namespace { int traceback(lua_State* L) { const char* msg = lua_tostring(L, 1); luaL_traceback(L, L, msg ? msg : "(non-string error)", 1); return 1; } bool pcall_traced(lua_State* L, int nargs, int nresults, std::string* err) { const int base = lua_gettop(L) - nargs; lua_pushcfunction(L, traceback); lua_insert(L, base); const int rc = lua_pcall(L, nargs, nresults, base); lua_remove(L, base); if (rc != 0) { if (err) *err = lua_tostring(L, -1) ? lua_tostring(L, -1) : "unknown error"; lua_pop(L, 1); return false; } return true; } double num(lua_State* L, int i, double def = 0) { return lua_isnumber(L, i) ? lua_tonumber(L, i) : def; } 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> cache; // the scripts of the entities run again and again: read the archive once static std::unordered_map> 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 raw; if (!h.wak().read(path, raw)) { if (err) *err = "script not found in archive: " + path; return false; } it = cache.emplace(path, std::move(raw)).first; } const std::vector& raw0 = it->second; // ModLuaFileAppend: the appended files run after the file, in its environment, every time it is loaded (the file's own return values are kept) std::vector combined; const auto ap = LuaHost::mod_appends().find(path); if (ap != LuaHost::mod_appends().end() && !ap->second.empty()) { std::string s = "local __ng_r = {(function(...) "; s.append((const char*)raw0.data(), raw0.size()); s += "\nend)(...)}"; for (const std::string& f : ap->second) s += " dofile_always(\"" + f + "\")"; s += " return unpack(__ng_r)"; combined.assign(s.begin(), s.end()); } const std::vector& raw = combined.empty() ? raw0 : combined; if (luaL_loadbuffer(L, (const char*)raw.data(), raw.size(), ("@" + path).c_str()) != 0) { if (err) *err = lua_tostring(L, -1); lua_pop(L, 1); return false; } std::vector bytecode; const auto writer = [](lua_State*, const void* bytes, size_t size, void* output) -> int { auto& out = *static_cast*>(output); const auto* begin = static_cast(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; } int n_dofile_impl(LuaHost& h, lua_State* L, bool once) { const std::string path = str(L, 1); // the chunk runs in the environment of the function that called dofile (each Lua VM of the engine loads its own copy) lua_Debug ar; if (lua_getstack(L, 1, &ar) && lua_getinfo(L, "f", &ar)) { lua_getfenv(L, -1); lua_remove(L, -2); } else lua_pushvalue(L, LUA_GLOBALSINDEX); char key[32]; std::snprintf(key, sizeof key, "|%p", lua_topointer(L, -1)); if (once && !h.load_once(path + key)) { lua_pop(L, 1); return 0; } std::string err; if (!load_from_wak(h, L, path, &err)) { lua_pop(L, 1); return luaL_error(L, "%s", err.c_str()); } lua_pushvalue(L, -2); lua_setfenv(L, -2); lua_remove(L, -2); const int top = lua_gettop(L) - 1; // stack height before the loaded chunk was pushed lua_call(L, 0, LUA_MULTRET); return lua_gettop(L) - top; // whatever the chunk returned } int n_dofile(LuaHost& h, lua_State* L) { return n_dofile_impl(h, L, false); } int n_dofile_once(LuaHost& h, lua_State* L) { return n_dofile_impl(h, L, true); } // ---- randomness ---- int n_SetRandomSeed(LuaHost& h, lua_State* L) { h.rng().set_random_seed(num(L, 1), num(L, 2)); return 0; } int n_SetWorldSeed(LuaHost& h, lua_State* L) { h.rng().set_world_seed((uint32_t)num(L, 1)); return 0; } int n_GetWorldSeed(LuaHost& h, lua_State* L) { lua_pushnumber(L, h.rng().world_seed()); return 1; } // Random(): float 0..1; Random(a): int 0..a; Random(a,b): int a..b (banker's rounding on args) int n_Random(LuaHost& h, lua_State* L) { const int n = lua_gettop(L); if (n == 0) { lua_pushnumber(L, h.rng().randomf()); return 1; } const int32_t a = n == 1 ? 0 : round_half_even_i32(num(L, 1)); const int32_t b = round_half_even_i32(num(L, n == 1 ? 1 : 2)); lua_pushnumber(L, h.rng().random(a, b)); return 1; } int n_Randomf(LuaHost& h, lua_State* L) { const int n = lua_gettop(L); const double r = h.rng().prng().next(); if (n == 0) lua_pushnumber(L, (float)r); else if (n == 1) lua_pushnumber(L, (float)(num(L, 1) * r)); else lua_pushnumber(L, (float)(num(L, 1) + (num(L, 2) - num(L, 1)) * r)); return 1; } // ProceduralRandomf(x,y[,a[,b]]): 2 args 0..1, 3 args 0..a, 4 args a..b int n_ProceduralRandomf(LuaHost& h, lua_State* L) { const int n = lua_gettop(L); double lo = 0, hi = 1; if (n == 3) hi = num(L, 3); else if (n >= 4) { lo = num(L, 3); hi = num(L, 4); } lua_pushnumber(L, h.rng().procedural_randomf(num(L, 1), num(L, 2), lo, hi)); return 1; } // ProceduralRandomi(x,y[,a[,b]]): 2 args 0..1, 3 args 0..a, 4 args a..b (ints) int n_ProceduralRandomi(LuaHost& h, lua_State* L) { const int n = lua_gettop(L); double lo = 0, hi = 1; if (n == 3) hi = num(L, 3); else if (n >= 4) { lo = num(L, 3); hi = num(L, 4); } lua_pushnumber(L, h.rng().procedural_randomi(num(L, 1), num(L, 2), lo, hi)); return 1; } // ProceduralRandom(x,y[,a[,b]]): 2 args float 0..1, 3 args int 0..a, 4 args float a..b int n_ProceduralRandom(LuaHost& h, lua_State* L) { return lua_gettop(L) == 3 ? n_ProceduralRandomi(h, L) : n_ProceduralRandomf(h, L); } float get_distribution(NollaPrng& rng, float mean, int sharpness, float baseline) { const float pi = 3.1415f; const float mean_offset = 0.5f - mean; for (int i = 0; i < 100; ++i) { const float r1 = (float)rng.next(); const float r2 = (float)rng.next(); const float div = std::fabs(r1 - mean); if (r2 < (1.0f - div) * baseline) return r1; if (div < 0.5f) { const float wave = (float)std::sin((double)((mean_offset + r1) * pi)); const float shaped = (float)std::pow((double)wave, (double)sharpness); if (shaped > r2) return r1; } } return (float)rng.next(); } int n_RandomDistribution(LuaHost& h, lua_State* L) { const int32_t lo = (int32_t)num(L, 1), hi = (int32_t)num(L, 2), mean = (int32_t)num(L, 3); const int sharp = (int)num(L, 4, 1); if (sharp == 0) { lua_pushnumber(L, h.rng().random(lo, hi)); return 1; } const float adj = (float)(mean - lo) / (float)(hi - lo); const float d = get_distribution(h.rng().prng(), adj, sharp, (float)num(L, 5, 0.005)); lua_pushnumber(L, lo + (int32_t)std::round((float)(hi - lo) * d)); return 1; } int n_RandomDistributionf(LuaHost& h, lua_State* L) { const float lo = (float)num(L, 1), hi = (float)num(L, 2), mean = (float)num(L, 3); const int sharp = (int)num(L, 4, 1); if (sharp == 0) { lua_pushnumber(L, (float)h.rng().prng().next() * (hi - lo) + lo); return 1; } const float adj = (mean - lo) / (hi - lo); lua_pushnumber(L, lo + (hi - lo) * get_distribution(h.rng().prng(), adj, sharp, (float)num(L, 5, 0.005))); return 1; } // ---- spells ---- double seeded_random(uint32_t seed, double x, double y) { NollaPrng p; p.set_random_seed(seed, (double)(float)x, (double)(float)y); return p.next(); } int n_GetRandomAction(LuaHost& h, lua_State* L) { const double x = num(L, 1), y = num(L, 2); const int level = (int)num(L, 3); const uint32_t seed = h.rng().world_seed() + (uint32_t)(int)num(L, 4); const auto& rows = h.actions(); if (rows.empty()) return luaL_error(L, "GetRandomAction: global 'actions' is not loaded"); auto prob = [&](const LuaHost::ActionRow& r) { auto it = r.prob.find(level); return it == r.prob.end() ? 0.0 : it->second; }; double sum = 0; for (auto& r : rows) sum += prob(r); double acc = sum * seeded_random(seed, x, y); for (auto& r : rows) { const double p = prob(r); if (p == 0) continue; if (p >= acc) { lua_pushstring(L, r.id.c_str()); return 1; } acc -= p; } lua_pushstring(L, rows[0].id.c_str()); return 1; } int n_GetRandomActionWithType(LuaHost& h, lua_State* L) { const double x = num(L, 1), y = num(L, 2); const int level = (int)num(L, 3), type = (int)num(L, 4); const uint32_t seed = h.rng().world_seed() + (uint32_t)(int)num(L, 5); const auto& rows = h.actions(); if (rows.empty()) return luaL_error(L, "GetRandomActionWithType: global 'actions' is not loaded"); auto prob = [&](const LuaHost::ActionRow& r) { auto it = r.prob.find(level); return it == r.prob.end() ? 0.0 : it->second; }; double sum = 0; for (auto& r : rows) if (r.type == type) sum += prob(r); double acc = sum * seeded_random(seed, x, y); for (auto& r : rows) { if (r.type != type) continue; const double p = prob(r); if (p > 0 && p >= acc) { lua_pushstring(L, r.id.c_str()); return 1; } acc -= p; } // fallback: walk from a random start for the first matching spell const size_t start = (size_t)(seeded_random(seed, x, y) * (double)rows.size()); for (size_t j = 0; j < rows.size(); ++j) { const auto& r = rows[(j + start) % rows.size()]; if (r.type == type && prob(r) > 0) { lua_pushstring(L, r.id.c_str()); return 1; } } lua_pushstring(L, rows[0].id.c_str()); return 1; } // ---- spawning ---- int n_RegisterSpawnFunction(LuaHost& h, lua_State* L) { h.spawn_functions_[(uint32_t)(int64_t)num(L, 1)] = str(L, 2); return 0; } int load_entity(LuaHost& h, lua_State* L, HostEvent::Kind k) { HostEvent e; e.kind = k; e.path = str(L, 1); e.x = num(L, 2); e.y = num(L, 3); e.id = h.next_entity_id++; lua_pushnumber(L, e.id); h.add_event(std::move(e)); return 1; } int n_EntityLoad(LuaHost& h, lua_State* L) { return load_entity(h, L, HostEvent::Kind::EntityLoad); } int n_EntityLoadCameraBound(LuaHost& h, lua_State* L) { return load_entity(h, L, HostEvent::Kind::EntityLoadCameraBound); } int n_EntityLoadEndGameItem(LuaHost& h, lua_State* L) { return load_entity(h, L, HostEvent::Kind::EntityLoadEndGameItem); } int n_LoadPixelScene(LuaHost& h, lua_State* L) { HostEvent e; e.kind = HostEvent::Kind::PixelScene; e.path = str(L, 1); e.path2 = str(L, 2); e.x = num(L, 3); e.y = num(L, 4); e.background = str(L, 5); h.add_event(std::move(e)); return 0; } int n_LoadBackgroundSprite(LuaHost& h, lua_State* L) { // (file, x, y, z_index, check_biome_corners): world generation records it as a scene with a background only HostEvent e; e.kind = HostEvent::Kind::PixelScene; e.background = str(L, 1); e.x = num(L, 2); e.y = num(L, 3); h.add_event(std::move(e)); return 0; } // ---- run flags / globals / misc ---- int n_GameHasFlagRun(LuaHost& h, lua_State* L) { const std::string k = str(L, 1); bool has; if (h.buffer_world_writes && (h.run_flags.count(k) || h.removed_run_flags.count(k))) has = h.run_flags.count(k) != 0; else { std::lock_guard lk(LuaHost::shared_mutex); has = h.world_run_flags().count(k) != 0; } lua_pushboolean(L, has); return 1; } int n_GameAddFlagRun(LuaHost& h, lua_State* L) { if (h.buffer_world_writes) { h.run_flags.insert(str(L, 1)); h.removed_run_flags.erase(str(L, 1)); h.wrote_world = true; return 0; } std::lock_guard lk(LuaHost::shared_mutex); h.world_run_flags().insert(str(L, 1)); return 0; } int n_GameRemoveFlagRun(LuaHost& h, lua_State* L) { if (h.buffer_world_writes) { h.run_flags.erase(str(L, 1)); h.removed_run_flags.insert(str(L, 1)); h.wrote_world = true; return 0; } std::lock_guard lk(LuaHost::shared_mutex); h.world_run_flags().erase(str(L, 1)); return 0; } int n_GameIsModeFullyDeterministic(LuaHost&, lua_State* L) { lua_pushboolean(L, 0); return 1; } // (a normal run) int n_HasFlagPersistent(LuaHost& h, lua_State* L) { lua_pushboolean(L, h.assume_all_unlocked); return 1; } int n_GlobalsGetValue(LuaHost& h, lua_State* L) { std::string v; if (const auto own = h.globals.find(str(L, 1)); h.buffer_world_writes && own != h.globals.end()) v = own->second; else { std::lock_guard lk(LuaHost::shared_mutex); const auto& g = h.world_globals(); const auto it = g.find(str(L, 1)); v = it == g.end() ? str(L, 2) : it->second; } lua_pushstring(L, v.c_str()); return 1; } int n_StatsGetValue(LuaHost& h, lua_State* L) { const auto it = h.stats.find(str(L, 1)); lua_pushnumber(L, it == h.stats.end() ? 0.0 : it->second); return 1; } int n_StatsBiomeGetValue(LuaHost& h, lua_State* L) { const std::string k = str(L, 1); const auto it = h.stats.find(k); const auto ib = h.biome_stats_base.find(k); lua_pushnumber(L, (it == h.stats.end() ? 0.0 : it->second) - (ib == h.biome_stats_base.end() ? 0.0 : ib->second)); return 1; } int n_StatsResetBiome(LuaHost& h, lua_State*) { h.biome_stats_base = h.stats; return 0; } int n_GlobalsSetValue(LuaHost& h, lua_State* L) { const auto key = str(L, 1), value = str(L, 2); if (std::getenv("NG_EVENTLOG") && key.rfind("TEMPLE_", 0) == 0) std::printf("temple global %s = %s%s\n", key.c_str(), value.c_str(), h.buffer_world_writes ? " (generation trial)" : ""); if (h.buffer_world_writes) { h.globals[key] = value; h.wrote_world = true; return 0; } std::lock_guard lk(LuaHost::shared_mutex); h.world_globals()[key] = value; return 0; } // session numbers (new game plus count etc.): a fresh run has everything at 0 int n_SessionNumbersGetValue(LuaHost&, lua_State* L) { lua_pushstring(L, "0"); return 1; } int n_GameGetFrameNum(LuaHost&, lua_State* L) { lua_pushnumber(L, 0); return 1; } int n_GameGetIsGamepadConnected(LuaHost&, lua_State* L) { lua_pushboolean(L, 0); return 1; } // World geometry: the biome map is `biome_map_w` chunks wide; the main world spans // [-w*256, w*256) and parallel worlds repeat every w*512 cells. int n_BiomeMapGetSize(LuaHost& h, lua_State* L) { lua_pushnumber(L, h.biome_map_w); lua_pushnumber(L, h.biome_map_h); return 2; } int n_BiomeMapGetVerticalPositionInsideBiome(LuaHost& h, lua_State* L) { lua_pushnumber(L, h.vertical_position ? h.vertical_position(num(L, 1), num(L, 2)) : 0.0); return 1; } int n_GetParallelWorldPosition(LuaHost& h, lua_State* L) { const double width = h.biome_map_w * 512.0; lua_pushnumber(L, std::floor((num(L, 1) + width / 2) / width)); lua_pushnumber(L, 0); return 2; } // RaytracePlatforms(x1,y1,x2,y2) -> hit, hit_x, hit_y. Needs terrain; delegated to a hook. // Without one, nothing is hit and the ray end point is returned. int n_RaytracePlatforms(LuaHost& h, lua_State* L) { const double x1 = num(L, 1), y1 = num(L, 2), x2 = num(L, 3), y2 = num(L, 4); double hx = x2, hy = y2; bool hit = false; if (h.raytrace) hit = h.raytrace(x1, y1, x2, y2, hx, hy); lua_pushboolean(L, hit); lua_pushnumber(L, hx); lua_pushnumber(L, hy); return 3; } // CellFactory_GetType(name) -> the material's id (-1: none); CellFactory_GetName(id) -> its name int n_CellFactory_GetType(LuaHost& h, lua_State* L) { if (!h.material_type) { h.note_missing("CellFactory_GetType"); return 0; } lua_pushnumber(L, h.material_type(luaL_optstring(L, 1, ""))); return 1; } int n_CellFactory_GetUIName(LuaHost& h, lua_State* L) { if (!h.material_ui_name) { h.note_missing("CellFactory_GetUIName"); lua_pushstring(L, ""); return 1; } lua_pushstring(L, h.material_ui_name((int)num(L, 1)).c_str()); return 1; } int n_CellFactory_GetName(LuaHost& h, lua_State* L) { if (!h.material_name) { h.note_missing("CellFactory_GetName"); return 0; } lua_pushstring(L, h.material_name((int)num(L, 1)).c_str()); return 1; } // GameGetDateAndTimeLocal() -> year, month, day, hour, minute, second (overridable for tests) int n_GameGetDateAndTimeLocal(LuaHost& h, lua_State* L) { int v[6]; if (h.fixed_date[0] != 0) { for (int i = 0; i < 6; ++i) v[i] = h.fixed_date[i]; } else { const std::time_t t = std::time(nullptr); std::tm tm{}; localtime_s(&tm, &t); v[0] = tm.tm_year + 1900; v[1] = tm.tm_mon + 1; v[2] = tm.tm_mday; v[3] = tm.tm_hour; v[4] = tm.tm_min; v[5] = tm.tm_sec; } for (int i = 0; i < 6; ++i) lua_pushnumber(L, v[i]); return 6; } int n_print(LuaHost& h, lua_State* L) { std::string line; for (int i = 1; i <= lua_gettop(L); ++i) { size_t n = 0; lua_getglobal(L, "tostring"); lua_pushvalue(L, i); lua_call(L, 1, 1); const char* s = lua_tolstring(L, -1, &n); if (i > 1) line += '\t'; if (s) line.append(s, n); lua_pop(L, 1); } static const bool echo = std::getenv("NG_LUAPRINT") != nullptr; // (the game writes them to logger.txt; here to stdout on request) if (echo) std::printf("lua print: %s%c", line.c_str(), 10); if (h.printed_.size() >= 1000) h.printed_.erase(h.printed_.begin(), h.printed_.begin() + 500); // (kept for tests; bounded) h.printed_.push_back(std::move(line)); return 0; } } // namespace 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[] = { {0xff0000ff, "spawn_small_enemies"}, {0x800000ff, "spawn_big_enemies"}, {0x00ff00ff, "spawn_items"}, {0xc88d1aff, "spawn_props"}, {0xc88000ff, "spawn_props2"}, {0xc80040ff, "spawn_props3"}, {0xffff00ff, "spawn_lamp"}, {0xff0affff, "load_pixel_scene"}, {0xff0080ff, "load_pixel_scene2"}, {0xff8000ff, "spawn_unique_enemy"}, {0xc84040ff, "spawn_unique_enemy2"}, {0x804040ff, "spawn_unique_enemy3"}, {0x96c850ff, "spawn_ghostlamp"}, {0x60a064ff, "spawn_candles"}, {0x50a000ff, "spawn_potion_altar"}, {0xbca0f0ff, "spawn_potions"}, {0x00ff5aff, "spawn_apparition"}, {0x78ffffff, "spawn_heart"}, {0x50a0f0ff, "spawn_wands"}, {0xbf26a6ff, "spawn_portal"}, {0x04a977ff, "spawn_end_portal"}, {0xffd171ff, "spawn_orb"}, {0xffd181ff, "spawn_perk"}, {0xffff81ff, "spawn_all_perks"}, {0xc7eb28ff, "spawn_wand_trap"}, {0xe8ff80ff, "spawn_wand_trap_ignite"}, {0x2768deff, "spawn_wand_trap_electricity_source"}, {0x2768dfff, "spawn_wand_trap_electricity"}, {0x6b4f9bff, "spawn_moon"}, {0xd7b3e8ff, "spawn_collapse"}, }; if (!std::getenv("NG_NO_DEFAULT_SPAWNS")) for (const auto& d : kDefaults) spawn_functions_[(d.rgba >> 8) | (d.rgba << 24)] = d.fn; // RGBA -> ARGB } LuaHost::~LuaHost() { if (L_) lua_close(L_); } const std::string& LuaHost::translate(const std::string& key) { return translate_in(wak_, loose_dir, key); } const std::string& LuaHost::translate_in(const Wak& wak_, const std::string& loose_dir, const std::string& key) { static const std::string none; if (!texts_loaded_) { texts_loaded_ = true; std::vector raw; if (!wak_.read("data/translations/common.csv", raw) && !loose_dir.empty()) { std::ifstream in(loose_dir + "/data/translations/common.csv", std::ios::binary); raw.assign(std::istreambuf_iterator(in), std::istreambuf_iterator()); } if (!raw.empty()) { const std::string t((const char*)raw.data(), raw.size()); std::vector row; std::string f; bool q = false; auto end_row = [&] { row.push_back(f); f.clear(); if (row.size() > 1 && !row[0].empty() && !row[1].empty()) { std::string v = row[1]; for (size_t p = 0; (p = v.find("\\n", p)) != std::string::npos;) { v.replace(p, 2, "\n"); ++p; } texts_[row[0]] = v; } row.clear(); }; for (size_t i = 0; i < t.size(); ++i) { const char c = t[i]; if (q) { if (c == '"') { if (i + 1 < t.size() && t[i + 1] == '"') { f += '"'; ++i; } else q = false; } else f += c; } else if (c == '"') q = true; else if (c == ',') { row.push_back(f); f.clear(); } else if (c == '\n') { if (!f.empty() && f.back() == '\r') f.pop_back(); end_row(); } else f += c; } if (!f.empty() || !row.empty()) end_row(); } } const auto it = texts_.find(!key.empty() && key[0] == '$' ? key.substr(1) : key); return it == texts_.end() ? none : it->second; } void LuaHost::register_api() { // 1. every documented engine function starts as a counting no-op stub for (const char* n : kApiNames) { lua_pushlightuserdata(L_, this); lua_pushstring(L_, n); lua_pushcclosure(L_, stub_thunk, 2); lua_setglobal(L_, n); } // 2. real implementations override the stubs struct { const char* name; Native fn; } natives[] = { {"dofile", n_dofile}, {"dofile_once", n_dofile_once}, {"dofile_always", n_dofile}, {"print", n_print}, {"SetRandomSeed", n_SetRandomSeed}, {"SetWorldSeed", n_SetWorldSeed}, {"GetWorldSeed", n_GetWorldSeed}, {"Random", n_Random}, {"Randomf", n_Randomf}, {"ProceduralRandom", n_ProceduralRandom}, {"ProceduralRandomf", n_ProceduralRandomf}, {"ProceduralRandomi", n_ProceduralRandomi}, {"RandomDistribution", n_RandomDistribution}, {"RandomDistributionf", n_RandomDistributionf}, {"GetRandomAction", n_GetRandomAction}, {"GetRandomActionWithType", n_GetRandomActionWithType}, {"RegisterSpawnFunction", n_RegisterSpawnFunction}, {"EntityLoad", n_EntityLoad}, {"EntityLoadCameraBound", n_EntityLoadCameraBound}, {"EntityLoadEndGameItem", n_EntityLoadEndGameItem}, {"LoadPixelScene", n_LoadPixelScene}, {"LoadBackgroundSprite", n_LoadBackgroundSprite}, {"GameHasFlagRun", n_GameHasFlagRun}, {"GameAddFlagRun", n_GameAddFlagRun}, {"GameRemoveFlagRun", n_GameRemoveFlagRun}, {"HasFlagPersistent", n_HasFlagPersistent}, {"GameIsModeFullyDeterministic", n_GameIsModeFullyDeterministic}, {"StatsGetValue", n_StatsGetValue}, {"StatsBiomeGetValue", n_StatsBiomeGetValue}, {"StatsResetBiome", n_StatsResetBiome}, {"GlobalsGetValue", n_GlobalsGetValue}, {"GlobalsSetValue", n_GlobalsSetValue}, {"GameGetFrameNum", n_GameGetFrameNum}, {"SessionNumbersGetValue", n_SessionNumbersGetValue}, {"GameGetIsGamepadConnected", n_GameGetIsGamepadConnected}, {"BiomeMapGetSize", n_BiomeMapGetSize}, {"BiomeMapGetVerticalPositionInsideBiome", n_BiomeMapGetVerticalPositionInsideBiome}, {"GetParallelWorldPosition", n_GetParallelWorldPosition}, {"RaytracePlatforms", n_RaytracePlatforms}, {"RaytraceSurfaces", n_RaytracePlatforms}, {"Raytrace", n_RaytracePlatforms}, {"GameGetDateAndTimeLocal", n_GameGetDateAndTimeLocal}, {"CellFactory_GetType", n_CellFactory_GetType}, {"CellFactory_GetName", n_CellFactory_GetName}, {"CellFactory_GetUIName", n_CellFactory_GetUIName}, }; for (auto& n : natives) reg(L_, *this, n.name, n.fn); register_entity_natives(*this); register_host_natives(*this); } 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) { if (key.empty()) { lua_pushvalue(L, LUA_GLOBALSINDEX); return; } lua_getfield(L, LUA_REGISTRYINDEX, "ng_envs"); if (!lua_istable(L, -1)) { lua_pop(L, 1); lua_newtable(L); lua_pushvalue(L, -1); lua_setfield(L, LUA_REGISTRYINDEX, "ng_envs"); } lua_getfield(L, -1, key.c_str()); if (!lua_istable(L, -1)) { lua_pop(L, 1); lua_newtable(L); lua_newtable(L); lua_pushvalue(L, LUA_GLOBALSINDEX); lua_setfield(L, -2, "__index"); lua_setmetatable(L, -2); lua_pushvalue(L, -1); lua_setfield(L, -3, key.c_str()); } lua_remove(L, -2); } static void get_fn(lua_State* L, const std::string& env, const std::string& name, bool own = false) { if (env.empty() && !own) { lua_getglobal(L, name.c_str()); return; } push_env_table(L, env); lua_pushstring(L, name.c_str()); if (own) lua_rawget(L, -2); else lua_gettable(L, -2); lua_remove(L, -2); } 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); } bool LuaHost::has_function(const std::string& name, bool own) { get_fn(L_, env, name, own); const bool f = lua_isfunction(L_, -1); lua_pop(L_, 1); 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 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& args, std::string* err) { ++execution_revision_; get_fn(L_, env, name); if (!lua_isfunction(L_, -1)) { lua_pop(L_, 1); if (err) *err = "global function not defined: " + name; return false; } for (const LVal& a : args) { switch (a.t) { case LVal::T::Nil: lua_pushnil(L_); break; case LVal::T::Bool: lua_pushboolean(L_, a.b); break; case LVal::T::Num: lua_pushnumber(L_, a.n); break; case LVal::T::Str: lua_pushstring(L_, a.s.c_str()); break; } } return pcall_traced(L_, (int)args.size(), 0, err); } bool LuaHost::call_function_numbers(const std::string& name, const std::vector& args, int nresults, std::vector& 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) { switch (a.t) { case LVal::T::Nil: lua_pushnil(L_); break; case LVal::T::Bool: lua_pushboolean(L_, a.b); break; case LVal::T::Num: lua_pushnumber(L_, a.n); break; case LVal::T::Str: lua_pushstring(L_, a.s.c_str()); break; } } if (!pcall_traced(L_, (int)args.size(), nresults, err)) return false; for (int k = 0; k < nresults; ++k) results[k] = lua_tonumber(L_, k - nresults); lua_pop(L_, nresults); return true; } // ---- components of the world generation's spawns ---- const std::vector& LuaHost::file_components(const std::string& path) { auto it = file_comps_.find(path); if (it != file_comps_.end()) return it->second; std::vector& out = file_comps_[path]; std::vector raw; if (path.empty() || !wak_.read(path, raw)) return out; std::unique_ptr root; try { root = parse_xml(std::string_view((const char*)raw.data(), raw.size())); } catch (...) { return out; } if (!root) return out; std::vector mine; for (const auto& c : root->children) { if (c->name == "Base") { const std::vector base = file_components(c->attr_or("file")); out.insert(out.end(), base.begin(), base.end()); continue; } // (the base file's components first) if (c->name == "Entity") continue; SpawnComp sc; sc.type = c->name; for (const auto& a : c->attrs) sc.attrs[a.first] = a.second; for (const auto& sub : c->children) for (const auto& a : sub->attrs) sc.attrs[sub->name + "." + a.first] = a.second; // (objects: config_explosion.damage ...) mine.push_back(std::move(sc)); } std::vector& out2 = file_comps_[path]; out2.insert(out2.end(), mine.begin(), mine.end()); return out2; } std::vector LuaHost::gen_components(int ent, const std::string& type, const std::string& tag) { std::vector ids; const HostEvent* ev = nullptr; for (size_t i = events_.size(); i-- > 0;) if (events_[i].id == ent) { ev = &events_[i]; break; } if (!ev) return ids; auto has_tag = [&](const std::map& a) { if (tag.empty()) return true; auto t = a.find("_tags"); return t != a.end() && ("," + t->second + ",").find("," + tag + ",") != std::string::npos; }; auto handle = [&](int index, const std::string& ty) { for (const auto& [id, g] : gen_comps_) if (g.ent == ent && g.index == index) return id; return gen_handle(ent, index, ty); }; const std::vector fc = file_components(ev->path); // (a copy: handle() may not touch it, but the cache may grow) for (size_t k = 0; k < fc.size(); ++k) if ((type.empty() || fc[k].type == type) && has_tag(fc[k].attrs)) ids.push_back(handle(-1 - (int)k, fc[k].type)); for (size_t k = 0; k < ev->comps.size(); ++k) if ((type.empty() || ev->comps[k].type == type) && has_tag(ev->comps[k].attrs)) ids.push_back(handle((int)k, ev->comps[k].type)); return ids; } bool LuaHost::gen_get(int cid, const std::string& key, std::string& out) { auto g = gen_comps_.find(cid); if (g == gen_comps_.end()) return false; for (size_t i = events_.size(); i-- > 0;) { HostEvent& ev = events_[i]; if (ev.id != g->second.ent) continue; if (g->second.index >= 0) { if (g->second.index >= (int)ev.comps.size()) return false; auto a = ev.comps[g->second.index].attrs.find(key); if (a == ev.comps[g->second.index].attrs.end()) return false; out = a->second; return true; } for (size_t k = ev.comps.size(); k-- > 0;) if (ev.comps[k].type == "=" + g->second.type) { auto a = ev.comps[k].attrs.find(key); if (a != ev.comps[k].attrs.end()) { out = a->second; return true; } } const std::vector& fc = file_components(ev.path); const int k = -1 - g->second.index; if (k < 0 || k >= (int)fc.size()) return false; auto a = fc[k].attrs.find(key); if (a == fc[k].attrs.end()) return false; out = a->second; return true; } return false; } bool LuaHost::gen_set(int cid, const std::string& key, const std::string& value) { auto g = gen_comps_.find(cid); if (g == gen_comps_.end()) return false; for (size_t i = events_.size(); i-- > 0;) { HostEvent& ev = events_[i]; if (ev.id != g->second.ent) continue; if (g->second.index >= 0) { if (g->second.index < (int)ev.comps.size()) ev.comps[g->second.index].attrs[key] = value; return true; } for (SpawnComp& sc : ev.comps) if (sc.type == "=" + g->second.type) { sc.attrs[key] = value; return true; } SpawnComp sc; sc.type = "=" + g->second.type; sc.attrs[key] = value; ev.comps.push_back(std::move(sc)); return true; } return false; } 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) { ++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); return false; } return pcall_traced(L_, 0, 0, err); } bool LuaHost::call_global(const std::string& name, const std::vector& args, std::string* err) { ++execution_revision_; lua_getglobal(L_, name.c_str()); if (!lua_isfunction(L_, -1)) { lua_pop(L_, 1); if (err) *err = "global function not defined: " + name; return false; } for (double a : args) lua_pushnumber(L_, a); return pcall_traced(L_, (int)args.size(), 0, err); } const std::vector& LuaHost::actions() { lua_getglobal(L_, "actions"); if (!lua_istable(L_, -1) && !tried_actions_) { // the engine knows the spell list natively: load the game's table on demand tried_actions_ = true; lua_pop(L_, 1); std::string e; const std::string saved = env; // (into the globals, whatever environment the script that asked runs in) env.clear(); if (load_once("data/scripts/gun/gun_actions.lua")) run_file("data/scripts/gun/gun_actions.lua", &e); env = saved; lua_getglobal(L_, "actions"); } if (!lua_istable(L_, -1)) { lua_pop(L_, 1); return actions_; } // (the list read before stays) const size_t len = lua_objlen(L_, -1); if (len != actions_len_) { actions_.clear(); for (size_t i = 1; i <= len; ++i) { lua_rawgeti(L_, -1, (int)i); ActionRow r; lua_getfield(L_, -1, "id"); r.id = str(L_, -1); lua_pop(L_, 1); lua_getfield(L_, -1, "type"); r.type = (int)num(L_, -1); lua_pop(L_, 1); lua_getfield(L_, -1, "spawn_level"); const std::string lv = str(L_, -1); lua_pop(L_, 1); lua_getfield(L_, -1, "spawn_probability"); const std::string pr = str(L_, -1); lua_pop(L_, 1); std::stringstream a(lv), b(pr); std::string x, y; while (std::getline(a, x, ',') && std::getline(b, y, ',')) r.prob[std::atoi(x.c_str())] = std::atof(y.c_str()); actions_.push_back(std::move(r)); lua_pop(L_, 1); } actions_len_ = len; } lua_pop(L_, 1); return actions_; } static int g_profileDeaths = 0; void set_profile_death_count(int deaths) { g_profileDeaths = deaths; } int lua_death_count() { if (const char* d = std::getenv("NG_DEATHS")) return std::atoi(d); return g_profileDeaths; } } // namespace ng