Also fixed: * flying wands not pathing properly. * Lantern particles offset * Wand fire offset * Water spray from flask
905 lines
40 KiB
C++
905 lines
40 KiB
C++
#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 ----
|
|
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
|
|
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;
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
|
|
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) {
|
|
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<std::mutex> 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<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) { ++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<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);
|
|
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) {
|
|
++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<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) {
|
|
++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<double>& 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::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
|