This commit is contained in:
Gaël committed 2026-10-19 20:59:15 +02:00
commit 87bff164f1
416 files changed
+279512

No files matched your search

+791
View File
@@ -0,0 +1,791 @@
#include <cstdio>
#include <unordered_map>
#include "lua/lua_host.h"
#include "core/xml.h"
#include <cstdlib>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <sstream>
#include <fstream>
#include <iterator>
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<LuaHost*>(lua_touserdata(L, lua_upvalueindex(1))); }
using Native = int (*)(LuaHost&, lua_State*);
int thunk(lua_State* L) {
auto fn = reinterpret_cast<Native>(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<void*>(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<std::string> LuaHost::stub_names() {
std::vector<std::string> 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 ----
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)
auto it = cache.find(path);
if (it == cache.end()) {
std::vector<uint8_t> 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<uint8_t>& 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<uint8_t> 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<uint8_t>& 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;
}
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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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) { if (h.buffer_world_writes) { h.globals[str(L, 1)] = str(L, 2); h.wrote_world = true; return 0; } std::lock_guard<std::mutex> lk(LuaHost::shared_mutex); h.world_globals()[str(L, 1)] = str(L, 2); 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();
// 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) {
static const std::string none;
if (!texts_loaded_) {
texts_loaded_ = true;
std::vector<uint8_t> 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<char>(in), std::istreambuf_iterator<char>());
}
if (!raw.empty()) {
const std::string t((const char*)raw.data(), raw.size());
std::vector<std::string> 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) { 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) {
if (key.empty()) return;
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::call_function(const std::string& name, const std::vector<LVal>& args, std::string* err) {
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<LVal>& args, int nresults, std::vector<double>& results, std::string* err) {
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<SpawnComp>& LuaHost::file_components(const std::string& path) {
auto it = file_comps_.find(path);
if (it != file_comps_.end()) return it->second;
std::vector<SpawnComp>& out = file_comps_[path];
std::vector<uint8_t> raw;
if (path.empty() || !wak_.read(path, raw)) return out;
std::unique_ptr<XmlNode> root;
try { root = parse_xml(std::string_view((const char*)raw.data(), raw.size())); } catch (...) { return out; }
if (!root) return out;
std::vector<SpawnComp> mine;
for (const auto& c : root->children) {
if (c->name == "Base") { const std::vector<SpawnComp> 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<SpawnComp>& out2 = file_comps_[path];
out2.insert(out2.end(), mine.begin(), mine.end());
return out2;
}
std::vector<int> LuaHost::gen_components(int ent, const std::string& type, const std::string& tag) {
std::vector<int> 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<std::string, std::string>& 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<SpawnComp> 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<SpawnComp>& 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) {
if (!load_from_wak(*this, L_, path, err)) return false;
if (!env.empty()) { push_env_table(L_, env); 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) {
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<double>& args, std::string* err) {
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::ActionRow>& 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