Less serialization and deserialization of Lua Source, also, Invocation is now simpler

This commit is contained in:
2023-10-19 19:42:33 -04:00
parent 049b0b893a
commit 7104a523b5
16 changed files with 134 additions and 150 deletions

View File

@@ -165,8 +165,10 @@ void DrivenEngine::set_console_prompt(const eng::string &prompt) {
console_prompt_ = prompt;
}
util::LuaSourcePtr DrivenEngine::get_lua_source() {
return std::move(lua_source_);
eng::string DrivenEngine::get_lua_source_pack() {
eng::string result = std::move(lua_source_pack_);
lua_source_pack_.clear();
return result;
}
void DrivenEngine::rescan_lua_source() {
@@ -219,7 +221,7 @@ enum DrvAction {
PLAY_NOTIFY_CLOSE,
PLAY_NOTIFY_ACCEPT,
PLAY_INVOKE_EVENT_UPDATE,
PLAY_SET_LUA_SOURCE,
PLAY_SET_LUA_SOURCE_PACK,
PLAY_RELEASE,
};
@@ -232,7 +234,7 @@ inline static const char *action_string(DrvAction act) {
case PLAY_NOTIFY_CLOSE: return "PLAY_NOTIFY_CLOSE";
case PLAY_NOTIFY_ACCEPT: return "PLAY_NOTIFY_ACCEPT";
case PLAY_INVOKE_EVENT_UPDATE: return "PLAY_INVOKE_EVENT_UPDATE";
case PLAY_SET_LUA_SOURCE: return "PLAY_SET_LUA_SOURCE";
case PLAY_SET_LUA_SOURCE_PACK: return "PLAY_SET_LUA_SOURCE_PACK";
case PLAY_RELEASE: return "PLAY_RELEASE";
default: return "unknown";
}
@@ -442,7 +444,7 @@ void DrivenEngine::drv_get_animation_queues(uint32_t count, const int64_t *ids,
//////////////////////////////////////////////////////////////////////////////
void DrivenEngine::drv_initialize(uint32_t srcpklen, const char *srcpk, int argc, char **argv) {
drv_set_lua_source(srcpklen, srcpk);
drv_set_lua_source_pack(srcpklen, srcpk);
event_init(argc, argv);
}
@@ -481,22 +483,12 @@ void DrivenEngine::drv_invoke_event_update(double clock) {
event_update();
}
void DrivenEngine::drv_set_lua_source(uint32_t srcpklen, const char *srcpk) {
StreamBuffer sb(std::string_view(srcpk, srcpklen));
uint32_t nfiles = sb.read_uint32();
lua_source_.reset(new util::LuaSourceVec);
lua_source_->resize(nfiles);
for (uint32_t i = 0; i < nfiles; i++) {
(*lua_source_)[i].first = sb.read_string();
}
for (uint32_t i = 0; i < nfiles; i++) {
(*lua_source_)[i].second = sb.read_string();
}
void DrivenEngine::drv_set_lua_source_pack(uint32_t srcpklen, const char *srcpk) {
lua_source_pack_ = std::string_view(srcpk, srcpklen);
rescan_lua_source_ = false;
}
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
//
@@ -821,25 +813,25 @@ static void replay_invoke_event_update(EngineWrapper *w) {
////////////////////////
void play_set_lua_source(EngineWrapper *w, uint32_t srcpklen, const char *srcpk) {
void play_set_lua_source_pack(EngineWrapper *w, uint32_t srcpklen, const char *srcpk) {
assert(w->rlog == nullptr);
if (w->wlog != nullptr) {
w->wlog->write_cmd_hash(PLAY_SET_LUA_SOURCE, eng::memhash());
w->wlog->write_cmd_hash(PLAY_SET_LUA_SOURCE_PACK, eng::memhash());
w->wlog->write_string(std::string_view(srcpk, srcpklen));
w->wlog->flush();
}
w->engine->drv_set_lua_source(srcpklen, srcpk);
w->engine->drv_set_lua_source_pack(srcpklen, srcpk);
}
void replay_set_lua_source(EngineWrapper *w) {
void replay_set_lua_source_pack(EngineWrapper *w) {
std::string srcpack = w->rlog->read_string();
if (!w->rlog->good()) {
return reset_wrapper(w, "replay log corrupt in replay_set_lua_source");
return reset_wrapper(w, "replay log corrupt in replay_set_lua_source_pack");
}
w->engine->drv_set_lua_source(srcpack.size(), srcpack.c_str());
w->engine->drv_set_lua_source_pack(srcpack.size(), srcpack.c_str());
}
@@ -910,7 +902,7 @@ static void replaycore_step(EngineWrapper *w) {
case PLAY_NOTIFY_CLOSE: replay_notify_close(w); return;
case PLAY_NOTIFY_ACCEPT: replay_notify_accept(w); return;
case PLAY_INVOKE_EVENT_UPDATE: replay_invoke_event_update(w); return;
case PLAY_SET_LUA_SOURCE: replay_set_lua_source(w); return;
case PLAY_SET_LUA_SOURCE_PACK: replay_set_lua_source_pack(w); return;
case PLAY_RELEASE: release(w); return;
default: return reset_wrapper(w, "Replay log corrupt in command dispatcher");
}
@@ -971,7 +963,7 @@ static void init_engine_wrapper_helper(EngineWrapper *w) {
w->play_notify_close = play_notify_close;
w->play_notify_accept = play_notify_accept;
w->play_invoke_event_update = play_invoke_event_update;
w->play_set_lua_source = play_set_lua_source;
w->play_set_lua_source_pack = play_set_lua_source_pack;
w->replay_initialize = replaycore_initialize;
w->replay_step = replaycore_step;

View File

@@ -210,10 +210,13 @@ public:
//
void set_console_prompt(const eng::string &prompt);
// Fetches the lua source, and takes ownership of it. The DrivenEngine
// no longer contains the source after calling this.
// Fetches the lua 'sourcepack'. The sourcepack is a packaged collection
// of all the lua sourcefiles, see drvutil::package_lua_source for
// documentation of the format. This also clears the sourcepack stored
// in the DrivenEngine. Returns empty string if there is no sourcepack
// in the DrivenEngine.
//
util::LuaSourcePtr get_lua_source();
eng::string get_lua_source_pack();
// Rescan the lua source directory. The lua source directory is read once,
// automatically, at engine creation time. If you want to read it again,
@@ -291,7 +294,7 @@ public:
void drv_notify_close(uint32_t chid, uint32_t len, const char *data);
uint32_t drv_notify_accept(uint32_t port);
void drv_invoke_event_update(double clock);
void drv_set_lua_source(uint32_t srcpklen, const char *srcpk);
void drv_set_lua_source_pack(uint32_t srcpklen, const char *srcpk);
private:
// Find a currently-unused channel ID. Channel IDs
@@ -308,12 +311,12 @@ private:
std::unique_ptr<std::ostream> stdostream_;
eng::vector<SharedChannel> accepted_channels_;
eng::vector<uint32_t> new_outgoing_;
util::LuaSourcePtr lua_source_;
eng::vector<uint32_t> listen_ports_;
World *visible_world_;
int64_t visible_actor_id_;
util::IdVector scan_result_;
std::vector<util::SharedStdString> anim_queues_;
eng::string lua_source_pack_;
bool rescan_lua_source_;
double clock_;
bool stop_driver_;

View File

@@ -115,7 +115,7 @@ private:
void event_init(int argc, char *argv[])
{
world_.reset(new World(WORLD_TYPE_MASTER));
world_->update_source(get_lua_source());
world_->update_source(get_lua_source_pack());
world_->run_unittests();
stop_driver();
}

View File

@@ -102,7 +102,7 @@ struct EngineWrapper {
// Check the 'rescan_lua_source' flag. If this flag is set, it means
// that the engine wants the driver to rescan the lua source code.
// When the driver sees this flag, it should rescan the source and call
// set_lua_source.
// set_lua_source_pack.
//
bool (*get_rescan_lua_source)(EngineWrapper *w);
@@ -199,7 +199,7 @@ struct EngineWrapper {
// Store the lua source code.
//
void (*play_set_lua_source)(EngineWrapper *w, uint32_t srcpklen, const char *srcpk);
void (*play_set_lua_source_pack)(EngineWrapper *w, uint32_t srcpklen, const char *srcpk);
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////

View File

@@ -30,7 +30,7 @@ void Gui::menu_item(const eng::string &action, const eng::string &label) {
elts_.push_back(elt);
}
bool Gui::has_action(const eng::string &action) const {
bool Gui::has_action(std::string_view action) const {
for (const GuiElt &elt : elts_) {
if (elt.action_ == action) {
return true;

View File

@@ -38,7 +38,7 @@ public:
int64_t place() { return place_; }
const EltVec &elts() const { return elts_; }
void clear(int64_t p) { place_ = p; elts_.clear(); }
bool has_action(const eng::string &action) const;
bool has_action(std::string_view action) const;
void menu_item(const eng::string &action, const eng::string &label);
eng::string get_action(int64_t index);
eng::string menu_debug_string() const;

View File

@@ -6,57 +6,24 @@
#include "invocation.hpp"
const eng::string &InvocationData::get(const eng::string &key) const {
static eng::string blank_;
auto iter = find(key);
if (iter == end()) {
return blank_;
} else {
return iter->second;
}
}
void InvocationData::serialize(StreamBuffer *sb) const {
assert(int(size()) < 65536);
sb->write_uint16(size());
for (const auto &pair : *this) {
sb->write_string(pair.first);
sb->write_string(pair.second);
}
}
void InvocationData::deserialize(StreamBuffer *sb) {
clear();
int size = sb->read_uint16();
for (int i = 0; i < size; i++) {
eng::string key = sb->read_string();
eng::string val = sb->read_string();
(*this)[key] = val;
}
}
Invocation::Invocation() : kind_(KIND_INVALID), actor_(0), place_(0) {}
Invocation::Invocation(Kind kind, int64_t actor, int64_t place, const eng::string &action, const InvocationData &data)
: kind_(kind), actor_(actor), place_(place), action_(action), data_(data) {}
Invocation::Invocation(Kind kind, int64_t actor, int64_t place, std::string_view datapack)
: kind_(kind), actor_(actor), place_(place), datapack_(datapack) {}
Invocation::Invocation(Kind kind, int64_t actor, int64_t place, const eng::string &action)
: kind_(kind), actor_(actor), place_(place), action_(action) {}
void Invocation::serialize(StreamBuffer *sb) const {
sb->write_uint8(kind_);
sb->write_int64(actor_);
sb->write_int64(place_);
sb->write_string(action_);
data_.serialize(sb);
sb->write_string(datapack_);
}
void Invocation::deserialize(StreamBuffer *sb) {
kind_ = Kind(sb->read_uint8());
actor_ = sb->read_int64();
place_ = sb->read_int64();
action_ = sb->read_string();
data_.deserialize(sb);
datapack_ = sb->read_string();
}
eng::string Invocation::debug_string() const {
@@ -73,12 +40,7 @@ eng::string Invocation::debug_string() const {
}
oss << " a=" << actor_;
oss << " p=" << place_;
if (kind_ != KIND_LUA_SOURCE) {
oss << " " << action_;
}
for (const auto &pair : data_) {
oss << " " << pair.first << "=" << pair.second;
}
oss << " dp=" << datapack_.size() << " bytes";
oss << "]";
return oss.str();
}

View File

@@ -1,3 +1,39 @@
//////////////////////////////////////////////////////////////////////////
//
// DATAPACK
//
// Invocations contain a field 'datapack' which contains additional
// data for the invocation, packed up in a serialized format. Executing
// the invocation involves unpacking the datapack.
//
// The actual contents of the datapack depends on the type of invocation:
//
// KIND_INVALID:
//
// Nothing.
//
// KIND_PLAN:
//
// Name of a callback function, in plaintext.
//
// KIND_LUA:
//
// A block of lua source code, in plaintext.
//
// KIND_FLUSH_PRINTS:
//
// Line number in ascii.
//
// KIND_TICK:
//
// Nothing.
//
// KIND_LUA_SOURCE:
//
// Packaged lua sourcecode. See drvutil::package_lua_source.
//
//////////////////////////////////////////////////////////////////////////
#ifndef INVOCATION_HPP
#define INVOCATION_HPP
@@ -8,14 +44,6 @@
#include "streambuffer.hpp"
class InvocationData : public eng::map<eng::string, eng::string> {
public:
const eng::string &get(const eng::string &key) const;
void serialize(StreamBuffer *sb) const;
void deserialize(StreamBuffer *sb);
};
class Invocation : public eng::nevernew {
public:
enum Kind {
@@ -31,19 +59,16 @@ private:
Kind kind_;
int64_t actor_;
int64_t place_;
eng::string action_;
InvocationData data_;
eng::string datapack_;
public:
Invocation();
Invocation(Kind kind, int64_t actor, int64_t place, const eng::string &action, const InvocationData &data);
Invocation(Kind kind, int64_t actor, int64_t place, const eng::string &action);
Invocation(Kind kind, int64_t actor, int64_t place, std::string_view datapack);
bool valid() const { return kind_ != KIND_INVALID; }
Kind kind() const { return kind_; }
int64_t actor() const { return actor_; }
int64_t place() const { return place_; }
const eng::string &action() const { return action_; }
const InvocationData &data() const { return data_; }
const eng::string &datapack() const { return datapack_; }
void serialize(StreamBuffer *sb) const;
void deserialize(StreamBuffer *sb);

View File

@@ -81,7 +81,7 @@ public:
// The driver loads the lua source automatically.
// However, we don't need it. Throw it out.
get_lua_source();
get_lua_source_pack();
}
void send_invocation(const Invocation &inv) {
@@ -97,11 +97,8 @@ public:
inv.serialize(sb);
}
void send_lua_source(const util::LuaSourceVec &sv) {
StreamBuffer serial;
SourceDB::serialize_source(sv, &serial);
eng::string sstr(serial.view());
Invocation inv(Invocation::KIND_LUA_SOURCE, actor_id_, actor_id_, sstr);
void send_lua_source(std::string_view sourcepack) {
Invocation inv(Invocation::KIND_LUA_SOURCE, actor_id_, actor_id_, sourcepack);
send_invocation(inv);
}
@@ -239,10 +236,10 @@ public:
virtual void event_update() {
// Check for lua source code. If this returns non-null,
// it is because somebody typed CPL.
util::LuaSourcePtr lua_source = get_lua_source();
if (lua_source != nullptr) {
send_lua_source(*lua_source);
lua_source.reset();
eng::string lua_source_pack = get_lua_source_pack();
if (!lua_source_pack.empty()) {
send_lua_source(lua_source_pack);
lua_source_pack.clear();
}
// Check for keyboard input on stdin.

View File

@@ -36,7 +36,7 @@ public:
master_.reset(new World(WORLD_TYPE_MASTER));
// Update the source code of the master model.
master_->update_source(get_lua_source());
master_->update_source(get_lua_source_pack());
// Create an actor for administrative commands.
admin_id_ = master_->create_login_actor();
@@ -168,7 +168,7 @@ public:
virtual void event_update() {
// If the driver has reloaded the source, put it into master model.
master_->update_source(get_lua_source());
master_->update_source(get_lua_source_pack());
// Check for keyboard input on stdin.
while (true) {

View File

@@ -192,7 +192,7 @@ bool PrintChanneler::channel(const PrintBuffer *printbuffer, std::ostream &ostre
Invocation PrintChanneler::invocation(int64_t actor_id) {
char buf[80];
sprintf(buf, "%" PRId64, line_);
return Invocation(Invocation::KIND_FLUSH_PRINTS, actor_id, actor_id, buf, InvocationData());
return Invocation(Invocation::KIND_FLUSH_PRINTS, actor_id, actor_id, buf);
}
LuaDefine(unittests_printbuffer, "", "some unit tests") {

View File

@@ -99,7 +99,7 @@ private:
void event_init(int argc, char *argv[])
{
world_.reset(new World(WORLD_TYPE_MASTER));
world_->update_source(get_lua_source());
world_->update_source(get_lua_source_pack());
world_->run_unittests();
actor_id_ = world_->create_login_actor();
stdostream() << "Login actor ID: " << actor_id_ << std::endl;
@@ -108,7 +108,7 @@ private:
void event_update()
{
world_->update_source(get_lua_source());
world_->update_source(get_lua_source_pack());
while (true) {
eng::string line = get_stdio_channel()->in()->readline();
if (line == "") break;

View File

@@ -357,12 +357,6 @@ void World::update_gui(int64_t actor_id, int64_t place_id, Gui *gui) {
}
}
void World::update_source(const util::LuaSourcePtr &source) {
if (source != nullptr) {
update_source(*source);
}
}
// This is called from World::update_source, and also
// from World::patch_source in the difference transmitter.
//
@@ -397,6 +391,25 @@ void World::update_source(const util::LuaSourceVec &source) {
assert(stack_is_clear());
}
void World::update_source(const util::LuaSourcePtr &source) {
if (source != nullptr) {
update_source(*source);
}
}
void World::update_source(std::string_view sourcepack) {
if (!sourcepack.empty()) {
try {
StreamBuffer sb(sourcepack);
util::LuaSourceVec sv;
SourceDB::deserialize_source(&sv, &sb);
update_source(sv);
} catch (const StreamException &ex) {
return;
}
}
}
void World::http_response(const HttpParser &response) {
// Find the request.
auto iter = http_requests_.find(response.request_id());
@@ -554,19 +567,19 @@ void World::run_unittests() {
void World::invoke(const Invocation &inv) {
switch (inv.kind()) {
case Invocation::KIND_PLAN:
invoke_plan(inv.actor(), inv.place(), inv.action(), inv.data());
invoke_plan(inv.actor(), inv.place(), inv.datapack());
break;
case Invocation::KIND_LUA:
invoke_lua(inv.actor(), inv.place(), inv.action(), inv.data());
invoke_lua(inv.actor(), inv.place(), inv.datapack());
break;
case Invocation::KIND_FLUSH_PRINTS:
invoke_flush_prints(inv.actor(), inv.place(), inv.action(), inv.data());
invoke_flush_prints(inv.actor(), inv.place(), inv.datapack());
break;
case Invocation::KIND_TICK:
invoke_tick(inv.actor(), inv.place(), inv.action(), inv.data());
invoke_tick(inv.actor(), inv.place(), inv.datapack());
break;
case Invocation::KIND_LUA_SOURCE:
invoke_lua_source(inv.actor(), inv.place(), inv.action(), inv.data());
invoke_lua_source(inv.actor(), inv.place(), inv.datapack());
break;
default:
// Do nothing. Standard behavior for any invalid command is to
@@ -578,13 +591,13 @@ void World::invoke(const Invocation &inv) {
}
}
void World::invoke_flush_prints(int64_t actor_id, int64_t place_id, const eng::string &action, const InvocationData &data) {
void World::invoke_flush_prints(int64_t actor_id, int64_t place_id, std::string_view datapack) {
assert(stack_is_clear());
// Check argument sanity.
if (actor_id != place_id) {
return;
}
int64_t line = sv::to_int64(action, -1);
int64_t line = sv::to_int64(datapack, -1);
if ((line < 0)||(line > INT_MAX)) {
return;
}
@@ -598,7 +611,7 @@ void World::invoke_flush_prints(int64_t actor_id, int64_t place_id, const eng::s
void World::invoke_lua(int64_t actor_id, int64_t place_id, const eng::string &action, const InvocationData &data) {
void World::invoke_lua(int64_t actor_id, int64_t place_id, std::string_view datapack) {
assert(stack_is_clear());
// Make sure that actor and place exist and are not stubs.
@@ -618,7 +631,7 @@ void World::invoke_lua(int64_t actor_id, int64_t place_id, const eng::string &ac
LuaExtStack LS(L, func, tangibles, place, mt, thread, thinfo, threads);
// create the compiled closure.
int status = luaL_loadbuffer(L, action.c_str(), action.size(), "=invoke");
int status = luaL_loadbuffer(L, datapack.data(), datapack.size(), "=invoke");
lua_replace(L, func.index());
if (status != LUA_OK) {
// The closure is actually an error message. Do nothing.
@@ -666,18 +679,18 @@ void World::invoke_lua(int64_t actor_id, int64_t place_id, const eng::string &ac
assert(stack_is_clear());
}
void World::invoke_plan(int64_t actor_id, int64_t place_id, const eng::string &action, const InvocationData &data) {
void World::invoke_plan(int64_t actor_id, int64_t place_id, std::string_view datapack) {
assert(stack_is_clear());
// Validate that the action is legal.
Gui validation_gui;
update_gui(actor_id, place_id, &validation_gui);
if (!validation_gui.has_action(action)) {
if (!validation_gui.has_action(datapack)) {
return;
}
// Make sure the action starts with "cb_"
if (!sv::has_prefix(action, "cb_")) {
if (!sv::has_prefix(datapack, "cb_")) {
return;
}
@@ -715,16 +728,14 @@ void World::invoke_plan(int64_t actor_id, int64_t place_id, const eng::string &a
if (!LS.istable(index)) {
return;
}
LS.rawget(func, index, action);
LS.rawget(func, index, datapack);
if (!LS.isfunction(func)) {
return;
}
// Convert the InvocationData into a lua table.
// TODO: maybe add the ability to pass data in as a table?
// For now the table is always empty.
LS.newtable(invdata);
for (const auto &p : data) {
LS.rawset(invdata, p.first, p.second);
}
// Create a new thread, set up function and parameters.
lua_State *CO = LS.newthread(thread);
@@ -757,7 +768,7 @@ void World::invoke_plan(int64_t actor_id, int64_t place_id, const eng::string &a
assert(stack_is_clear());
}
void World::invoke_tick(int64_t actor_id, int64_t place_id, const eng::string &action, const InvocationData &data) {
void World::invoke_tick(int64_t actor_id, int64_t place_id, std::string_view datapack) {
if (!is_authoritative()) {
return;
}
@@ -765,19 +776,12 @@ void World::invoke_tick(int64_t actor_id, int64_t place_id, const eng::string &a
run_scheduled_threads();
}
void World::invoke_lua_source(int64_t actor_id, int64_t place_id, const eng::string &action, const InvocationData &data) {
void World::invoke_lua_source(int64_t actor_id, int64_t place_id, std::string_view datapack) {
if (!is_authoritative()) {
return;
}
// We need some kind of authentication here.
try {
StreamBuffer sb(action);
util::LuaSourceVec sv;
SourceDB::deserialize_source(&sv, &sb);
update_source(sv);
} catch (const StreamException &ex) {
return;
}
update_source(datapack);
}
void World::guard_blockable(lua_State *L, const char *fn) {

View File

@@ -220,9 +220,12 @@ public:
// Update the source database from disk.
//
// Special case: if the source pointer is nullptr, does not update.
// The final form takes a sourcepk, a serialized representation
// of a LuaSourceVec.
//
void update_source(const util::LuaSourcePtr &source);
void update_source(const util::LuaSourceVec &source);
void update_source(const util::LuaSourcePtr &source);
void update_source(std::string_view sourcepk);
// Rebuild the source database.
//
@@ -348,23 +351,23 @@ private:
// Invoke a plan.
//
void invoke_plan(int64_t actor_id, int64_t place_id, const eng::string &action, const InvocationData &data);
void invoke_plan(int64_t actor_id, int64_t place_id, std::string_view datapack);
// Invoke a lua string.
//
void invoke_lua(int64_t actor_id, int64_t place_id, const eng::string &action, const InvocationData &data);
void invoke_lua(int64_t actor_id, int64_t place_id, std::string_view datapack);
// Invoke the flush-prints operation.
//
void invoke_flush_prints(int64_t actor_id, int64_t place_id, const eng::string &action, const InvocationData &data);
void invoke_flush_prints(int64_t actor_id, int64_t place_id, std::string_view datapack);
// Invoke the tick operation.
//
void invoke_tick(int64_t actor_id, int64_t place_id, const eng::string &action, const InvocationData &data);
void invoke_tick(int64_t actor_id, int64_t place_id, std::string_view datapack);
// Invoke the lua_source operation.
//
void invoke_lua_source(int64_t actor_id, int64_t place_id, const eng::string &action, const InvocationData &data);
void invoke_lua_source(int64_t actor_id, int64_t place_id, std::string_view datapack);
public:
////////////////////////////////////////////////////////////////////////////

View File

@@ -193,7 +193,7 @@ class Driver {
std::string err = drvutil::package_lua_source(".", &oss);
if_error_print_and_exit(err);
std::string_view ossv = oss.view();
engw.play_set_lua_source(&engw, ossv.size(), ossv.data());
engw.play_set_lua_source_pack(&engw, ossv.size(), ossv.data());
}
}

View File

@@ -345,8 +345,6 @@ std::string package_lua_source(const std::filesystem::path &base, std::ostream *
sbwrite_uint32(s, names.size());
for (int i = 0; i < int(names.size()); i++) {
sbwrite_string(s, names[i]);
}
for (int i = 0; i < int(names.size()); i++) {
std::filesystem::path lfn = base / "lua" / names[i];
if (!sbwrite_file(s, lfn)) {
return std::string("Cannot read source file: ") + lfn.u8string();