Merge remote-tracking branch 'luprex/main'
This commit is contained in:
15
luprex/.gitattributes
vendored
Normal file
15
luprex/.gitattributes
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
* text=auto
|
||||
* text eol=lf
|
||||
*.bat text eol=crlf
|
||||
*.lib filter=lfs diff=lfs merge=lfs -text
|
||||
*.exe filter=lfs diff=lfs merge=lfs -text
|
||||
*.a filter=lfs diff=lfs merge=lfs -text
|
||||
*.o filter=lfs diff=lfs merge=lfs -text
|
||||
*.obj filter=lfs diff=lfs merge=lfs -text
|
||||
*.dll filter=lfs diff=lfs merge=lfs -text
|
||||
*.pdb filter=lfs diff=lfs merge=lfs -text
|
||||
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.psd filter=lfs diff=lfs merge=lfs -text
|
||||
*.so filter=lfs diff=lfs merge=lfs -text
|
||||
27
luprex/.gitignore
vendored
Normal file
27
luprex/.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
a.out
|
||||
gprof.out
|
||||
gmon.out
|
||||
luprex
|
||||
|
||||
*~
|
||||
\#*#
|
||||
.#*
|
||||
*.obj
|
||||
*.o
|
||||
*.dll
|
||||
*.exe
|
||||
*.pdb
|
||||
*.so
|
||||
*.a
|
||||
*.lib
|
||||
*.ilk
|
||||
*.exp
|
||||
*.pdb
|
||||
|
||||
build/**
|
||||
.vscode/**
|
||||
ext/eris-master/src/lua
|
||||
ext/eris-master/src/luac
|
||||
ext/eris-master/test/persist
|
||||
ext/eris-master/test/unpersist
|
||||
|
||||
30
luprex/.vscode/tasks.json
vendored
Normal file
30
luprex/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build luprex",
|
||||
"type": "shell",
|
||||
"command": "make",
|
||||
"problemMatcher": {
|
||||
"base": "$gcc",
|
||||
"fileLocation": [
|
||||
"relative",
|
||||
"${workspaceFolder}"
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"clear": true,
|
||||
"reveal": "always",
|
||||
"panel": "shared",
|
||||
"revealProblems": "never"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
221
luprex/Makefile
Normal file
221
luprex/Makefile
Normal file
@@ -0,0 +1,221 @@
|
||||
#######################################################################
|
||||
##
|
||||
## Auto detect Operating System
|
||||
##
|
||||
#######################################################################
|
||||
|
||||
ifneq "" "$(findstring -linux-,$(MAKE_HOST))"
|
||||
OS=linux
|
||||
else ifneq "" "$(VSINSTALLDIR)"
|
||||
OS=visual
|
||||
else ifneq "" "$(findstring cmd.exe,$(COMSPEC))"
|
||||
OS=mingw
|
||||
else
|
||||
OS=""
|
||||
endif
|
||||
|
||||
ifeq "$(OS)" ""
|
||||
$(error Cannot figure out which OS to build for."
|
||||
endif
|
||||
$(info Building for $(OS)...)
|
||||
|
||||
|
||||
#######################################################################
|
||||
##
|
||||
## List of all OBJ files
|
||||
##
|
||||
#######################################################################
|
||||
|
||||
OBJ_ERIS=\
|
||||
build/$(OS)/eris/lapi.obj \
|
||||
build/$(OS)/eris/lcode.obj \
|
||||
build/$(OS)/eris/lctype.obj \
|
||||
build/$(OS)/eris/ldebug.obj \
|
||||
build/$(OS)/eris/ldo.obj \
|
||||
build/$(OS)/eris/ldump.obj \
|
||||
build/$(OS)/eris/lfunc.obj \
|
||||
build/$(OS)/eris/lgc.obj \
|
||||
build/$(OS)/eris/llex.obj \
|
||||
build/$(OS)/eris/lmem.obj \
|
||||
build/$(OS)/eris/lobject.obj \
|
||||
build/$(OS)/eris/lopcodes.obj \
|
||||
build/$(OS)/eris/lparser.obj \
|
||||
build/$(OS)/eris/lstate.obj \
|
||||
build/$(OS)/eris/lstring.obj \
|
||||
build/$(OS)/eris/ltable.obj \
|
||||
build/$(OS)/eris/ltm.obj \
|
||||
build/$(OS)/eris/lundump.obj \
|
||||
build/$(OS)/eris/lvm.obj \
|
||||
build/$(OS)/eris/lzio.obj \
|
||||
build/$(OS)/eris/lauxlib.obj \
|
||||
build/$(OS)/eris/lbaselib.obj \
|
||||
build/$(OS)/eris/lbitlib.obj \
|
||||
build/$(OS)/eris/lcorolib.obj \
|
||||
build/$(OS)/eris/ldblib.obj \
|
||||
build/$(OS)/eris/liolib.obj \
|
||||
build/$(OS)/eris/lmathlib.obj \
|
||||
build/$(OS)/eris/loslib.obj \
|
||||
build/$(OS)/eris/lstrlib.obj \
|
||||
build/$(OS)/eris/ltablib.obj \
|
||||
build/$(OS)/eris/loadlib.obj \
|
||||
build/$(OS)/eris/linit.obj \
|
||||
build/$(OS)/eris/eris.obj \
|
||||
|
||||
OBJ_CORE=\
|
||||
build/$(OS)/core/invocation.obj\
|
||||
build/$(OS)/core/spookyv2.obj\
|
||||
build/$(OS)/core/eng-malloc.obj\
|
||||
build/$(OS)/core/debugcollector.obj\
|
||||
build/$(OS)/core/drivenengine.obj\
|
||||
build/$(OS)/core/util.obj\
|
||||
build/$(OS)/core/luastack.obj\
|
||||
build/$(OS)/core/traceback.obj\
|
||||
build/$(OS)/core/planemap.obj\
|
||||
build/$(OS)/core/pprint.obj\
|
||||
build/$(OS)/core/luaconsole.obj\
|
||||
build/$(OS)/core/idalloc.obj\
|
||||
build/$(OS)/core/globaldb.obj\
|
||||
build/$(OS)/core/sched.obj\
|
||||
build/$(OS)/core/http.obj\
|
||||
build/$(OS)/core/json.obj\
|
||||
build/$(OS)/core/table.obj\
|
||||
build/$(OS)/core/luasnap.obj\
|
||||
build/$(OS)/core/animqueue.obj\
|
||||
build/$(OS)/core/streambuffer.obj\
|
||||
build/$(OS)/core/source.obj\
|
||||
build/$(OS)/core/world-core.obj\
|
||||
build/$(OS)/core/world-accessor.obj\
|
||||
build/$(OS)/core/world-difftab.obj\
|
||||
build/$(OS)/core/world-diffxmit.obj\
|
||||
build/$(OS)/core/world-pairtab.obj\
|
||||
build/$(OS)/core/world-testing.obj\
|
||||
build/$(OS)/core/lpxserver.obj\
|
||||
build/$(OS)/core/lpxclient.obj\
|
||||
build/$(OS)/core/eng-tests.obj\
|
||||
build/$(OS)/core/printbuffer.obj\
|
||||
build/$(OS)/core/serializelua.obj\
|
||||
|
||||
|
||||
OBJ_DRV=\
|
||||
build/$(OS)/drv/driver.obj\
|
||||
build/$(OS)/drv/drvutil.obj\
|
||||
build/$(OS)/drv/osdrvutil.obj\
|
||||
build/$(OS)/drv/sslutil.obj\
|
||||
build/$(OS)/drv/readline.obj\
|
||||
|
||||
|
||||
#######################################################################
|
||||
##
|
||||
## Make rules for linux
|
||||
##
|
||||
#######################################################################
|
||||
|
||||
ifeq "$(OS)" "linux"
|
||||
OPT=-g -O0
|
||||
LUPREX_EXE=luprex
|
||||
LUPREXLIB_DLL=luprexlib.so
|
||||
LUPREXSTATIC_EXE=luprexstatic
|
||||
COMPILE=g++ -Wall $(OPT) -std=c++17 -fvisibility=hidden -c -MMD -fPIC -o
|
||||
LINKDLL=g++ -Wall $(OPT) -std=c++17 -export-dynamic -Wl,--no-allow-shlib-undefined -Wl,-z,defs -shared -o
|
||||
LINKEXE=g++ -Wall $(OPT) -std=c++17 -o
|
||||
MAKEDEPS=true
|
||||
OPENSSL_INCLUDE=-I./ext/openssl-3.0.1/inc
|
||||
LUA_FLAGS=-DLUA_USE_APICHECK -DLUA_USE_POSIX
|
||||
LIBS=-L./ext/openssl-3.0.1/lib/linux -lssl -lcrypto -ldl
|
||||
endif
|
||||
|
||||
|
||||
#######################################################################
|
||||
##
|
||||
## Make rules for mingw
|
||||
##
|
||||
#######################################################################
|
||||
|
||||
ifeq "$(OS)" "mingw"
|
||||
OPT=-g -O0
|
||||
LUPREX_EXE=luprex.exe
|
||||
LUPREXLIB_DLL=luprexlib.dll
|
||||
LUPREXSTATIC_EXE=luprexstatic.exe
|
||||
COMPILE=g++ -Wall $(OPT) -std=c++17 -fvisibility=hidden -c -MMD -fPIC -o
|
||||
LINKDLL=g++ -Wall $(OPT) -std=c++17 -Wl,--no-allow-shlib-undefined -shared -o
|
||||
LINKEXE=g++ -Wall $(OPT) -std=c++17 -o
|
||||
MAKEDEPS=true
|
||||
OPENSSL_INCLUDE=-I./ext/openssl-3.0.1/inc
|
||||
LUA_FLAGS=-DLUA_USE_APICHECK -DLUA_COMPAT_ALL
|
||||
LIBS=-L./ext/openssl-3.0.1/lib/mingw -lssl -lcrypto -lws2_32 -lcrypt32 -lcryptui
|
||||
endif
|
||||
|
||||
|
||||
#######################################################################
|
||||
##
|
||||
## Make rules for visual
|
||||
##
|
||||
#######################################################################
|
||||
|
||||
ifeq "$(OS)" "visual"
|
||||
ifeq "" "$(VSINSTALLDIR)"
|
||||
$(error You must use vcvars64.bat to set up the visual studio environment variables)
|
||||
endif
|
||||
OPT=/Od /Zi
|
||||
LUPREX_EXE=luprex.exe
|
||||
LUPREXLIB_DLL=luprexlib.dll
|
||||
LUPREXSTATIC_EXE=luprexstatic.exe
|
||||
COMPILE=CL $(OPT) /std:c++17 /EHsc /nologo /MD /TP /c /Fo:
|
||||
LINKDLL=CL $(OPT) /std:c++17 /EHsc /nologo /LDd /Fe:
|
||||
LINKEXE=CL $(OPT) /std:c++17 /EHsc /nologo /Fe:
|
||||
MAKEDEPS=g++ -Wall -std=c++17 -MMD -E -o
|
||||
OPENSSL_INCLUDE=-I./ext/openssl-3.1.0/inc
|
||||
LUA_FLAGS=-DLUA_USE_APICHECK -DLUA_COMPAT_ALL
|
||||
LIBS=ext/openssl-3.1.0/lib/visual/libcrypto.lib ext/openssl-3.1.0/lib/visual/libssl.lib ws2_32.lib crypt32.lib cryptui.lib user32.lib advapi32.lib
|
||||
endif
|
||||
|
||||
#######################################################################
|
||||
##
|
||||
## Make Rules
|
||||
##
|
||||
#######################################################################
|
||||
|
||||
all: build/$(OS)/$(LUPREX_EXE) build/$(OS)/$(LUPREXSTATIC_EXE) build/$(OS)/$(LUPREXLIB_DLL)
|
||||
|
||||
build/$(OS)/DIRECTORY:
|
||||
mkdir -p build/$(OS)/core build/$(OS)/eris build/$(OS)/drv
|
||||
touch build/$(OS)/DIRECTORY
|
||||
|
||||
build/$(OS)/$(LUPREX_EXE): build/$(OS)/$(LUPREXLIB_DLL) $(OBJ_DRV)
|
||||
$(LINKEXE) $@ $(OBJ_DRV) $(LIBS)
|
||||
|
||||
build/$(OS)/$(LUPREXSTATIC_EXE): $(OBJ_DRV) $(OBJ_ERIS) $(OBJ_CORE)
|
||||
$(LINKEXE) $@ $^ $(LIBS)
|
||||
|
||||
build/$(OS)/$(LUPREXLIB_DLL): $(OBJ_ERIS) $(OBJ_CORE)
|
||||
$(LINKDLL) $@ $^
|
||||
|
||||
build/$(OS)/eris/%.obj: ext/eris-master/src/%.c build/$(OS)/DIRECTORY
|
||||
$(MAKEDEPS) $@d $(LUA_FLAGS) $<
|
||||
$(COMPILE) $@ $(LUA_FLAGS) $<
|
||||
|
||||
build/$(OS)/core/%.obj: cpp/core/%.cpp build/$(OS)/DIRECTORY
|
||||
$(MAKEDEPS) $@d -I./ext/eris-master/src -I./cpp/wrap -I./cpp/core -I./ext $<
|
||||
$(COMPILE) $@ -I./ext/eris-master/src -I./cpp/wrap -I./cpp/core -I./ext $<
|
||||
|
||||
build/$(OS)/drv/%.obj: cpp/drv/%.cpp build/$(OS)/DIRECTORY
|
||||
$(MAKEDEPS) $@d $(OPENSSL_INCLUDE) -I./src/drv -I./ext $<
|
||||
$(COMPILE) $@ $(OPENSSL_INCLUDE) -I./src/drv -I./ext $<
|
||||
|
||||
clean:
|
||||
rm -f luprex* luprex*.* *.pdb
|
||||
rm -rf build
|
||||
|
||||
clean-os:
|
||||
rm -f luprex* luprex*.* *.pdb build/$(OS)/* build/$(OS)/*/*
|
||||
|
||||
#######################################################################
|
||||
##
|
||||
## Automatically generated Make Dependencies
|
||||
##
|
||||
#######################################################################
|
||||
|
||||
-include $(OBJ_ERIS:%.obj=%.d)
|
||||
-include $(OBJ_CORE:%.obj=%.d)
|
||||
-include $(OBJ_DRV:%.obj=%.d)
|
||||
|
||||
28
luprex/README.md
Normal file
28
luprex/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
"# luprex"
|
||||
|
||||
Directory Structure:
|
||||
|
||||
cpp - C++ source code
|
||||
|
||||
cpp/core - C++ source code for the engine proper
|
||||
cpp/drv - C++ source code for the driver
|
||||
cpp/wrap - C++ STL classes reconfigured to use eng-malloc
|
||||
|
||||
ext - external third-party libraries
|
||||
|
||||
ext/dlmalloc.c - Doug Lea's malloc (used by eng-malloc)
|
||||
ext/openssl - Header files for our version of openssl
|
||||
ext/openssl-linux - Library files for openssl, compiled for linux
|
||||
ext/openssl-mingw - Library files for openssl, compiled for mingw
|
||||
ext/eris-master - The C code for the lua interpreter, eris lua.
|
||||
|
||||
obj - a place to store compiled C++ object code
|
||||
|
||||
obj/core - Compiled from cpp/core
|
||||
obj/drv - Compiled from cpp/drv
|
||||
obj/eris - Compiled from ext/eris-master
|
||||
|
||||
lua - game code written in lua
|
||||
|
||||
misc - a place for tinkering, not part of the main project
|
||||
|
||||
746
luprex/cpp/core/animqueue.cpp
Normal file
746
luprex/cpp/core/animqueue.cpp
Normal file
@@ -0,0 +1,746 @@
|
||||
#define _USE_MATH_DEFINES
|
||||
|
||||
#include "wrap-sstream.hpp"
|
||||
#include "wrap-map.hpp"
|
||||
#include "util.hpp"
|
||||
#include "animqueue.hpp"
|
||||
#include "luastack.hpp"
|
||||
#include "streambuffer.hpp"
|
||||
|
||||
#include <limits>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
|
||||
|
||||
util::SharedStdString AnimQueue::blankqueue_;
|
||||
|
||||
void AnimQueue::initialize_module() {
|
||||
AnimQueue queue;
|
||||
blankqueue_ = queue.get_encoded_queue();
|
||||
}
|
||||
|
||||
static uint64_t hash_encstep(uint64_t prev, std::string_view s) {
|
||||
return util::hash_string(util::HashValue(123, prev), s).first;
|
||||
}
|
||||
|
||||
// Parse a value. This is meant for unit testing only. The
|
||||
// parser isn't powerful enough to express all possible values.
|
||||
static void parse_value(std::string_view vstr, AnimValue *v) {
|
||||
// Try to interpret vstr as a boolean.
|
||||
bool is_true = (vstr == "true");
|
||||
bool is_false = (vstr == "false");
|
||||
if (is_true || is_false) {
|
||||
v->set_boolean(is_true);
|
||||
return;
|
||||
}
|
||||
// Try to interpret vstr as a number.
|
||||
if (sv::valid_number(vstr, true, true, true, false)) {
|
||||
v->set_number(std::atof(std::string(vstr).c_str()));
|
||||
return;
|
||||
}
|
||||
// Try to interpret vstr as a vector.
|
||||
eng::vector<eng::string> parts = util::split(eng::string(vstr), ',');
|
||||
if ((parts.size() == 3) &&
|
||||
(sv::valid_number(parts[0], true, true, true, false)) &&
|
||||
(sv::valid_number(parts[1], true, true, true, false)) &&
|
||||
(sv::valid_number(parts[2], true, true, true, false))) {
|
||||
double x = std::atof(parts[0].c_str());
|
||||
double y = std::atof(parts[1].c_str());
|
||||
double z = std::atof(parts[2].c_str());
|
||||
v->set_dxyz(util::DXYZ(x,y,z));
|
||||
return;
|
||||
}
|
||||
// If it doesn't parse as any of the above, it's a string.
|
||||
v->set_string(vstr);
|
||||
}
|
||||
|
||||
|
||||
static AnimValue parse_anim_value(LuaCoreStack &LS, LuaSlot val, LuaSlot tmp) {
|
||||
AnimValue result;
|
||||
auto tboolean = LS.tryboolean(val);
|
||||
if (tboolean) {
|
||||
result.set_boolean(*tboolean);
|
||||
return result;
|
||||
}
|
||||
auto tnumber = LS.trynumber(val);
|
||||
if (tnumber) {
|
||||
result.set_number(*tnumber);
|
||||
return result;
|
||||
}
|
||||
auto tstring = LS.trystringview(val);
|
||||
if (tstring) {
|
||||
result.set_string(*tstring);
|
||||
return result;
|
||||
}
|
||||
auto txyz = LS.tryxyz(val);
|
||||
if (txyz) {
|
||||
result.set_dxyz(*txyz);
|
||||
return result;
|
||||
}
|
||||
if (LS.rawequal(val, LuaToken("auto"))) {
|
||||
result.set_auto();
|
||||
return result;
|
||||
}
|
||||
result.set_uninitialized();
|
||||
return result;
|
||||
}
|
||||
|
||||
void AnimState::set_persistent(const eng::string &name) {
|
||||
map_[name].persistent = true;
|
||||
}
|
||||
|
||||
void AnimState::set_boolean(const eng::string &name, bool v) {
|
||||
AnimValue &value = map_[name];
|
||||
value.set_boolean(v);
|
||||
}
|
||||
|
||||
void AnimState::set_number(const eng::string &name, double v) {
|
||||
AnimValue &value = map_[name];
|
||||
value.set_number(v);
|
||||
}
|
||||
|
||||
void AnimState::set_dxyz(const eng::string &name, const util::DXYZ &v) {
|
||||
AnimValue &value = map_[name];
|
||||
value.set_dxyz(v);
|
||||
}
|
||||
|
||||
void AnimState::set_string(const eng::string &name, std::string_view v) {
|
||||
AnimValue &value = map_[name];
|
||||
value.set_string(v);
|
||||
}
|
||||
|
||||
void AnimState::print_debug_string(eng::ostringstream &oss) {
|
||||
bool first = true;
|
||||
if (map_.empty()) {
|
||||
oss << "[empty]";
|
||||
}
|
||||
for (const auto &pair : map_) {
|
||||
if (!first) oss << " ";
|
||||
const eng::string &name = pair.first;
|
||||
const AnimValue &value = pair.second;
|
||||
oss << name;
|
||||
if (value.persistent) {
|
||||
oss << "=";
|
||||
} else {
|
||||
oss << ":";
|
||||
}
|
||||
switch (value.type) {
|
||||
case SimpleDynamicTag::UNINITIALIZED: oss << "UNINITIALIZED"; break;
|
||||
case SimpleDynamicTag::AUTO: oss << "AUTO"; break;
|
||||
case SimpleDynamicTag::NUMBER: oss << value.x; break;
|
||||
case SimpleDynamicTag::BOOLEAN: oss << ((value.x == 1.0) ? "true":"false"); break;
|
||||
case SimpleDynamicTag::VECTOR: oss << value.x << "," << value.y << "," << value.z; break;
|
||||
case SimpleDynamicTag::STRING: oss << value.s; break;
|
||||
default: assert(false);
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
eng::string AnimState::debug_string() {
|
||||
eng::ostringstream oss;
|
||||
print_debug_string(oss);
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
eng::string AnimState::encode() const {
|
||||
StreamBuffer sb;
|
||||
for (const auto &pair : map_) {
|
||||
const eng::string &name = pair.first;
|
||||
const AnimValue &value = pair.second;
|
||||
sb.write_string(name);
|
||||
sb.write_bool(value.persistent);
|
||||
sb.write_simple_dynamic(value);
|
||||
}
|
||||
return eng::string(sb.view());
|
||||
}
|
||||
|
||||
void AnimState::decode(std::string_view s) {
|
||||
map_.clear();
|
||||
StreamBuffer sb(s);
|
||||
while (!sb.empty()) {
|
||||
eng::string name = sb.read_string();
|
||||
AnimValue &value = map_[name];
|
||||
value.persistent = sb.read_bool();
|
||||
sb.read_simple_dynamic(&value);
|
||||
}
|
||||
}
|
||||
|
||||
void AnimState::decode_persistent(std::string_view s) {
|
||||
map_.clear();
|
||||
StreamBuffer sb(s);
|
||||
AnimValue dummy;
|
||||
while (!sb.empty()) {
|
||||
eng::string name = sb.read_string();
|
||||
bool persistent = sb.read_bool();
|
||||
if (persistent) {
|
||||
AnimValue &value = map_[name];
|
||||
value.persistent = persistent;
|
||||
sb.read_simple_dynamic(&value);
|
||||
} else {
|
||||
sb.read_simple_dynamic(&dummy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eng::string AnimState::add_default(const eng::string &name, const AnimValue &def, const AnimState *other) {
|
||||
AnimValue &value = map_[name];
|
||||
value.persistent = true;
|
||||
if (value.type == SimpleDynamicTag::UNINITIALIZED) {
|
||||
if (other != nullptr) {
|
||||
auto otheriter = other->map_.find(name);
|
||||
if (otheriter != other->map_.end()) {
|
||||
if (otheriter->second.persistent && otheriter->second.type == def.type) {
|
||||
value.copy_value(otheriter->second);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
value.copy_value(def);
|
||||
return "";
|
||||
}
|
||||
if (value.type != def.type) {
|
||||
return util::ss("Animation key ", name, " must be a ", def.type_name());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
eng::string AnimState::add_defaults(const AnimState *other) {
|
||||
eng::string err;
|
||||
AnimValue defval;
|
||||
|
||||
defval.set_dxyz(util::DXYZ(0,0,0));
|
||||
err = add_default("xyz", defval, other);
|
||||
if (!err.empty()) return err;
|
||||
|
||||
defval.set_string("nowhere");
|
||||
err = add_default("plane", defval, other);
|
||||
if (!err.empty()) return err;
|
||||
|
||||
defval.set_number(0.0);
|
||||
err = add_default("facing", defval, other);
|
||||
if (!err.empty()) return err;
|
||||
|
||||
defval.set_string("unknown");
|
||||
err = add_default("bp", defval, other);
|
||||
if (!err.empty()) return err;
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
eng::string AnimState::from_lua(LuaCoreStack &LS0, LuaSlot tab, bool persistent, bool allowauto) {
|
||||
LuaVar key, val, tmp;
|
||||
LuaExtStack LS(LS0.state(), key, val, tmp);
|
||||
util::DXYZ xyz;
|
||||
|
||||
clear();
|
||||
if (!LS.istable(tab)) {
|
||||
return "A lua animstate must be a table.";
|
||||
}
|
||||
LS.set(key, LuaNil);
|
||||
while (LS.next(tab, key, val)) {
|
||||
if (!LS.isstring(key)) {
|
||||
return "in animation key-value pairs, key must be a string.";
|
||||
}
|
||||
eng::string name = LS.ckstring(key);
|
||||
if (!sv::is_lua_id(name)) {
|
||||
return "in animation key-value pairs, key must be a valid lua identifier.";
|
||||
}
|
||||
AnimValue parsedvalue = parse_anim_value(LS, val, tmp);
|
||||
if (parsedvalue.type == SimpleDynamicTag::UNINITIALIZED) {
|
||||
return "in animation key-value pairs, value must be number, string, boolean, or xyz";
|
||||
}
|
||||
if ((parsedvalue.type == SimpleDynamicTag::AUTO) && !allowauto) {
|
||||
return "in animation key-value pairs, value must not be AUTO here.";
|
||||
}
|
||||
AnimValue &mapentry = map_[name];
|
||||
mapentry.copy_value(parsedvalue);
|
||||
mapentry.persistent = persistent;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
eng::string AnimState::merge(const AnimState &previous, const AnimState &update) {
|
||||
// Copy everything over from the previous entry.
|
||||
map_ = previous.map_;
|
||||
|
||||
for (const auto &pair : update.map_) {
|
||||
const eng::string &name = pair.first;
|
||||
AnimValue &dst = map_[name];
|
||||
const AnimValue &src = pair.second;
|
||||
|
||||
// Handle autocalculation rules.
|
||||
if (src.type == SimpleDynamicTag::AUTO) {
|
||||
if (name == "facing") {
|
||||
if (!dst.persistent || dst.type != SimpleDynamicTag::NUMBER) {
|
||||
return "Cannot auto-calculate facing because facing has not been specified as a persistent number";
|
||||
}
|
||||
const auto xyz_previous = previous.map_.find("xyz");
|
||||
const auto xyz_update = update.map_.find("xyz");
|
||||
if ((xyz_previous == previous.map_.end()) ||
|
||||
(xyz_update == update.map_.end()) ||
|
||||
(xyz_previous->second.type != SimpleDynamicTag::VECTOR) ||
|
||||
(xyz_update->second.type != SimpleDynamicTag::VECTOR)) {
|
||||
return "Cannot auto-calculate facing because before/after xyz coordinates are not present";
|
||||
}
|
||||
double dx = xyz_update->second.x - xyz_previous->second.x;
|
||||
double dy = xyz_update->second.y - xyz_previous->second.y;
|
||||
// If dx and dy are both zero, leave the facing unmodified.
|
||||
if ((dx != 0.0) || (dy != 0.0)) {
|
||||
double facing = atan2(dy, dx) * 180.0 / M_PI;
|
||||
dst.set_number(facing);
|
||||
}
|
||||
} else {
|
||||
return util::ss("No rule to automatically calculate ", name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dst.persistent && (src.type != dst.type)) {
|
||||
return util::ss("Wrong data type for ", name, ", should be ", dst.type_name());
|
||||
}
|
||||
dst.copy_value(src);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
void AnimState::to_lua(LuaCoreStack &LS0, LuaSlot tab, bool persistent) {
|
||||
LuaVar name, val;
|
||||
LuaExtStack LS(LS0.state(), name, val);
|
||||
LS.newtable(tab);
|
||||
for (const auto &pair : map_) {
|
||||
if (pair.second.persistent != persistent) continue;
|
||||
LS.set(name, pair.first);
|
||||
const AnimValue &value = pair.second;
|
||||
if (value.type == SimpleDynamicTag::BOOLEAN) {
|
||||
LS.set(val, (value.x == 1.0));
|
||||
} else if (value.type == SimpleDynamicTag::NUMBER) {
|
||||
LS.set(val, value.x);
|
||||
} else if (value.type == SimpleDynamicTag::STRING) {
|
||||
LS.set(val, std::string_view(value.s));
|
||||
} else if (value.type == SimpleDynamicTag::VECTOR) {
|
||||
LS.newtable(val);
|
||||
LS.rawset(val, 1, value.x);
|
||||
LS.rawset(val, 2, value.y);
|
||||
LS.rawset(val, 3, value.z);
|
||||
}
|
||||
LS.rawset(tab, name, val);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// The syntax used by this parser is not general enough to represent all
|
||||
// possible strings. That's OK, though, since it's just for unit testing.
|
||||
void AnimState::parse(std::string_view config) {
|
||||
while (true) {
|
||||
config = sv::ltrim(config);
|
||||
if (config.empty()) break;
|
||||
eng::string name(sv::read_ascii_identifier(config));
|
||||
assert(!name.empty());
|
||||
AnimValue &value = map_[name];
|
||||
bool has_equal = sv::has_prefix(config, "=");
|
||||
bool has_colon = sv::has_prefix(config, ":");
|
||||
assert(has_equal || has_colon);
|
||||
config.remove_prefix(1);
|
||||
value.persistent = has_equal;
|
||||
eng::string vstr(sv::read_to_space(config));
|
||||
parse_value(vstr, &value);
|
||||
}
|
||||
}
|
||||
|
||||
void AnimState::clear_and_parse(std::string_view config) {
|
||||
map_.clear();
|
||||
parse(config);
|
||||
}
|
||||
|
||||
void AnimCoreState::decode(std::string_view s) {
|
||||
plane.clear();
|
||||
xyz = 0.0;
|
||||
StreamBuffer sb(s);
|
||||
AnimValue value;
|
||||
while (!sb.empty()) {
|
||||
eng::string name = sb.read_string();
|
||||
bool persistent = sb.read_bool();
|
||||
sb.read_simple_dynamic(&value);
|
||||
if (persistent) {
|
||||
if ((name == "xyz") && (value.type == SimpleDynamicTag::VECTOR)) xyz = util::DXYZ(value.x, value.y, value.z);
|
||||
if ((name == "plane") && (value.type == SimpleDynamicTag::STRING)) plane = value.s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int AnimQueue::get_size_limit() const {
|
||||
if (encqueue_ == nullptr) return 0;
|
||||
StreamBuffer sb(*encqueue_);
|
||||
return sb.read_uint8();
|
||||
}
|
||||
|
||||
int AnimQueue::get_actual_size() const {
|
||||
if (encqueue_ == nullptr) return 0;
|
||||
StreamBuffer sb(*encqueue_);
|
||||
sb.read_bytes(1);
|
||||
return sb.read_uint8();
|
||||
}
|
||||
|
||||
uint64_t AnimQueue::get_final_hash() const {
|
||||
if (encqueue_ == nullptr) return 0;
|
||||
StreamBuffer sb(*encqueue_);
|
||||
sb.read_bytes(2);
|
||||
return sb.read_uint64();
|
||||
}
|
||||
|
||||
std::string_view AnimQueue::get_final_encstep() const {
|
||||
if (encqueue_ == nullptr) return std::string_view();
|
||||
StreamBuffer sb(*encqueue_);
|
||||
sb.read_bytes(10);
|
||||
return sb.read_string_view();
|
||||
}
|
||||
|
||||
AnimQueue::QueueRange AnimQueue::get_range(int lo, int hi) {
|
||||
// Clamp lo and hi to the valid range (0 to actual_size).
|
||||
//
|
||||
int actual_size = get_actual_size();
|
||||
if (lo < 0) lo = 0;
|
||||
if (hi > actual_size) hi = actual_size;
|
||||
|
||||
// Abort early if the range is empty. This avoids several edge cases.
|
||||
//
|
||||
if (lo >= hi) return QueueRange(0, std::string_view());
|
||||
|
||||
// Get the entries.
|
||||
//
|
||||
std::string_view queueview(*encqueue_);
|
||||
StreamBuffer sb(queueview);
|
||||
sb.read_bytes(2); // Skip over the header.
|
||||
for (int i = 0; i < lo; i++) {
|
||||
sb.read_uint64();
|
||||
sb.read_string_view();
|
||||
}
|
||||
int pos1 = sb.total_reads();
|
||||
for (int i = lo; i < hi; i++) {
|
||||
sb.read_uint64();
|
||||
sb.read_string_view();
|
||||
}
|
||||
int pos2 = sb.total_reads();
|
||||
return QueueRange(hi-lo, queueview.substr(pos1, pos2 - pos1));
|
||||
}
|
||||
|
||||
uint64_t AnimQueue::hash_encstep(const QueueRange &prev, std::string_view s) {
|
||||
uint64_t prev_hash = 0;
|
||||
if (prev.size > 0) {
|
||||
StreamBuffer retsb(prev.entries);
|
||||
prev_hash = retsb.read_uint64();
|
||||
}
|
||||
return ::hash_encstep(prev_hash, s);
|
||||
}
|
||||
|
||||
void AnimQueue::update_encqueue(int limit, bool add, std::string_view add_enc, int keeplo, int keephi) {
|
||||
// Get the retained entries.
|
||||
QueueRange keeprange = get_range(keeplo, keephi);
|
||||
|
||||
// Encode everything into a binary blob.
|
||||
StreamBuffer result;
|
||||
result.write_uint8(limit);
|
||||
result.write_uint8(keeprange.size + (add ? 1:0));
|
||||
if (add) {
|
||||
uint64_t add_hash = hash_encstep(keeprange, add_enc);
|
||||
result.write_uint64(add_hash);
|
||||
result.write_string(add_enc);
|
||||
}
|
||||
result.write_bytes(keeprange.entries);
|
||||
|
||||
// Replace the shared string.
|
||||
encqueue_ = std::make_shared<std::string>(result.view());
|
||||
}
|
||||
|
||||
AnimQueue::AnimQueue() {
|
||||
update_encqueue(10, true, AnimState().encode(), 0, 0);
|
||||
}
|
||||
|
||||
void AnimQueue::clear(const AnimState &state) {
|
||||
update_encqueue(get_size_limit(), true, state.encode(), 0, 0);
|
||||
}
|
||||
|
||||
void AnimQueue::clear() {
|
||||
update_encqueue(get_size_limit(), true, AnimState().encode(), 0, 0);
|
||||
}
|
||||
|
||||
void AnimQueue::set_limit(int limit) {
|
||||
assert((limit >= 2) && (limit <= 250));
|
||||
update_encqueue(limit, false, std::string_view(), 0, limit);
|
||||
}
|
||||
|
||||
void AnimQueue::add(const AnimState &state) {
|
||||
int limit = get_size_limit();
|
||||
update_encqueue(limit, true, state.encode(), 0, limit - 1);
|
||||
}
|
||||
|
||||
void AnimQueue::replace(const AnimState &state) {
|
||||
int limit = get_size_limit();
|
||||
update_encqueue(limit, true, state.encode(), 1, limit);
|
||||
}
|
||||
|
||||
void AnimQueue::serialize(StreamBuffer *sb) const {
|
||||
sb->write_string(*encqueue_);
|
||||
}
|
||||
|
||||
void AnimQueue::deserialize(StreamBuffer *sb) {
|
||||
encqueue_ = std::make_shared<std::string>(sb->read_string_view());
|
||||
}
|
||||
|
||||
bool AnimQueue::diff(const AnimQueue &auth, StreamBuffer *sb) const {
|
||||
// Fast check for exactly equivalent. If equivalent, skip all the work.
|
||||
if (exactly_equal_fast(auth)) {
|
||||
assert(exactly_equal(auth));
|
||||
sb->write_bool(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: maybe send less data?
|
||||
sb->write_bool(true);
|
||||
sb->write_string(*auth.encqueue_);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AnimQueue::patch(StreamBuffer *sb, DebugCollector *dbc) {
|
||||
bool changed = sb->read_bool();
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
DebugLine(dbc) << "AnimQueue modified";
|
||||
encqueue_ = std::make_shared<std::string>(sb->read_string_view());
|
||||
}
|
||||
|
||||
bool AnimQueue::exactly_equal(const AnimQueue &other) const {
|
||||
if (*encqueue_ != *other.encqueue_) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AnimQueue::exactly_equal_fast(const AnimQueue &other) const {
|
||||
if (encqueue_->size() != other.encqueue_->size()) return false;
|
||||
if (encqueue_->compare(0, 10, *other.encqueue_) != 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void AnimQueue::print_debug_string(eng::ostringstream &oss, bool full) const {
|
||||
bool first = true;
|
||||
// Break out the steps.
|
||||
eng::vector<std::string_view> encsteps;
|
||||
StreamBuffer sb(*encqueue_);
|
||||
int size_limit = sb.read_uint8();
|
||||
int actual_size = sb.read_uint8();
|
||||
if (full) {
|
||||
oss << "limit=" << size_limit;
|
||||
first = false;
|
||||
}
|
||||
for (int i = 0; i < actual_size; i++) {
|
||||
sb.read_uint64();
|
||||
encsteps.push_back(sb.read_string_view());
|
||||
}
|
||||
assert(sb.empty());
|
||||
for (int i = encsteps.size() - 1; i >= 0; i --) {
|
||||
if (!first) oss << "; ";
|
||||
AnimState state(encsteps[i]);
|
||||
state.print_debug_string(oss);
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
eng::string AnimQueue::steps_debug_string() const {
|
||||
eng::ostringstream oss;
|
||||
print_debug_string(oss, false);
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
eng::string AnimQueue::full_debug_string() const {
|
||||
eng::ostringstream oss;
|
||||
print_debug_string(oss, true);
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
AnimCoreState AnimQueue::get_final_core_state() const {
|
||||
std::string_view encstep = get_final_encstep();
|
||||
AnimCoreState result;
|
||||
result.decode(encstep);
|
||||
return result;
|
||||
}
|
||||
|
||||
AnimState AnimQueue::get_final_persistent() const {
|
||||
std::string_view encstep = get_final_encstep();
|
||||
AnimState result;
|
||||
result.decode_persistent(encstep);
|
||||
return result;
|
||||
}
|
||||
|
||||
AnimState AnimQueue::get_final_everything() const {
|
||||
std::string_view encstep = get_final_encstep();
|
||||
AnimState result;
|
||||
result.decode(encstep);
|
||||
return result;
|
||||
}
|
||||
|
||||
LuaDefine(unittests_animqueue, "", "some unit tests") {
|
||||
// Useful objects.
|
||||
AnimQueue aq, aqs;
|
||||
StreamBuffer sb;
|
||||
AnimState astate;
|
||||
eng::string enc;
|
||||
AnimCoreState core;
|
||||
|
||||
// Debug string of a newly initialized queue
|
||||
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; [empty]");
|
||||
|
||||
// Test AnimState simple setters.
|
||||
astate.set_string("color", "blue");
|
||||
astate.set_dxyz("xyz", util::DXYZ(1,2,3));
|
||||
astate.set_number("half", 0.5);
|
||||
astate.set_boolean("nice", true);
|
||||
LuaAssertStrEq(L, astate.debug_string(), "color:blue half:0.5 nice:true xyz:1,2,3");
|
||||
|
||||
// // Test AnimState simple getters.
|
||||
// LuaAssert(L, astate.get_string("color") == "blue");
|
||||
// LuaAssert(L, astate.get_xyz("xyz") == util::DXYZ(1,2,3));
|
||||
// LuaAssert(L, astate.get_number("half") == 0.5);
|
||||
// LuaAssert(L, astate.get_boolean("nice") == true);
|
||||
|
||||
// // Test AnimState simple getters on nonexistent data.
|
||||
// LuaAssert(L, astate.get_string("q") == "");
|
||||
// LuaAssert(L, astate.get_xyz("q") == util::DXYZ(0,0,0));
|
||||
// LuaAssert(L, astate.get_number("q") == 0.0);
|
||||
// LuaAssert(L, astate.get_boolean("q") == false);
|
||||
|
||||
// // Test AnimState simple getters on wrong-type data.
|
||||
// LuaAssert(L, astate.get_string("half") == "");
|
||||
// LuaAssert(L, astate.get_xyz("half") == util::DXYZ(0,0,0));
|
||||
// LuaAssert(L, astate.get_number("color") == 0.0);
|
||||
// LuaAssert(L, astate.get_boolean("color") == false);
|
||||
|
||||
// Test AnimState persistence manipulation.
|
||||
astate.set_persistent("color");
|
||||
astate.set_persistent("nice");
|
||||
LuaAssertStrEq(L, astate.debug_string(), "color=blue half:0.5 nice=true xyz:1,2,3");
|
||||
|
||||
// Test AnimState parser.
|
||||
astate.clear_and_parse("color:green mean=true pos=3,4,5 ok:false");
|
||||
LuaAssertStrEq(L, astate.debug_string(), "color:green mean=true ok:false pos=3,4,5");
|
||||
|
||||
// Test animstate encoding and decoding.
|
||||
astate.clear_and_parse("color:green mean=true pos=3,4,5 ok:false");
|
||||
enc = astate.encode();
|
||||
astate.clear();
|
||||
astate.decode(enc);
|
||||
LuaAssertStrEq(L, astate.debug_string(), "color:green mean=true ok:false pos=3,4,5");
|
||||
astate.decode_persistent(enc);
|
||||
LuaAssertStrEq(L, astate.debug_string(), "mean=true pos=3,4,5");
|
||||
|
||||
// Test AnimCoreState.decode
|
||||
//
|
||||
astate.clear_and_parse("color=blue xyz=1,2,3 plane=banana chicken=3");
|
||||
core.decode(astate.encode());
|
||||
LuaAssert(L, core.plane == "banana");
|
||||
LuaAssert(L, core.xyz == util::DXYZ(1,2,3));
|
||||
|
||||
// Verify that a newly-constructed AnimQueue is in a reasonable default state.
|
||||
//
|
||||
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; [empty]");
|
||||
|
||||
// Clear an AnimQueue to a specified initial state.
|
||||
//
|
||||
astate.clear_and_parse("color=blue xyz=1,2,3 plane=somewhere");
|
||||
aq.clear(astate);
|
||||
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; color=blue plane=somewhere xyz=1,2,3");
|
||||
|
||||
// Add animation steps to animation queue.
|
||||
// Note: each step is independent of the previous one, no composition is being done.
|
||||
//
|
||||
astate.clear_and_parse("xyz=1,2,3 plane=earth");
|
||||
aq.clear(astate);
|
||||
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3");
|
||||
astate.clear_and_parse("xyz=4,5,6 action:jump");
|
||||
aq.add(astate);
|
||||
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6");
|
||||
astate.clear_and_parse("plane=moon airline:southwest");
|
||||
aq.add(astate);
|
||||
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon");
|
||||
astate.clear_and_parse("color=blue");
|
||||
aq.add(astate);
|
||||
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
|
||||
|
||||
// Try reducing the animation queue size limit.
|
||||
//
|
||||
aq.set_limit(2);
|
||||
LuaAssertStrEq(L, aq.full_debug_string(), "limit=2; airline:southwest plane=moon; color=blue");
|
||||
|
||||
// Test get_final_persistent, get_final_everything, get_final_core_state
|
||||
//
|
||||
astate.clear_and_parse("action:jump plane=earth xyz=1,2,3 bouncy:true");
|
||||
aq.clear(astate);
|
||||
astate = aq.get_final_persistent();
|
||||
LuaAssertStrEq(L, astate.debug_string(), "plane=earth xyz=1,2,3");
|
||||
astate = aq.get_final_everything();
|
||||
LuaAssertStrEq(L, astate.debug_string(), "action:jump bouncy:true plane=earth xyz=1,2,3");
|
||||
core = aq.get_final_core_state();
|
||||
LuaAssert(L, core.plane == "earth");
|
||||
LuaAssert(L, core.xyz == util::DXYZ(1,2,3));
|
||||
|
||||
// Serialize a queue.
|
||||
//
|
||||
aq.set_limit(10);
|
||||
astate.clear_and_parse("xyz=1,2,3 plane=earth");
|
||||
aq.clear(astate);
|
||||
astate.clear_and_parse("xyz=4,5,6 action:jump");
|
||||
aq.add(astate);
|
||||
astate.clear_and_parse("plane=moon airline:southwest");
|
||||
aq.add(astate);
|
||||
astate.clear_and_parse("color=blue");
|
||||
aq.add(astate);
|
||||
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
|
||||
aq.serialize(&sb);
|
||||
|
||||
// Deserialize a queue.
|
||||
//
|
||||
aqs.set_limit(7);
|
||||
aqs.clear();
|
||||
LuaAssert(L, !aqs.exactly_equal(aq));
|
||||
aqs.deserialize(&sb);
|
||||
LuaAssert(L, aqs.exactly_equal(aq));
|
||||
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
|
||||
|
||||
// Test diff and patch.
|
||||
//
|
||||
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
|
||||
aqs.set_limit(7);
|
||||
aqs.clear();
|
||||
sb.clear();
|
||||
aqs.diff(aq, &sb);
|
||||
//int difflen1 = sb.fill();
|
||||
LuaAssert(L, !aqs.exactly_equal(aq));
|
||||
aqs.patch(&sb, nullptr);
|
||||
LuaAssert(L, aqs.exactly_equal(aq));
|
||||
LuaAssertStrEq(L, aqs.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
|
||||
|
||||
// Test that diff and patch are more efficient when the two queues contain some shared steps.
|
||||
//
|
||||
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
|
||||
astate.clear_and_parse("xyz=4,5,6 action:jump");
|
||||
aqs.clear(astate);
|
||||
astate.clear_and_parse("plane=earth xyz=1,2,3");
|
||||
aqs.add(astate);
|
||||
astate.clear_and_parse("plane=moon airline:southwest");
|
||||
aqs.add(astate);
|
||||
LuaAssertStrEq(L, aqs.full_debug_string(), "limit=10; action:jump xyz=4,5,6; plane=earth xyz=1,2,3; airline:southwest plane=moon");
|
||||
sb.clear();
|
||||
aqs.diff(aq, &sb);
|
||||
//int difflen2 = sb.fill();
|
||||
LuaAssert(L, !aqs.exactly_equal(aq));
|
||||
aqs.patch(&sb, nullptr);
|
||||
LuaAssert(L, aqs.exactly_equal(aq));
|
||||
LuaAssertStrEq(L, aqs.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
|
||||
|
||||
// TODO: if we make the diff routine more efficient, this should be true.
|
||||
// LuaAssert(L, difflen2 < (difflen1 / 2));
|
||||
|
||||
return 0;
|
||||
}
|
||||
403
luprex/cpp/core/animqueue.hpp
Normal file
403
luprex/cpp/core/animqueue.hpp
Normal file
@@ -0,0 +1,403 @@
|
||||
///////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// ANIMATION QUEUES
|
||||
//
|
||||
// An animation queue is a fifo queue of animation steps. New animations are
|
||||
// pushed on the back, and old ones are popped from the front.
|
||||
//
|
||||
// An animation step is a set of key-value pairs, where each key is an
|
||||
// identifier, and each value is either a number, a boolean, an XYZ coordinate,
|
||||
// or a string. A key-value pair can be either persistent or nonpersistent.
|
||||
// So a typical animation step might be:
|
||||
//
|
||||
// action=walk [nonpersistent]
|
||||
// xyz=3,4,5 [persistent]
|
||||
// facing=320 [persistent]
|
||||
// plane=earth [persistent]
|
||||
//
|
||||
// Persistent values are retained from one animation step to the next,
|
||||
// nonpersistent values exist for one animation step only.
|
||||
//
|
||||
// Each animation step has a hash value. The hash value is generated
|
||||
// by mixing the hash value of the previous step with the hash value
|
||||
// of the encoded string of key-value pairs.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// SERIALIZED STORAGE
|
||||
//
|
||||
// The entired animation queue is stored in a serialized format,
|
||||
// as a shared string. This means that the animation queue can be
|
||||
// passed to the graphics engine as a single string. This can be
|
||||
// accomplished by the function EngineWrapper.get_animation_queues.
|
||||
//
|
||||
// The fact that the queue is stored in a serialized format means
|
||||
// that when manipulating the animation queue, we have to decode it,
|
||||
// manipulate it, and then reencode it.
|
||||
//
|
||||
// From an efficiency perspective, this means that manipulation is
|
||||
// slower, but passing the strings to the graphics engine is faster
|
||||
// and simpler. This is a good tradeoff: we manipulate animation
|
||||
// queues rarely, compared to how often we pass them to the graphics
|
||||
// engine.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// THE SERIALIZED REPRESENTATION
|
||||
//
|
||||
// So first, you need to know how to serialize a single animation
|
||||
// step. Remember, an animation step consists of a list of key-value
|
||||
// pairs (see above). The key-value pairs are serialized as follows:
|
||||
//
|
||||
// for all key-value pairs do:
|
||||
// write_string(key)
|
||||
// write_bool(persistent)
|
||||
// write_uint8(type) // T_STRING, T_NUMBER, T_BOOL, or T_XYZ
|
||||
// switch type:
|
||||
// T_STRING: write_string(value)
|
||||
// T_NUMBER: write_double(value)
|
||||
// T_BOOL: write_bool(value)
|
||||
// T_XYZ: write_xyz(value)
|
||||
//
|
||||
// The encoded string produced by the loop above is called an "encstep".
|
||||
// That's short for "encoded animation step". The encstep has a hash
|
||||
// value, which is a function that accepts the encstep and also the hash
|
||||
// of the previous encstep. Note that the hash is not part of the encstep.
|
||||
//
|
||||
// A serialized animation queue consists of the following information:
|
||||
//
|
||||
// write_uint8(size_limit);
|
||||
// write_uint8(actual_size);
|
||||
// for all animation steps, starting with the most recent, do:
|
||||
// write_uint64(hash)
|
||||
// write_string(encstep)
|
||||
//
|
||||
// The encoded string produced by the loop above is called an "encqueue",
|
||||
// because it encodes everything in the animation queue.
|
||||
//
|
||||
// Note that the 'serialize' routine for animation queues just returns
|
||||
// the encqueue string, which is the whole thing.
|
||||
//
|
||||
// Since the steps in an encqueue are stored most-recent first, if you
|
||||
// want some information about the most recent animation entry, you
|
||||
// don't need to decode the entire encqueue. You only need to
|
||||
// decode the first step.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef ANIMQUEUE_HPP
|
||||
#define ANIMQUEUE_HPP
|
||||
|
||||
#include "wrap-set.hpp"
|
||||
#include "wrap-string.hpp"
|
||||
#include "wrap-deque.hpp"
|
||||
#include "wrap-unordered-map.hpp"
|
||||
|
||||
#include "base-buffer.hpp"
|
||||
#include "luastack.hpp"
|
||||
#include "streambuffer.hpp"
|
||||
#include "debugcollector.hpp"
|
||||
#include "util.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <ostream>
|
||||
|
||||
struct AnimValue : public SimpleDynamicValue {
|
||||
bool persistent;
|
||||
|
||||
AnimValue() { persistent = false; }
|
||||
|
||||
void set_dxyz(const util::DXYZ &xyz) {
|
||||
type = SimpleDynamicTag::VECTOR;
|
||||
s.clear(); x = xyz.x; y = xyz.y; z = xyz.z;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
class AnimState
|
||||
{
|
||||
private:
|
||||
using Map = eng::map<eng::string, AnimValue>;
|
||||
Map map_;
|
||||
|
||||
// Set the default value, internal
|
||||
//
|
||||
eng::string add_default(const eng::string &name, const AnimValue &v, const AnimState *other);
|
||||
|
||||
public:
|
||||
// Clear everything
|
||||
//
|
||||
void clear() { map_.clear(); }
|
||||
|
||||
// Set the persistent flag on a single variable.
|
||||
// If the variable isn't present, add it.
|
||||
//
|
||||
void set_persistent(const eng::string &name);
|
||||
|
||||
// Return if it contains a value for the specified name.
|
||||
//
|
||||
bool contains(const eng::string &name) { return map_.find(name) != map_.end(); }
|
||||
|
||||
// Set a single variable to a value of a specified type.
|
||||
// If the variable isn't present, add it.
|
||||
//
|
||||
void set_boolean(const eng::string &name, bool v);
|
||||
void set_number(const eng::string &name, double v);
|
||||
void set_dxyz(const eng::string &name, const util::DXYZ &value);
|
||||
void set_string(const eng::string &name, std::string_view value);
|
||||
|
||||
// Print a debug string into the stringstream.
|
||||
//
|
||||
void print_debug_string(eng::ostringstream &oss);
|
||||
|
||||
// Return the debug string.
|
||||
eng::string debug_string();
|
||||
|
||||
// Constructs an empty state.
|
||||
//
|
||||
AnimState() {}
|
||||
|
||||
// Convert to an encoded string.
|
||||
//
|
||||
eng::string encode() const;
|
||||
|
||||
// Decode an encoded string.
|
||||
//
|
||||
void decode(std::string_view enc);
|
||||
|
||||
// Decode an encoded string, discarding non-persistent data.
|
||||
//
|
||||
void decode_persistent(std::string_view enc);
|
||||
|
||||
// Add default values for all builtin persistent variables.
|
||||
//
|
||||
// For each builtin default (plane, xyz, facing, bp)
|
||||
//
|
||||
// - Will generate an error if a value is already present,
|
||||
// but the present value is of the wrong type.
|
||||
//
|
||||
// - If 'other' is not nullptr, then we look in 'other' for a default
|
||||
// value. If a valid value of the correct type is present, it is
|
||||
// used as the default value.
|
||||
//
|
||||
// - If no default value can be found in 'other', then a hardwired
|
||||
// default value is provided.
|
||||
//
|
||||
eng::string add_defaults(const AnimState *other);
|
||||
|
||||
// Parse an animstate from a Lua Table.
|
||||
//
|
||||
// Table keys must be valid lua identifiers. Table values may be string,
|
||||
// floats, bool, or coordinates. Persistent flags are all set the same,
|
||||
// from the persistent parameter. Returns empty string on success, or an
|
||||
// error message on failure.
|
||||
//
|
||||
// If 'allowauto' is true, then the lua table may contain a key-value
|
||||
// pair of the form (key, math.auto). These keys will be stored in the
|
||||
// AnimState map with mapentry.type == SimpleDynamicTag::AUTO.
|
||||
// This is done to express an intent that the value should be
|
||||
// automatically computer later.
|
||||
//
|
||||
eng::string from_lua(LuaCoreStack &LS0, LuaSlot tab, bool persistent, bool allowauto);
|
||||
|
||||
// Generate a merged animstate using a previous state and an update.
|
||||
//
|
||||
// Keys from both previous and update are combined to create this AnimState.
|
||||
// Values from 'update' override values from 'previous'. Persistent flags
|
||||
// are taken from 'previous'. If a key exists in both previous and update,
|
||||
// and the key is persistent in 'previous', then the types must match,
|
||||
// otherwise an error is generated. Returns empty string on success or
|
||||
// an error message on failure.
|
||||
//
|
||||
// If a key in the 'update' map has type AUTO, then we will attempt to find
|
||||
// a rule to compute that value automatically. Failure to find a rule
|
||||
// results in an error.
|
||||
//
|
||||
eng::string merge(const AnimState &previous, const AnimState &update);
|
||||
|
||||
// Convert an animstate to a lua table.
|
||||
//
|
||||
// You can either convert the persistent key-value pairs, or the
|
||||
// nonpersistent. So you'll need two lua tables to store both.
|
||||
//
|
||||
void to_lua(LuaCoreStack &LS, LuaSlot tab, bool persistent);
|
||||
|
||||
// Parse a string, for unit testing.
|
||||
//
|
||||
// This parses a simple notation designed to facilitate writing unit tests.
|
||||
// The notation looks like this:
|
||||
//
|
||||
// parse("plane=earth xyz=1,2,3 action:jump");
|
||||
//
|
||||
// Determining the type of the value is done as follows: First, try
|
||||
// interpreting it as boolean true or false. If that fails,
|
||||
// try interpreting it as a number. If that fails, try interpreting it as
|
||||
// a coordinate. If all else fails, it's a string. Obviously, this is a
|
||||
// limited approach: for example, there's no way to express the string "123"
|
||||
// because "123" will get interpreted as a number. But that's ok, since this
|
||||
// is purely intended for unit testing.
|
||||
//
|
||||
// Key-value pairs can have either an equal sign or a colon. If it's an equal
|
||||
// sign, the persistent bit is set, colon means not persistent.
|
||||
//
|
||||
void parse(std::string_view s);
|
||||
void clear_and_parse(std::string_view s);
|
||||
|
||||
// Constructor from an encoded string.
|
||||
//
|
||||
AnimState(std::string_view s) { decode(s); }
|
||||
};
|
||||
|
||||
struct AnimCoreState
|
||||
{
|
||||
util::DXYZ xyz;
|
||||
eng::string plane;
|
||||
|
||||
void decode(std::string_view enc);
|
||||
};
|
||||
|
||||
class AnimQueue : public eng::nevernew {
|
||||
public:
|
||||
// Construct an empty animation queue.
|
||||
// clears the state to a valid state.
|
||||
//
|
||||
AnimQueue();
|
||||
|
||||
// Clear the steps to an initial state.
|
||||
//
|
||||
void clear();
|
||||
void clear(const AnimState &initial);
|
||||
|
||||
// Change the size limit.
|
||||
//
|
||||
void set_limit(int limit);
|
||||
|
||||
// Add an animation step.
|
||||
//
|
||||
// Note: add does not automatically compose the step with the previous
|
||||
// step, you have to do that yourself.
|
||||
//
|
||||
void add(const AnimState &state);
|
||||
|
||||
// Replace the most recent animation step.
|
||||
//
|
||||
// Note: replace does not automatically compose the step with the previous
|
||||
// step, you have to do that yourself.
|
||||
//
|
||||
void replace(const AnimState &state);
|
||||
|
||||
// Serialize or deserialize to a StreamBuffer
|
||||
//
|
||||
void serialize(StreamBuffer *sb) const;
|
||||
void deserialize(StreamBuffer *sb);
|
||||
|
||||
// Difference transmission
|
||||
//
|
||||
bool diff(const AnimQueue &auth, StreamBuffer *sb) const;
|
||||
void patch(StreamBuffer *sb, DebugCollector *dbc);
|
||||
|
||||
// Check for exactly equal. This does the full check of all
|
||||
// fields, it is used exclusively for debugging.
|
||||
//
|
||||
bool exactly_equal(const AnimQueue &aq) const;
|
||||
|
||||
// Check for exactly equal (fast). Compares the size, limit,
|
||||
// and hash of the last entry. If these are equal, then the whole
|
||||
// thing should be equal.
|
||||
//
|
||||
bool exactly_equal_fast(const AnimQueue &aq) const;
|
||||
|
||||
// Debug strings.
|
||||
//
|
||||
void print_debug_string(eng::ostringstream &oss, bool full) const;
|
||||
eng::string steps_debug_string() const;
|
||||
eng::string full_debug_string() const;
|
||||
|
||||
// Get the final entry, xyz and plane only.
|
||||
//
|
||||
AnimCoreState get_final_core_state() const;
|
||||
|
||||
// Get the final entry, all persistent variables.
|
||||
//
|
||||
AnimState get_final_persistent() const;
|
||||
|
||||
// Get the final entry, everything persistent and non-persistent.
|
||||
//
|
||||
AnimState get_final_everything() const;
|
||||
|
||||
// Get a serialized representation of the animation queue.
|
||||
//
|
||||
// Get the entire animation queue in a serialized format (encqueue).
|
||||
// The string returned is a shared string. No string copy
|
||||
// is made during this process.
|
||||
//
|
||||
util::SharedStdString get_encoded_queue() const { return encqueue_; }
|
||||
|
||||
// Get a serialized representation of a blank queue.
|
||||
//
|
||||
// Since an animqueue must have at least one step, the blank queue
|
||||
// contains a single default step.
|
||||
//
|
||||
static util::SharedStdString get_encoded_blank_queue() { return blankqueue_; }
|
||||
|
||||
// Initialize the animqueue module.
|
||||
//
|
||||
static void initialize_module();
|
||||
|
||||
private:
|
||||
struct QueueRange {
|
||||
int size;
|
||||
std::string_view entries;
|
||||
QueueRange(int sz, std::string_view ent) : size(sz), entries(ent) {}
|
||||
};
|
||||
|
||||
// Get a range of entries from the queue.
|
||||
//
|
||||
// You must specify a range (lo-hi) of steps. In this numbering, 0 is the
|
||||
// most recent entry in the queue. The indices lo and hi are automatically
|
||||
// clamped to the valid range (0 to actual_size). If lo >= hi, then an
|
||||
// empty range is returned.
|
||||
//
|
||||
QueueRange get_range(int lo, int hi);
|
||||
|
||||
// Hash a new step given the range of steps that precede it.
|
||||
//
|
||||
static uint64_t hash_encstep(const QueueRange &prev, std::string_view step);
|
||||
|
||||
// Update the animation queue.
|
||||
//
|
||||
// The range (keeplo to keephi) specifies which old steps should be
|
||||
// retained. The numbers keephi and keeplo are automatically clamped
|
||||
// so that they lie inside the actual size of the queue.
|
||||
//
|
||||
// If add is true, then an additional step is added to the queue.
|
||||
// The hash of the new step is calculated automatically.
|
||||
//
|
||||
// There is no enforcement that you respected the size limit that you
|
||||
// specified. For example, you could say "keep 0-5, and add 1." That
|
||||
// would make 6 entries in the queue. It is up to the caller to respect
|
||||
// the size limit. The value passed in is just for reporting.
|
||||
//
|
||||
void update_encqueue(int limit, bool add, std::string_view add_enc, int keeplo, int keephi);
|
||||
|
||||
// Read values from the header of the encqueue.
|
||||
//
|
||||
int get_size_limit() const;
|
||||
int get_actual_size() const;
|
||||
uint64_t get_final_hash() const;
|
||||
std::string_view get_final_encstep() const;
|
||||
|
||||
private:
|
||||
// Note: this is stored as a std::string, not an eng::string, because the
|
||||
// ownership ends up being shared between us and the graphics engine. We
|
||||
// can't have the graphics engine affecting the behavior of the engine heap.
|
||||
//
|
||||
util::SharedStdString encqueue_;
|
||||
|
||||
// The blank animation queue.
|
||||
//
|
||||
static util::SharedStdString blankqueue_;
|
||||
};
|
||||
|
||||
#endif // ANIMQUEUE_HPP
|
||||
|
||||
1260
luprex/cpp/core/bytell-hash-map.hpp
Normal file
1260
luprex/cpp/core/bytell-hash-map.hpp
Normal file
File diff suppressed because it is too large
Load Diff
89
luprex/cpp/core/debugcollector.cpp
Normal file
89
luprex/cpp/core/debugcollector.cpp
Normal file
@@ -0,0 +1,89 @@
|
||||
|
||||
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
|
||||
#include "debugcollector.hpp"
|
||||
#include "util.hpp"
|
||||
|
||||
void DebugCollector::flush() {
|
||||
if (need_flush_) {
|
||||
eng::string str = oss_.str();
|
||||
if (!str.empty()) {
|
||||
if (is_regular_) n_regular_ += 1;
|
||||
lines_.push_back(str);
|
||||
oss_.str("");
|
||||
}
|
||||
need_flush_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
DebugCollector::DebugCollector() : n_regular_(0), need_flush_(false), is_regular_(true), active_(false) {
|
||||
}
|
||||
|
||||
DebugCollector::DebugCollector(const eng::string &targets)
|
||||
: n_regular_(0), need_flush_(false), is_regular_(true), active_(false) {
|
||||
targets_ = util::split(targets, ',');
|
||||
std::sort(targets_.begin(), targets_.end());
|
||||
}
|
||||
|
||||
bool DebugCollector::use(const char *target) {
|
||||
int lo = 0;
|
||||
int hi = targets_.size();
|
||||
while (hi > lo) {
|
||||
int mid = (hi + lo) >> 1;
|
||||
int cmp = strcmp(targets_[mid].c_str(), target);
|
||||
if (cmp == 0) return true;
|
||||
if (cmp > 0) hi = mid;
|
||||
else lo = mid + 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DebugCollector::do_line() {
|
||||
if (active_) {
|
||||
flush();
|
||||
need_flush_ = true;
|
||||
is_regular_ = true;
|
||||
return true;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
bool DebugCollector::do_header() {
|
||||
flush();
|
||||
need_flush_ = true;
|
||||
is_regular_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DebugCollector::dump(std::ostream &os) {
|
||||
flush();
|
||||
for (const eng::string &line : lines_) {
|
||||
os << line << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
DebugBlock::DebugBlock(DebugCollector *dbc, const char *target) {
|
||||
dbc_ = dbc;
|
||||
if (dbc) {
|
||||
n_regular_ = dbc->n_regular_;
|
||||
n_lines_ = dbc->lines_.size();
|
||||
active_ = (!dbc->active_) && (dbc->use(target));
|
||||
if (active_) dbc_->active_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
DebugBlock::~DebugBlock() {
|
||||
if (dbc_) {
|
||||
dbc_->flush();
|
||||
// If no regular lines have been added,
|
||||
// remove all header lines that were added.
|
||||
if (dbc_->n_regular_ == n_regular_) {
|
||||
if (dbc_->lines_.size() != n_lines_) {
|
||||
dbc_->lines_.resize(n_lines_);
|
||||
}
|
||||
}
|
||||
if (active_) dbc_->active_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
51
luprex/cpp/core/debugcollector.hpp
Normal file
51
luprex/cpp/core/debugcollector.hpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#ifndef DEBUGCOLLECTOR_HPP
|
||||
#define DEBUGCOLLECTOR_HPP
|
||||
|
||||
#include "wrap-vector.hpp"
|
||||
#include "wrap-string.hpp"
|
||||
#include "wrap-sstream.hpp"
|
||||
|
||||
#include <ostream>
|
||||
#include <memory>
|
||||
|
||||
class DebugCollector : public eng::nevernew {
|
||||
private:
|
||||
// At any given time, the stringstream may contain one
|
||||
// extra line that hasn't yet been appended to the list of lines.
|
||||
// The flag 'need_flush_' is true if the stringstream contains
|
||||
// an extra line. In that case, is_regular_ indicates whether
|
||||
// the extra line is a header or a regular line.
|
||||
eng::vector<eng::string> lines_;
|
||||
eng::vector<eng::string> targets_;
|
||||
int n_regular_;
|
||||
bool need_flush_;
|
||||
bool is_regular_;
|
||||
bool active_;
|
||||
void flush();
|
||||
bool use(const char *target);
|
||||
public:
|
||||
eng::ostringstream oss_;
|
||||
DebugCollector();
|
||||
DebugCollector(const eng::string &targets);
|
||||
bool do_header();
|
||||
bool do_line();
|
||||
void dump(std::ostream &os);
|
||||
friend class DebugBlock;
|
||||
};
|
||||
|
||||
class DebugBlock : public eng::nevernew {
|
||||
private:
|
||||
DebugCollector *dbc_;
|
||||
int n_regular_;
|
||||
size_t n_lines_;
|
||||
bool active_;
|
||||
public:
|
||||
DebugBlock(DebugCollector *dbc, const char *target);
|
||||
~DebugBlock();
|
||||
};
|
||||
|
||||
#define DebugHeader(dbc) if (((dbc)!=nullptr) && (dbc)->do_header()) ((dbc)->oss_)
|
||||
#define DebugLine(dbc) if (((dbc)!=nullptr) && (dbc)->do_line()) ((dbc)->oss_)
|
||||
|
||||
#endif // DEBUGCOLLECTOR_HPP
|
||||
|
||||
980
luprex/cpp/core/drivenengine.cpp
Normal file
980
luprex/cpp/core/drivenengine.cpp
Normal file
@@ -0,0 +1,980 @@
|
||||
#include "wrap-string.hpp"
|
||||
#include "wrap-vector.hpp"
|
||||
#include "util.hpp"
|
||||
#include "drivenengine.hpp"
|
||||
#include "world.hpp"
|
||||
#include "base-buffer.hpp"
|
||||
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <cassert>
|
||||
|
||||
DrivenEngineReg *DrivenEngineReg::All;
|
||||
|
||||
DrivenEngineReg::DrivenEngineReg(const char *n, DrivenEngineMaker fn) {
|
||||
name = n;
|
||||
maker = fn;
|
||||
next = All;
|
||||
All = this;
|
||||
}
|
||||
|
||||
DrivenEngineInitializer DrivenEngineInitializerReg::func;
|
||||
|
||||
DrivenEngineInitializerReg::DrivenEngineInitializerReg(DrivenEngineInitializer fn) {
|
||||
assert(func == nullptr);
|
||||
func = fn;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DrivenEngine private methods
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
int DrivenEngine::find_unused_chid() {
|
||||
// Note: channel ID zero is special, it is never reused.
|
||||
for (int i = 0; i < DRV_MAX_CHAN; i++) {
|
||||
int id = next_unused_chid_++;
|
||||
if (next_unused_chid_ == DRV_MAX_CHAN) next_unused_chid_ = 1;
|
||||
if (channels_[id] == nullptr) return id;
|
||||
}
|
||||
assert(false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Channel *DrivenEngine::get_chid(int chid) const {
|
||||
assert(unsigned(chid) < DRV_MAX_CHAN);
|
||||
assert(channels_[chid].get() != nullptr);
|
||||
return channels_[chid].get();
|
||||
}
|
||||
|
||||
static DrivenEngine *make_engine(std::string_view kind) {
|
||||
for (auto reg = DrivenEngineReg::All; reg != nullptr; reg=reg->next) {
|
||||
if (kind == reg->name) {
|
||||
UniqueDrivenEngine result = reg->maker();
|
||||
return result.release();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// The stdostream
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class StreamBufferWriter : public std::streambuf, public eng::opnew {
|
||||
private:
|
||||
StreamBuffer *target_;
|
||||
public:
|
||||
StreamBufferWriter(StreamBuffer *t) : target_(t) {}
|
||||
|
||||
virtual int_type overflow(int_type c) {
|
||||
if (c != EOF) {
|
||||
target_->write_uint8(c);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
};
|
||||
|
||||
class StreamBufferOStream : public std::ostream, public eng::opnew {
|
||||
private:
|
||||
StreamBufferWriter writer_;
|
||||
public:
|
||||
StreamBufferOStream(StreamBuffer *t) : std::ostream(nullptr), writer_(t) {
|
||||
rdbuf(&writer_);
|
||||
}
|
||||
virtual ~StreamBufferOStream() {
|
||||
}
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Class Channel
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Channel::Channel(DrivenEngine *de, int chid, int port, const eng::string &target, bool stop) {
|
||||
chid_ = chid;
|
||||
port_ = port;
|
||||
closed_ = false;
|
||||
target_ = target;
|
||||
stop_driver_ = stop;
|
||||
sb_in_ = eng::make_shared<StreamBuffer>();
|
||||
sb_out_ = eng::make_shared<StreamBuffer>();
|
||||
}
|
||||
|
||||
std::string_view Channel::peek_outgoing() const {
|
||||
return sb_out_->view();
|
||||
}
|
||||
|
||||
void Channel::sent_outgoing(int nbytes) {
|
||||
sb_out_->read_bytes(nbytes);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DrivenEngine Client-Side API
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void DrivenEngine::listen_port(int port) {
|
||||
assert(listen_ports_.size() < DRV_MAX_LISTEN_PORTS);
|
||||
listen_ports_.push_back(port);
|
||||
}
|
||||
|
||||
double DrivenEngine::get_clock() {
|
||||
return clock_;
|
||||
}
|
||||
|
||||
SharedChannel DrivenEngine::new_outgoing_channel(const eng::string &target) {
|
||||
int chid = find_unused_chid();
|
||||
new_outgoing_.push_back(chid);
|
||||
SharedChannel result = eng::make_shared<Channel>(this, chid, 0, target, stop_driver_);
|
||||
channels_[chid] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
SharedChannel DrivenEngine::new_incoming_channel() {
|
||||
if (accepted_channels_.empty()) {
|
||||
return nullptr;
|
||||
} else {
|
||||
SharedChannel result = std::move(accepted_channels_.back());
|
||||
accepted_channels_.pop_back();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
SharedChannel DrivenEngine::get_stdio_channel() {
|
||||
return stdio_channel_;
|
||||
}
|
||||
|
||||
void DrivenEngine::set_console_prompt(const eng::string &prompt) {
|
||||
console_prompt_ = prompt;
|
||||
}
|
||||
|
||||
void DrivenEngine::rescan_lua_source() {
|
||||
rescan_lua_source_ = true;
|
||||
}
|
||||
|
||||
void DrivenEngine::set_visible_world_and_actor(World *w, int64_t id) {
|
||||
visible_world_ = w;
|
||||
visible_actor_id_ = id;
|
||||
}
|
||||
|
||||
void DrivenEngine::stop_driver() {
|
||||
stop_driver_ = true;
|
||||
for (int i = 0; i < DRV_MAX_CHAN; i++) {
|
||||
if (channels_[i] != nullptr) {
|
||||
channels_[i]->stop_driver_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DrivenEngine::DrivenEngine() {
|
||||
next_unused_chid_ = 1;
|
||||
stdio_channel_ = eng::make_shared<Channel>(this, 0, 0, "", false);
|
||||
stdostream_.reset(new StreamBufferOStream(stdio_channel_->out()));
|
||||
channels_[0] = stdio_channel_;
|
||||
rescan_lua_source_ = false;
|
||||
clock_ = 0.0;
|
||||
stop_driver_ = false;
|
||||
}
|
||||
|
||||
DrivenEngine::~DrivenEngine() {}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// LOGFILE EVENT IDS.
|
||||
//
|
||||
// There's one event ID for each mutator, plus one for 'release'.
|
||||
//
|
||||
// There are no event IDs for getters, these aren't considered loggable events.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
enum DrvAction {
|
||||
PLAY_INITIALIZE,
|
||||
PLAY_CLEAR_NEW_OUTGOING,
|
||||
PLAY_SENT_OUTGOING,
|
||||
PLAY_RECV_INCOMING,
|
||||
PLAY_NOTIFY_CLOSE,
|
||||
PLAY_NOTIFY_ACCEPT,
|
||||
PLAY_UPDATE,
|
||||
PLAY_CALL_FUNCTION,
|
||||
PLAY_RELEASE,
|
||||
};
|
||||
|
||||
inline static const char *action_string(DrvAction act) {
|
||||
switch(act) {
|
||||
case PLAY_INITIALIZE: return "PLAY_INITIALIZE";
|
||||
case PLAY_CLEAR_NEW_OUTGOING: return "PLAY_CLEAR_NEW_OUTGOING";
|
||||
case PLAY_SENT_OUTGOING: return "PLAY_SENT_OUTGOING";
|
||||
case PLAY_RECV_INCOMING: return "PLAY_RECV_INCOMING";
|
||||
case PLAY_NOTIFY_CLOSE: return "PLAY_NOTIFY_CLOSE";
|
||||
case PLAY_NOTIFY_ACCEPT: return "PLAY_NOTIFY_ACCEPT";
|
||||
case PLAY_UPDATE: return "PLAY_UPDATE";
|
||||
case PLAY_CALL_FUNCTION: return "PLAY_CALL_FUNCTION";
|
||||
case PLAY_RELEASE: return "PLAY_RELEASE";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// RLOG and WLOG, functions to read and write binary data to logfiles.
|
||||
//
|
||||
// After doing an rlog operation, you should check the stream
|
||||
// for "good" to find out if there was any error.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class PlayLogfile : public BaseWriter<PlayLogfile>, public std::ofstream {
|
||||
using std::ofstream::ofstream;
|
||||
public:
|
||||
void write_bytes(const char *n, size_t size) { write(n, size); }
|
||||
void raise_truncated() {
|
||||
fprintf(stderr, "number exceeds allowable size\n");
|
||||
std::abort();
|
||||
}
|
||||
void write_short_string(std::string_view v) {
|
||||
assert(v.size() < DRV_SHORTSTRING_SIZE);
|
||||
write_string(v);
|
||||
}
|
||||
void write_cmd_hash(DrvAction act, uint32_t hash) {
|
||||
write_uint8(act);
|
||||
write_uint32(hash);
|
||||
}
|
||||
};
|
||||
|
||||
class ReplayLogfile : public BaseReader<ReplayLogfile>, public std::ifstream {
|
||||
using std::ifstream::ifstream;
|
||||
public:
|
||||
using read_string_type = std::string;
|
||||
void read_bytes_into(char *n, size_t size) {
|
||||
read(n, size);
|
||||
if (!good()) {
|
||||
memset(n, 0, size);
|
||||
}
|
||||
}
|
||||
void raise_string_too_long() {
|
||||
fprintf(stderr, "string in logfile is too long");
|
||||
std::abort();
|
||||
}
|
||||
std::string_view read_short_string(EngineWrapper *w) {
|
||||
size_t size = read_length();
|
||||
assert(size <= DRV_SHORTSTRING_SIZE);
|
||||
if (size > 0) read(w->databuffer, size);
|
||||
if (!good()) return std::string_view();
|
||||
return std::string_view(w->databuffer, size);
|
||||
}
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// reset_wrapper
|
||||
//
|
||||
// Shut down a EngineWrapper, store an optional error message.
|
||||
//
|
||||
// release
|
||||
//
|
||||
// Shut down an EngineWrapper cleanly, with no error message, and
|
||||
// log the step if the logfile is open.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void reset_wrapper(EngineWrapper *w, const char *format, ...) {
|
||||
va_list argp;
|
||||
va_start(argp, format);
|
||||
memset(w->error, 0, DRV_ERRMSG_SIZE);
|
||||
vsnprintf(w->error, DRV_ERRMSG_SIZE, format, argp);
|
||||
w->error[DRV_ERRMSG_SIZE - 1] = 0;
|
||||
|
||||
if (w->wlog != nullptr) {
|
||||
w->wlog->close();
|
||||
delete w->wlog;
|
||||
w->wlog = nullptr;
|
||||
}
|
||||
|
||||
if (w->rlog != nullptr) {
|
||||
w->rlog->close();
|
||||
delete w->rlog;
|
||||
w->rlog = nullptr;
|
||||
}
|
||||
|
||||
if (w->engine != nullptr) {
|
||||
delete w->engine;
|
||||
w->engine = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static void release(EngineWrapper *w) {
|
||||
if (w->wlog != nullptr) {
|
||||
w->wlog->write_cmd_hash(PLAY_RELEASE, eng::memhash());
|
||||
}
|
||||
reset_wrapper(w, "");
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DRIVER Methods: Getters
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void DrivenEngine::drv_get_listen_ports(uint32_t *nports, const uint32_t **ports) const {
|
||||
*nports = listen_ports_.size();
|
||||
*ports = &listen_ports_[0];
|
||||
}
|
||||
|
||||
void DrivenEngine::drv_get_new_outgoing(uint32_t *nchids, const uint32_t **chids) const {
|
||||
*nchids = new_outgoing_.size();
|
||||
*chids = &new_outgoing_[0];
|
||||
}
|
||||
|
||||
const char *DrivenEngine::drv_get_target(uint32_t chid) const {
|
||||
return get_chid(chid)->target_.c_str();
|
||||
}
|
||||
|
||||
bool DrivenEngine::drv_get_channel_released(uint32_t chid) const {
|
||||
return channels_[chid].use_count() == 1;
|
||||
}
|
||||
|
||||
void DrivenEngine::drv_get_outgoing(uint32_t chid, uint32_t *len, const char **data) const {
|
||||
std::string_view v = get_chid(chid)->peek_outgoing();
|
||||
*len = v.size();
|
||||
*data = v.data();
|
||||
}
|
||||
|
||||
bool DrivenEngine::drv_get_outgoing_empty(uint32_t chid) const {
|
||||
std::string_view v = get_chid(chid)->peek_outgoing();
|
||||
return (v.size() == 0);
|
||||
}
|
||||
|
||||
void DrivenEngine::drv_get_console_prompt(uint32_t *len, const char **data) const {
|
||||
*len = console_prompt_.size();
|
||||
*data = console_prompt_.c_str();
|
||||
}
|
||||
|
||||
double DrivenEngine::drv_get_clock() const {
|
||||
return clock_;
|
||||
}
|
||||
|
||||
bool DrivenEngine::drv_get_rescan_lua_source() const {
|
||||
return rescan_lua_source_;
|
||||
}
|
||||
|
||||
bool DrivenEngine::drv_get_stop_driver() const {
|
||||
return stop_driver_;
|
||||
}
|
||||
|
||||
int64_t DrivenEngine::drv_get_actor_id() const {
|
||||
return visible_actor_id_;
|
||||
}
|
||||
|
||||
void DrivenEngine::drv_get_tangibles_near(int64_t tanid, double rx, double ry, double rz, uint32_t *count, int64_t **ids) {
|
||||
uint32_t hash1 = eng::memhash();
|
||||
scan_result_.clear();
|
||||
if ((visible_world_ != 0) && (tanid != 0)) {
|
||||
PlaneScan scan;
|
||||
scan.set_near(tanid, true);
|
||||
scan.set_omit_nowhere(true);
|
||||
scan.set_sorted(false);
|
||||
scan.set_radius(util::XYZ(rx, ry, rz));
|
||||
scan.set_shape(PlaneScan::CYLINDER);
|
||||
visible_world_->get_near(scan, &scan_result_);
|
||||
}
|
||||
*count = scan_result_.size();
|
||||
if (*count > 0) {
|
||||
*ids = &scan_result_[0];
|
||||
} else {
|
||||
*ids = nullptr;
|
||||
}
|
||||
uint32_t hash2 = eng::memhash();
|
||||
assert(hash1 == hash2);
|
||||
}
|
||||
|
||||
void DrivenEngine::drv_get_animation_queues(uint32_t count, const int64_t *ids, uint32_t *lengths, const char **strings) {
|
||||
anim_queues_.resize(count);
|
||||
|
||||
if (visible_world_ == nullptr) {
|
||||
for (int i = 0; i < int(count); i++) {
|
||||
anim_queues_[i] = AnimQueue::get_encoded_blank_queue();
|
||||
}
|
||||
} else {
|
||||
visible_world_->get_encoded_animation_queues(count, ids, anim_queues_);
|
||||
}
|
||||
for (int i = 0; i < int(count); i++) {
|
||||
lengths[i] = anim_queues_[i]->size();
|
||||
strings[i] = anim_queues_[i]->c_str();
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DRIVER Methods: Mutators
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void DrivenEngine::drv_initialize(uint32_t srcpklen, const char *srcpk, int argc, char **argv) {
|
||||
event_init(std::string_view(srcpk, srcpklen), argc, argv);
|
||||
}
|
||||
|
||||
void DrivenEngine::drv_clear_new_outgoing() {
|
||||
new_outgoing_.clear();
|
||||
}
|
||||
|
||||
void DrivenEngine::drv_sent_outgoing(uint32_t chid, uint32_t nbytes) {
|
||||
return get_chid(chid)->sent_outgoing(nbytes);
|
||||
}
|
||||
|
||||
void DrivenEngine::drv_recv_incoming(uint32_t chid, uint32_t nbytes, const char *bytes) {
|
||||
if (nbytes > 0) {
|
||||
std::string_view sbytes(bytes, nbytes);
|
||||
Channel *ch = get_chid(chid);
|
||||
ch->sb_in_->write_bytes(sbytes);
|
||||
}
|
||||
}
|
||||
|
||||
void DrivenEngine::drv_notify_close(uint32_t chid, uint32_t len, const char *data) {
|
||||
Channel *ch = get_chid(chid);
|
||||
ch->closed_ = true;
|
||||
ch->error_ = std::string(data, len);
|
||||
channels_[chid].reset();
|
||||
}
|
||||
|
||||
uint32_t DrivenEngine::drv_notify_accept(uint32_t port) {
|
||||
int chid = find_unused_chid();
|
||||
channels_[chid] = eng::make_shared<Channel>(this, chid, port, "", stop_driver_);
|
||||
accepted_channels_.push_back(channels_[chid]);
|
||||
return chid;
|
||||
}
|
||||
|
||||
void DrivenEngine::drv_update(double clock) {
|
||||
clock_ = clock;
|
||||
event_update();
|
||||
}
|
||||
|
||||
void DrivenEngine::drv_call_function(InvocationKind kind, int64_t place, uint32_t datapklen, const char *datapk, uint32_t *retpklen, const char **retpk) {
|
||||
// This next line is a hack, because the DrivenEngine is not supposed to care about 'kind'.
|
||||
if (kind == InvocationKind::LUA_SOURCE) rescan_lua_source_ = false;
|
||||
call_function_retpk_.clear();
|
||||
event_call_function(kind, place, std::string_view(datapk, datapklen), &call_function_retpk_);
|
||||
if (retpklen != nullptr) {
|
||||
std::string_view view = call_function_retpk_.view();
|
||||
*retpklen = view.size();
|
||||
*retpk = view.data();
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// C Wrappers: Getters
|
||||
//
|
||||
// These wrappers make it possible to call the drv_get routines using C
|
||||
// functions instead of methods. This is important if the engine is compiled
|
||||
// with one C++ compiler, but the driver is compiled with a different C++
|
||||
// compiler.
|
||||
//
|
||||
// Some of these take parameter 'EngineWrapper', some take 'EngineWrapper',
|
||||
// and some come in two versions. This all depends on whether they are used
|
||||
// during play, during replay, or both.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void drv_get_listen_ports(EngineWrapper *w, uint32_t *nports, const uint32_t **ports) {
|
||||
return w->engine->drv_get_listen_ports(nports, ports);
|
||||
}
|
||||
|
||||
static void drv_get_new_outgoing(EngineWrapper *w, uint32_t *nchanids, const uint32_t **chanids) {
|
||||
return w->engine->drv_get_new_outgoing(nchanids, chanids);
|
||||
}
|
||||
|
||||
static const char *drv_get_target(EngineWrapper *w, uint32_t chid) {
|
||||
return w->engine->drv_get_target(chid);
|
||||
}
|
||||
|
||||
static bool drv_get_channel_released(EngineWrapper *w, uint32_t chid) {
|
||||
return w->engine->drv_get_channel_released(chid);
|
||||
}
|
||||
|
||||
static void drv_get_outgoing(EngineWrapper *w, uint32_t chid, uint32_t *len, const char **data) {
|
||||
return w->engine->drv_get_outgoing(chid, len, data);
|
||||
}
|
||||
|
||||
static bool drv_get_outgoing_empty(EngineWrapper *w, uint32_t chid) {
|
||||
return w->engine->drv_get_outgoing_empty(chid);
|
||||
}
|
||||
|
||||
static void drv_get_console_prompt(EngineWrapper *w, uint32_t *len, const char **data) {
|
||||
return w->engine->drv_get_console_prompt(len, data);
|
||||
}
|
||||
|
||||
static double drv_get_clock(EngineWrapper *w) {
|
||||
return w->engine->drv_get_clock();
|
||||
}
|
||||
|
||||
static bool drv_get_rescan_lua_source(EngineWrapper *w) {
|
||||
return w->engine->drv_get_rescan_lua_source();
|
||||
}
|
||||
|
||||
static bool drv_get_stop_driver(EngineWrapper *w) {
|
||||
return w->engine->drv_get_stop_driver();
|
||||
}
|
||||
|
||||
static uint64_t drv_get_actor_id(EngineWrapper *w) {
|
||||
return w->engine->drv_get_actor_id();
|
||||
}
|
||||
|
||||
static void drv_get_tangibles_near(EngineWrapper *w, uint64_t tanid, double rx, double ry, double rz, uint32_t *count, int64_t **ids) {
|
||||
return w->engine->drv_get_tangibles_near(tanid, rx, ry, rz, count, ids);
|
||||
}
|
||||
|
||||
static void drv_get_animation_queues(EngineWrapper *w, uint32_t count, const int64_t *ids, uint32_t *lengths, const char **strings) {
|
||||
return w->engine->drv_get_animation_queues(count, ids, lengths, strings);
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// C Wrappers: Mutators
|
||||
//
|
||||
// The wrapper for a mutator consists of two parts: the wrapper which is used at
|
||||
// 'play' time, and the wrapper which is used at 'replay' time.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
static void play_initialize(EngineWrapper *w, uint32_t argc, char **argv, uint32_t srcpklen, const char *srcpk, const char *logfn) {
|
||||
if (w->engine != nullptr) {
|
||||
return reset_wrapper(w, "Cannot initialize wrapper, it's already initialized.");
|
||||
}
|
||||
|
||||
// Clear the error message.
|
||||
memset(w->error, 0, DRV_ERRMSG_SIZE);
|
||||
|
||||
// Open the logfile, if any is specified.
|
||||
if ((logfn != nullptr) && (logfn[0] != 0)) {
|
||||
w->wlog = new PlayLogfile(logfn, std::ios_base::out | std::ios_base::binary | std::ios_base::trunc);
|
||||
if (!w->wlog->good()) {
|
||||
return reset_wrapper(w, "Could not open replay log for writing: %s", logfn);
|
||||
}
|
||||
} else {
|
||||
w->wlog = nullptr;
|
||||
}
|
||||
|
||||
// If we have a logfile, then log this initialization.
|
||||
if (w->wlog != nullptr) {
|
||||
w->wlog->write_cmd_hash(PLAY_INITIALIZE, eng::memhash());
|
||||
w->wlog->write_uint32(argc);
|
||||
for (uint32_t i = 0; i < argc; i++) {
|
||||
w->wlog->write_string(argv[i]);
|
||||
}
|
||||
w->wlog->write_string(std::string_view(srcpk, srcpklen));
|
||||
w->wlog->flush();
|
||||
}
|
||||
|
||||
// Create the engine of the appropriate type.
|
||||
if (argc < 1) {
|
||||
std::ostringstream oss;
|
||||
oss << "Must pass an engine type on the command line. Known types:\n";
|
||||
for (auto reg = DrivenEngineReg::All; reg != nullptr; reg=reg->next) {
|
||||
oss << " " << reg->name << std::endl;
|
||||
}
|
||||
std::string err = oss.str();
|
||||
return reset_wrapper(w, err.c_str());
|
||||
}
|
||||
w->engine = make_engine(argv[0]);
|
||||
if (w->engine == nullptr) {
|
||||
return reset_wrapper(w, "No such driven engine type: %s", argv[0]);
|
||||
}
|
||||
|
||||
// Call the engine initialization sequence.
|
||||
w->engine->drv_initialize(srcpklen, srcpk, argc - 1, argv + 1);
|
||||
}
|
||||
|
||||
|
||||
static void replay_initialize(EngineWrapper *w) {
|
||||
assert(w->rlog != nullptr);
|
||||
std::vector<std::string> argvstr;
|
||||
uint32_t argc = w->rlog->read_uint32();
|
||||
for (uint32_t i = 0; i < argc; i++) {
|
||||
argvstr.push_back(w->rlog->read_string());
|
||||
}
|
||||
std::string srcpk = w->rlog->read_string();
|
||||
|
||||
if (!w->rlog->good()) {
|
||||
return reset_wrapper(w, "replay log corrupt in replay_initialize");
|
||||
}
|
||||
|
||||
// We need to convert the argument vector from an array
|
||||
// of C++ strings into the canonical argc, argv format.
|
||||
std::vector<char *> argvec;
|
||||
for (uint32_t i = 0; i < argc; i++) {
|
||||
argvec.push_back(&argvstr[i][0]);
|
||||
}
|
||||
char **argv = &argvec[0];
|
||||
|
||||
// Create the engine.
|
||||
w->engine = make_engine(argv[0]);
|
||||
if (w->engine == nullptr) {
|
||||
return reset_wrapper(w, "No such driven engine type: %s", argvstr[0]);
|
||||
}
|
||||
|
||||
|
||||
w->engine->drv_initialize(srcpk.size(), srcpk.c_str(), argc - 1, argv + 1);
|
||||
}
|
||||
|
||||
|
||||
////////////////////////
|
||||
|
||||
|
||||
static void play_clear_new_outgoing(EngineWrapper *w) {
|
||||
assert(w->rlog == nullptr);
|
||||
if (w->wlog != nullptr) {
|
||||
w->wlog->write_cmd_hash(PLAY_CLEAR_NEW_OUTGOING, eng::memhash());
|
||||
w->wlog->flush();
|
||||
}
|
||||
w->engine->drv_clear_new_outgoing();
|
||||
}
|
||||
|
||||
static void replay_clear_new_outgoing(EngineWrapper *w) {
|
||||
w->engine->drv_clear_new_outgoing();
|
||||
}
|
||||
|
||||
|
||||
////////////////////////
|
||||
|
||||
|
||||
static void play_sent_outgoing(EngineWrapper *w, uint32_t chid, uint32_t nbytes) {
|
||||
assert(w->rlog == nullptr);
|
||||
if (w->wlog != nullptr) {
|
||||
uint32_t ndata; const char *data;
|
||||
w->engine->drv_get_outgoing(chid, &ndata, &data);
|
||||
assert(nbytes <= ndata);
|
||||
w->wlog->write_cmd_hash(PLAY_SENT_OUTGOING, eng::memhash());
|
||||
w->wlog->write_uint32(chid);
|
||||
w->wlog->write_uint32(nbytes);
|
||||
w->wlog->write_uint64(SpookyHash::QkHash64(data, nbytes));
|
||||
w->wlog->flush();
|
||||
}
|
||||
w->engine->drv_sent_outgoing(chid, nbytes);
|
||||
}
|
||||
|
||||
static void replay_sent_outgoing(EngineWrapper *w) {
|
||||
uint32_t chid = w->rlog->read_uint32();
|
||||
uint32_t nbytes = w->rlog->read_uint32();
|
||||
uint64_t hash = w->rlog->read_uint64();
|
||||
|
||||
if (!w->rlog->good()) {
|
||||
return reset_wrapper(w, "replay log corrupt in replay_sent_outgoing");
|
||||
}
|
||||
|
||||
uint32_t ndata; const char *data;
|
||||
w->engine->drv_get_outgoing(chid, &ndata, &data);
|
||||
if ((nbytes > ndata) || (hash != SpookyHash::QkHash64(data, nbytes))) {
|
||||
return reset_wrapper(w, "nondeterministic in replay_sent_outgoing");
|
||||
}
|
||||
if (w->replay_cb_sent_outgoing != nullptr) {
|
||||
w->replay_cb_sent_outgoing(w->replay_cb_vp, chid, ndata, data);
|
||||
}
|
||||
w->engine->drv_sent_outgoing(chid, nbytes);
|
||||
}
|
||||
|
||||
|
||||
////////////////////////
|
||||
|
||||
|
||||
static void play_recv_incoming(EngineWrapper *w, uint32_t chid, uint32_t len, const char *data) {
|
||||
assert(w->rlog == nullptr);
|
||||
if (w->wlog != nullptr) {
|
||||
w->wlog->write_cmd_hash(PLAY_RECV_INCOMING, eng::memhash());
|
||||
w->wlog->write_uint32(chid);
|
||||
w->wlog->write_short_string(std::string_view(data, len));
|
||||
w->wlog->flush();
|
||||
}
|
||||
w->engine->drv_recv_incoming(chid, len, data);
|
||||
}
|
||||
|
||||
static void replay_recv_incoming(EngineWrapper *w) {
|
||||
uint32_t chid = w->rlog->read_uint32();
|
||||
std::string_view data = w->rlog->read_short_string(w);
|
||||
|
||||
if (!w->rlog->good()) {
|
||||
return reset_wrapper(w, "replay log corrupt in replay_recv_incoming");
|
||||
}
|
||||
|
||||
w->engine->drv_recv_incoming(chid, data.size(), data.data());
|
||||
}
|
||||
|
||||
|
||||
////////////////////////
|
||||
|
||||
|
||||
static void play_notify_close(EngineWrapper *w, uint32_t chid, uint32_t len, const char *data) {
|
||||
assert(w->rlog == nullptr);
|
||||
if (w->wlog != nullptr) {
|
||||
w->wlog->write_cmd_hash(PLAY_NOTIFY_CLOSE, eng::memhash());
|
||||
w->wlog->write_uint32(chid);
|
||||
w->wlog->write_string(std::string_view(data, len));
|
||||
w->wlog->flush();
|
||||
}
|
||||
|
||||
w->engine->drv_notify_close(chid, len, data);
|
||||
}
|
||||
|
||||
static void replay_notify_close(EngineWrapper *w) {
|
||||
uint32_t chid = w->rlog->read_uint32();
|
||||
std::string message = w->rlog->read_string();
|
||||
|
||||
if (!w->rlog->good()) {
|
||||
return reset_wrapper(w, "replay log corrupt in replay_notify_close");
|
||||
}
|
||||
|
||||
w->engine->drv_notify_close(chid, message.size(), message.c_str());
|
||||
}
|
||||
|
||||
|
||||
////////////////////////
|
||||
|
||||
|
||||
static uint32_t play_notify_accept(EngineWrapper *w, uint32_t port) {
|
||||
assert(w->rlog == nullptr);
|
||||
if (w->wlog != nullptr) {
|
||||
w->wlog->write_cmd_hash(PLAY_NOTIFY_ACCEPT, eng::memhash());
|
||||
w->wlog->write_uint32(port);
|
||||
w->wlog->flush();
|
||||
}
|
||||
|
||||
return w->engine->drv_notify_accept(port);
|
||||
}
|
||||
|
||||
static void replay_notify_accept(EngineWrapper *w) {
|
||||
uint32_t port = w->rlog->read_uint32();
|
||||
|
||||
if (!w->rlog->good()) {
|
||||
return reset_wrapper(w, "replay log corrupt in replay_notify_accept");
|
||||
}
|
||||
|
||||
w->engine->drv_notify_accept(port);
|
||||
}
|
||||
|
||||
|
||||
////////////////////////
|
||||
|
||||
|
||||
static void play_update(EngineWrapper *w, double clock) {
|
||||
assert(w->rlog == nullptr);
|
||||
if (w->wlog != nullptr) {
|
||||
w->wlog->write_cmd_hash(PLAY_UPDATE, eng::memhash());
|
||||
w->wlog->write_double(clock);
|
||||
w->wlog->flush();
|
||||
}
|
||||
|
||||
w->engine->drv_update(clock);
|
||||
}
|
||||
|
||||
static void replay_update(EngineWrapper *w) {
|
||||
double clock = w->rlog->read_double();
|
||||
if (!w->rlog->good()) {
|
||||
return reset_wrapper(w, "replay log corrupt in replay_event_update");
|
||||
}
|
||||
|
||||
w->engine->drv_update(clock);
|
||||
}
|
||||
|
||||
|
||||
////////////////////////
|
||||
|
||||
|
||||
void play_call_function(EngineWrapper *w, InvocationKind kind, int64_t place, uint32_t datapklen, const char *datapk, uint32_t *retpklen, const char **retpk) {
|
||||
assert(w->rlog == nullptr);
|
||||
if (w->wlog != nullptr) {
|
||||
w->wlog->write_cmd_hash(PLAY_CALL_FUNCTION, eng::memhash());
|
||||
w->wlog->write_uint8(int64_t(kind));
|
||||
w->wlog->write_int64(place);
|
||||
w->wlog->write_string(std::string_view(datapk, datapklen));
|
||||
w->wlog->flush();
|
||||
}
|
||||
|
||||
w->engine->drv_call_function(kind, place, datapklen, datapk, retpklen, retpk);
|
||||
}
|
||||
|
||||
void replay_call_function(EngineWrapper *w) {
|
||||
InvocationKind kind = InvocationKind(w->rlog->read_uint8());
|
||||
int64_t place = w->rlog->read_int64();
|
||||
std::string srcpack = w->rlog->read_string();
|
||||
if (!w->rlog->good()) {
|
||||
return reset_wrapper(w, "replay log corrupt in replay_call_function_lua_call");
|
||||
}
|
||||
|
||||
w->engine->drv_call_function(kind, place, srcpack.size(), srcpack.c_str(), nullptr, nullptr);
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Replay Core
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
static void replaycore_initialize(EngineWrapper *w, const char *logfn) {
|
||||
if (w->engine != nullptr) {
|
||||
return reset_wrapper(w, "Cannot initialize wrapper, it's already initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the error message.
|
||||
memset(w->error, 0, DRV_ERRMSG_SIZE);
|
||||
|
||||
// Open the logfile.
|
||||
w->rlog = new ReplayLogfile(logfn, std::ios_base::in | std::ios_base::binary);
|
||||
if (!w->rlog->good()) {
|
||||
return reset_wrapper(w, "Could not open replay log for reading: %s", logfn);
|
||||
}
|
||||
|
||||
// Read one step from the logfile, and make sure it's an initialize step.
|
||||
uint8_t code = w->rlog->read_uint8();
|
||||
int hash = w->rlog->read_uint32();
|
||||
if (!w->rlog->good()) {
|
||||
return reset_wrapper(w, "logfile corrupt in initial step");
|
||||
}
|
||||
if (hash != eng::memhash()) {
|
||||
return reset_wrapper(w, "nondeterminism detected in initial step");
|
||||
}
|
||||
if (code != PLAY_INITIALIZE) {
|
||||
return reset_wrapper(w, "replay log doesn't begin with initialize step");
|
||||
}
|
||||
|
||||
// Replay the initialize step from the logfile.
|
||||
// Doing this immediately, rather than waiting for the driver
|
||||
// to call 'step', enforces the invariant that after calling
|
||||
// initialize, there's an engine.
|
||||
replay_initialize(w);
|
||||
}
|
||||
|
||||
static void replaycore_step(EngineWrapper *w) {
|
||||
if (w->rlog == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t code = w->rlog->read_uint8();
|
||||
if (w->rlog->eof()) {
|
||||
return reset_wrapper(w, "logfile terminated abruptly");
|
||||
}
|
||||
int hash = w->rlog->read_uint32();
|
||||
if (!w->rlog->good()) {
|
||||
return reset_wrapper(w, "logfile corrupt in replay step");
|
||||
}
|
||||
if (hash != eng::memhash()) {
|
||||
return reset_wrapper(w, "nondeterminism detected");
|
||||
}
|
||||
switch (code) {
|
||||
case PLAY_CLEAR_NEW_OUTGOING: replay_clear_new_outgoing(w); return;
|
||||
case PLAY_SENT_OUTGOING: replay_sent_outgoing(w); return;
|
||||
case PLAY_RECV_INCOMING: replay_recv_incoming(w); return;
|
||||
case PLAY_NOTIFY_CLOSE: replay_notify_close(w); return;
|
||||
case PLAY_NOTIFY_ACCEPT: replay_notify_accept(w); return;
|
||||
case PLAY_UPDATE: replay_update(w); return;
|
||||
case PLAY_CALL_FUNCTION: replay_call_function(w); return;
|
||||
case PLAY_RELEASE: release(w); return;
|
||||
default: return reset_wrapper(w, "Replay log corrupt in command dispatcher");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Wrapper Initialization
|
||||
//
|
||||
// To access the engine across a DLL boundary, you first use
|
||||
// GetProcAddress or dlsym to fetch the addresses of 'init_play_engine'
|
||||
// and 'init_replay_engine'. Then, you use those two functions to
|
||||
// initialize a EngineWrapper or a EngineWrapper, which contain the addresses
|
||||
// of all the other functions you need. These are the only two functions
|
||||
// marked 'DLLEXPORT', all other functions are exported from the DLL
|
||||
// indirectly.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#if defined(__linux__)
|
||||
#define DLLEXPORT __attribute__((visibility("default")))
|
||||
#elif defined(_WIN32)
|
||||
#define DLLEXPORT __declspec(dllexport)
|
||||
#endif
|
||||
|
||||
static void init_engine_wrapper_helper(EngineWrapper *w) {
|
||||
static bool called_initializer;
|
||||
assert(DrivenEngineInitializerReg::func != nullptr);
|
||||
if (!called_initializer) {
|
||||
DrivenEngineInitializerReg::func();
|
||||
called_initializer = true;
|
||||
}
|
||||
|
||||
memset(w, 0, sizeof(EngineWrapper));
|
||||
|
||||
w->get_listen_ports = drv_get_listen_ports;
|
||||
w->get_new_outgoing = drv_get_new_outgoing;
|
||||
w->get_target = drv_get_target;
|
||||
w->get_channel_released = drv_get_channel_released;
|
||||
w->get_outgoing = drv_get_outgoing;
|
||||
w->get_outgoing_empty = drv_get_outgoing_empty;
|
||||
w->get_console_prompt = drv_get_console_prompt;
|
||||
w->get_clock = drv_get_clock;
|
||||
w->get_rescan_lua_source = drv_get_rescan_lua_source;
|
||||
w->get_stop_driver = drv_get_stop_driver;
|
||||
w->get_actor_id = drv_get_actor_id;
|
||||
w->get_tangibles_near = drv_get_tangibles_near;
|
||||
w->get_animation_queues = drv_get_animation_queues;
|
||||
|
||||
w->play_initialize = play_initialize;
|
||||
w->play_clear_new_outgoing = play_clear_new_outgoing;
|
||||
w->play_sent_outgoing = play_sent_outgoing;
|
||||
w->play_recv_incoming = play_recv_incoming;
|
||||
w->play_notify_close = play_notify_close;
|
||||
w->play_notify_accept = play_notify_accept;
|
||||
w->play_update = play_update;
|
||||
w->play_call_function = play_call_function;
|
||||
|
||||
w->replay_initialize = replaycore_initialize;
|
||||
w->replay_step = replaycore_step;
|
||||
|
||||
w->hook_dprint = util::hook_dprint;
|
||||
w->release = release;
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
DLLEXPORT void init_engine_wrapper(EngineWrapper *w) {
|
||||
init_engine_wrapper_helper(w);
|
||||
}
|
||||
}
|
||||
352
luprex/cpp/core/drivenengine.hpp
Normal file
352
luprex/cpp/core/drivenengine.hpp
Normal file
@@ -0,0 +1,352 @@
|
||||
//////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DrivenEngine
|
||||
//
|
||||
// This module embodies the idea of an "event-driven game engine." The
|
||||
// DrivenEngine module provides two APIs: the engine-side API, and the
|
||||
// driver-side API.
|
||||
//
|
||||
// The engine-side API looks like a typical collection of I/O primitives. It
|
||||
// includes methods to open sockets, read and write sockets, read lua source,
|
||||
// get the clock, and so forth.
|
||||
//
|
||||
// But in reality, these I/O functions don't ever call operating system
|
||||
// functions like "read" or "write" or "connect." They don't call the operating
|
||||
// system at all - not even indirectly, through a wrapper. Therefore, they
|
||||
// can't really do any I/O. When you use one of these I/O functions to (say)
|
||||
// write some data to a communication channel, the only thing that happens is
|
||||
// that the data is put into a buffer. The actual transmission of the data
|
||||
// happens elsewhere, in what is called the "Driver." Likewise, when you use
|
||||
// one of these I/O functions to read data, it only returns data that was
|
||||
// previously stored by the "Driver."
|
||||
//
|
||||
// The "Driver" is a module that implements the actual I/O. It is highly
|
||||
// OS-dependent code, because it contains code to manipulate sockets, time
|
||||
// clocks, and the like.
|
||||
//
|
||||
// From the perspective of the driver, the DrivenEngine is a C++ object that
|
||||
// acts like a state machine. This state machine is driven forward by I/O
|
||||
// events. The DrivenEngine provides an API where the driver can feed in these
|
||||
// I/O events.
|
||||
//
|
||||
// Notice that the usual call graph is inverted: in most application programs,
|
||||
// the application calls the operating system to do I/O. But when using class
|
||||
// DrivenEngine, it's the other way around: the driver calls into class
|
||||
// DrivenEngine to drive it forward. I/O routines drive computation.
|
||||
//
|
||||
// So the upshot of all this is that the DrivenEngine is a deterministic state
|
||||
// machine, free of all OS-specific code.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef DRIVENENGINE_HPP
|
||||
#define DRIVENENGINE_HPP
|
||||
|
||||
#include "wrap-string.hpp"
|
||||
#include "wrap-vector.hpp"
|
||||
|
||||
#include <ostream>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#include "util.hpp"
|
||||
#include "streambuffer.hpp"
|
||||
#include "enginewrapper.hpp"
|
||||
#include "planemap.hpp"
|
||||
#include "invocation.hpp"
|
||||
|
||||
class DrivenEngine;
|
||||
class World;
|
||||
using UniqueDrivenEngine = std::unique_ptr<DrivenEngine>;
|
||||
using DrivenEngineMaker = UniqueDrivenEngine (*)();
|
||||
using DrivenEngineInitializer = void (*)();
|
||||
|
||||
class Channel : public eng::opnew {
|
||||
public:
|
||||
// Get the buffers associated with this channel.
|
||||
//
|
||||
StreamBuffer *out() { return sb_out_.get(); }
|
||||
StreamBuffer *in() { return sb_in_.get(); }
|
||||
|
||||
// The channel ID. These are reused.
|
||||
//
|
||||
int chid() const { return chid_; }
|
||||
|
||||
// If this is a socket connection, the receiver's port number.
|
||||
//
|
||||
int port() const { return port_; }
|
||||
|
||||
// If this is an outgoing socket connection, get the target host.
|
||||
//
|
||||
const eng::string &target() const { return target_; }
|
||||
|
||||
// True if the remote has closed the connection.
|
||||
//
|
||||
bool closed() const { return closed_; }
|
||||
|
||||
// Get the channel's error message.
|
||||
//
|
||||
// If this is an empty string, there is no error. If this is set,
|
||||
// then the channel is also closed.
|
||||
//
|
||||
const eng::string &error() const { return error_; }
|
||||
|
||||
// Do not construct your own Channels. Instead,
|
||||
// use methods of class DrivenEngine like new_outgoing_channel.
|
||||
// Channels are referenced by shared_ptr. You can
|
||||
// release your shared_ptr at any time.
|
||||
//
|
||||
Channel(DrivenEngine *de, int chid, int port, const eng::string &target, bool stop);
|
||||
~Channel() {};
|
||||
|
||||
private:
|
||||
// Constructor is deliberately private. Use
|
||||
// DrivenEngine::new_outgoing_channel to create outgoing socket channels.
|
||||
//
|
||||
std::string_view peek_outgoing() const;
|
||||
void sent_outgoing(int nbytes);
|
||||
|
||||
private:
|
||||
int chid_;
|
||||
|
||||
// These are the in/out buffers presented to the user.
|
||||
std::shared_ptr<StreamBuffer> sb_in_;
|
||||
std::shared_ptr<StreamBuffer> sb_out_;
|
||||
|
||||
int port_;
|
||||
bool closed_;
|
||||
eng::string error_;
|
||||
eng::string target_;
|
||||
bool stop_driver_;
|
||||
|
||||
friend class DrivenEngine;
|
||||
};
|
||||
|
||||
using SharedChannel = std::shared_ptr<Channel>;
|
||||
|
||||
class DrivenEngine : public eng::opnew {
|
||||
public:
|
||||
//////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Build the named engine
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
static UniqueDrivenEngine make(std::string_view name);
|
||||
|
||||
static void print_usage(std::ostream &strm, std::string_view progname);
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
//
|
||||
// The following methods are the 'engine' side of the pipe.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
// The init callback. You may override this in a subclass.
|
||||
// This will be called once at program initialization.
|
||||
//
|
||||
virtual void event_init(std::string_view srcpk, int argc, char *argv[]) = 0;
|
||||
|
||||
// The call-function callback. This is invoked whenever drv_call_function
|
||||
// is called. This is the main entry point for "general" access into the
|
||||
// DrivenEngine. The datapk parameter can contain any arbitrary data needed
|
||||
// by the call. If the call wants to return anything, it can write the
|
||||
// return data into the retpk datapack.
|
||||
//
|
||||
virtual void event_call_function(InvocationKind kind, int64_t place, std::string_view datapk, StreamBuffer *retpk) {};
|
||||
|
||||
// The update callback. You may override this in a subclass.
|
||||
// This will be called whenever anything changes.
|
||||
//
|
||||
virtual void event_update() = 0;
|
||||
|
||||
// Specify the set of listening ports.
|
||||
// This can only be used during the init routine.
|
||||
//
|
||||
void listen_port(int port);
|
||||
|
||||
// Get the current time.
|
||||
//
|
||||
// DRIVER: This returns the time most recently stored by the driver
|
||||
// using drv_set_clock.
|
||||
//
|
||||
double get_clock();
|
||||
|
||||
// Create a channel and open an outgoing connection. The channel creation
|
||||
// always succeeds. You can write to the channel immediately. You can
|
||||
// read, too, but of course there won't be anything in the incoming buffer
|
||||
// yet. In future update events, data will show up in the incoming buffer,
|
||||
// and will have been sent from the outgoing buffer. In future update
|
||||
// events, the channel may get closed by the remote. If the connection
|
||||
// fails (say, the remote host doesn't exist), then the Channel will get
|
||||
// closed with an error.
|
||||
//
|
||||
// DRIVER: The channel object is created instantly, but it does nothing
|
||||
// until the driver notices the new channel. The driver is responsible for
|
||||
// actually opening the connection and relaying data into the channel using
|
||||
// drv_get_target, drv_peek_outgoing, drv_sent_outgoing, drv_recv_incoming.
|
||||
//
|
||||
SharedChannel new_outgoing_channel(const eng::string &target);
|
||||
|
||||
// Create a new channel from any pending incoming connection. If there is no
|
||||
// incoming connection, returns nullptr.
|
||||
//
|
||||
// DRIVER: The driver must be hardwired to know what ports to listen on.
|
||||
// When the driver notices a new incoming connection, it calls
|
||||
// drv_notify_accept, which triggers the creation of the channel. The
|
||||
// channel is put into the incoming channel queue, which is fetched by this
|
||||
// method. The driver is responsible for relaying data into the channel
|
||||
// using drv_get_target, drv_peek_outgoing, drv_sent_outgoing,
|
||||
// drv_recv_incoming.
|
||||
//
|
||||
SharedChannel new_incoming_channel();
|
||||
|
||||
// Obtain the stdio channel. There is only one stdio channel.
|
||||
//
|
||||
// DRIVER: the stdio channel is created automatically when the DrivenEngine
|
||||
// is created. Stdio should be connected to a console which is in
|
||||
// line-at-a-time mode. The driver is responsible for relaying data from
|
||||
// the console into the stdio channel using drv_peek_outgoing,
|
||||
// drv_sent_outgoing, drv_recv_incoming.
|
||||
//
|
||||
SharedChannel get_stdio_channel();
|
||||
|
||||
// Obtain the output buffer of the stdio channel as an ostream.
|
||||
//
|
||||
std::ostream &stdostream() { return *stdostream_; }
|
||||
|
||||
// Set the prompt for the console.
|
||||
//
|
||||
void set_console_prompt(const eng::string &prompt);
|
||||
|
||||
// Set the flag to rescan the lua source directory. The lua source
|
||||
// directory is read once, automatically, at engine creation time.
|
||||
// If you want to read it again, you must trigger a rescan.
|
||||
// The rescan is not instantaneous.
|
||||
//
|
||||
// DRIVER: this merely sets a flag, which the driver will notice later,
|
||||
// causing the driver to update the lua source.
|
||||
//
|
||||
void rescan_lua_source();
|
||||
|
||||
// Set the world pointer and the actor ID.
|
||||
//
|
||||
// This allows the graphics engine to query the DrivenEngine
|
||||
// about the state of the world and the player. It is legal to set these
|
||||
// to zero, in which case queries will return null results.
|
||||
//
|
||||
void set_visible_world_and_actor(World *w, int64_t actor);
|
||||
|
||||
// Stop the driver. The engine should call this when it's done
|
||||
// and there's nothing left to do.
|
||||
//
|
||||
void stop_driver();
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Creation and Destruction.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
// Constructor.
|
||||
//
|
||||
// Most initialization is achieved by 'drv_xxx' functions, so
|
||||
// this constructor takes no arguments.
|
||||
//
|
||||
DrivenEngine();
|
||||
|
||||
// Destructor.
|
||||
//
|
||||
// It is necessary to delete all channels before deleting the
|
||||
// DrivenEngine. The destructor will verify that this has been done.
|
||||
//
|
||||
virtual ~DrivenEngine();
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
//
|
||||
// The following accessors are for use by PlayWrapper and ReplayWrapper.
|
||||
//
|
||||
// The PlayWrapper and ReplayWrapper use C stubs to access
|
||||
// the engine. The C stubs, in turn, call these C++ methods.
|
||||
//
|
||||
// The stubs for the getters are trivial, one-line stubs.
|
||||
//
|
||||
// The stubs for the mutators add logging.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
void drv_get_listen_ports(uint32_t *nports, const uint32_t **ports) const;
|
||||
void drv_get_new_outgoing(uint32_t *nchids, const uint32_t **chids) const;
|
||||
const char *drv_get_target(uint32_t chid) const;
|
||||
bool drv_get_channel_released(uint32_t chid) const;
|
||||
void drv_get_outgoing(uint32_t chid, uint32_t *len, const char **data) const;
|
||||
bool drv_get_outgoing_empty(uint32_t chid) const;
|
||||
void drv_get_console_prompt(uint32_t *len, const char **data) const;
|
||||
double drv_get_clock() const;
|
||||
bool drv_get_rescan_lua_source() const;
|
||||
bool drv_get_stop_driver() const;
|
||||
int64_t drv_get_actor_id() const;
|
||||
void drv_get_tangibles_near(int64_t tanid, double rx, double ry, double rz, uint32_t *count, int64_t **ids);
|
||||
void drv_get_animation_queues(uint32_t count, const int64_t *ids, uint32_t *lengths, const char **strings);
|
||||
|
||||
void drv_initialize(uint32_t srcpklen, const char *srcpk, int argc, char **argv);
|
||||
void drv_clear_new_outgoing();
|
||||
void drv_sent_outgoing(uint32_t chid, uint32_t nbytes);
|
||||
void drv_recv_incoming(uint32_t chid, uint32_t nbytes, const char *bytes);
|
||||
void drv_notify_close(uint32_t chid, uint32_t len, const char *data);
|
||||
uint32_t drv_notify_accept(uint32_t port);
|
||||
void drv_update(double clock);
|
||||
void drv_call_function(InvocationKind kind, int64_t place, uint32_t datapklen, const char *datapk, uint32_t *retpklen, const char **retpk);
|
||||
|
||||
private:
|
||||
// Find a currently-unused channel ID. Channel IDs
|
||||
// are small integers that are reused.
|
||||
int find_unused_chid();
|
||||
|
||||
// Get the channel associated with the specified channel ID.
|
||||
Channel *get_chid(int chid) const;
|
||||
|
||||
private:
|
||||
SharedChannel channels_[DRV_MAX_CHAN];
|
||||
int next_unused_chid_;
|
||||
SharedChannel stdio_channel_;
|
||||
std::unique_ptr<std::ostream> stdostream_;
|
||||
eng::vector<SharedChannel> accepted_channels_;
|
||||
eng::vector<uint32_t> new_outgoing_;
|
||||
eng::vector<uint32_t> listen_ports_;
|
||||
World *visible_world_;
|
||||
int64_t visible_actor_id_;
|
||||
util::IdVector scan_result_;
|
||||
std::vector<util::SharedStdString> anim_queues_;
|
||||
StreamBuffer call_function_retpk_;
|
||||
bool rescan_lua_source_;
|
||||
double clock_;
|
||||
bool stop_driver_;
|
||||
eng::string console_prompt_;
|
||||
friend class Channel;
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
struct DrivenEngineReg {
|
||||
const char *name;
|
||||
DrivenEngineMaker maker;
|
||||
DrivenEngineReg *next;
|
||||
static DrivenEngineReg *All;
|
||||
DrivenEngineReg(const char *name, DrivenEngineMaker f);
|
||||
};
|
||||
|
||||
#define DrivenEngineDefine(name, cname) \
|
||||
UniqueDrivenEngine dengmake_##cname() { \
|
||||
return UniqueDrivenEngine(new cname); \
|
||||
} \
|
||||
DrivenEngineReg dengreg_##cname(name, dengmake_##cname);
|
||||
|
||||
struct DrivenEngineInitializerReg {
|
||||
static DrivenEngineInitializer func;
|
||||
DrivenEngineInitializerReg(DrivenEngineInitializer f);
|
||||
};
|
||||
|
||||
|
||||
#endif // DRIVENENGINE_HPP
|
||||
122
luprex/cpp/core/eng-malloc.cpp
Normal file
122
luprex/cpp/core/eng-malloc.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
|
||||
// We only use a custom allocator on linux (for now)
|
||||
// The allocator we chose is 'dlmalloc' (doug lea's malloc).
|
||||
// It's a good allocator for single-threaded programs.
|
||||
// It needs to be configured by setting a bunch of defines.
|
||||
//
|
||||
#ifdef __linux__
|
||||
|
||||
// We need this to define dlmalloc, not malloc.
|
||||
#define USE_DL_PREFIX 1
|
||||
|
||||
// We don't need mspaces.
|
||||
#define ONLY_MSPACES 0
|
||||
#define MSPACES 0
|
||||
|
||||
// We don't need mallinfo
|
||||
#define NO_MALLINFO 1
|
||||
|
||||
// We don't need dlmalloc_inspect_all
|
||||
#define MALLOC_INSPECT_ALL 0
|
||||
|
||||
// Disable locking. The entire engine is single-threaded.
|
||||
#define USE_LOCKS 0
|
||||
|
||||
// This allocator can use sbrk to obtain RAM. We're going to
|
||||
// emulate sbrk, in order to put it in a different memory region
|
||||
// than the system malloc, which also uses sbrk.
|
||||
#define HAVE_MORECORE 1
|
||||
#define MORECORE emulated_sbrk
|
||||
#define MORECORE_CANNOT_TRIM 1
|
||||
|
||||
// We won't let the memory allocator use mmap. This is because
|
||||
// opening the replay log may be implemented in stdio using mmap.
|
||||
// So using mmap might trigger different results under replay.
|
||||
#define HAVE_MMAP 0
|
||||
|
||||
// Fix a warning in dlmalloc.
|
||||
#define MAX_RELEASE_CHECK_RATE INT_MAX
|
||||
|
||||
// Don't generate random seeds, or time-based seeds.
|
||||
#define USE_DEV_RANDOM 0
|
||||
#define LACKS_TIME_H 1
|
||||
|
||||
// Don't export any dlmalloc functions.
|
||||
#define DLMALLOC_EXPORT static inline
|
||||
|
||||
#include <sys/mman.h>
|
||||
#include <cstdint>
|
||||
#include <cassert>
|
||||
#include <climits>
|
||||
|
||||
static char *emulated_sbrk_base;
|
||||
static intptr_t emulated_sbrk_used;
|
||||
static intptr_t emulated_sbrk_limit;
|
||||
|
||||
// We assume that the increments are already aligned to the system
|
||||
// page size, because dlmalloc does that for us.
|
||||
//
|
||||
// Our emulated sbrk cannot trim the memory size. It can only grow.
|
||||
//
|
||||
static void *emulated_sbrk(intptr_t increment) {
|
||||
if (emulated_sbrk_base == 0) {
|
||||
emulated_sbrk_limit = intptr_t(64) * 1024 * 1024 * 1024;
|
||||
void *map = mmap(nullptr, emulated_sbrk_limit, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
assert(map != MAP_FAILED);
|
||||
assert(map != nullptr);
|
||||
emulated_sbrk_base = (char *)map;
|
||||
emulated_sbrk_used = 0;
|
||||
}
|
||||
int64_t old_used = emulated_sbrk_used;
|
||||
int64_t new_used = emulated_sbrk_used + increment;
|
||||
if (new_used > emulated_sbrk_limit) {
|
||||
return (void *)(-1);
|
||||
}
|
||||
assert(new_used >= old_used);
|
||||
emulated_sbrk_used = new_used;
|
||||
if (new_used > old_used) {
|
||||
int status = mprotect(emulated_sbrk_base + old_used, new_used - old_used, PROT_READ | PROT_WRITE);
|
||||
assert(status == 0);
|
||||
}
|
||||
return emulated_sbrk_base + old_used;
|
||||
}
|
||||
|
||||
#include "../../ext/dlmalloc.c"
|
||||
|
||||
namespace eng {
|
||||
static uint32_t hash;
|
||||
void *malloc(size_t size) {
|
||||
void *result = dlmalloc(size);
|
||||
hash += uint32_t(uintptr_t(result)) + 0x83748374;
|
||||
hash += hash << 10;
|
||||
hash ^= hash >> 6;
|
||||
hash += uint32_t(size) + 0x85893489;
|
||||
hash += hash << 10;
|
||||
hash ^= hash >> 6;
|
||||
return result;
|
||||
}
|
||||
void *realloc(void *p, size_t size) {
|
||||
void *result = dlrealloc(p, size);
|
||||
hash += uint32_t(uintptr_t(p)) + 0x74741912;
|
||||
hash += hash << 10;
|
||||
hash ^= hash >> 6;
|
||||
hash += uint32_t(uintptr_t(result)) + 0x68843823;
|
||||
hash += hash << 10;
|
||||
hash ^= hash >> 6;
|
||||
hash += uint32_t(size) + 0x81444120;
|
||||
hash += hash << 10;
|
||||
hash ^= hash >> 6;
|
||||
return result;
|
||||
}
|
||||
void free(void *p) {
|
||||
hash += uint32_t(uintptr_t(p)) + 0x87823448;
|
||||
dlfree(p);
|
||||
}
|
||||
int memhash() {
|
||||
return (hash & 0x3FFFFFFF) | 0x40000000;
|
||||
}
|
||||
} // namespace eng
|
||||
|
||||
#endif // ifdef __linux__
|
||||
|
||||
|
||||
213
luprex/cpp/core/eng-malloc.hpp
Normal file
213
luprex/cpp/core/eng-malloc.hpp
Normal file
@@ -0,0 +1,213 @@
|
||||
//
|
||||
// eng-malloc
|
||||
//
|
||||
// The engine has its own private eng::malloc which it uses to allocate
|
||||
// everything. The engine's malloc heap only contains engine data structures.
|
||||
// This helps achieve determinism when playing a replay log.
|
||||
//
|
||||
// About determinism: one of the key rules for maintaining deterministic
|
||||
// behavior is to not ever use operations that execute in arbitrary order.
|
||||
// For example, don't iterate over an unordered map, because there's no
|
||||
// rule about what order the items are produced. They could be produced
|
||||
// in a different order during replay than during the original recording.
|
||||
// Actually, you can occasionally get away with it
|
||||
//
|
||||
// The engine's eng::malloc is a thin wrapper around Doug Lea's Malloc, a good
|
||||
// general-purpose single-threaded malloc. It's probably not the fastest any
|
||||
// more (it was, once), but it's still quite good. It's also fairly easy
|
||||
// to work with.
|
||||
//
|
||||
// In order to get all engine data structures into the eng::malloc heap, you
|
||||
// need to jump through quite a few hoops:
|
||||
//
|
||||
// * When writing a class that gets allocated using operator new,
|
||||
// always derive from eng::opnew. This adds a custom operator new
|
||||
// to your class, which causes your class to be allocated using eng::malloc.
|
||||
// If you write a class that isn't ever supposed to be allocated using
|
||||
// operator new, derive from eng::nevernew instead.
|
||||
//
|
||||
// * When using STL containers, you need to use the eng variant:
|
||||
// eng::map, eng::set, eng::vector, eng::unordered_map, eng::unordered_set,
|
||||
// and eng::deque. These classes derive from eng::opnew, and they also
|
||||
// use eng::malloc for their internal nodes.
|
||||
//
|
||||
// * Use eng::string instead of std::string. Use eng::ostringstream
|
||||
// instead of std::ostringstream.
|
||||
//
|
||||
// * Simple classes like std::pair, std::string_view, std::less, std::hash, and
|
||||
// so forth are not wrapped, because it is not normal to allocate these
|
||||
// classes using operator new. Do not use operator new or delete on
|
||||
// these classes.
|
||||
//
|
||||
// * Instead of std::make_shared, use eng::make_shared. You need this
|
||||
// because std::make_shared doesn't respect your custom operator new.
|
||||
//
|
||||
// * Failing to jump through all these hoops won't break your code in any
|
||||
// obvious way - you'll just have some of your data structures in the malloc
|
||||
// heap instead of the eng::malloc heap. This won't break
|
||||
// determinism unless you iterate over a data structure like an unordered map
|
||||
// but it creates a situation where we can't detect
|
||||
// nondeterminism.
|
||||
//
|
||||
// * Sometimes we deliberately put certain data structures into the malloc
|
||||
// heap, because we know that those particular data structures won't be
|
||||
// identical between record and replay. In that situation, the fact that
|
||||
// we don't detect the nondeterminism is actually a benefit.
|
||||
//
|
||||
// * Be aware that most C++ streams use the system malloc heap, and there's no
|
||||
// way to change that. That's ok, it's fine if some small percentage of our
|
||||
// data goes into the malloc heap. By the way, eng::ostringstream uses
|
||||
// the eng::malloc heap.
|
||||
//
|
||||
#ifndef ENG_MALLOC_HPP
|
||||
#define ENG_MALLOC_HPP
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <cassert>
|
||||
|
||||
namespace eng {
|
||||
#ifdef __linux__
|
||||
void* malloc(size_t x);
|
||||
void free(void *p);
|
||||
void* realloc(void*, size_t);
|
||||
int memhash();
|
||||
#else
|
||||
inline void *malloc(size_t x) { return ::malloc(x); }
|
||||
inline void free(void *p) { return ::free(p); }
|
||||
inline void *realloc(void *p, size_t x) { return ::realloc(p, x); }
|
||||
inline int memhash() { return 0; }
|
||||
#endif
|
||||
} // namespace eng
|
||||
|
||||
// An allocator for lua states that uses eng::malloc and eng::free
|
||||
namespace eng {
|
||||
inline void *l_alloc(void *ud, void *ptr, size_t osize, size_t nsize) {
|
||||
if (nsize == 0) {
|
||||
::eng::free(ptr);
|
||||
return NULL;
|
||||
} else {
|
||||
return ::eng::realloc(ptr, nsize);
|
||||
}
|
||||
}
|
||||
} // namespace eng
|
||||
|
||||
// eng_allocator is similar to std::allocator, but allocates
|
||||
// objects using eng::malloc and eng::free.
|
||||
template <class T>
|
||||
class eng_allocator
|
||||
{
|
||||
public:
|
||||
using value_type = T;
|
||||
eng_allocator() noexcept {}
|
||||
template <class U> eng_allocator(eng_allocator<U> const&) noexcept {}
|
||||
|
||||
value_type* allocate(std::size_t n)
|
||||
{
|
||||
return static_cast<value_type*>(eng::malloc(n*sizeof(value_type)));
|
||||
}
|
||||
|
||||
void deallocate(value_type* p, std::size_t) noexcept
|
||||
{
|
||||
eng::free(p);
|
||||
}
|
||||
};
|
||||
|
||||
// Another name for eng_allocator is eng::allocator.
|
||||
namespace eng {
|
||||
template<class T>
|
||||
using allocator = ::eng_allocator<T>;
|
||||
} // namespace eng
|
||||
|
||||
// Mandated equality and inequality operators for eng_allocator.
|
||||
template <class T, class U>
|
||||
bool operator==(const eng_allocator<T> &, const eng_allocator<U> &) noexcept
|
||||
{
|
||||
return true;
|
||||
}
|
||||
template <class T, class U>
|
||||
bool operator!=(const eng_allocator<T> &, const eng_allocator<U> &) noexcept
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// eng::opnew. A class containing operator new and operator delete,
|
||||
// meant to be used as a base class for inheritance.
|
||||
namespace eng {
|
||||
class opnew {
|
||||
public:
|
||||
void *operator new(size_t size)
|
||||
{
|
||||
return ::eng::malloc(size);
|
||||
}
|
||||
|
||||
void operator delete(void *p, size_t size)
|
||||
{
|
||||
return ::eng::free(p);
|
||||
}
|
||||
|
||||
void *operator new[](size_t size)
|
||||
{
|
||||
return ::eng::malloc(size);
|
||||
}
|
||||
|
||||
void operator delete[](void *p, size_t size)
|
||||
{
|
||||
return ::eng::free(p);
|
||||
}
|
||||
};
|
||||
} // namespace eng
|
||||
|
||||
// eng::nevernew. A class containing private operator new and
|
||||
// operator delete, making it impossible to 'new' the class.
|
||||
// This means the class must be embedded as a field in some other
|
||||
// class, and it gets allocated when its enclosing object gets
|
||||
// allocated.
|
||||
namespace eng {
|
||||
class nevernew {
|
||||
private:
|
||||
void *operator new(size_t size)
|
||||
{
|
||||
assert(false && "not supposed to 'new' this class");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void operator delete(void *p, size_t size)
|
||||
{
|
||||
assert(false && "not supposed to 'delete' this class");
|
||||
}
|
||||
|
||||
void *operator new[](size_t size)
|
||||
{
|
||||
assert(false && "not supposed to 'new' this class");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void operator delete[](void *p, size_t size)
|
||||
{
|
||||
assert(false && "not supposed to 'delete' this class");
|
||||
}
|
||||
};
|
||||
} // namespace eng
|
||||
|
||||
|
||||
// eng::make_shared allocates shared objects using eng::malloc.
|
||||
namespace eng {
|
||||
template<class T, class... Args>
|
||||
inline ::std::shared_ptr<T> make_shared(Args&&... args) {
|
||||
return std::allocate_shared<T>(eng::allocator<T>(), args...);
|
||||
}
|
||||
} // namespace eng
|
||||
|
||||
// eng::make_unique doesn't do anything different than std::make_unique: they
|
||||
// both use operator new and delete. You must derive from eng::opnew to change
|
||||
// operator new and delete.
|
||||
namespace eng {
|
||||
template<class T, class... Args>
|
||||
inline ::std::unique_ptr<T> make_unique(Args&&... args) {
|
||||
return std::make_unique<T>(args...);
|
||||
}
|
||||
} // namespace eng
|
||||
|
||||
#endif // ENG_MALLOC_HPP
|
||||
|
||||
133
luprex/cpp/core/eng-tests.cpp
Normal file
133
luprex/cpp/core/eng-tests.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
#include "wrap-string.hpp"
|
||||
|
||||
#include "drivenengine.hpp"
|
||||
#include "streambuffer.hpp"
|
||||
#include "world.hpp"
|
||||
|
||||
#include <iomanip>
|
||||
|
||||
static void write_closed_message(Channel *ch, StreamBuffer *out) {
|
||||
eng::ostringstream oss;
|
||||
oss << "Chan " << ch->chid() << " closed [" << ch->error() << "]\n";
|
||||
out->write_bytes(oss.str());
|
||||
}
|
||||
|
||||
static void dump_lines(StreamBuffer *in, StreamBuffer *out, int chid) {
|
||||
while (true) {
|
||||
eng::string l = in->readline();
|
||||
if (l == "") break;
|
||||
eng::ostringstream oss;
|
||||
oss << "Chan " << chid << ": " << l;
|
||||
out->write_bytes(oss.str());
|
||||
}
|
||||
}
|
||||
|
||||
// This test is the minimal possible DrivenEngine.
|
||||
class DriverStubTest : public DrivenEngine {
|
||||
virtual void event_init(std::string_view srcpk, int argc, char *argv[]) override {
|
||||
stop_driver();
|
||||
}
|
||||
virtual void event_update() override {
|
||||
}
|
||||
};
|
||||
|
||||
// This test connects to a public webserver and prints
|
||||
// the output from the server.
|
||||
class DriverWebServerTest : public DrivenEngine {
|
||||
public:
|
||||
eng::vector<SharedChannel> channels_;
|
||||
virtual void event_init(std::string_view srcpk, int argc, char *argv[]) override {
|
||||
SharedChannel ch = new_outgoing_channel("cert:stanford.edu:443");
|
||||
ch->out()->write_bytes("GET https://stanford.edu/xbanankjdsh.html HTTP/1.1\n\n");
|
||||
channels_.emplace_back(std::move(ch));
|
||||
}
|
||||
|
||||
virtual void event_update() override {
|
||||
SharedChannel stdioch = get_stdio_channel();
|
||||
dump_lines(stdioch->in(), stdioch->out(), 0);
|
||||
eng::vector<SharedChannel> keep;
|
||||
for (SharedChannel &ch : channels_) {
|
||||
dump_lines(ch->in(), stdioch->out(), ch->chid());
|
||||
if (ch->closed()) {
|
||||
write_closed_message(ch.get(), stdioch->out());
|
||||
} else {
|
||||
keep.emplace_back(std::move(ch));
|
||||
}
|
||||
}
|
||||
channels_ = std::move(keep);
|
||||
}
|
||||
};
|
||||
|
||||
// This test produces a DNS resolution failure.
|
||||
class DriverDNSFailTest : public DrivenEngine {
|
||||
public:
|
||||
eng::vector<SharedChannel> channels_;
|
||||
virtual void event_init(std::string_view srcpk, int argc, char *argv[]) override {
|
||||
SharedChannel ch = new_outgoing_channel("akjsdkajshdakjshd.alk:80");
|
||||
ch->out()->write_bytes("GET http://stanford.edu/index.html HTTP/1.1\n\n");
|
||||
channels_.emplace_back(std::move(ch));
|
||||
}
|
||||
|
||||
virtual void event_update() override {
|
||||
SharedChannel stdioch = get_stdio_channel();
|
||||
dump_lines(stdioch->in(), stdioch->out(), 0);
|
||||
eng::vector<SharedChannel> keep;
|
||||
for (SharedChannel &ch : channels_) {
|
||||
dump_lines(ch->in(), stdioch->out(), ch->chid());
|
||||
if (ch->closed()) {
|
||||
write_closed_message(ch.get(), stdioch->out());
|
||||
} else {
|
||||
keep.emplace_back(std::move(ch));
|
||||
}
|
||||
}
|
||||
channels_ = std::move(keep);
|
||||
}
|
||||
};
|
||||
|
||||
// This test just prints the time.
|
||||
class DriverPrintClockTest : public DrivenEngine {
|
||||
public:
|
||||
int count_;
|
||||
double last_clock_;
|
||||
virtual void event_init(std::string_view srcpk, int argc, char *argv[]) override {
|
||||
count_ = 0;
|
||||
last_clock_ = 0.0;
|
||||
}
|
||||
|
||||
virtual void event_update() override {
|
||||
double clock = get_clock();
|
||||
if (clock > last_clock_ + 0.5) {
|
||||
int ms = eng::memhash();
|
||||
stdostream() << std::fixed << std::setprecision(2) << clock << " " << std::hex << ms << " ";
|
||||
count_++;
|
||||
last_clock_ = clock;
|
||||
}
|
||||
if (count_ == 4) {
|
||||
stdostream() << std::endl;
|
||||
count_ = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
class RunUnitTests : public DrivenEngine {
|
||||
private:
|
||||
UniqueWorld world_;
|
||||
|
||||
void event_init(std::string_view srcpk, int argc, char *argv[]) override
|
||||
{
|
||||
world_.reset(new World(WORLD_TYPE_MASTER));
|
||||
world_->update_source(srcpk);
|
||||
world_->run_unittests();
|
||||
stop_driver();
|
||||
}
|
||||
|
||||
void event_update() override {}
|
||||
};
|
||||
|
||||
DrivenEngineDefine("driverstubtest", DriverStubTest);
|
||||
DrivenEngineDefine("driverwebservertest", DriverWebServerTest);
|
||||
DrivenEngineDefine("driverdnsfailtest", DriverDNSFailTest);
|
||||
DrivenEngineDefine("driverprintclocktest", DriverPrintClockTest);
|
||||
DrivenEngineDefine("rununittests", RunUnitTests);
|
||||
|
||||
5
luprex/cpp/core/eng-tests.hpp
Normal file
5
luprex/cpp/core/eng-tests.hpp
Normal file
@@ -0,0 +1,5 @@
|
||||
#ifndef DRIVERTESTS_HPP
|
||||
#define DRIVERTESTS_HPP
|
||||
|
||||
#endif // DRIVERTESTS_HPP
|
||||
|
||||
303
luprex/cpp/core/enginewrapper.hpp
Normal file
303
luprex/cpp/core/enginewrapper.hpp
Normal file
@@ -0,0 +1,303 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// enginewrapper.hpp
|
||||
//
|
||||
// This header file contains driver's interface to class DrivenEngine.
|
||||
// This is meant to be used across a DLL boundary. Since the DLL may have
|
||||
// been compiled by a different compiler than the driver, we use only simple
|
||||
// POD types and we only use C calling conventions.
|
||||
//
|
||||
// When calling a wrapper function, you must always pass in the wrapper as
|
||||
// the first parameter.
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef ENGINEWRAPPER_H
|
||||
#define ENGINEWRAPPER_H
|
||||
|
||||
#define DRV_MAX_CHAN 256
|
||||
#define DRV_MAX_LISTEN_PORTS 256
|
||||
#define DRV_ERRMSG_SIZE 8192
|
||||
#define DRV_SHORTSTRING_SIZE 65536
|
||||
|
||||
enum class InvocationKind {
|
||||
INVALID,
|
||||
LUA_INVOKE,
|
||||
LUA_PROBE,
|
||||
LUA_EXPR,
|
||||
LUA_SOURCE,
|
||||
FLUSH_PRINTS,
|
||||
TICK,
|
||||
};
|
||||
|
||||
class DrivenEngine;
|
||||
class PlayLogfile;
|
||||
class ReplayLogfile;
|
||||
|
||||
struct EngineWrapper {
|
||||
char error[DRV_ERRMSG_SIZE];
|
||||
char databuffer[DRV_SHORTSTRING_SIZE];
|
||||
DrivenEngine *engine;
|
||||
PlayLogfile *wlog;
|
||||
ReplayLogfile *rlog;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// CONSTRUCTION
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Of course, there's no constructor, since this is a C struct.
|
||||
// To initialize it, you use 'dlsym' or 'GetProcAddress' to get the
|
||||
// address of the function 'init_engine_wrapper'. Then, you call
|
||||
// the function init_engine_wrapper(&wrapper).
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// GETTERS
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Get a list of all the listening ports. The driver is expected
|
||||
// to fetch this set shortly after the event_init callback is invoked.
|
||||
//
|
||||
void (*get_listen_ports)(EngineWrapper *w, uint32_t *nports, const uint32_t **ports);
|
||||
|
||||
// Get a list of all recently-opened channels that were created using
|
||||
// new_outgoing_channel. The driver should initiate outgoing
|
||||
// connections for these channels.
|
||||
//
|
||||
void (*get_new_outgoing)(EngineWrapper *w, uint32_t *nchanids, const uint32_t **chanids);
|
||||
|
||||
// Get a string_view of the target of a channel. A target is a string like
|
||||
// "cert:whatever.com:80" or "nocert:whatever.com:80".
|
||||
// The first word indicate whether or not a valid SSL certificate
|
||||
// is required. The second word is the hostname. The third word is
|
||||
// the port number. The char string returned here is valid until
|
||||
// the channel is closed.
|
||||
//
|
||||
const char *(*get_target)(EngineWrapper *w, uint32_t chid);
|
||||
|
||||
// Return true if the user has released all references to this channel.
|
||||
// In this case, the driver should initiate shutdown of the channel,
|
||||
// and the driver should eventually call notify_close.
|
||||
//
|
||||
bool (*get_channel_released)(EngineWrapper *w, uint32_t chid);
|
||||
|
||||
// Get a pointer to the bytes in the outgoing buffer. The char pointer
|
||||
// returned here is naturally only valid until the buffer is changed.
|
||||
// This function is used for all channels, including sockets and stdio.
|
||||
//
|
||||
void (*get_outgoing)(EngineWrapper *w, uint32_t chid, uint32_t *len, const char **data);
|
||||
|
||||
// Return true if the outgoing buffer is empty.
|
||||
//
|
||||
bool (*get_outgoing_empty)(EngineWrapper *w, uint32_t chid);
|
||||
|
||||
// Get the console prompt.
|
||||
//
|
||||
void (*get_console_prompt)(EngineWrapper *w, uint32_t *len, const char **data);
|
||||
|
||||
// Get the clock.
|
||||
//
|
||||
// Get the current time. This is equal to the last value passed
|
||||
// in by invoke_event_update.
|
||||
//
|
||||
double (*get_clock)(EngineWrapper *w);
|
||||
|
||||
// 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_pack.
|
||||
//
|
||||
bool (*get_rescan_lua_source)(EngineWrapper *w);
|
||||
|
||||
// If true, the engine is done. Stop the driver.
|
||||
//
|
||||
bool (*get_stop_driver)(EngineWrapper *w);
|
||||
|
||||
// Get the actor ID. May return zero if the server is down.
|
||||
//
|
||||
uint64_t (*get_actor_id)(EngineWrapper *w);
|
||||
|
||||
// Do a scan to find tangibles near the specified player.
|
||||
//
|
||||
// Returns a count and a pointer to an array of tangible IDs. The returned
|
||||
// pointer is valid until the next call to get_tangibles_near.
|
||||
//
|
||||
void (*get_tangibles_near)(EngineWrapper *w, uint64_t tanid, double rx, double ry, double rz, uint32_t *count, int64_t **ids);
|
||||
|
||||
// Get the animation queues for the specified tangibles.
|
||||
//
|
||||
// You must supply an array of tangible IDs. For each tangible, returns the
|
||||
// animation queue as a serialized string. The serialized format is
|
||||
// documented in header file animqueue.hpp.
|
||||
//
|
||||
// You must also supply a buffer for the string lengths, and a buffer for
|
||||
// the string pointers. Both buffers must have space for number of
|
||||
// tangibles specified in the call.
|
||||
//
|
||||
// The returned character pointers remain valid until the next call to
|
||||
// get_animation_queues.
|
||||
//
|
||||
void (*get_animation_queues)(EngineWrapper *w, uint32_t count, const int64_t *ids, uint32_t *lengths, const char **strings);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// MUTATORS USED ONLY IN PLAY MODE
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Create the driven engine. argc and argv allow you to specify what
|
||||
// kind of engine you want. You must pass in the initial state of the lua
|
||||
// source, if you have any. You may optionally also specify a replay log.
|
||||
// If you don't want to create a replay log, pass a null pointer.
|
||||
//
|
||||
// Check to see if the error buffer contains a message after calling
|
||||
// this function.
|
||||
//
|
||||
void (*play_initialize)(EngineWrapper *w, uint32_t argc, char **argv, uint32_t srcpklen, const char *srcpk, const char *logfn);
|
||||
|
||||
// Clear the list of recently-opened channels. You are meant to fetch
|
||||
// new outgoing channels using get_new_outgoing, then you call
|
||||
// clear_new_outgoing after you've opened those channels.
|
||||
//
|
||||
void (*play_clear_new_outgoing)(EngineWrapper *w);
|
||||
|
||||
// Notifies the channel that some bytes were transmitted. This causes those
|
||||
// bytes to be removed from the outgoing buffer. This function is used for
|
||||
// all channels, including sockets and stdio.
|
||||
//
|
||||
void (*play_sent_outgoing)(EngineWrapper *w, uint32_t chid, uint32_t nbytes);
|
||||
|
||||
// Notifies the channel that some bytes were received. This causes those
|
||||
// bytes to be appended to the incoming buffer. This function is used for
|
||||
// all channels, including sockets and stdio.
|
||||
//
|
||||
void (*play_recv_incoming)(EngineWrapper *w, uint32_t chid, uint32_t len, const char *data);
|
||||
|
||||
// Notify the channel that the connection was closed. This includes all
|
||||
// sorts of closes, including friendly termination, all the way to network
|
||||
// failure. Closing the channel doesn't delete it. The engine is
|
||||
// responsible for noticing that the channel closed and the engine must
|
||||
// delete it. Closing a channel prevents it from showing up in
|
||||
// 'list_channels'.
|
||||
//
|
||||
void (*play_notify_close)(EngineWrapper *w, uint32_t chid, uint32_t len, const char *data);
|
||||
|
||||
// Notify the DrivenEngine that somebody connected to an incoming port.
|
||||
// This will cause the DrivenEngine to allocate a new channel and put the
|
||||
// new channel into the incoming channels queue. Returns the new channel
|
||||
// ID. The new incoming channel appears in the 'list_channels' list,
|
||||
// even before the engine pops the channel from the incoming channels queue.
|
||||
//
|
||||
uint32_t (*play_notify_accept)(EngineWrapper *w, uint32_t port);
|
||||
|
||||
// Invoke the update event.
|
||||
//
|
||||
// The clock value must absolutely be monotonically increasing,
|
||||
// and it should roughly be equal to the number of seconds since
|
||||
// the program started.
|
||||
//
|
||||
void (*play_update)(EngineWrapper *w, double clock);
|
||||
|
||||
// Send an invoke.
|
||||
//
|
||||
// This is the main pathway for blueprints, or the unreal C++ code,
|
||||
// to change the state of the world.
|
||||
//
|
||||
void (*play_call_function)(EngineWrapper *w, InvocationKind kind, int64_t place, uint32_t datapklen, const char *datapk, uint32_t *retpklen, const char **retpk);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// MUTATORS USED ONLY IN REPLAY MODE
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Begin a replay.
|
||||
//
|
||||
// Opens the logfile and prepares to replay the log.
|
||||
// If an error occurs, the error buffer contains a message,
|
||||
// and the done flag is set to true.
|
||||
//
|
||||
void (*replay_initialize)(EngineWrapper *w, const char *logfn);
|
||||
|
||||
// Execute a single step from the replay log.
|
||||
//
|
||||
// Calling this when 'done' is true is a no-op.
|
||||
//
|
||||
void (*replay_step)(EngineWrapper *w);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// CALLBACKS USED ONLY IN REPLAY MODE
|
||||
//
|
||||
// The driver can store function pointers here. If it does so, these
|
||||
// functions will get called during replay_step operations.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void (*replay_cb_sent_outgoing)(void *vp, int chid, int nbytes, const char *data);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// VOID POINTER USED IN REPLAY CALLBACKS
|
||||
//
|
||||
// The driver can store a void pointer here. This void pointer will get passed
|
||||
// to all the replay callbacks.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void *replay_cb_vp;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// FUNCTIONS THAT CAN BE USED AT ANY TIME
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Hook dprintf
|
||||
//
|
||||
// The engine provides a function 'util::dprintf' to print debugging
|
||||
// messages. It does not use stderr or std::cerr, because in windows, those
|
||||
// go to the bit-bucket.
|
||||
//
|
||||
// This routine hooks dprintf to change where the output goes. Ideally,
|
||||
// the intent is to send the output somewhere easily accessible, like the
|
||||
// visual studio debug output log, or the unreal editor output log, or
|
||||
// something like that. Or, better yet, to all those places at once.
|
||||
//
|
||||
// The hook function will get passed one line of output at a time. The line
|
||||
// will only contain printable characters. The line will not contain a
|
||||
// newline - the newline is implied.
|
||||
//
|
||||
void (*hook_dprint)(void (*func)(const char *oneline, size_t size));
|
||||
|
||||
// Release: delete the engine object and close log files.
|
||||
//
|
||||
// Note that the wrapper must have already been initialized using
|
||||
// init_engine_wrapper. Otherwise, the 'release' function pointer would not
|
||||
// be initialized. If writing a logfile, this stores a 'clean exit' marker
|
||||
// in the logfile, indicating that the engine exited cleanly, as opposed to
|
||||
// crashing.
|
||||
//
|
||||
// If the wrapper is already in its clear state, this is a no-op.
|
||||
//
|
||||
void (*release)(EngineWrapper *w);
|
||||
};
|
||||
|
||||
#endif // ENGINEWRAPPER_HPP
|
||||
2979
luprex/cpp/core/fast-float.hpp
Normal file
2979
luprex/cpp/core/fast-float.hpp
Normal file
File diff suppressed because it is too large
Load Diff
1496
luprex/cpp/core/flat-hash-map.hpp
Normal file
1496
luprex/cpp/core/flat-hash-map.hpp
Normal file
File diff suppressed because it is too large
Load Diff
3
luprex/cpp/core/globaldb.cpp
Normal file
3
luprex/cpp/core/globaldb.cpp
Normal file
@@ -0,0 +1,3 @@
|
||||
#include "luastack.hpp"
|
||||
#include "globaldb.hpp"
|
||||
|
||||
14
luprex/cpp/core/globaldb.hpp
Normal file
14
luprex/cpp/core/globaldb.hpp
Normal file
@@ -0,0 +1,14 @@
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// GLOBALDB
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef GLOBALDB_HPP
|
||||
#define GLOBALDB_HPP
|
||||
|
||||
|
||||
|
||||
#endif // GLOBALDB_HPP
|
||||
|
||||
|
||||
1955
luprex/cpp/core/http.cpp
Normal file
1955
luprex/cpp/core/http.cpp
Normal file
File diff suppressed because it is too large
Load Diff
457
luprex/cpp/core/http.hpp
Normal file
457
luprex/cpp/core/http.hpp
Normal file
@@ -0,0 +1,457 @@
|
||||
/////////////////////////////////////////////////////////
|
||||
//
|
||||
// HTTP Implementation.
|
||||
//
|
||||
// This is a fairly limited implementation of HTTP.
|
||||
//
|
||||
// It only supports HTTP 1.1
|
||||
//
|
||||
// It only supports GET, POST, and HEAD.
|
||||
//
|
||||
/////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef HTTP_HPP
|
||||
#define HTTP_HPP
|
||||
|
||||
#include "eng-malloc.hpp"
|
||||
#include "wrap-string.hpp"
|
||||
#include "wrap-vector.hpp"
|
||||
#include "wrap-map.hpp"
|
||||
#include "luastack.hpp"
|
||||
#include "streambuffer.hpp"
|
||||
#include "drivenengine.hpp"
|
||||
#include <ostream>
|
||||
|
||||
using UrlParameters = eng::map<eng::string, eng::string>;
|
||||
|
||||
class HttpClientRequest : public eng::nevernew {
|
||||
private:
|
||||
// Request IDs.
|
||||
int64_t request_id_;
|
||||
int64_t place_id_;
|
||||
int64_t thread_id_;
|
||||
|
||||
// When we detect that we generated an invalid
|
||||
// outgoing request, the error is stored here.
|
||||
eng::string check_fail_;
|
||||
|
||||
// If true, verify the server's certificate.
|
||||
// True is the default.
|
||||
bool verify_certificate_;
|
||||
|
||||
// Method: GET, HEAD, or POST. Must be all-caps.
|
||||
eng::string method_;
|
||||
|
||||
// The hostname. Not yet url-encoded.
|
||||
eng::string host_;
|
||||
|
||||
// Port number.
|
||||
int port_;
|
||||
|
||||
// The path. Not yet url-encoded. Can not include URL parameters.
|
||||
eng::string path_;
|
||||
|
||||
// URL parameters to append to the path. Not yet url-encoded.
|
||||
UrlParameters params_;
|
||||
|
||||
// The mime type of the content, only for POST requests.
|
||||
eng::string mime_type_;
|
||||
|
||||
// The content as a string, only for POST requests.
|
||||
eng::string content_;
|
||||
bool content_assigned_;
|
||||
|
||||
private:
|
||||
void check_fail(std::string_view error);
|
||||
void send_internal(StreamBuffer *target, bool debug_string) const;
|
||||
|
||||
public:
|
||||
// Construct an empty HTTP request.
|
||||
// All of the fields have empty values.
|
||||
HttpClientRequest();
|
||||
|
||||
// Populate an request-related fields one piece at a time.
|
||||
// If you pass an invalid value, or if the field is
|
||||
// already set, the routine will generate an error message
|
||||
// and store it in the error field. In that case, the set
|
||||
// will not happen. If there's already an error in the error
|
||||
// field, it will not be overwritten.
|
||||
//
|
||||
void set_verify_certificate(bool flag);
|
||||
void set_method(const eng::string &method);
|
||||
void set_host(const eng::string &host);
|
||||
void set_port(int port);
|
||||
void set_path(std::string_view path);
|
||||
void set_param(const eng::string &key, const eng::string &value);
|
||||
void set_url(std::string_view url);
|
||||
void set_mime_type(const eng::string &mime_type);
|
||||
void set_content(const eng::string &content);
|
||||
|
||||
void set_verify_certificate(LuaCoreStack &LS, LuaSlot val);
|
||||
void set_method(LuaCoreStack &LS, LuaSlot val);
|
||||
void set_host(LuaCoreStack &LS, LuaSlot val);
|
||||
void set_port(LuaCoreStack &LS, LuaSlot val);
|
||||
void set_path(LuaCoreStack &LS, LuaSlot path);
|
||||
void set_param(LuaCoreStack &LS, LuaSlot key, LuaSlot val);
|
||||
void set_params(LuaCoreStack &LS, LuaSlot tab);
|
||||
void set_url(LuaCoreStack &LS, LuaSlot val);
|
||||
void set_mime_type(LuaCoreStack &LS, LuaSlot val);
|
||||
void set_content(LuaCoreStack &LS, LuaSlot val);
|
||||
void set_jsonvalue(LuaCoreStack &LS, LuaSlot val);
|
||||
|
||||
// Set default values for method and port.
|
||||
// This must be done after setting regular values.
|
||||
void set_defaults();
|
||||
|
||||
// Populate request-related fields from a Lua table.
|
||||
// Doesn't throw errors, instead, returns errors via check().
|
||||
void configure(LuaKeywordParser &kp);
|
||||
|
||||
// Get or Set the request IDs.
|
||||
//
|
||||
// This class does not use these fields, it just stores
|
||||
// them as a convenience.
|
||||
//
|
||||
int64_t request_id() const { return request_id_; }
|
||||
int64_t place_id() const { return place_id_; }
|
||||
int64_t thread_id() const { return thread_id_; }
|
||||
void set_request_id(int64_t request_id) { request_id_ = request_id; }
|
||||
void set_place_id(int64_t place_id) { place_id_ = place_id; }
|
||||
void set_thread_id(int64_t thread_id) { thread_id_ = thread_id; }
|
||||
|
||||
// Get the network target, eg, "cert:host:port"
|
||||
//
|
||||
eng::string target() const;
|
||||
|
||||
// Verify that the request is error free and that
|
||||
// defaults have been set.
|
||||
//
|
||||
eng::string check() const;
|
||||
|
||||
// Get the method.
|
||||
//
|
||||
eng::string method() const { return method_; }
|
||||
|
||||
// Put the request into the stream, assuming HTTP/1.1
|
||||
//
|
||||
void send(StreamBuffer *target) const { send_internal(target, false); }
|
||||
|
||||
// Serialize and deserialize.
|
||||
//
|
||||
void serialize(StreamBuffer *sb) const;
|
||||
void deserialize(StreamBuffer *sb);
|
||||
|
||||
// Get the request as a debug string.
|
||||
//
|
||||
eng::string debug_string();
|
||||
};
|
||||
|
||||
class HttpServerResponse {
|
||||
private:
|
||||
// When we detect that our own response is
|
||||
// invalid, the error is stored here.
|
||||
//
|
||||
eng::string check_fail_;
|
||||
|
||||
// The status code to send back to the client
|
||||
// as part of the response status line.
|
||||
//
|
||||
int status_;
|
||||
|
||||
// Maximum age for the cache-control directive.
|
||||
//
|
||||
int max_age_;
|
||||
|
||||
// Mime type of the content.
|
||||
//
|
||||
eng::string mime_type_;
|
||||
|
||||
// Content to send to the client.
|
||||
//
|
||||
eng::string content_;
|
||||
bool content_assigned_;
|
||||
|
||||
// If the keep-alive flag is set, then a connection: keep-alive
|
||||
// header is sent. Otherwise, a connection: close is sent.
|
||||
// The keep-alive flag cannot be controlled from Lua. It is
|
||||
// designed to be handled automatically by the server code.
|
||||
//
|
||||
bool keep_alive_;
|
||||
|
||||
// The protocol is taken from the request.
|
||||
//
|
||||
bool http11_;
|
||||
|
||||
private:
|
||||
void check_fail(std::string_view error);
|
||||
void send_internal(StreamBuffer *target, bool debug_string) const;
|
||||
|
||||
public:
|
||||
// Construct an empty response.
|
||||
// All of the fields have empty values.
|
||||
//
|
||||
HttpServerResponse();
|
||||
|
||||
void set_method(const eng::string &method);
|
||||
void set_status(int status);
|
||||
void set_max_age(int max_age);
|
||||
void set_mime_type(const eng::string &mime_type);
|
||||
void set_content(const eng::string &content);
|
||||
|
||||
void set_status(LuaCoreStack &LS, LuaSlot val);
|
||||
void set_max_age(LuaCoreStack &LS, LuaSlot val);
|
||||
void set_mime_type(LuaCoreStack &LS, LuaSlot val);
|
||||
void set_content(LuaCoreStack &LS, LuaSlot val);
|
||||
void set_jsonvalue(LuaCoreStack &LS, LuaSlot val);
|
||||
|
||||
// Set default values.
|
||||
//
|
||||
void set_defaults();
|
||||
|
||||
// Populate request-related fields from a Lua table.
|
||||
// Doesn't throw errors, instead, returns them via check()
|
||||
//
|
||||
void configure(LuaKeywordParser &kp);
|
||||
|
||||
// Set the keep_alive flag.
|
||||
//
|
||||
void set_keep_alive(bool k) { keep_alive_ = k; };
|
||||
|
||||
// Set the protocol.
|
||||
//
|
||||
void set_http11(bool k) { http11_= k; }
|
||||
|
||||
// Store a fail response.
|
||||
//
|
||||
void fail(int status, const eng::string &message);
|
||||
|
||||
// Verify that the response is error free.
|
||||
//
|
||||
eng::string check() const;
|
||||
|
||||
// Get the status of the response.
|
||||
//
|
||||
int status() const { return status_; }
|
||||
bool errstatus() const { return (status_ >= 400)&&(status_ <= 599); }
|
||||
|
||||
// Put the response into the stream, assuming HTTP/1.1
|
||||
//
|
||||
void send(StreamBuffer *target) const { send_internal(target, false); }
|
||||
|
||||
// Get the response as a debug string.
|
||||
//
|
||||
eng::string debug_string();
|
||||
};
|
||||
|
||||
// HttpParser is used for parsing both requests and responses.
|
||||
//
|
||||
class HttpParser : public eng::nevernew {
|
||||
protected:
|
||||
// The request ID is not used by the parser, but
|
||||
// you can store a request ID in there for convenience.
|
||||
int64_t request_id_;
|
||||
|
||||
// True if this parser parsed a request. If false,
|
||||
// then it parsed a response.
|
||||
bool is_request_;
|
||||
|
||||
// The status code, a 3-digit number.
|
||||
int status_;
|
||||
|
||||
// If the communication contains an error, the error can
|
||||
// be stored here. In the event of a successful communication,
|
||||
// this should always be empty string.
|
||||
eng::string error_;
|
||||
|
||||
// Only if content-length header present, otherwise, -1.
|
||||
int64_t content_length_;
|
||||
|
||||
// If empty, it means there was no transfer-encoding header.
|
||||
eng::string transfer_encoding_;
|
||||
|
||||
// If empty, it means there was no content-encoding header.
|
||||
eng::string content_encoding_;
|
||||
|
||||
// Only if location header present.
|
||||
eng::string location_;
|
||||
|
||||
// MIME type of the content.
|
||||
eng::string mime_type_;
|
||||
|
||||
// Charset of the content before the content was translated to utf-8.
|
||||
eng::string charset_;
|
||||
|
||||
// The protocol: true for HTTP/1.1, false for HTTP/1.0
|
||||
bool http11_;
|
||||
|
||||
// The method. In a response, this is assigned before parsing.
|
||||
eng::string method_;
|
||||
|
||||
// The URL path, not url-encoded. (only when parsing requests)
|
||||
eng::string path_;
|
||||
|
||||
// The URL parameters, not url-encoded (only when parsing requests)
|
||||
UrlParameters params_;
|
||||
|
||||
// The content as string. If it's text, it's been translated to utf-8.
|
||||
eng::string content_;
|
||||
|
||||
// The length in bytes of the entire communication.
|
||||
// If the response is complete, but the comm_length_
|
||||
// is zero, it means we couldn't find the end of
|
||||
// the request.
|
||||
int comm_length_;
|
||||
|
||||
protected:
|
||||
// Store a message indicating that we haven't received enough
|
||||
// bytes yet. If the connection is closed and we still haven't
|
||||
// received enough bytes, that's a fatal error.
|
||||
//
|
||||
void incomplete(bool closed);
|
||||
|
||||
// Store a message indicating that we couldn't parse because
|
||||
// something was syntactically malformed, ie, not valid HTTP.
|
||||
//
|
||||
void syntax(std::string_view detail);
|
||||
|
||||
// Store a message indicating that the content was too large
|
||||
// and we refused to download it.
|
||||
//
|
||||
void oversized();
|
||||
|
||||
// Parse a response status line, such as "HTTP/1.1 200 OK"
|
||||
// returns false if we couldn't parse the whole line.
|
||||
//
|
||||
bool parse_status_line(std::string_view &view, bool closed);
|
||||
|
||||
// Parse a request line, such as "GET /index.html HTTP/1.1"
|
||||
// returns false if we couldn't parse the whole line.
|
||||
//
|
||||
bool parse_request_line(std::string_view &view, bool closed);
|
||||
|
||||
// Parse specific headers.
|
||||
//
|
||||
void parse_content_encoding(std::string_view value);
|
||||
void parse_content_length(std::string_view value);
|
||||
void parse_content_type(std::string_view value);
|
||||
void parse_location(std::string_view value);
|
||||
void parse_transfer_encoding(std::string_view value);
|
||||
|
||||
// Parse a single response header. Most headers are ignored.
|
||||
// If the header contains an error, the error is stored.
|
||||
//
|
||||
void parse_header(std::string_view header, std::string_view value);
|
||||
|
||||
// Parse all of the headers, including the blank line after
|
||||
// the headers. Return true if the view is at the end of the headers.
|
||||
//
|
||||
bool parse_headers(std::string_view &view, bool closed);
|
||||
|
||||
// parse the content, for different transfer encodings.
|
||||
// Returns true if the view is at the end of the content.
|
||||
//
|
||||
bool parse_content_basic(std::string_view &view, bool closed);
|
||||
bool parse_content_chunked(std::string_view &view, bool closed);
|
||||
|
||||
// Parse the content. Return true if the view is at the end of the
|
||||
// content.
|
||||
//
|
||||
// - Uses the appropriate transfer-encoding specified in the headers.
|
||||
// - Decompresses the content using the appropriate content-encoding.
|
||||
// - Guesses a mimetype and charset if one was not specified in headers.
|
||||
// - If it's text, converts the charset to utf-8.
|
||||
//
|
||||
bool parse_content(std::string_view &view, bool closed);
|
||||
|
||||
public:
|
||||
// Construct a blank parser.
|
||||
//
|
||||
HttpParser();
|
||||
|
||||
// Get the parsed status or error message.
|
||||
//
|
||||
int status() const { return status_; }
|
||||
bool errstatus() const { return (status_ >= 400) && (status_ <= 599); }
|
||||
eng::string error() const { return error_; }
|
||||
|
||||
// Return true if the communication was complete.
|
||||
//
|
||||
bool complete() const { return status_ != 0; }
|
||||
|
||||
// Get the first component of the path.
|
||||
// If the path is empty, return defval.
|
||||
eng::string first_path_component(std::string_view defval) const;
|
||||
|
||||
// Get the parsed protocol.
|
||||
//
|
||||
bool http11() const { return http11_; }
|
||||
|
||||
// Store the parsed fields into a lua table.
|
||||
//
|
||||
void store(LuaCoreStack &LS, LuaSlot tab) const;
|
||||
|
||||
// The parser will not try to parse content longer than this.
|
||||
//
|
||||
static constexpr int64_t MAX_CONTENT_LENGTH = 1000000;
|
||||
|
||||
// Parse a request or a response.
|
||||
//
|
||||
// You can only parse once. If you need to parse again,
|
||||
// construct a new parser.
|
||||
//
|
||||
void parse_request(std::string_view view, bool closed);
|
||||
void parse_response(std::string_view view, bool closed, std::string_view method);
|
||||
|
||||
// Store a status code and an error message, and clear the content.
|
||||
// This is generally used when the client detects an error,
|
||||
// such as a DNS lookup fail, a connection failed, an SSL negotiation
|
||||
// failed, or the like.
|
||||
//
|
||||
void fail(int status, std::string_view error);
|
||||
|
||||
// Get or set the request ID.
|
||||
//
|
||||
int64_t request_id() const { return request_id_; }
|
||||
void set_request_id(int64_t v) { request_id_ = v; }
|
||||
|
||||
// Return a debug string.
|
||||
//
|
||||
eng::string debug_string() const;
|
||||
|
||||
// Synthesize an error and store it in lua.
|
||||
//
|
||||
static void store_fail(LuaCoreStack &LS, LuaSlot tab, int status, std::string_view error);
|
||||
|
||||
// Convert a path component into a lua identifier.
|
||||
//
|
||||
// Returns the empty string if the path component cannot be converted
|
||||
// to a lua identifier.
|
||||
//
|
||||
static eng::string to_lua_identifier(std::string_view pathcomp);
|
||||
};
|
||||
|
||||
class HttpClientRequestMap : public eng::map<int64_t, HttpClientRequest> {
|
||||
public:
|
||||
void serialize(StreamBuffer *sb) const;
|
||||
void deserialize(StreamBuffer *sb);
|
||||
};
|
||||
|
||||
using HttpParserVec = eng::vector<HttpParser>;
|
||||
|
||||
// This class is used by LpxServer to store the
|
||||
// incoming and outgoing http channels.
|
||||
//
|
||||
class HttpChannel {
|
||||
public:
|
||||
SharedChannel channel_;
|
||||
eng::string method_;
|
||||
int64_t parsed_bytes_;
|
||||
|
||||
bool marked_for_deletion() const { return channel_ == nullptr; }
|
||||
HttpChannel() : parsed_bytes_(0) {}
|
||||
};
|
||||
|
||||
using HttpChannelMap = eng::map<int64_t, HttpChannel>;
|
||||
using HttpChannelVec = eng::vector<HttpChannel>;
|
||||
|
||||
#endif // HTTP_HPP
|
||||
433
luprex/cpp/core/idalloc.cpp
Normal file
433
luprex/cpp/core/idalloc.cpp
Normal file
@@ -0,0 +1,433 @@
|
||||
#include "wrap-map.hpp"
|
||||
#include "wrap-sstream.hpp"
|
||||
#include "wrap-deque.hpp"
|
||||
|
||||
#include "idalloc.hpp"
|
||||
|
||||
#include <ostream>
|
||||
|
||||
|
||||
static bool ranges_equal(const eng::deque<int64_t> &dq, int64_t a, int64_t b, int64_t c) {
|
||||
if (dq.size() != 3) return false;
|
||||
if (dq[0] != a) return false;
|
||||
if (dq[1] != b) return false;
|
||||
if (dq[2] != c) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
IdGlobalPool::IdGlobalPool() {
|
||||
salvaged_.clear();
|
||||
next_batch_ = 0;
|
||||
next_id_ = 0;
|
||||
next_seqno_ = 0;
|
||||
}
|
||||
|
||||
IdGlobalPool::~IdGlobalPool() {
|
||||
}
|
||||
|
||||
void IdGlobalPool::init_master() {
|
||||
salvaged_.clear();
|
||||
next_batch_ = 0x0001000000000000;
|
||||
next_id_ = 0x0010000000000000;
|
||||
}
|
||||
|
||||
void IdGlobalPool::init_synch() {
|
||||
salvaged_.clear();
|
||||
next_batch_ = 0;
|
||||
next_id_ = 0x001E000000000000;
|
||||
}
|
||||
|
||||
int64_t IdGlobalPool::get_one() {
|
||||
return next_id_++;
|
||||
}
|
||||
|
||||
int64_t IdGlobalPool::get_batch() {
|
||||
int64_t batch;
|
||||
if (salvaged_.empty()) {
|
||||
if (next_batch_ == 0) {
|
||||
batch = 0;
|
||||
} else {
|
||||
batch = next_batch_;
|
||||
next_batch_ += 256;
|
||||
}
|
||||
} else {
|
||||
batch = salvaged_.back();
|
||||
salvaged_.pop_back();
|
||||
}
|
||||
return batch;
|
||||
}
|
||||
|
||||
void IdGlobalPool::salvage(int64_t batch) {
|
||||
if (batch == 0) return;
|
||||
if (next_batch_ == 0) return;
|
||||
if ((batch & 0xFF) >= 128) return;
|
||||
salvaged_.push_back(batch);
|
||||
}
|
||||
|
||||
void IdGlobalPool::serialize(StreamBuffer *sb) const {
|
||||
sb->write_int64(next_batch_);
|
||||
sb->write_int64(next_id_);
|
||||
sb->write_uint64(next_seqno_);
|
||||
sb->write_uint32(salvaged_.size());
|
||||
for (int64_t batch : salvaged_) {
|
||||
sb->write_int64(batch);
|
||||
}
|
||||
}
|
||||
|
||||
void IdGlobalPool::deserialize(StreamBuffer *sb) {
|
||||
next_batch_ = sb->read_int64();
|
||||
next_id_ = sb->read_int64();
|
||||
next_seqno_ = sb->read_uint64();
|
||||
uint32_t salvaged_size = sb->read_uint32();
|
||||
salvaged_.resize(salvaged_size);
|
||||
for (int i=0; i < int(salvaged_size); i++) {
|
||||
salvaged_[i] = sb->read_int64();
|
||||
}
|
||||
}
|
||||
|
||||
eng::string IdGlobalPool::debug_string() const {
|
||||
eng::ostringstream oss;
|
||||
oss << "next_batch:" << util::hex64.val(next_batch_) << " ";
|
||||
oss << "next_id:" << util::hex64.val(next_id_) << " ";
|
||||
oss << "next_seqno: " << util::hex64.val(next_seqno_) << " ";
|
||||
oss << "salvaged:";
|
||||
for (const int64_t val : salvaged_) {
|
||||
oss << " " << util::hex64.val(val);
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
IdPlayerPool::IdPlayerPool(IdGlobalPool *g) {
|
||||
global_ = g;
|
||||
fifo_capacity_ = 0;
|
||||
next_seqno_ = 0;
|
||||
}
|
||||
|
||||
IdPlayerPool::~IdPlayerPool() {
|
||||
}
|
||||
|
||||
void IdPlayerPool::set_fifo_capacity(int n) {
|
||||
assert((n >= 0) && (n <= 250));
|
||||
fifo_capacity_ = n;
|
||||
while (int(ranges_.size()) > n) {
|
||||
global_->salvage(ranges_.back());
|
||||
ranges_.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
void IdPlayerPool::refill() {
|
||||
while (int(ranges_.size()) < fifo_capacity_) {
|
||||
int64_t batch = global_->get_batch();
|
||||
if (batch == 0) break;
|
||||
ranges_.push_back(batch);
|
||||
}
|
||||
}
|
||||
|
||||
void IdPlayerPool::test_push_back(int64_t range) {
|
||||
ranges_.push_back(range);
|
||||
}
|
||||
|
||||
void IdPlayerPool::test_pop_front() {
|
||||
ranges_.pop_front();
|
||||
}
|
||||
|
||||
void IdPlayerPool::test_clear_ranges() {
|
||||
ranges_.clear();
|
||||
}
|
||||
|
||||
int64_t IdPlayerPool::get_one() {
|
||||
refill();
|
||||
if (ranges_.empty()) {
|
||||
return global_->get_one();
|
||||
} else {
|
||||
int64_t id = ranges_.front();
|
||||
if ((id & 0xFF) == 0xFF) {
|
||||
ranges_.pop_front();
|
||||
refill();
|
||||
} else {
|
||||
ranges_.front() = id + 1;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
void IdPlayerPool::serialize(StreamBuffer *sb) const {
|
||||
sb->write_uint8(fifo_capacity_);
|
||||
sb->write_uint8(ranges_.size());
|
||||
sb->write_uint64(next_seqno_);
|
||||
for (int64_t batch : ranges_) {
|
||||
sb->write_int64(batch);
|
||||
}
|
||||
}
|
||||
|
||||
void IdPlayerPool::deserialize(StreamBuffer *sb) {
|
||||
fifo_capacity_ = sb->read_uint8();
|
||||
int ranges_size = sb->read_uint8();
|
||||
next_seqno_ = sb->read_uint64();
|
||||
ranges_.resize(ranges_size);
|
||||
for (int i=0; i < ranges_size; i++) {
|
||||
ranges_[i] = sb->read_int64();
|
||||
}
|
||||
}
|
||||
|
||||
bool IdPlayerPool::exactly_equal(const IdPlayerPool &other) const {
|
||||
if (fifo_capacity_ != other.fifo_capacity_) return false;
|
||||
if (ranges_.size() != other.ranges_.size()) return false;
|
||||
if (next_seqno_ != other.next_seqno_) return false;
|
||||
for (int i = 0; i < int(ranges_.size()); i++) {
|
||||
if (ranges_[i] != other.ranges_[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IdPlayerPool::valid() const {
|
||||
if ((fifo_capacity_ < 0) || (fifo_capacity_ > 250)) return false;
|
||||
if (int(ranges_.size()) > fifo_capacity_) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void IdPlayerPool::diff(const IdPlayerPool &auth, StreamBuffer *sb) const {
|
||||
assert(valid());
|
||||
assert(auth.valid());
|
||||
|
||||
// Special case: there's nothing to fix.
|
||||
if (exactly_equal(auth)) {
|
||||
sb->write_uint8(255);
|
||||
return;
|
||||
}
|
||||
|
||||
// Write the fifo capacity, nranges, and next seqno
|
||||
assert(auth.fifo_capacity_ != 255);
|
||||
sb->write_uint8(auth.fifo_capacity_);
|
||||
sb->write_uint8(auth.ranges_.size());
|
||||
sb->write_uint64(auth.next_seqno_);
|
||||
|
||||
// Build up an index of the known IDs.
|
||||
eng::map<int64_t, int> index;
|
||||
for (int i = 0; i < int(ranges_.size()); i++) {
|
||||
index[ranges_[i]] = i;
|
||||
}
|
||||
|
||||
// Write the ranges, but encode known IDs in one byte.
|
||||
for (int i = 0; i < int(auth.ranges_.size()); i++) {
|
||||
int64_t n = auth.ranges_[i];
|
||||
auto iter = index.find(n);
|
||||
if (iter == index.end()) {
|
||||
sb->write_uint8(255);
|
||||
sb->write_int64(n);
|
||||
} else {
|
||||
int slot = iter->second;
|
||||
sb->write_uint8(slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IdPlayerPool::patch(StreamBuffer *sb, DebugCollector *dbc) {
|
||||
// read the header byte
|
||||
int fifo_cap = sb->read_uint8();
|
||||
// If the fifo_cap is 255, it means there's nothing to fix.
|
||||
if (fifo_cap == 255) {
|
||||
return;
|
||||
}
|
||||
DebugLine(dbc) << "IdPlayerPool modified";
|
||||
fifo_capacity_ = fifo_cap;
|
||||
int nranges = sb->read_uint8();
|
||||
next_seqno_ = sb->read_uint64();
|
||||
eng::deque<int64_t> old = std::move(ranges_);
|
||||
ranges_.clear();
|
||||
for (int i = 0; i < nranges; i++) {
|
||||
int index = sb->read_uint8();
|
||||
if (index < 255) {
|
||||
assert(index < int(old.size()));
|
||||
ranges_.push_back(old[index]);
|
||||
} else {
|
||||
ranges_.push_back(sb->read_int64());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eng::string IdPlayerPool::debug_string() const {
|
||||
eng::ostringstream oss;
|
||||
oss << "cap:" << fifo_capacity_ << " ids:";
|
||||
for (int i = 0; i < int(ranges_.size()); i++) {
|
||||
if (i > 0) oss << ",";
|
||||
oss << util::hex64.val(ranges_[i]);
|
||||
}
|
||||
oss << " seqno:" << util::hex64.val(next_seqno_);
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
static int64_t nthbatch(int64_t n) {
|
||||
return int64_t(0x0001000000000000) + n*256;
|
||||
}
|
||||
|
||||
LuaDefine(unittests_idalloc, "", "some unit tests") {
|
||||
IdGlobalPool gp;
|
||||
IdPlayerPool pp(&gp);
|
||||
IdGlobalPool gpds;
|
||||
IdPlayerPool ppds(&gpds);
|
||||
StreamBuffer sb;
|
||||
|
||||
// Synchronous pools produce IDs starting at 0x001E000000000000
|
||||
gp.init_synch();
|
||||
LuaAssert(L, gp.get_one() == 0x001E000000000000);
|
||||
LuaAssert(L, gp.get_one() == 0x001E000000000001);
|
||||
LuaAssert(L, gp.get_one() == 0x001E000000000002);
|
||||
|
||||
// Master pools produce IDs starting at 0x0010000000000000
|
||||
gp.init_master();
|
||||
LuaAssert(L, gp.get_one() == 0x0010000000000000);
|
||||
LuaAssert(L, gp.get_one() == 0x0010000000000001);
|
||||
LuaAssert(L, gp.get_one() == 0x0010000000000002);
|
||||
|
||||
// Synchronous pools produce only null batches.
|
||||
gp.init_synch();
|
||||
LuaAssert(L, gp.get_batch() == 0);
|
||||
LuaAssert(L, gp.get_batch() == 0);
|
||||
gp.salvage(nthbatch(5));
|
||||
LuaAssert(L, gp.get_batch() == 0);
|
||||
|
||||
// Simple fetch batches with a few salvages.
|
||||
gp.init_master();
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(0));
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(1));
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(2));
|
||||
gp.salvage(nthbatch(182));
|
||||
gp.salvage(nthbatch(183));
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(183));
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(182));
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(3));
|
||||
|
||||
// Salvage of a zero-batch does nothing.
|
||||
gp.init_master();
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(0));
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(1));
|
||||
gp.salvage(0);
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(2));
|
||||
|
||||
// Salvage of a partial batch.
|
||||
gp.init_master();
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(0));
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(1));
|
||||
gp.salvage(nthbatch(142) + 10);
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(142) + 10);
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(2));
|
||||
|
||||
// Salvage of a half-empty batch does nothing.
|
||||
gp.init_master();
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(0));
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(1));
|
||||
gp.salvage(nthbatch(142) + 145);
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(2));
|
||||
|
||||
// In the synchronous model, refill should do nothing.
|
||||
// The player pool should shunt through to the global.
|
||||
pp.test_clear_ranges();
|
||||
gp.init_synch();
|
||||
pp.set_fifo_capacity(3);
|
||||
pp.refill();
|
||||
LuaAssert(L, pp.size() == 0);
|
||||
LuaAssert(L, pp.get_one() == 0x001E000000000000);
|
||||
LuaAssert(L, pp.size() == 0);
|
||||
|
||||
// In the master model, with fifo disabled. Fifo should remain
|
||||
// empty, and the player pool should shunt through to the global.
|
||||
gp.init_master();
|
||||
pp.test_clear_ranges();
|
||||
pp.set_fifo_capacity(0);
|
||||
pp.refill();
|
||||
LuaAssert(L, pp.size() == 0);
|
||||
LuaAssert(L, pp.get_one() == 0x0010000000000000);
|
||||
LuaAssert(L, pp.size() == 0);
|
||||
|
||||
// Test refill from master (with enabled fifo).
|
||||
gp.init_master();
|
||||
pp.test_clear_ranges();
|
||||
pp.set_fifo_capacity(3);
|
||||
pp.refill();
|
||||
LuaAssert(L, ranges_equal(pp.ranges_, nthbatch(0), nthbatch(1), nthbatch(2)));
|
||||
|
||||
// Now test that get_one() keeps the pool filled from master.
|
||||
for (int i = 0; i < 256; i++) {
|
||||
LuaAssert(L, pp.get_one() == nthbatch(0) + i);
|
||||
}
|
||||
for (int i = 0; i < 256; i++) {
|
||||
LuaAssert(L, pp.get_one() == nthbatch(1) + i);
|
||||
}
|
||||
LuaAssert(L, ranges_equal(pp.ranges_, nthbatch(2), nthbatch(3), nthbatch(4)));
|
||||
|
||||
// Test unqueueing the batches.
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(5));
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(6));
|
||||
pp.set_fifo_capacity(0);
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(2));
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(3));
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(4));
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(7));
|
||||
|
||||
// Serialize and deserialize a global pool.
|
||||
gp.init_master();
|
||||
gpds.init_master();
|
||||
LuaAssert(L, gp.get_one() == 0x0010000000000000);
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(0));
|
||||
gp.salvage(nthbatch(182));
|
||||
gp.salvage(nthbatch(183));
|
||||
gp.serialize(&sb);
|
||||
gpds.deserialize(&sb);
|
||||
LuaAssert(L, gpds.get_one() == 0x0010000000000001);
|
||||
LuaAssert(L, gpds.get_batch() == nthbatch(183));
|
||||
LuaAssert(L, gpds.get_batch() == nthbatch(182));
|
||||
LuaAssert(L, gpds.get_batch() == nthbatch(1));
|
||||
|
||||
// Serialize and deserialize a player pool.
|
||||
gp.init_master();
|
||||
gpds.init_synch();
|
||||
LuaAssert(L, gp.get_batch() == nthbatch(0));
|
||||
pp.test_clear_ranges();
|
||||
pp.set_fifo_capacity(3);
|
||||
pp.refill();
|
||||
ppds.test_clear_ranges();
|
||||
pp.serialize(&sb);
|
||||
ppds.deserialize(&sb);
|
||||
LuaAssert(L, ppds.get_fifo_capacity()==3);
|
||||
LuaAssert(L, ppds.size() == 3);
|
||||
LuaAssert(L, ranges_equal(ppds.ranges_, nthbatch(1), nthbatch(2), nthbatch(3)));
|
||||
|
||||
// Difference transmit compare two empty pools.
|
||||
gp.init_master();
|
||||
gpds.init_master();
|
||||
pp.test_clear_ranges();
|
||||
ppds.test_clear_ranges();
|
||||
pp.set_fifo_capacity(3);
|
||||
ppds.set_fifo_capacity(3);
|
||||
|
||||
// Check case: no differences.
|
||||
sb.clear();
|
||||
ppds.diff(pp, &sb);
|
||||
ppds.patch(&sb, nullptr);
|
||||
LuaAssert(L, ppds.exactly_equal(pp));
|
||||
|
||||
// Add some values to master pool
|
||||
pp.test_push_back(123);
|
||||
pp.test_push_back(456);
|
||||
|
||||
// transmit and compare.
|
||||
sb.clear();
|
||||
ppds.diff(pp, &sb);
|
||||
ppds.patch(&sb, nullptr);
|
||||
LuaAssert(L, ppds.exactly_equal(pp));
|
||||
|
||||
// Pop a value from master pool
|
||||
pp.test_pop_front();
|
||||
pp.test_push_back(789);
|
||||
|
||||
// transmit and compare.
|
||||
sb.clear();
|
||||
ppds.diff(pp, &sb);
|
||||
ppds.patch(&sb, nullptr);
|
||||
LuaAssert(L, ppds.exactly_equal(pp));
|
||||
|
||||
return 0;
|
||||
}
|
||||
221
luprex/cpp/core/idalloc.hpp
Normal file
221
luprex/cpp/core/idalloc.hpp
Normal file
@@ -0,0 +1,221 @@
|
||||
///////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// THE ID ALLOCATOR
|
||||
//
|
||||
// This ID allocator's goal is to allocate IDs in such a way that the
|
||||
// synchronous model gets the same IDs as the master model.
|
||||
//
|
||||
// DESIGN PRINCIPLES
|
||||
//
|
||||
// There are two classes defined here: IdGlobalPool, and IdPlayerPool.
|
||||
//
|
||||
// Every logged-in player maintains an IdPlayerPool. That's basically a fifo
|
||||
// queue of ID batches. An ID batch is a contiguous range of IDs, containing
|
||||
// between 128 and 256 contiguous IDs. The IdPlayerPool is difference
|
||||
// transmitted, to ensure that the synchronous model has the same batches as the
|
||||
// master model.
|
||||
//
|
||||
// When a player creates a thread, that thread gets an ID batch from the
|
||||
// IdPlayerPool. To make this possible, the Lua runtime has been modified so
|
||||
// that a thread can contain an ID batch. When that thread allocates IDs, it
|
||||
// uses the batch it was allocated. In the unlikely event that a thread's batch
|
||||
// is used up, the thread falls back to using the IdGlobalPool. Such fallback
|
||||
// IDs are not likely to be predicted correctly.
|
||||
//
|
||||
// When a player creates a thread, he uses up one batch from his IdPlayerPool.
|
||||
// In the master model, the player pool is 'refilled' by asking the IdGlobalPool
|
||||
// to create a new batch. In the synchronous model, the IdPlayerPool is not
|
||||
// directly refilled, but the difference transmitter effectively refills it. It
|
||||
// is imperative that this difference transmission happen before the player's
|
||||
// asynchronous model runs out of batches, otherwise we'll get prediction
|
||||
// failures.
|
||||
//
|
||||
// It is common that a thread will only use 1 or 2 IDs. If a thread exits
|
||||
// without using up most of its IDs, then the batch it contains is still a
|
||||
// pretty usable batch. The batch gets returned to the IdGlobalPool, which will
|
||||
// eventually use that salvaged batch to satisfy an IdPlayerPool refill request.
|
||||
//
|
||||
// THE NUMERIC RANGES
|
||||
//
|
||||
// The largest integer that can be stored losslessly in a lua_Number is:
|
||||
//
|
||||
// * 0x0020000000000000
|
||||
//
|
||||
// In other words, any 53-bit number can be stored. We subdivide the range as
|
||||
// follows:
|
||||
//
|
||||
// * 0x0000+ : manually created IDs.
|
||||
// * 0x0001+ : used by master model's IdGlobalPool to create batches.
|
||||
// * 0x0010+ : used by master model's IdGlobalPool to create individual IDs.
|
||||
// * 0x001E+ : used by sync model's IdGlobalPool to create individual IDs.
|
||||
//
|
||||
// If you exhaust the Master Model's invididual pool at a rate of 10,000,000 IDs
|
||||
// per second, the individual pool will last for 12 years.
|
||||
//
|
||||
// BATCH REPRESENTATION
|
||||
//
|
||||
// A batch is represented as a 64-bit integer. The batch contains all IDs
|
||||
// starting with that integer, and incrementing, until the the two lowest digits
|
||||
// are 0x00.
|
||||
//
|
||||
// For example, consider the batch ID 0x11111111111111FC. That batch includes
|
||||
// these IDs:
|
||||
//
|
||||
// * 0x11111111111111FC
|
||||
// * 0x11111111111111FD
|
||||
// * 0x11111111111111FE
|
||||
// * 0x11111111111111FF
|
||||
//
|
||||
// But it does not include 0x1111111111111200.
|
||||
//
|
||||
// As a special case, the number 0 is used to indicate "invalid batch".
|
||||
//
|
||||
// SEQUENCE NUMBERS
|
||||
//
|
||||
// If you allocate IDs from a player pool rapidly, you will exhaust the fifo in
|
||||
// the player pool. As a result, ID allocation will fall back to the global
|
||||
// pool, which will cause prediction failures.
|
||||
//
|
||||
// Depending on your requirements, you may be able to use sequence numbers
|
||||
// instead of IDs. The player pool contains a monotonically increasing counter
|
||||
// for generating sequence numbers. These can be allocated extremely fast
|
||||
// and they'll never run out. These are highly predictable as long as the
|
||||
// only person fetching sequence numbers is the player himself. The downside
|
||||
// is that sequence numbers are not globally unique - they're only locally
|
||||
// unique for a single player. That may be enough for your requirements.
|
||||
//
|
||||
// Currently, sequence numbers are used by the random number generator.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef IDALLOC_HPP
|
||||
#define IDALLOC_HPP
|
||||
|
||||
#include "wrap-map.hpp"
|
||||
#include "wrap-sstream.hpp"
|
||||
#include "wrap-deque.hpp"
|
||||
#include "luastack.hpp"
|
||||
#include "streambuffer.hpp"
|
||||
#include "debugcollector.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <ostream>
|
||||
|
||||
class IdGlobalPool : public eng::nevernew {
|
||||
public:
|
||||
// Construct and destroy global pools. Note that after constructing
|
||||
// a global pool, it is generally also necessary to initialize it
|
||||
// for Master or Synchronous operation using init_master or init_synch.
|
||||
// Any attempt to use a pool that hasn't been init_master or init_synch
|
||||
// will fail.
|
||||
IdGlobalPool();
|
||||
~IdGlobalPool();
|
||||
|
||||
// Initialize the pool for use in a master model.
|
||||
void init_master();
|
||||
|
||||
// Initialize the pool for use in a synchronous model.
|
||||
void init_synch();
|
||||
|
||||
// Create a single unique ID. Ids allocated this way are
|
||||
// unlikely to be predicted correctly.
|
||||
int64_t get_one();
|
||||
|
||||
// Obtain a batch of IDs from the global pool. In a master
|
||||
// model, the batch is guaranteed to contain at least 128 IDs.
|
||||
// In a synchronous model, the batch is always zero (invalid).
|
||||
int64_t get_batch();
|
||||
|
||||
// Try to return the specified batch to the global pool.
|
||||
// The salvage operation quietly does nothing if the batch is
|
||||
// zero, or the batch contains fewer than 128 IDs.
|
||||
void salvage(int64_t batch);
|
||||
|
||||
// Get a global sequence number. These are not the same as IDs.
|
||||
// Sequence numbers are unlikely to be predicted correctly.
|
||||
int64_t get_seqno() { return next_seqno_++; }
|
||||
|
||||
// Serialize to or deserialize from a streambuffer.
|
||||
void serialize(StreamBuffer *sb) const;
|
||||
void deserialize(StreamBuffer *sb);
|
||||
|
||||
// Generate a debug string.
|
||||
eng::string debug_string() const;
|
||||
|
||||
private:
|
||||
eng::vector<int64_t> salvaged_;
|
||||
int64_t next_batch_;
|
||||
int64_t next_id_;
|
||||
int64_t next_seqno_;
|
||||
friend int unittests_idalloc(lua_State *L);
|
||||
};
|
||||
|
||||
class IdPlayerPool : public eng::nevernew {
|
||||
public:
|
||||
// Construct a player pool.
|
||||
//
|
||||
// The fifo is initially in the disabled state. In the disabled state, the
|
||||
// fifo is not kept filled. You can still use the allocator when the fifo is
|
||||
// disabled - doing so just fetches a batch from the global pool.
|
||||
//
|
||||
IdPlayerPool(IdGlobalPool *g);
|
||||
~IdPlayerPool();
|
||||
|
||||
// Set the fifo capacity. Max=255.
|
||||
void set_fifo_capacity(int n);
|
||||
int get_fifo_capacity() const { return fifo_capacity_; }
|
||||
|
||||
// Return all batches from the fifo to the global pool.
|
||||
void unqueue();
|
||||
|
||||
// Refill the fifo of batches from the global pool.
|
||||
void refill();
|
||||
|
||||
// Get a single ID from the fifo. Also refills the fifo.
|
||||
int64_t get_one();
|
||||
|
||||
// Get a player sequence number. This is not the same as an ID: player
|
||||
// sequence numbers are unique per player, but not globally.
|
||||
int64_t get_seqno() { return next_seqno_++; }
|
||||
|
||||
// Return the size of the queue.
|
||||
int size() { return ranges_.size(); }
|
||||
|
||||
// Return true if the two pools are identical.
|
||||
bool exactly_equal(const IdPlayerPool &other) const;
|
||||
|
||||
// Check that the pool is valid.
|
||||
bool valid() const;
|
||||
|
||||
// unit testing functions.
|
||||
//
|
||||
// These let you manipulate the deque explicitly. But that's
|
||||
// only useful for unit testing.
|
||||
//
|
||||
void test_push_back(int64_t range);
|
||||
void test_pop_front();
|
||||
void test_clear_ranges();
|
||||
|
||||
// Serialize to or deserialize from a streambuffer.
|
||||
// Caution: the pointer to the global pool is not serialized or deserialized.
|
||||
void serialize(StreamBuffer *sb) const;
|
||||
void deserialize(StreamBuffer *sb);
|
||||
|
||||
// Difference transmission
|
||||
//
|
||||
void diff(const IdPlayerPool &auth, StreamBuffer *sb) const;
|
||||
void patch(StreamBuffer *sb, DebugCollector *dbc);
|
||||
|
||||
// Debug string.
|
||||
eng::string debug_string() const;
|
||||
|
||||
private:
|
||||
IdGlobalPool *global_;
|
||||
int fifo_capacity_;
|
||||
int64_t next_seqno_;
|
||||
eng::deque<int64_t> ranges_;
|
||||
friend int lfn_unittests_idalloc(lua_State *L);
|
||||
};
|
||||
|
||||
#endif // IDALLOC_HPP
|
||||
|
||||
47
luprex/cpp/core/invocation.cpp
Normal file
47
luprex/cpp/core/invocation.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
|
||||
#include "wrap-string.hpp"
|
||||
#include "wrap-map.hpp"
|
||||
#include "wrap-deque.hpp"
|
||||
#include "wrap-sstream.hpp"
|
||||
|
||||
#include "invocation.hpp"
|
||||
|
||||
Invocation::Invocation() : kind_(InvocationKind::INVALID), actor_(0), place_(0) {}
|
||||
|
||||
Invocation::Invocation(InvocationKind kind, int64_t actor, int64_t place, std::string_view datapack)
|
||||
: kind_(kind), actor_(actor), place_(place), datapack_(datapack) {}
|
||||
|
||||
|
||||
void Invocation::serialize(StreamBuffer *sb) const {
|
||||
sb->write_uint8(int64_t(kind_));
|
||||
sb->write_int64(actor_);
|
||||
sb->write_int64(place_);
|
||||
sb->write_string(datapack_);
|
||||
}
|
||||
|
||||
void Invocation::deserialize(StreamBuffer *sb) {
|
||||
kind_ = InvocationKind(sb->read_uint8());
|
||||
actor_ = sb->read_int64();
|
||||
place_ = sb->read_int64();
|
||||
datapack_ = sb->read_string();
|
||||
}
|
||||
|
||||
eng::string Invocation::debug_string() const {
|
||||
eng::ostringstream oss;
|
||||
oss << "inv[";
|
||||
switch (kind_) {
|
||||
case InvocationKind::INVALID: oss << "invalid"; break;
|
||||
case InvocationKind::LUA_INVOKE: oss << "lua_invoke"; break;
|
||||
case InvocationKind::LUA_PROBE: oss << "lua_probe"; break;
|
||||
case InvocationKind::LUA_EXPR: oss << "lua_expr"; break;
|
||||
case InvocationKind::LUA_SOURCE: oss << "lua_source"; break;
|
||||
case InvocationKind::FLUSH_PRINTS: oss << "flush_prints"; break;
|
||||
case InvocationKind::TICK: oss << "tick"; break;
|
||||
default: oss << "UNKNOWN"; break;
|
||||
}
|
||||
oss << " a=" << actor_;
|
||||
oss << " p=" << place_;
|
||||
oss << " dp=" << datapack_.size() << " bytes";
|
||||
oss << "]";
|
||||
return oss.str();
|
||||
}
|
||||
77
luprex/cpp/core/invocation.hpp
Normal file
77
luprex/cpp/core/invocation.hpp
Normal file
@@ -0,0 +1,77 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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:
|
||||
//
|
||||
// InvocationKind::INVALID:
|
||||
//
|
||||
// Nothing.
|
||||
//
|
||||
// InvocationKind::LUA_CALL:
|
||||
//
|
||||
// A lua function call. The class name, the function name, and then
|
||||
// the function arguments.
|
||||
//
|
||||
// InvocationKind::LUA_EXPR:
|
||||
//
|
||||
// A block of lua source code, in plaintext.
|
||||
//
|
||||
// InvocationKind::FLUSH_PRINTS:
|
||||
//
|
||||
// Line number in ascii.
|
||||
//
|
||||
// InvocationKind::TICK:
|
||||
//
|
||||
// Nothing.
|
||||
//
|
||||
// InvocationKind::LUA_SOURCE:
|
||||
//
|
||||
// Packaged lua sourcecode. See drvutil::package_lua_source.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef INVOCATION_HPP
|
||||
#define INVOCATION_HPP
|
||||
|
||||
#include "wrap-string.hpp"
|
||||
#include "wrap-map.hpp"
|
||||
#include "wrap-deque.hpp"
|
||||
#include "streambuffer.hpp"
|
||||
#include "enginewrapper.hpp"
|
||||
|
||||
|
||||
class Invocation : public eng::opnew {
|
||||
public:
|
||||
private:
|
||||
InvocationKind kind_;
|
||||
int64_t actor_;
|
||||
int64_t place_;
|
||||
eng::string datapack_;
|
||||
public:
|
||||
Invocation();
|
||||
Invocation(InvocationKind kind, int64_t actor, int64_t place, std::string_view datapack);
|
||||
|
||||
bool valid() const { return kind_ != InvocationKind::INVALID; }
|
||||
InvocationKind kind() const { return kind_; }
|
||||
int64_t actor() const { return actor_; }
|
||||
int64_t place() const { return place_; }
|
||||
const eng::string &datapack() const { return datapack_; }
|
||||
|
||||
void serialize(StreamBuffer *sb) const;
|
||||
void deserialize(StreamBuffer *sb);
|
||||
|
||||
eng::string debug_string() const;
|
||||
};
|
||||
|
||||
using UniqueInvocation = std::unique_ptr<Invocation>;
|
||||
|
||||
class InvocationQueue : public eng::deque<Invocation> {
|
||||
};
|
||||
|
||||
|
||||
#endif // INVOCATION_HPP
|
||||
729
luprex/cpp/core/json.cpp
Normal file
729
luprex/cpp/core/json.cpp
Normal file
@@ -0,0 +1,729 @@
|
||||
#include "json.hpp"
|
||||
#include "luastack.hpp"
|
||||
#include "util.hpp"
|
||||
#include <string_view>
|
||||
#include <ostream>
|
||||
#include <cmath>
|
||||
#include <iomanip>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
|
||||
#define NOINDENT_LEVEL 1000
|
||||
|
||||
LuaTokenConstant(json_null, "null", "");
|
||||
LuaTokenConstant(json_object, "object", "");
|
||||
LuaTokenConstant(json_error, "error", "");
|
||||
|
||||
static void indent(eng::ostringstream &oss, int level) {
|
||||
if (level < NOINDENT_LEVEL) {
|
||||
oss << std::endl;
|
||||
for (int i = 0; i < level; i++) {
|
||||
oss << " ";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool length_exceeded(eng::ostringstream &oss, int maxlen) {
|
||||
return oss.tellp() > maxlen;
|
||||
}
|
||||
|
||||
template <class... ARGS>
|
||||
inline void store_error(eng::ostringstream &oss, const ARGS & ... args) {
|
||||
oss.str("");
|
||||
util::send_to_stream(oss, args...);
|
||||
}
|
||||
|
||||
static void store_length_error(eng::ostringstream &oss, int maxlen) {
|
||||
store_error(oss, "maximum json length exceeded: ", maxlen);
|
||||
}
|
||||
|
||||
static bool use_array_representation(lua_State *L) {
|
||||
int top = lua_gettop(L);
|
||||
int nfound = 0;
|
||||
while (true) {
|
||||
lua_rawgeti(L, top, nfound + 1);
|
||||
bool null = lua_isnil(L, -1);
|
||||
lua_settop(L, top);
|
||||
if (null) break;
|
||||
nfound += 1;
|
||||
}
|
||||
return (nfound == lua_nkeys(L, top));
|
||||
}
|
||||
|
||||
static bool encode_key(lua_State *L, eng::ostringstream &oss);
|
||||
static bool encode_value(lua_State *L, eng::ostringstream &oss, int level, int maxlen);
|
||||
|
||||
|
||||
// The goal here is to emit a double in such a way that
|
||||
// when we read it back in, we get the *exact* same number.
|
||||
//
|
||||
// In the worst case, you can accomplish this by using 17
|
||||
// digits of precision - that's enough to uniquely identify
|
||||
// all double values (see the following URL). However, 17
|
||||
// digits tends to produce unnecessary repeating decimals.
|
||||
// So we try 16 digits first, which tends to remove those
|
||||
// repeating decimals, but sometimes produces losses.
|
||||
// If that doesn't work, we fall back to 17 digits.
|
||||
//
|
||||
// https://randomascii.wordpress.com/2012/03/08/float-precisionfrom-zero-to-100-digits-2/
|
||||
//
|
||||
static void encode_double_lossless(double value, eng::ostringstream &oss) {
|
||||
char buffer[80];
|
||||
sprintf(buffer, "%.16g", value);
|
||||
if (strtod(buffer, nullptr) != value) {
|
||||
sprintf(buffer, "%.17g", value);
|
||||
assert(strtod(buffer, nullptr) == value);
|
||||
}
|
||||
oss << buffer;
|
||||
}
|
||||
|
||||
static bool encode_nil(lua_State *L, eng::ostringstream &oss) {
|
||||
oss << "null";
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool encode_token(lua_State *L, eng::ostringstream &oss) {
|
||||
LuaToken token(lua_touserdata(L, -1));
|
||||
if (token == ltoken_json_null) {
|
||||
oss << "null";
|
||||
return true;
|
||||
} else {
|
||||
store_error(oss, "cannot encode token: [", token.str(), "]");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool encode_number(lua_State *L, eng::ostringstream &oss) {
|
||||
lua_Number value = lua_tonumber(L, -1);
|
||||
if (std::isnan(value) || std::isinf(value)) {
|
||||
store_error(oss, "cannot encode infinity or NAN");
|
||||
return false;
|
||||
}
|
||||
int64_t ivalue = int64_t(value);
|
||||
if (double(ivalue) == value) {
|
||||
oss << ivalue;
|
||||
} else {
|
||||
encode_double_lossless(value, oss);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool encode_number_key(lua_State *L, eng::ostringstream &oss) {
|
||||
lua_Number value = lua_tonumber(L, -1);
|
||||
int64_t ivalue = int64_t(value);
|
||||
if (double(ivalue) != value) {
|
||||
store_error(oss, "cannot encode floating point numbers in table keys");
|
||||
return false;
|
||||
}
|
||||
if (ivalue >= 0) {
|
||||
oss << "\"\\uE000+" << ivalue << '"';
|
||||
} else {
|
||||
oss << "\"\\uE000-" << -ivalue << '"';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool encode_boolean(lua_State *L, eng::ostringstream &oss) {
|
||||
int flag = lua_toboolean(L, -1);
|
||||
oss << (flag ? "true" : "false");
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool encode_string(lua_State *L, eng::ostringstream &oss) {
|
||||
size_t len;
|
||||
const char *s = lua_tolstring(L, -1, &len);
|
||||
std::string_view str(s, len);
|
||||
oss << '"';
|
||||
if (sv::valid_utf8(str) && !sv::has_prefix(str, "")) {
|
||||
while (!str.empty()) {
|
||||
int32_t cp = sv::read_codepoint_utf8(str);
|
||||
assert(cp >= 0);
|
||||
switch (cp) {
|
||||
case '\\': oss << "\\\\"; break;
|
||||
case '"' : oss << "\\\""; break;
|
||||
case '\b': oss << "\\b"; break;
|
||||
case '\f': oss << "\\f"; break;
|
||||
case '\r': oss << "\\r"; break;
|
||||
case '\n': oss << "\\n"; break;
|
||||
case '\t': oss << "\\t"; break;
|
||||
default: {
|
||||
if (cp < 32) {
|
||||
oss << "\\u" << util::hex16.val(cp);
|
||||
} else {
|
||||
bool ok = util::write_codepoint_utf8(cp, &oss);
|
||||
assert(ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Output as a base64-encoded string.
|
||||
oss << "\\uE000=";
|
||||
util::base64_encode(str, &oss);
|
||||
}
|
||||
oss << '"';
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool encode_array(lua_State *L, eng::ostringstream &oss, int level, int maxlen) {
|
||||
lua_checkstack(L, 20);
|
||||
int top = lua_gettop(L);
|
||||
oss << "[";
|
||||
level ++;
|
||||
int i = 1;
|
||||
while (true) {
|
||||
lua_rawgeti(L, top, i);
|
||||
if (lua_isnil(L, -1)) break;
|
||||
if (i > 1) oss << ",";
|
||||
indent(oss, level);
|
||||
bool ok = encode_value(L, oss, level, maxlen);
|
||||
lua_settop(L, top);
|
||||
if (!ok) return false;
|
||||
if (length_exceeded(oss, maxlen)) {
|
||||
store_length_error(oss, maxlen);
|
||||
return false;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
lua_settop(L, top);
|
||||
level --;
|
||||
indent(oss, level);
|
||||
oss << "]";
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool encode_object(lua_State *L, eng::ostringstream &oss, int level, int maxlen) {
|
||||
lua_checkstack(L, 20);
|
||||
int top = lua_gettop(L);
|
||||
oss << "{";
|
||||
level ++;
|
||||
lua_pushnil(L);
|
||||
int i = 1;
|
||||
while (lua_next(L, top) != 0) {
|
||||
// Check for [json.object]=true, if so skip.
|
||||
if (lua_islightuserdata(L, -2) &&
|
||||
lua_isboolean(L, -1) &&
|
||||
(LuaToken(lua_touserdata(L, -2)) == LuaToken("object")) &&
|
||||
(lua_toboolean(L, -1) == 1)) {
|
||||
lua_pop(L, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
lua_pushvalue(L, -2);
|
||||
// Stack now has key, value, key
|
||||
assert(lua_gettop(L) == top + 3);
|
||||
if (i > 1) oss << ",";
|
||||
indent(oss, level);
|
||||
bool ok = encode_key(L, oss);
|
||||
if (!ok) {
|
||||
lua_settop(L, top);
|
||||
return false;
|
||||
}
|
||||
if (length_exceeded(oss, maxlen)) {
|
||||
store_length_error(oss, maxlen);
|
||||
lua_settop(L, top);
|
||||
return false;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
// Stack now has key, value
|
||||
assert(lua_gettop(L) == top + 2);
|
||||
oss << ((level < NOINDENT_LEVEL) ? " : " : ":");
|
||||
ok = encode_value(L, oss, level, maxlen);
|
||||
assert(lua_gettop(L) == top + 2);
|
||||
if (!ok) {
|
||||
lua_settop(L, top);
|
||||
return false;
|
||||
}
|
||||
if (length_exceeded(oss, maxlen)) {
|
||||
store_length_error(oss, maxlen);
|
||||
lua_settop(L, top);
|
||||
return false;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
// Stack now just has key.
|
||||
assert(lua_gettop(L) == top + 1);
|
||||
i += 1;
|
||||
}
|
||||
// Stack should be back to where we started.
|
||||
assert(lua_gettop(L) == top);
|
||||
level --;
|
||||
indent(oss, level);
|
||||
oss << "}";
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool encode_key(lua_State *L, eng::ostringstream &oss) {
|
||||
int type = lua_type(L, -1);
|
||||
switch (type) {
|
||||
case LUA_TSTRING: return encode_string(L, oss);
|
||||
case LUA_TNUMBER: return encode_number_key(L, oss);
|
||||
case LUA_TBOOLEAN:
|
||||
case LUA_TTABLE: {
|
||||
store_error(oss, "cannot encode '", lua_typename(L, type), "' in table keys");
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
store_error(oss, "cannot encode '", lua_typename(L, type), "'");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool encode_value(lua_State *L, eng::ostringstream &oss, int level, int maxlen) {
|
||||
int type = lua_type(L, -1);
|
||||
switch (type) {
|
||||
case LUA_TNIL: return encode_nil(L, oss);
|
||||
case LUA_TNUMBER: return encode_number(L, oss);
|
||||
case LUA_TBOOLEAN: return encode_boolean(L, oss);
|
||||
case LUA_TSTRING: return encode_string(L, oss);
|
||||
case LUA_TLIGHTUSERDATA: return encode_token(L, oss);
|
||||
case LUA_TTABLE: {
|
||||
if (use_array_representation(L)) {
|
||||
return encode_array(L, oss, level, maxlen);
|
||||
} else {
|
||||
return encode_object(L, oss, level, maxlen);
|
||||
}
|
||||
}
|
||||
default: {
|
||||
store_error(oss, "cannot encode '", lua_typename(L, type), "'");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool decode_value(lua_State *L, std::string_view &v);
|
||||
|
||||
static bool decode_id(lua_State *L, std::string_view &v) {
|
||||
std::string_view id = sv::read_ascii_identifier(v);
|
||||
if (id == "null") lua_pushlightuserdata(L, LuaToken("null").voidvalue());
|
||||
else if (id == "true") lua_pushboolean(L, 1);
|
||||
else if (id == "false") lua_pushboolean(L, 0);
|
||||
else return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool decode_number(lua_State *L, std::string_view &v) {
|
||||
std::string_view n = sv::read_number(v, true, true, true, true);
|
||||
if (n.empty()) return false;
|
||||
|
||||
// If it's an integer, make sure it fits in a lua double
|
||||
// losslessly. If it's a double, some loss in precision
|
||||
// is OK.
|
||||
if (sv::valid_number(n, true, true, false, false)) {
|
||||
int64_t i = sv::to_int64(n);
|
||||
if (!LuaCoreStack::validinteger(i)) return false;
|
||||
lua_pushnumber(L, double(i));
|
||||
return true;
|
||||
} else {
|
||||
double d = sv::to_double(n);
|
||||
if (std::isnan(d) || std::isinf(d)) return false;
|
||||
lua_pushnumber(L, d);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static bool decode_base64_string(lua_State *L, std::string_view &v) {
|
||||
// We've already read the starting quote and the E000
|
||||
// escape sequence at this point.
|
||||
|
||||
// Skip the equal sign.
|
||||
if (!sv::read_prefix(v, "=")) return false;
|
||||
|
||||
// Find the end of the quoted string.
|
||||
const char *p = v.data();
|
||||
const char *l = p + v.size();
|
||||
while (true) {
|
||||
if (p == l) return false;
|
||||
if (*p < 32) return false;
|
||||
if (*p == '"') break;
|
||||
p++;
|
||||
}
|
||||
std::string_view b64 = v.substr(0, p - v.data());
|
||||
v.remove_prefix(b64.size() + 1);
|
||||
eng::ostringstream oss;
|
||||
if (!util::base64_decode(b64, &oss)) return false;
|
||||
eng::string str = oss.str();
|
||||
lua_pushlstring(L, str.c_str(), str.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool decode_int_string(lua_State *L, std::string_view &v) {
|
||||
// We've already read the starting quote and the E000
|
||||
// escape sequence at this point.
|
||||
|
||||
// Parse the number and the closing quote.
|
||||
std::string_view n = sv::read_number(v, true, true, false, false);
|
||||
if (n.empty()) return false;
|
||||
if (!sv::read_prefix(v, "\"")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure the number fits in a lua double,
|
||||
// and push it on the stack.
|
||||
int64_t i = sv::to_int64(n);
|
||||
if (!LuaCoreStack::validinteger(i)) {
|
||||
return false;
|
||||
}
|
||||
lua_pushnumber(L, double(i));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool decode_standard_string(lua_State *L, std::string_view &v) {
|
||||
// We've already read the starting quote at this point.
|
||||
eng::ostringstream oss;
|
||||
while (true) {
|
||||
// Get the next codepoint.
|
||||
int32_t c = sv::read_codepoint_utf8(v);
|
||||
|
||||
// If it's a control character or invalid codepoint, reject.
|
||||
if (c < 32) return false;
|
||||
|
||||
// If it is an unescaped quote, that's end of string.
|
||||
if (c == '"') break;
|
||||
|
||||
// If it's a backslash, then deal with the escape sequence.
|
||||
if (c == '\\') {
|
||||
char next = sv::read_ascii_char(v);
|
||||
switch (next) {
|
||||
case '"': oss << '"'; break;
|
||||
case '\\': oss << '\\'; break;
|
||||
case '/': oss << '/'; break;
|
||||
case 'r': oss << '\r'; break;
|
||||
case 'n': oss <<'\n'; break;
|
||||
case 'b': oss << '\b'; break;
|
||||
case 'f': oss << '\f'; break;
|
||||
case 't': oss << '\t'; break;
|
||||
case 'u': {
|
||||
std::string_view hexdigits = sv::read_nbytes(v, 4);
|
||||
if (hexdigits.size() != 4) return false;
|
||||
uint64_t codepoint = sv::to_hex64(hexdigits, 0x10000);
|
||||
if (codepoint >= 0x10000) return false;
|
||||
if (!util::write_codepoint_utf8(codepoint, &oss)) return false;
|
||||
break;
|
||||
}
|
||||
default: return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Any other codepoint should be echoed into stream.
|
||||
util::write_codepoint_utf8(c, &oss);
|
||||
}
|
||||
eng::string result = oss.str();
|
||||
lua_pushlstring(L, result.c_str(), result.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool decode_string(lua_State *L, std::string_view &v) {
|
||||
if (!sv::read_prefix(v, "\"")) return false;
|
||||
|
||||
// Check for codepoint E000, the escape sequence.
|
||||
if (sv::read_prefix(v, "") ||
|
||||
sv::read_prefix(v, "\\uE000") ||
|
||||
sv::read_prefix(v, "\\ue000")) {
|
||||
char c = sv::zfront(v);
|
||||
if (c == '=') return decode_base64_string(L, v);
|
||||
else if ((c=='-') || (c=='+')) return decode_int_string(L, v);
|
||||
else return false;
|
||||
} else {
|
||||
return decode_standard_string(L, v);
|
||||
}
|
||||
}
|
||||
|
||||
static bool decode_array(lua_State *L, std::string_view &v) {
|
||||
if (!sv::read_prefix(v, "[")) return false;
|
||||
lua_newtable(L);
|
||||
int tabpos = lua_gettop(L);
|
||||
int next = 1;
|
||||
while (true) {
|
||||
v = sv::ltrim(v);
|
||||
if (sv::zfront(v) == ']') {
|
||||
v.remove_prefix(1);
|
||||
return true;
|
||||
}
|
||||
if (!decode_value(L, v)) {
|
||||
return false;
|
||||
}
|
||||
v = sv::ltrim(v);
|
||||
if (sv::zfront(v) == ',') {
|
||||
v.remove_prefix(1);
|
||||
}
|
||||
lua_rawseti(L, tabpos, next++);
|
||||
}
|
||||
}
|
||||
|
||||
static bool decode_object(lua_State *L, std::string_view &v) {
|
||||
if (!sv::read_prefix(v, "{")) return false;
|
||||
lua_newtable(L);
|
||||
int tabpos = lua_gettop(L);
|
||||
while (true) {
|
||||
v = sv::ltrim(v);
|
||||
if (sv::zfront(v) == '}') {
|
||||
v.remove_prefix(1);
|
||||
return true;
|
||||
}
|
||||
if (!decode_string(L, v)) {
|
||||
return false;
|
||||
}
|
||||
v = sv::ltrim(v);
|
||||
if (!sv::read_prefix(v, ":")) {
|
||||
return false;
|
||||
}
|
||||
if (!decode_value(L, v)) {
|
||||
return false;
|
||||
}
|
||||
v = sv::ltrim(v);
|
||||
if (sv::zfront(v) == ',') {
|
||||
v.remove_prefix(1);
|
||||
}
|
||||
lua_rawset(L, tabpos);
|
||||
}
|
||||
}
|
||||
|
||||
// Decode a single value.
|
||||
//
|
||||
// On success, pushes the value on the stack and returns true.
|
||||
// On failure, pushes NIL on the stack and returns false.
|
||||
//
|
||||
static bool decode_value(lua_State *L, std::string_view &v) {
|
||||
lua_checkstack(L, 20);
|
||||
int top = lua_gettop(L);
|
||||
|
||||
// Skip blanks.
|
||||
v = sv::ltrim(v);
|
||||
|
||||
// Try to read something.
|
||||
char c = sv::zfront(v);
|
||||
bool result;
|
||||
if (c == '"') result = decode_string(L, v);
|
||||
else if (c == '[') result = decode_array(L, v);
|
||||
else if (c == '{') result = decode_object(L, v);
|
||||
else if (sv::ascii_isalpha(c)) result = decode_id(L, v);
|
||||
else result = decode_number(L, v);
|
||||
|
||||
// On failure, the decode routines may leave junk
|
||||
// on the stack, in which case it's our job to clean up.
|
||||
if (result == false) {
|
||||
lua_settop(L, top);
|
||||
lua_pushnil(L);
|
||||
}
|
||||
|
||||
// Now there should be exactly one new value on the stack.
|
||||
assert(lua_gettop(L) == top + 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
namespace json {
|
||||
|
||||
eng::string encode(LuaCoreStack &LS, LuaSlot in, eng::string &out, bool indent, int maxlen) {
|
||||
eng::ostringstream oss;
|
||||
|
||||
// Call the recursive encoder. Clean up any crap on the lua stack afterward.
|
||||
int top = lua_gettop(LS.state());
|
||||
lua_pushvalue(LS.state(), in.index());
|
||||
bool ok = encode_value(LS.state(), oss, indent ? 0 : NOINDENT_LEVEL, maxlen);
|
||||
lua_settop(LS.state(), top);
|
||||
|
||||
// One last check for overruns.
|
||||
if (ok && length_exceeded(oss, maxlen)) {
|
||||
store_length_error(oss, maxlen);
|
||||
ok = false;
|
||||
}
|
||||
|
||||
// Produce the return value.
|
||||
if (ok) {
|
||||
out = oss.str();
|
||||
return "";
|
||||
} else {
|
||||
out = "";
|
||||
return oss.str();
|
||||
}
|
||||
}
|
||||
|
||||
bool decode(LuaCoreStack &LS, LuaSlot out, std::string_view v) {
|
||||
lua_State *L = LS.state();
|
||||
|
||||
// Try to read a single value from the view.
|
||||
int top = lua_gettop(L);
|
||||
bool ok = decode_value(L, v);
|
||||
lua_replace(L, out.index());
|
||||
lua_settop(L, top);
|
||||
if (!ok) {
|
||||
LS.set(out, LuaToken("error"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Special case: if the top level value is 'null', change
|
||||
// it to 'nil.'
|
||||
if (LS.istoken(out)) {
|
||||
LS.set(out, LuaNil);
|
||||
}
|
||||
|
||||
// There should be nothing left of the input text.
|
||||
if (v.size() > 0) {
|
||||
LS.set(out, LuaToken("error"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
|
||||
|
||||
LuaDefine(json_encode, "data, indent, maxlen",
|
||||
"|Encode a lua data structure returning a json string."
|
||||
"|"
|
||||
"|Data is the value being encoded. Indent is a flag,"
|
||||
"|if it's true, then the json is indented nicely,"
|
||||
"|otherwise, it is packed tightly. Maxlen is the maximum"
|
||||
"|length in bytes of the encoded json string."
|
||||
"|"
|
||||
"|Usually, Lua data translates straightforwardly to json."
|
||||
"|However, there are a number of special cases to be"
|
||||
"|aware of:"
|
||||
"|"
|
||||
"|- Closures and threads cannot be encoded. These will"
|
||||
"| cause the encoder to abort."
|
||||
"|"
|
||||
"|- The numbers infinity and NAN cannot be encoded."
|
||||
"| Both of these will cause the encoder to abort."
|
||||
"|"
|
||||
"|- You must specify a size-limit to the encoded"
|
||||
"| string. Exceeding the size limit causes the"
|
||||
"| encoder to abort."
|
||||
"|"
|
||||
"|- Recursive data structures will cause the encoder to"
|
||||
"| loop infinitely until the size-limit is exceeded,"
|
||||
"| causing the encoder to abort."
|
||||
"|"
|
||||
"|- There is no way to represent math.huge or math.nan in"
|
||||
"| json. Encoding math.nan will cause the encoder to abort,"
|
||||
"| as expected. However, encoding math.huge will emit null,"
|
||||
"| which is probably not what you would expect."
|
||||
"|"
|
||||
"|- Lua tables cannot contain 'nil', but json objects and"
|
||||
"| arrays can contain null. If you want the encoder to"
|
||||
"| emit a json object or array containing null, you must"
|
||||
"| use token json.null to represent null."
|
||||
"|"
|
||||
"|- Json objects, like lua tables, are key-value stores."
|
||||
"| However, json objects can only have string keys. Our"
|
||||
"| encoder uses a workaround to transparently"
|
||||
"| allow mixing string and integer keys in json tables."
|
||||
"| See 'encoding difficult data' below."
|
||||
"|"
|
||||
"|- Json strings are required to be valid utf-8. Our encoder"
|
||||
"| uses a workaround to transparently allow the use of"
|
||||
"| arbitrary 8-bit-clean strings. See 'encoding difficult"
|
||||
"| data' below."
|
||||
"|"
|
||||
"|- Lua tables containing contiguous integer keys from 1-n are"
|
||||
"| autodetected to be json arrays. Empty tables are also"
|
||||
"| emitted as json arrays. All other tables are emitted"
|
||||
"| as json objects."
|
||||
"|"
|
||||
"|- You can force a table to be emitted as a json object"
|
||||
"| by putting the key-value pair table[json.object]=true"
|
||||
"| into the table. This special key is not emitted, but"
|
||||
"| it triggers json object mode. This is the only way"
|
||||
"| to emit an empty json object (a truly empty table is"
|
||||
"| emitted as a json array.)"
|
||||
"|"
|
||||
"|Encoding Difficult Data:"
|
||||
"|"
|
||||
"|Normally, json doesn't allow integer table keys, and it"
|
||||
"|doesn't allow strings that aren't valid utf-8. Our"
|
||||
"|json encoder and decoder, on the other hand, can"
|
||||
"|encode and decode integer table keys and 8-bit-clean"
|
||||
"|strings transparently. This is accomplished without"
|
||||
"|violating the json specification, by encoding such"
|
||||
"|values as utf-8 strings:"
|
||||
"|"
|
||||
"| '123' (encoded integer 123)"
|
||||
"| '=aGVsbG8=' (binary string encoded as base64)"
|
||||
"|"
|
||||
"|Those encodings start with utf-8 codepoint E000."
|
||||
"|This codepoint probably shows up in your text editor"
|
||||
"|as a little rectangle. When the decoder sees codepoint"
|
||||
"|E000 at the beginning of a string, it automatically"
|
||||
"|decodes the string back into its original form."
|
||||
"|"
|
||||
"|The one price for this behavior is that the encoder"
|
||||
"|cannot literally emit strings that start"
|
||||
"|with codepoint E000. If the encoder detects such a"
|
||||
"|string, it will emit it as a base64-encoded string."
|
||||
"|This should be uncommon, since codepoint E000 is"
|
||||
"|reserved."
|
||||
"|"
|
||||
"|Note that integers are only encoded when they are"
|
||||
"|used as table keys. Otherwise, numbers are emitted"
|
||||
"|straightforwardly."
|
||||
"|") {
|
||||
LuaArg data, indent, maxlen;
|
||||
LuaRet encoded;
|
||||
LuaDefStack LS(L, data, indent, maxlen, encoded);
|
||||
eng::string out;
|
||||
eng::string error = json::encode(LS, data, out, LS.ckboolean(indent), LS.ckint(maxlen));
|
||||
if (!error.empty()) {
|
||||
luaL_error(L, "%s", error.c_str());
|
||||
LS.set(encoded, LuaNil);
|
||||
return LS.result();
|
||||
} else {
|
||||
LS.set(encoded, out);
|
||||
return LS.result();
|
||||
}
|
||||
}
|
||||
|
||||
LuaDefine(json_decode, "data",
|
||||
"|Decode a json expression into a lua data structure."
|
||||
"|"
|
||||
"|Data that was generated by our own encoder is almost"
|
||||
"|8-bit clean. That includes difficult cases, like"
|
||||
"|binary strings, floating point numbers, and tables"
|
||||
"|with mixed string and integer keys. The exception"
|
||||
"|are the kinds of data that can't be encoded at all:"
|
||||
"|See doc(json.encode) for details about what"
|
||||
"|can and cannot be encoded."
|
||||
"|"
|
||||
"|Some json may contain 'null' inside objects and"
|
||||
"|arrays. Lua tables can't store nil, so instead, we"
|
||||
"|store the token json.null. If that's not what you"
|
||||
"|want, you can use json.stripnulls to strip out"
|
||||
"|the json.null values from a data structure and"
|
||||
"|replace them with nil."
|
||||
"|"
|
||||
"|") {
|
||||
LuaArg encoded;
|
||||
LuaRet data;
|
||||
LuaDefStack LS(L, encoded, data);
|
||||
std::string_view v = LS.ckstringview(encoded);
|
||||
bool ok = json::decode(LS, data, v);
|
||||
if (!ok) {
|
||||
luaL_error(L, "invalid json string.");
|
||||
}
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
// LuaDefine(base64_encode, "data", "") {
|
||||
// LuaArg str;
|
||||
// LuaRet ret;
|
||||
// LuaDefStack LS(L, str, ret);
|
||||
// eng::string cstr = LS.ckstring(str);
|
||||
// eng::ostringstream oss;
|
||||
// util::base64_encode(cstr, &oss);
|
||||
// LS.set(ret, oss.str());
|
||||
// return LS.result();
|
||||
// }
|
||||
|
||||
// LuaDefine(base64_decode, "data", "") {
|
||||
// LuaArg str;
|
||||
// LuaRet ret;
|
||||
// LuaDefStack LS(L, str, ret);
|
||||
// eng::string cstr = LS.ckstring(str);
|
||||
// eng::ostringstream oss;
|
||||
// util::base64_decode(cstr, &oss);
|
||||
// LS.set(ret, oss.str());
|
||||
// return LS.result();
|
||||
// }
|
||||
|
||||
34
luprex/cpp/core/json.hpp
Normal file
34
luprex/cpp/core/json.hpp
Normal file
@@ -0,0 +1,34 @@
|
||||
// Encode lua data structure into json, and decode them again.
|
||||
//
|
||||
// See the doc(http.jsonencode) to read about limitations of the encoder.
|
||||
//
|
||||
#ifndef JSON_HPP
|
||||
#define JSON_HPP
|
||||
|
||||
#include "luastack.hpp"
|
||||
#include "wrap-string.hpp"
|
||||
#include <string_view>
|
||||
|
||||
namespace json {
|
||||
// Encode json.
|
||||
//
|
||||
// See doc(http.jsonencode) for a lot more information.
|
||||
//
|
||||
// Returns an error message. If the error message is an
|
||||
// empty string, then the encoding was successful.
|
||||
//
|
||||
eng::string encode(LuaCoreStack &LS, LuaSlot in, eng::string &out, bool indent, int maxlen);
|
||||
|
||||
// Decode json.
|
||||
//
|
||||
// See doc(http.jsondecode) for a lot more information.
|
||||
//
|
||||
// The only error condition is syntactically invalid json.
|
||||
// In that case, we return false and set 'out' to the
|
||||
// token 'error'.
|
||||
//
|
||||
bool decode(LuaCoreStack &LS, LuaSlot out, std::string_view in);
|
||||
}
|
||||
|
||||
#endif // JSON_HPP
|
||||
|
||||
300
luprex/cpp/core/lpxclient.cpp
Normal file
300
luprex/cpp/core/lpxclient.cpp
Normal file
@@ -0,0 +1,300 @@
|
||||
#include "wrap-string.hpp"
|
||||
#include "wrap-vector.hpp"
|
||||
|
||||
#include "drivenengine.hpp"
|
||||
#include "world.hpp"
|
||||
#include "luaconsole.hpp"
|
||||
#include "invocation.hpp"
|
||||
#include "util.hpp"
|
||||
#include "printbuffer.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
class LpxClient : public DrivenEngine, public CommonCommands {
|
||||
public:
|
||||
using StringVec = LuaConsole::StringVec;
|
||||
UniqueWorld world_;
|
||||
int64_t actor_id_;
|
||||
InvocationQueue unack_;
|
||||
SharedChannel channel_;
|
||||
LuaConsole console_;
|
||||
PrintChanneler print_channeler_;
|
||||
eng::vector<Invocation> delayed_invocations_;
|
||||
|
||||
public:
|
||||
void set_initial_state_connect(const eng::string &hostspec) {
|
||||
// Create the world model.
|
||||
world_.reset(new World(WORLD_TYPE_PREDICTIVE));
|
||||
|
||||
// Create the communication channel.
|
||||
channel_ = new_outgoing_channel(hostspec);
|
||||
|
||||
// This is a temporary actor that will be used only until the server sends
|
||||
// us the first difference transmission. We do this only to establish
|
||||
// the invariant that there's always an actor. When the first difference
|
||||
// transmission arrives, this actor may be deleted, or it may just be
|
||||
// ignored, at the server's discretion.
|
||||
actor_id_ = world_->create_login_actor();
|
||||
|
||||
// Clear the unack command queue.
|
||||
unack_.clear();
|
||||
|
||||
// Export stuff to the graphics engine.
|
||||
set_visible_world_and_actor(world_.get(), actor_id_);
|
||||
|
||||
// Reset the print channeler
|
||||
print_channeler_.reset();
|
||||
}
|
||||
|
||||
void set_initial_state_standalone(std::string_view srcpk) {
|
||||
// Create the world model.
|
||||
world_.reset(new World(WORLD_TYPE_MASTER));
|
||||
|
||||
// Update the source code of the master model.
|
||||
world_->update_source(srcpk);
|
||||
|
||||
// Make sure the channel is empty.
|
||||
channel_.reset();
|
||||
|
||||
// Create the standalone actor.
|
||||
actor_id_ = world_->create_login_actor();
|
||||
|
||||
// TODO: initialize the standalone actor.
|
||||
|
||||
// Clear the unack command queue.
|
||||
unack_.clear();
|
||||
|
||||
// Export stuff to the graphics engine.
|
||||
set_visible_world_and_actor(world_.get(), actor_id_);
|
||||
|
||||
// Reset the print channeler
|
||||
print_channeler_.reset();
|
||||
}
|
||||
|
||||
|
||||
// When the world is in synchronous mode, there's no
|
||||
// snapshot, and the commands in the unack queue are not
|
||||
// reflected in the world. In asynchronous mode, there is
|
||||
// a snapshot that allows return to the synchronous mode,
|
||||
// and the unack commands are in the world model.
|
||||
|
||||
void world_to_synchronous() {
|
||||
if (!world_->snapshot_empty()) {
|
||||
world_->rollback();
|
||||
}
|
||||
}
|
||||
|
||||
void world_to_asynchronous() {
|
||||
if (!world_->is_authoritative()) {
|
||||
if (world_->snapshot_empty()) {
|
||||
world_->snapshot();
|
||||
for (const Invocation &inv : unack_) {
|
||||
world_->invoke(inv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void abandon_server() {
|
||||
// When we abandon the server, we leave the world model
|
||||
// hanging around to preserve the invariant that there
|
||||
// is always a world model. Then, we trigger a rescan
|
||||
// of the lua source. When the lua source shows up, then
|
||||
// we will create a standalone model to replace the client
|
||||
// model.
|
||||
|
||||
channel_.reset();
|
||||
rescan_lua_source();
|
||||
}
|
||||
|
||||
virtual void event_init(std::string_view srcpk, int argc, char *argv[]) {
|
||||
// Put the world into the starting state.
|
||||
set_initial_state_standalone(srcpk);
|
||||
|
||||
// Set the console prompt
|
||||
set_console_prompt(console_.get_prompt());
|
||||
}
|
||||
|
||||
void send_invocation(const Invocation &inv) {
|
||||
if (channel_ == nullptr) {
|
||||
if ((!world_->is_authoritative()) && (inv.kind() == InvocationKind::LUA_SOURCE)) {
|
||||
// We have a client model, but no client connection. That means we're
|
||||
// in the process of shutting down a client model. The client model
|
||||
// is supposed to linger until the lua source is reread. Once we have
|
||||
// the lua source, we're supposed to throw out the client model and
|
||||
// create a standalone model.
|
||||
set_initial_state_standalone(inv.datapack());
|
||||
} else {
|
||||
world_->invoke(inv);
|
||||
}
|
||||
} else {
|
||||
world_to_asynchronous();
|
||||
world_->invoke(inv);
|
||||
unack_.push_back(inv);
|
||||
StreamBuffer *sb = channel_->out();
|
||||
sb->write_uint8(util::MSG_INVOKE);
|
||||
inv.serialize(sb);
|
||||
}
|
||||
}
|
||||
|
||||
virtual void do_syntax_error(std::string_view error) override {
|
||||
stdostream() << "Syntax error: " << error << std::endl;
|
||||
}
|
||||
|
||||
virtual void do_unknown_command(std::string_view name) override {
|
||||
stdostream() << "Unknown command: " << name << std::endl;
|
||||
}
|
||||
|
||||
virtual void do_view_command() override {
|
||||
stdostream() << world_->tangibles_near_debug_string(actor_id_, 1000);
|
||||
}
|
||||
|
||||
virtual void do_moveto_command(int x, int y) override {
|
||||
do_unknown_command("moveto");
|
||||
}
|
||||
|
||||
virtual void do_quit_command() override {
|
||||
abandon_server();
|
||||
stop_driver();
|
||||
}
|
||||
|
||||
virtual void do_cpl_command() override {
|
||||
rescan_lua_source();
|
||||
}
|
||||
|
||||
virtual void do_work_command() override {
|
||||
do_unknown_command("work");
|
||||
}
|
||||
|
||||
virtual void do_display_command() override {
|
||||
do_unknown_command("display");
|
||||
}
|
||||
|
||||
virtual void do_aborthttp_command() override {
|
||||
do_unknown_command("aborthttp");
|
||||
}
|
||||
|
||||
virtual void do_connect_command(std::string_view hostname) override {
|
||||
set_initial_state_connect(util::ss("nocert:", hostname, ":8085"));
|
||||
}
|
||||
|
||||
virtual void do_luainvoke_command(std::string_view cmd) override {
|
||||
send_invocation(Invocation(InvocationKind::LUA_EXPR, actor_id_, actor_id_, cmd));
|
||||
}
|
||||
|
||||
virtual void do_luaprobe_command(std::string_view cmd) override {
|
||||
world_to_asynchronous();
|
||||
stdostream() << world_->probe_lua_expr(actor_id_, cmd);
|
||||
world_to_synchronous();
|
||||
}
|
||||
|
||||
void change_actor_id(int64_t actor_id) {
|
||||
stdostream() << "Actor ID changing: " << actor_id << std::endl;
|
||||
print_channeler_.reset();
|
||||
actor_id_ = actor_id;
|
||||
set_visible_world_and_actor(world_.get(), actor_id_);
|
||||
}
|
||||
|
||||
void receive_ack_from_server(StreamBuffer *sb) {
|
||||
// An ack is just a single byte, so there's nothing left to read.
|
||||
if (unack_.empty()) {
|
||||
// Invalid acknowledgement when theres' nothing in the unack queue.
|
||||
abandon_server();
|
||||
return;
|
||||
}
|
||||
world_to_synchronous();
|
||||
world_->invoke(unack_.front());
|
||||
unack_.pop_front();
|
||||
}
|
||||
|
||||
void receive_diff_from_server(StreamBuffer *sb) {
|
||||
world_to_synchronous();
|
||||
try {
|
||||
DebugCollector dbc("");
|
||||
int64_t nactor = world_->patch_everything(sb, &dbc);
|
||||
if (nactor != actor_id_) change_actor_id(nactor);
|
||||
dbc.dump(stdostream());
|
||||
} catch (const StreamException &sexcept) {
|
||||
abandon_server();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool receive_message_from_server(StreamBuffer *sb) {
|
||||
if (sb->fill() < 5) return false;
|
||||
int64_t tr_before = sb->total_reads();
|
||||
uint8_t message_type = sb->read_uint8();
|
||||
uint32_t message_body_len = sb->read_uint32();
|
||||
if (sb->fill() < message_body_len) {
|
||||
sb->unread_to(tr_before);
|
||||
return false;
|
||||
}
|
||||
const char *message_body = sb->read_bytes(message_body_len);
|
||||
StreamBuffer body(std::string_view(message_body, message_body_len));
|
||||
if (message_type == util::MSG_ACK) {
|
||||
receive_ack_from_server(&body);
|
||||
} else if (message_type == util::MSG_DIFF) {
|
||||
receive_diff_from_server(&body);
|
||||
} else {
|
||||
abandon_server();
|
||||
return false;
|
||||
}
|
||||
if (!body.empty()) {
|
||||
abandon_server();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void event_call_function(InvocationKind kind, int64_t place_id, std::string_view datapk, StreamBuffer *retpk) override {
|
||||
switch (kind) {
|
||||
case InvocationKind::LUA_PROBE: {
|
||||
world_to_asynchronous();
|
||||
world_->probe_lua_call(actor_id_, place_id, datapk, retpk);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
delayed_invocations_.emplace_back(kind, actor_id_, place_id, datapk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual void event_update() override {
|
||||
// Send invocations. We execute these using predictive execution.
|
||||
for (const Invocation &inv : delayed_invocations_) {
|
||||
send_invocation(inv);
|
||||
}
|
||||
delayed_invocations_.clear();
|
||||
|
||||
// Check for keyboard input on stdin.
|
||||
while (true) {
|
||||
eng::string line = get_stdio_channel()->in()->readline();
|
||||
if (line == "") break;
|
||||
console_.add(line);
|
||||
set_console_prompt(console_.get_prompt());
|
||||
do_command(console_.get_command());
|
||||
}
|
||||
|
||||
// Check for communication from server..
|
||||
if (channel_ != nullptr) {
|
||||
if (channel_->closed()) {
|
||||
stdostream() << "server closed connection: " << channel_->error() << std::endl;
|
||||
abandon_server();
|
||||
} else {
|
||||
while (true) {
|
||||
if (!receive_message_from_server(channel_->in())) break;
|
||||
if (channel_ == nullptr) break;
|
||||
}
|
||||
world_to_asynchronous();
|
||||
}
|
||||
}
|
||||
|
||||
// Channel print statements.
|
||||
if (print_channeler_.channel(world_->get_printbuffer(actor_id_), stdostream())) {
|
||||
send_invocation(print_channeler_.invocation(actor_id_));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DrivenEngineDefine("lpxclient", LpxClient);
|
||||
|
||||
4
luprex/cpp/core/lpxclient.hpp
Normal file
4
luprex/cpp/core/lpxclient.hpp
Normal file
@@ -0,0 +1,4 @@
|
||||
#ifndef LPXCLIENT_HPP
|
||||
#define LPXCLIENT_HPP
|
||||
|
||||
#endif // LPXCLIENT_HPP
|
||||
346
luprex/cpp/core/lpxserver.cpp
Normal file
346
luprex/cpp/core/lpxserver.cpp
Normal file
@@ -0,0 +1,346 @@
|
||||
#include "wrap-string.hpp"
|
||||
#include "wrap-vector.hpp"
|
||||
|
||||
#include "world.hpp"
|
||||
#include "drivenengine.hpp"
|
||||
#include "luaconsole.hpp"
|
||||
#include "util.hpp"
|
||||
#include "printbuffer.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
class Client : public eng::opnew {
|
||||
public:
|
||||
int64_t actor_id_;
|
||||
SharedChannel channel_;
|
||||
UniqueWorld sync_;
|
||||
double last_diff_;
|
||||
bool async_diff_;
|
||||
};
|
||||
using UniqueClient = std::unique_ptr<Client>;
|
||||
using ClientVector = eng::vector<UniqueClient>;
|
||||
|
||||
class LpxServer : public DrivenEngine, public CommonCommands {
|
||||
public:
|
||||
using StringVec = LuaConsole::StringVec;
|
||||
UniqueWorld master_;
|
||||
LuaConsole console_;
|
||||
ClientVector clients_;
|
||||
PrintChanneler print_channeler_;
|
||||
HttpChannelMap http_client_channels_;
|
||||
HttpChannelVec http_server_channels_;
|
||||
int64_t admin_id_;
|
||||
int next_diff_chan_;
|
||||
double next_tick_;
|
||||
eng::vector<Invocation> delayed_invocations_;
|
||||
|
||||
public:
|
||||
virtual void event_init(std::string_view srcpk, int argc, char *argv[]) override {
|
||||
// Create the master world model.
|
||||
master_.reset(new World(WORLD_TYPE_MASTER));
|
||||
|
||||
// Update the source code of the master model.
|
||||
master_->update_source(srcpk);
|
||||
|
||||
// Create an actor for administrative commands.
|
||||
admin_id_ = master_->create_login_actor();
|
||||
|
||||
// TODO: initialize the admin actor.
|
||||
|
||||
// Print out admin ID for debugging purposes.
|
||||
stdostream() << "Admin actor id = " << admin_id_ << std::endl;
|
||||
|
||||
// Enable listening on port 8085 (client connections)
|
||||
listen_port(8085);
|
||||
|
||||
// Enable listening on port 8080 (http server connections)
|
||||
listen_port(8080);
|
||||
|
||||
// Set the console prompt.
|
||||
set_console_prompt(console_.get_prompt());
|
||||
|
||||
// Export stuff to the graphics engine.
|
||||
set_visible_world_and_actor(master_.get(), admin_id_);
|
||||
|
||||
// for ticking.
|
||||
next_tick_ = 0.0;
|
||||
}
|
||||
|
||||
virtual void do_syntax_error(std::string_view error) override {
|
||||
stdostream() << "Syntax error: " << error << std::endl;
|
||||
}
|
||||
|
||||
virtual void do_unknown_command(std::string_view name) override {
|
||||
stdostream() << "Unknown command: " << name << std::endl;
|
||||
}
|
||||
|
||||
virtual void do_view_command() override {
|
||||
stdostream() << master_->tangibles_near_debug_string(admin_id_, 1000);
|
||||
}
|
||||
|
||||
virtual void do_moveto_command(int x, int y) override {
|
||||
do_unknown_command("moveto");
|
||||
}
|
||||
|
||||
virtual void do_quit_command() override {
|
||||
stop_driver();
|
||||
}
|
||||
|
||||
virtual void do_cpl_command() override {
|
||||
rescan_lua_source();
|
||||
}
|
||||
|
||||
virtual void do_work_command() override {
|
||||
master_->snapshot();
|
||||
StreamBuffer datapack;
|
||||
StreamBuffer retvals;
|
||||
datapack.write_string("engio");
|
||||
datapack.write_string("myfunction");
|
||||
datapack.write_simple_dynamic_tag(SimpleDynamicTag::NUMBER);
|
||||
datapack.write_double(3.7);
|
||||
datapack.write_simple_dynamic_tag(SimpleDynamicTag::STRING);
|
||||
datapack.write_string("banana");
|
||||
datapack.write_simple_dynamic_tag(SimpleDynamicTag::BOOLEAN);
|
||||
datapack.write_bool(true);
|
||||
master_->probe_lua_call(admin_id_, admin_id_, datapack.view(), &retvals);
|
||||
while (!retvals.empty()) {
|
||||
SimpleDynamicValue value;
|
||||
retvals.read_simple_dynamic(&value);
|
||||
if (value.type == SimpleDynamicTag::NUMBER) {
|
||||
util::dprint("retval: ", value.x);
|
||||
} else if (value.type == SimpleDynamicTag::STRING) {
|
||||
util::dprint("retval: ", value.s);
|
||||
} else if (value.type == SimpleDynamicTag::BOOLEAN) {
|
||||
util::dprint(value.x ? "retval: true" : "retval: false");
|
||||
} else if (value.type == SimpleDynamicTag::VECTOR) {
|
||||
util::dprint("retval: ", value.x, " ", value.y, " ", value.z);
|
||||
} else {
|
||||
util::dprint("retval: invalid");
|
||||
break;
|
||||
}
|
||||
}
|
||||
master_->rollback();
|
||||
}
|
||||
|
||||
virtual void do_display_command() override {
|
||||
do_unknown_command("display");
|
||||
}
|
||||
|
||||
virtual void do_aborthttp_command() override {
|
||||
master_->abort_all_http_requests(425, "http requests aborted from the server command line");
|
||||
}
|
||||
|
||||
virtual void do_connect_command(std::string_view hostname) override {
|
||||
do_unknown_command("connect");
|
||||
}
|
||||
|
||||
virtual void do_luainvoke_command(std::string_view cmd) override {
|
||||
master_->invoke(Invocation(InvocationKind::LUA_EXPR, admin_id_, admin_id_, cmd));
|
||||
}
|
||||
|
||||
virtual void do_luaprobe_command(std::string_view cmd) override {
|
||||
master_->snapshot();
|
||||
stdostream() << master_->probe_lua_expr(admin_id_, cmd);
|
||||
master_->rollback();
|
||||
}
|
||||
|
||||
void delete_client(UniqueClient &client) {
|
||||
stdostream() << "Client closed: actor id=" << client->actor_id_ << std::endl;
|
||||
master_->disconnected(client->actor_id_);
|
||||
client.reset();
|
||||
}
|
||||
|
||||
void send_diffs(UniqueClient &client) {
|
||||
StreamBuffer *sb = client->channel_->out();
|
||||
sb->write_uint8(util::MSG_DIFF);
|
||||
sb->write_uint32(0);
|
||||
int64_t tw_1 = sb->total_writes();
|
||||
//stdostream() << "Sending diffs to client " << client->actor_id_ << std::endl;
|
||||
client->sync_->diff_everything(client->actor_id_, master_.get(), sb);
|
||||
int64_t tw_2 = sb->total_writes();
|
||||
sb->overwrite_int32(tw_1, tw_2 - tw_1);
|
||||
}
|
||||
|
||||
bool handle_invocation(UniqueClient &client) {
|
||||
StreamBuffer *sb = client->channel_->in();
|
||||
int64_t tr_before = sb->total_reads();
|
||||
Invocation inv;
|
||||
try {
|
||||
uint8_t msg_type = sb->read_uint8();
|
||||
if (msg_type != util::MSG_INVOKE) {
|
||||
delete_client(client);
|
||||
return false;
|
||||
}
|
||||
inv.deserialize(sb);
|
||||
} catch (const StreamEofOnRead &seof) {
|
||||
sb->unread_to(tr_before);
|
||||
return false;
|
||||
} catch (const StreamException &sexcept) {
|
||||
delete_client(client);
|
||||
return false;
|
||||
}
|
||||
// Security check.
|
||||
if (inv.actor() != client->actor_id_) {
|
||||
delete_client(client);
|
||||
return false;
|
||||
}
|
||||
//stdostream() << "Invoking: " << inv.debug_string() << std::endl;
|
||||
master_->invoke(inv);
|
||||
client->channel_->out()->write_uint8(util::MSG_ACK);
|
||||
client->channel_->out()->write_uint32(0);
|
||||
client->sync_->invoke(inv);
|
||||
client->async_diff_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void event_call_function(InvocationKind kind, int64_t place_id, std::string_view datapk, StreamBuffer *retpk) override {
|
||||
switch (kind) {
|
||||
case InvocationKind::LUA_PROBE: {
|
||||
master_->snapshot();
|
||||
master_->probe_lua_call(admin_id_, place_id, datapk, retpk);
|
||||
master_->rollback();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
delayed_invocations_.emplace_back(kind, admin_id_, place_id, datapk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual void event_update() override {
|
||||
// Get the clock.
|
||||
double clock = get_clock();
|
||||
|
||||
// Execute any queued invocations.
|
||||
// We just feed these directly into the master model.
|
||||
for (const Invocation &inv : delayed_invocations_) {
|
||||
master_->invoke(inv);
|
||||
}
|
||||
delayed_invocations_.clear();
|
||||
|
||||
// Check for keyboard input on stdin.
|
||||
while (true) {
|
||||
eng::string line = get_stdio_channel()->in()->readline();
|
||||
if (line == "") break;
|
||||
console_.add(line);
|
||||
set_console_prompt(console_.get_prompt());
|
||||
do_command(console_.get_command());
|
||||
}
|
||||
|
||||
// Anything in the administrator printbuffer should go to stdostream.
|
||||
if (print_channeler_.channel(master_->get_printbuffer(admin_id_), stdostream())) {
|
||||
master_->invoke(print_channeler_.invocation(admin_id_));
|
||||
}
|
||||
|
||||
// Check for new incoming channels, set up client structures.
|
||||
while (true) {
|
||||
SharedChannel chan = new_incoming_channel();
|
||||
if (chan == nullptr) break;
|
||||
if (chan->port() == 8085) {
|
||||
Client *client = new Client;
|
||||
client->actor_id_ = master_->create_login_actor();
|
||||
// TODO: initialize the login actor on the master.
|
||||
client->channel_ = std::move(chan);
|
||||
client->async_diff_ = true;
|
||||
client->last_diff_ = -100.0;
|
||||
client->sync_.reset(new World(WORLD_TYPE_PREDICTIVE));
|
||||
// This login actor is never used, it is just to preserve the invariant that
|
||||
// the client model and the server synchronous model are identical.
|
||||
client->sync_->create_login_actor();
|
||||
clients_.emplace_back(client);
|
||||
stdostream() << "New client: actor id=" << client->actor_id_ << std::endl;
|
||||
} else if (chan->port() == 8080) {
|
||||
HttpChannel htchan;
|
||||
htchan.channel_ = chan;
|
||||
http_server_channels_.push_back(htchan);
|
||||
}
|
||||
}
|
||||
|
||||
// If the clock has advanced far enough, tick the master model.
|
||||
if (clock >= next_tick_) {
|
||||
master_->invoke(Invocation(InvocationKind::TICK, 0, 0, ""));
|
||||
for (UniqueClient &client : clients_) {
|
||||
client->async_diff_ = true;
|
||||
}
|
||||
next_tick_ += 1.0;
|
||||
if (next_tick_ < clock + 0.3) next_tick_ = clock + 0.3;
|
||||
}
|
||||
|
||||
// Traverse all existing channels, process any communication.
|
||||
for (UniqueClient &client : clients_) {
|
||||
if (client->channel_->closed()) {
|
||||
delete_client(client);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for received invocations.
|
||||
while (handle_invocation(client));
|
||||
|
||||
// Possibly send a diff.
|
||||
double diffdelay = 5.0;
|
||||
if (client->async_diff_) diffdelay = 0.1;
|
||||
if (clock >= client->last_diff_ + diffdelay) {
|
||||
send_diffs(client);
|
||||
client->last_diff_ = clock;
|
||||
client->async_diff_ = false;
|
||||
}
|
||||
}
|
||||
util::remove_nullptrs(clients_);
|
||||
|
||||
// Look for new outgoing HTTP client requests.
|
||||
for (const auto &pair : master_->http_requests()) {
|
||||
const HttpClientRequest &request = pair.second;
|
||||
HttpChannel &channel = http_client_channels_[request.request_id()];
|
||||
if (channel.channel_ == nullptr) {
|
||||
channel.channel_ = new_outgoing_channel(request.target());
|
||||
channel.method_ = request.method();
|
||||
channel.parsed_bytes_ = 0;
|
||||
request.send(channel.channel_->out());
|
||||
}
|
||||
}
|
||||
|
||||
// Maintain existing outgoing HTTP client requests.
|
||||
HttpParserVec http_responses;
|
||||
for (auto &pair : http_client_channels_) {
|
||||
HttpChannel &htchan = pair.second;
|
||||
Channel &channel = *htchan.channel_;
|
||||
if (channel.closed() || (channel.in()->fill() > htchan.parsed_bytes_)) {
|
||||
HttpParser response;
|
||||
if (!channel.error().empty()) {
|
||||
response.fail(503, util::ss("Service Unavailable: ", channel.error()));
|
||||
} else {
|
||||
htchan.parsed_bytes_ = channel.in()->fill();
|
||||
response.parse_response(channel.in()->view(), channel.closed(), htchan.method_);
|
||||
}
|
||||
if (response.complete()) {
|
||||
response.set_request_id(pair.first);
|
||||
http_responses.push_back(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const HttpParser &response : http_responses) {
|
||||
http_client_channels_.erase(response.request_id());
|
||||
}
|
||||
master_->http_responses(http_responses);
|
||||
|
||||
// Maintain incoming HTTP server channels.
|
||||
for (HttpChannel &htchan : http_server_channels_) {
|
||||
SharedChannel &chan = htchan.channel_;
|
||||
if (chan->in()->fill() > htchan.parsed_bytes_) {
|
||||
HttpParser parser;
|
||||
parser.parse_request(chan->in()->view(), chan->closed());
|
||||
htchan.parsed_bytes_ = chan->in()->fill();
|
||||
if (parser.complete()) {
|
||||
StreamBuffer *sb = chan->out();
|
||||
HttpServerResponse resp = master_->http_serve(parser);
|
||||
resp.send(sb);
|
||||
htchan.channel_ = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
util::remove_marked_items(http_server_channels_);
|
||||
}
|
||||
};
|
||||
|
||||
DrivenEngineDefine("lpxserver", LpxServer);
|
||||
|
||||
5
luprex/cpp/core/lpxserver.hpp
Normal file
5
luprex/cpp/core/lpxserver.hpp
Normal file
@@ -0,0 +1,5 @@
|
||||
#ifndef LPXSERVER_HPP
|
||||
#define LPXSERVER_HPP
|
||||
|
||||
#endif // LPXSERVER_HPP
|
||||
|
||||
200
luprex/cpp/core/luaconsole.cpp
Normal file
200
luprex/cpp/core/luaconsole.cpp
Normal file
@@ -0,0 +1,200 @@
|
||||
#include "wrap-string.hpp"
|
||||
#include "wrap-vector.hpp"
|
||||
#include "eng-malloc.hpp"
|
||||
#include "luastack.hpp"
|
||||
#include "luaconsole.hpp"
|
||||
#include "util.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
LuaConsole::LuaConsole() {
|
||||
lua_state_ = LuaCoreStack::newstate(eng::l_alloc);
|
||||
clear_raw_input();
|
||||
}
|
||||
|
||||
LuaConsole::~LuaConsole() {
|
||||
lua_close(lua_state_);
|
||||
}
|
||||
|
||||
static LuaConsole::StringVec split_words(const eng::string &raw) {
|
||||
LuaConsole::StringVec result;
|
||||
eng::string acc;
|
||||
for (char c : raw) {
|
||||
if ((c == ' ')||(c == '\n')||(c == '\r')) {
|
||||
if (!acc.empty()) {
|
||||
result.push_back(acc);
|
||||
acc = "";
|
||||
}
|
||||
} else {
|
||||
acc += c;
|
||||
}
|
||||
}
|
||||
if (!acc.empty()) {
|
||||
result.push_back(acc);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
LuaConsole::StringVec LuaConsole::get_command() {
|
||||
StringVec result = words_;
|
||||
words_.clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
void LuaConsole::clear_raw_input() {
|
||||
raw_input_ = "";
|
||||
lines_ = 0;
|
||||
prompt_ = "> ";
|
||||
}
|
||||
|
||||
void LuaConsole::add(eng::string line) {
|
||||
for (int i = 0; i < int(line.size()); i++) {
|
||||
if (line[i] == '\n') line[i] = ' ';
|
||||
}
|
||||
raw_input_ += line;
|
||||
raw_input_ += '\n';
|
||||
lines_ += 1;
|
||||
prompt_ = ">> ";
|
||||
words_.clear();
|
||||
|
||||
// Try to interpret it as a slash-command.
|
||||
if ((lines_ == 1)&&(raw_input_[0] == '/')) {
|
||||
words_ = split_words(raw_input_.substr(1));
|
||||
if ((words_.size() == 1) && (sv::valid_int64(words_[0]))) {
|
||||
eng::string num = words_[0];
|
||||
words_.clear();
|
||||
words_.push_back("choose");
|
||||
words_.push_back(num);
|
||||
}
|
||||
clear_raw_input();
|
||||
return;
|
||||
}
|
||||
|
||||
// Translate lua expression, deal with initial prefix.
|
||||
eng::string partial;
|
||||
eng::string lua_mode;
|
||||
if (sv::has_prefix(raw_input_, "?=")) {
|
||||
lua_mode = "luaprobe";
|
||||
partial = eng::string("return ") + raw_input_.substr(2);
|
||||
} else if (sv::has_prefix(raw_input_, "?")) {
|
||||
lua_mode = "luaprobe";
|
||||
partial = raw_input_.substr(1);
|
||||
} else if (sv::has_prefix(raw_input_, "=")) {
|
||||
lua_mode = "luainvoke";
|
||||
partial = eng::string("return ") + raw_input_.substr(1);
|
||||
} else {
|
||||
lua_mode = "luainvoke";
|
||||
partial = raw_input_;
|
||||
}
|
||||
|
||||
// Try to parse the lua expression
|
||||
int top = lua_gettop(lua_state_);
|
||||
int status = luaL_loadbuffer(lua_state_, partial.c_str(), partial.size(), "=stdin");
|
||||
if (status == LUA_ERRSYNTAX)
|
||||
{
|
||||
const char *eof = "<eof>";
|
||||
int leof = strlen(eof);
|
||||
size_t lmsg;
|
||||
const char *msg = lua_tolstring(lua_state_, -1, &lmsg);
|
||||
const char *tp = msg + lmsg - leof;
|
||||
if (strstr(msg, eof) != tp) {
|
||||
words_.push_back("syntax");
|
||||
words_.push_back(msg);
|
||||
clear_raw_input();
|
||||
}
|
||||
} else {
|
||||
words_.push_back(lua_mode);
|
||||
words_.emplace_back(sv::rtrim(partial));
|
||||
clear_raw_input();
|
||||
}
|
||||
lua_settop(lua_state_, top);
|
||||
}
|
||||
|
||||
void CommonCommands::do_command(const StringVec &words) {
|
||||
if (words.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (words[0] == "view") {
|
||||
if (words.size() == 1) {
|
||||
return do_view_command();
|
||||
} else {
|
||||
return do_syntax_error("/view takes no arguments");
|
||||
}
|
||||
}
|
||||
|
||||
if (words[0] == "moveto") {
|
||||
if ((words.size() == 3) && sv::valid_int64(words[1]) && sv::valid_int64(words[2])) {
|
||||
return do_moveto_command(sv::to_int64(words[1]), sv::to_int64(words[2]));
|
||||
} else {
|
||||
return do_syntax_error("/moveto [x] [y]");
|
||||
}
|
||||
}
|
||||
|
||||
if (words[0] == "quit") {
|
||||
if (words.size() == 1) {
|
||||
return do_quit_command();
|
||||
} else {
|
||||
return do_syntax_error("/quit takes no arguments");
|
||||
}
|
||||
}
|
||||
|
||||
if (words[0] == "cpl") {
|
||||
if (words.size() == 1) {
|
||||
return do_cpl_command();
|
||||
} else {
|
||||
return do_syntax_error("/cpl takes no arguments");
|
||||
}
|
||||
}
|
||||
|
||||
if (words[0] == "work") {
|
||||
if (words.size() == 1) {
|
||||
return do_work_command();
|
||||
} else {
|
||||
return do_syntax_error("/work takes no arguments");
|
||||
}
|
||||
}
|
||||
|
||||
if (words[0] == "display") {
|
||||
if (words.size() == 1) {
|
||||
return do_display_command();
|
||||
} else {
|
||||
return do_syntax_error("/display takes no arguments");
|
||||
}
|
||||
}
|
||||
|
||||
if (words[0] == "aborthttp") {
|
||||
if (words.size() == 1) {
|
||||
return do_aborthttp_command();
|
||||
} else {
|
||||
return do_syntax_error("/aborthttp takes no arguments");
|
||||
}
|
||||
}
|
||||
|
||||
if (words[0] == "connect") {
|
||||
if ((words.size() == 2)&&(sv::valid_hostname(words[1]))) {
|
||||
return do_connect_command(words[1]);
|
||||
} else {
|
||||
return do_syntax_error("/connect [hostname]");
|
||||
}
|
||||
}
|
||||
|
||||
if (words[0] == "luainvoke") {
|
||||
if (words.size() == 2) {
|
||||
return do_luainvoke_command(words[1]);
|
||||
} else {
|
||||
return do_syntax_error("/luainvoke [command]");
|
||||
}
|
||||
}
|
||||
|
||||
if (words[0] == "luaprobe") {
|
||||
if (words.size() == 2) {
|
||||
return do_luaprobe_command(words[1]);
|
||||
} else {
|
||||
return do_syntax_error("/luainvoke [command]");
|
||||
}
|
||||
}
|
||||
|
||||
return do_unknown_command(words[0]);
|
||||
}
|
||||
118
luprex/cpp/core/luaconsole.hpp
Normal file
118
luprex/cpp/core/luaconsole.hpp
Normal file
@@ -0,0 +1,118 @@
|
||||
//////////////////////////////////////////////////////
|
||||
//
|
||||
// LuaConsole:
|
||||
//
|
||||
// Used to parse commands that are being interactively typed
|
||||
// in by a user. The common usage pattern is:
|
||||
//
|
||||
// 1. Print the prompt suggested by 'get_prompt'.
|
||||
// 2. Read a line of text from stdin.
|
||||
// 3. Add the line to the LuaConsole using 'add'.
|
||||
// 4. Get the command word using get_command.
|
||||
// 5. If the command is empty, do nothing.
|
||||
// 6. If the command is nonempty, get the args and execute it.
|
||||
//
|
||||
// The LuaConsole expects you to type a lua command, or one
|
||||
// of the following slash-commands:
|
||||
//
|
||||
// /quit - exit the program
|
||||
// /view - display the nearby tangibles
|
||||
// /menu [tanid] - display the menu for tangible
|
||||
// /tick [timevalue] - advance the simulation clock
|
||||
// /choose [number] - choose menu item number
|
||||
// /1234 - choose menu item 1234
|
||||
//
|
||||
// If you type anything else, the LuaConsole will generate a
|
||||
// syntax error. Note that not all of the commands above are
|
||||
// supported in all interpreters.
|
||||
//
|
||||
// Lua commands can be one of the following:
|
||||
//
|
||||
// <expression> - invoke "<expression>""
|
||||
// = <expression> - invoke "return <expression>"
|
||||
// ? <expression> - probe "<expression>"
|
||||
// ?= <expression> - probe "return <expression>"
|
||||
//
|
||||
// Once a command has been typed (or a syntax error has been
|
||||
// typed), the LuaConsole will return the command. The command
|
||||
// can be fetched using command(), int_arg(), and str_arg()
|
||||
//
|
||||
//////////////////////////////////////////////////////
|
||||
|
||||
#ifndef LUACONSOLE_HPP
|
||||
#define LUACONSOLE_HPP
|
||||
|
||||
#include "wrap-string.hpp"
|
||||
#include "wrap-vector.hpp"
|
||||
|
||||
#include "luastack.hpp"
|
||||
|
||||
class LuaConsole : public eng::nevernew {
|
||||
public:
|
||||
using StringVec = eng::vector<eng::string>;
|
||||
|
||||
private:
|
||||
lua_State *lua_state_;
|
||||
eng::string raw_input_;
|
||||
int lines_;
|
||||
eng::string prompt_;
|
||||
StringVec words_;
|
||||
|
||||
void clear_raw_input();
|
||||
|
||||
public:
|
||||
LuaConsole();
|
||||
~LuaConsole();
|
||||
|
||||
// Fetch the recommended prompt.
|
||||
//
|
||||
// You should update the prompt immediately after 'add'.
|
||||
//
|
||||
const eng::string &get_prompt() { return prompt_; }
|
||||
|
||||
// Get the command words.
|
||||
//
|
||||
StringVec get_command();
|
||||
|
||||
// Add a line of text that was just read from the console.
|
||||
//
|
||||
void add(eng::string line);
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
//
|
||||
// CommonCommands is parses slash-commands that are understood by
|
||||
// lpxclient and lpxserver. That way, these two separate
|
||||
// sourcefiles don't have to duplicate the code for parsing.
|
||||
//
|
||||
// After parsing, this module calls a virtual function
|
||||
// to execute the command. lpxserver and lpxclient
|
||||
// implement these virtual functions in different ways.
|
||||
//
|
||||
//////////////////////////////////////////////////////
|
||||
|
||||
class CommonCommands {
|
||||
public:
|
||||
using StringVec = eng::vector<eng::string>;
|
||||
|
||||
protected:
|
||||
virtual void do_syntax_error(std::string_view error) = 0;
|
||||
virtual void do_unknown_command(std::string_view name) = 0;
|
||||
virtual void do_view_command() = 0;
|
||||
virtual void do_moveto_command(int x, int y) = 0;
|
||||
virtual void do_quit_command() = 0;
|
||||
virtual void do_cpl_command() = 0;
|
||||
virtual void do_work_command() = 0;
|
||||
virtual void do_display_command() = 0;
|
||||
virtual void do_aborthttp_command() = 0;
|
||||
virtual void do_connect_command(std::string_view hostname) = 0;
|
||||
virtual void do_luainvoke_command(std::string_view cmd) = 0;
|
||||
virtual void do_luaprobe_command(std::string_view cmd) = 0;
|
||||
|
||||
public:
|
||||
// Parse a command and call one of the virtual command functions above.
|
||||
//
|
||||
void do_command(const StringVec &command);
|
||||
};
|
||||
|
||||
#endif // LUACONSOLE_HPP
|
||||
140
luprex/cpp/core/luasnap.cpp
Normal file
140
luprex/cpp/core/luasnap.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
|
||||
#include "wrap-sstream.hpp"
|
||||
#include "wrap-string.hpp"
|
||||
|
||||
#include "luasnap.hpp"
|
||||
#include "luastack.hpp"
|
||||
#include "streambuffer.hpp"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
|
||||
LuaSnap::LuaSnap() {
|
||||
state_ = LuaCoreStack::newstate(eng::l_alloc);
|
||||
LuaExtStack LS(state_);
|
||||
|
||||
// Create the persist table and the unpersist table.
|
||||
//
|
||||
// These tables need to contain all C functions. Whenever
|
||||
// the source module inserts a C function into the lua environment,
|
||||
// it also needs to register the C function in these tables.
|
||||
//
|
||||
// I don't think anything else needs to go in these tables.
|
||||
//
|
||||
LS.rawset(LuaRegistry, "persist", LuaNewTable);
|
||||
LS.rawset(LuaRegistry, "unpersist", LuaNewTable);
|
||||
}
|
||||
|
||||
LuaSnap::~LuaSnap() {
|
||||
lua_close(state_);
|
||||
}
|
||||
|
||||
void LuaSnap::serialize(StreamBuffer *sb) {
|
||||
// Lua stack should be empty.
|
||||
assert(lua_gettop(state_) == 0);
|
||||
|
||||
// We'll allocate stack slots manually, to ensure
|
||||
// that the thread serializes as we expect it to.
|
||||
lua_pushnil(state_); // permstable
|
||||
lua_pushnil(state_); // regcopy
|
||||
lua_pushnil(state_); // key
|
||||
lua_pushnil(state_); // value
|
||||
LuaSpecial permstable(1);
|
||||
LuaSpecial regcopy(2);
|
||||
LuaSpecial key(3);
|
||||
LuaSpecial value(4);
|
||||
LuaCoreStack LS(state_);
|
||||
|
||||
// Construct a copy of the registry table.
|
||||
LS.set(regcopy, LuaNewTable);
|
||||
LS.set(key, LuaNil);
|
||||
while (LS.next(LuaRegistry, key, value) != 0) {
|
||||
LS.rawset(regcopy, key, value);
|
||||
}
|
||||
|
||||
// Remove certain things from the copy. These items
|
||||
// don't get included in the snapshot.
|
||||
//
|
||||
// From a modularity perspective, this is bad.
|
||||
//
|
||||
LS.rawset(regcopy, LUA_RIDX_MAINTHREAD, LuaNil);
|
||||
LS.rawset(regcopy, "persist", LuaNil);
|
||||
LS.rawset(regcopy, "unpersist", LuaNil);
|
||||
LS.rawset(regcopy, "world", LuaNil);
|
||||
LS.rawset(regcopy, "gui", LuaNil);
|
||||
LS.rawset(regcopy, "tnmap", LuaNil);
|
||||
LS.rawset(regcopy, "ntmap", LuaNil);
|
||||
|
||||
// Get the eris permanents table from the registry.
|
||||
LS.rawget(permstable, LuaRegistry, "persist");
|
||||
assert(LS.istable(permstable));
|
||||
|
||||
// Remove key and value from stack, leaving permstable and regcopy.
|
||||
lua_settop(state_, 2);
|
||||
|
||||
// Write dummy length, use eris to write data, then overwrite length.
|
||||
sb->write_int64(0);
|
||||
int64_t pos1 = sb->total_writes();
|
||||
|
||||
eris_dump(state_, lua_writer_into_streambuffer, sb);
|
||||
int64_t pos2 = sb->total_writes();
|
||||
sb->overwrite_int64(pos1, pos2 - pos1);
|
||||
lua_settop(state_, 0);
|
||||
}
|
||||
|
||||
void LuaSnap::deserialize(StreamBuffer *sb) {
|
||||
// Lua stack should be empty.
|
||||
assert(lua_gettop(state_) == 0);
|
||||
|
||||
// Get the eris data and convert it to a LuaByteReader.
|
||||
int64_t eris_len = sb->read_int64();
|
||||
const char *eris_bytes = sb->read_bytes(eris_len);
|
||||
LuaByteReader bytereader(eris_bytes, eris_len);
|
||||
|
||||
// Call eris with the permanents table and passing the snapshot as a lua_Reader.
|
||||
lua_pushstring(state_, "unpersist");
|
||||
lua_rawget(state_, LUA_REGISTRYINDEX);
|
||||
eris_undump(state_, bytereader.lua_reader, bytereader.lua_reader_userdata());
|
||||
|
||||
// Set up a stack frame manually. Eris deserialization
|
||||
// should have left permstable and regcopy on the stack.
|
||||
assert(lua_gettop(state_) == 2);
|
||||
LuaSpecial permstable(1);
|
||||
LuaSpecial regcopy(2);
|
||||
lua_pushnil(state_);
|
||||
lua_pushnil(state_);
|
||||
LuaSpecial key(3);
|
||||
LuaSpecial value(4);
|
||||
LuaCoreStack LS(state_);
|
||||
assert(LS.istable(regcopy));
|
||||
|
||||
// Copy the contents of the snapshot over to the registry.
|
||||
LS.set(key, LuaNil);
|
||||
while (LS.next(regcopy, key, value) != 0) {
|
||||
LS.rawset(LuaRegistry, key, value);
|
||||
}
|
||||
lua_settop(state_, 0);
|
||||
}
|
||||
|
||||
// Snapshot and rollback can trivially be implemented on top of serialize and
|
||||
// deserialize. However, it's also possible to implement snapshot and rollback
|
||||
// using an alternative technique:
|
||||
//
|
||||
// 1. When constructing the lua interpreter, use a custom memory allocator that
|
||||
// keeps track of all the memory blocks used by lua.
|
||||
//
|
||||
// 2. Snapshot simply copies all the memory blocks used by lua into a buffer.
|
||||
//
|
||||
// 3. Rollback restores lua's memory blocks back to their previous state. This
|
||||
// has the effect of restoring lua's state.
|
||||
//
|
||||
// A proof-of-concept implementation of the memory-snapshotting design was
|
||||
// created, and it worked. It is probably faster than using serialize and
|
||||
// deserialize.
|
||||
//
|
||||
// Note: even if we implement this alternative design, we still need to keep
|
||||
// serialize and deserialize around in order to implement the save-game
|
||||
// functionality. So for now, we're sticking with this design, which doesn't
|
||||
// require us to maintain any additional code.
|
||||
|
||||
|
||||
42
luprex/cpp/core/luasnap.hpp
Normal file
42
luprex/cpp/core/luasnap.hpp
Normal file
@@ -0,0 +1,42 @@
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// LUASNAP
|
||||
//
|
||||
// A lua interpreter that can be checkpointed (snapshotted). This makes it
|
||||
// possible to roll the entire interpreter back to a previously-snapshotted
|
||||
// state.
|
||||
//
|
||||
// To accomplish this, we use eris serialization. This is messy and not
|
||||
// very modular, but it does work.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef LUASNAP_HPP
|
||||
#define LUASNAP_HPP
|
||||
|
||||
#include "streambuffer.hpp"
|
||||
#include "luastack.hpp"
|
||||
|
||||
class LuaSnap : public eng::nevernew {
|
||||
private:
|
||||
lua_State *state_;
|
||||
|
||||
public:
|
||||
LuaSnap();
|
||||
~LuaSnap();
|
||||
|
||||
// Get the lua intepreter.
|
||||
//
|
||||
lua_State *state() const { return state_; }
|
||||
|
||||
// Serialize the state of the lua interpreter.
|
||||
//
|
||||
void serialize(StreamBuffer *sb);
|
||||
|
||||
// Restore the the lua interpreter given a serialized state.
|
||||
//
|
||||
void deserialize(StreamBuffer *sb);
|
||||
};
|
||||
|
||||
|
||||
#endif // LUASNAP_HPP
|
||||
709
luprex/cpp/core/luastack.cpp
Normal file
709
luprex/cpp/core/luastack.cpp
Normal file
@@ -0,0 +1,709 @@
|
||||
#include "luastack.hpp"
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <climits>
|
||||
#include "wrap-string.hpp"
|
||||
#include "wrap-set.hpp"
|
||||
#include "wrap-sstream.hpp"
|
||||
#include "util.hpp"
|
||||
|
||||
LuaSpecial LuaRegistry(LUA_REGISTRYINDEX);
|
||||
LuaNilMarker LuaNil;
|
||||
LuaNewTableMarker LuaNewTable;
|
||||
|
||||
LuaFunctionReg::LuaFunctionReg(const char *n, const char *a, const char *d, bool s, lua_CFunction f) {
|
||||
name_ = n;
|
||||
args_ = a;
|
||||
docs_ = d;
|
||||
func_ = f;
|
||||
sandbox_ = s;
|
||||
next_ = All;
|
||||
All = this;
|
||||
}
|
||||
|
||||
LuaConstantReg::LuaConstantReg(const char *n, const char *d, LuaToken tokenvalue, lua_Number numbervalue) {
|
||||
name_ = n;
|
||||
docs_ = d;
|
||||
tokenvalue_ = tokenvalue;
|
||||
numbervalue_ = numbervalue;
|
||||
next_ = All;
|
||||
All = this;
|
||||
}
|
||||
|
||||
const LuaFunctionReg *LuaFunctionReg::lookup(lua_CFunction fn) {
|
||||
for (const LuaFunctionReg *r = All; r != 0; r = r->next_) {
|
||||
if (r->func_ == fn) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
LuaFunctionReg *LuaFunctionReg::All;
|
||||
LuaConstantReg *LuaConstantReg::All;
|
||||
|
||||
|
||||
eng::string LuaToken::str() const {
|
||||
uint64_t token = (uint64_t)value;
|
||||
char buffer[9];
|
||||
for (int i = 0; i < 8; i++) {
|
||||
unsigned char c = token;
|
||||
buffer[7-i] = c;
|
||||
token >>= 8;
|
||||
}
|
||||
buffer[8] = 0;
|
||||
return eng::string(buffer);
|
||||
}
|
||||
|
||||
static int panicf(lua_State *L) {
|
||||
const char *p = lua_tostring(L, -1);
|
||||
fprintf(stderr, "PANIC: unprotected error in call to Lua API (%s)\n", p);
|
||||
fflush(stderr);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// An allocator for lua states that uses system malloc and system free.
|
||||
static void *l_alloc(void *ud, void *ptr, size_t osize, size_t nsize) {
|
||||
if (nsize == 0) {
|
||||
free(ptr);
|
||||
return NULL;
|
||||
} else {
|
||||
return realloc(ptr, nsize);
|
||||
}
|
||||
}
|
||||
|
||||
lua_State *LuaCoreStack::newstate (lua_Alloc allocf) {
|
||||
if (allocf == nullptr) allocf = l_alloc;
|
||||
lua_State *L = lua_newstate(allocf, NULL);
|
||||
assert(L != nullptr);
|
||||
lua_atpanic(L, &panicf);
|
||||
|
||||
// We want all states to have a classes table and
|
||||
// a tangibles table so that LS.makeclass and LS.maketan
|
||||
// work out-of-the-box.
|
||||
|
||||
lua_pushstring(L, "classes");
|
||||
lua_newtable(L);
|
||||
lua_rawset(L, LUA_REGISTRYINDEX);
|
||||
lua_pushstring(L, "classnames");
|
||||
lua_newtable(L);
|
||||
lua_rawset(L, LUA_REGISTRYINDEX);
|
||||
lua_pushstring(L, "tangibles");
|
||||
lua_newtable(L);
|
||||
lua_rawset(L, LUA_REGISTRYINDEX);
|
||||
lua_pushstring(L, "worldtype");
|
||||
lua_pushnumber(L, WORLD_TYPE_MASTER);
|
||||
lua_rawset(L, LUA_REGISTRYINDEX);
|
||||
|
||||
return L;
|
||||
}
|
||||
|
||||
void LuaCoreStack::argerr(const char *nm, const char *tp) const {
|
||||
luaL_error(L_, "'%s' should be %s", nm, tp);
|
||||
}
|
||||
|
||||
std::optional<bool> LuaCoreStack::tryboolean(LuaSlot s) const {
|
||||
if (lua_type(L_, s) == LUA_TBOOLEAN) {
|
||||
return lua_toboolean(L_, s);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<lua_Integer> LuaCoreStack::tryinteger(LuaSlot s) const {
|
||||
if (lua_type(L_, s) == LUA_TNUMBER) {
|
||||
lua_Number result = lua_tonumber(L_, s);
|
||||
if (lua_Integer(result) == result) {
|
||||
return lua_Integer(result);
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<int> LuaCoreStack::tryint(LuaSlot s) const {
|
||||
if (lua_type(L_, s) == LUA_TNUMBER) {
|
||||
lua_Number result = lua_tonumber(L_, s);
|
||||
if (int(result) == result) {
|
||||
return int(result);
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<lua_Number> LuaCoreStack::trynumber(LuaSlot s) const {
|
||||
if (lua_type(L_, s) == LUA_TNUMBER) {
|
||||
return lua_tonumber(L_, s);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<eng::string> LuaCoreStack::trystring(LuaSlot s) const {
|
||||
if (lua_type(L_, s) == LUA_TSTRING) {
|
||||
size_t len;
|
||||
const char *str = lua_tolstring(L_, s, &len);
|
||||
return eng::string(str, len);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::string_view> LuaCoreStack::trystringview(LuaSlot s) const {
|
||||
if (lua_type(L_, s) == LUA_TSTRING) {
|
||||
size_t len;
|
||||
const char *str = lua_tolstring(L_, s, &len);
|
||||
return std::string_view(str, len);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<lua_State*> LuaCoreStack::trythread(LuaSlot s) const {
|
||||
if (lua_type(L_, s) == LUA_TTHREAD) {
|
||||
return lua_tothread(L_, s);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<LuaToken> LuaCoreStack::trytoken(LuaSlot s) const {
|
||||
if (lua_type(L_, s) == LUA_TLIGHTUSERDATA) {
|
||||
return LuaToken(lua_touserdata(L_, s));
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<util::DXYZ> LuaCoreStack::tryxyz(LuaSlot s) const {
|
||||
if (lua_istable(L_, s) && (lua_nkeys(L_, s) == 3)) {
|
||||
int top = lua_gettop(L_);
|
||||
lua_rawgeti(L_, s, 3);
|
||||
lua_rawgeti(L_, s, 2);
|
||||
lua_rawgeti(L_, s, 1);
|
||||
if ((lua_type(L_, -1)==LUA_TNUMBER) && (lua_type(L_, -2)==LUA_TNUMBER) && (lua_type(L_, -3)==LUA_TNUMBER)) {
|
||||
util::DXYZ result;
|
||||
result.x = lua_tonumber(L_, -1);
|
||||
result.y = lua_tonumber(L_, -2);
|
||||
result.z = lua_tonumber(L_, -3);
|
||||
lua_settop(L_, top);
|
||||
return result;
|
||||
}
|
||||
lua_settop(L_, top);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool LuaCoreStack::trytable(LuaSlot s) const {
|
||||
return lua_istable(L_, s);
|
||||
}
|
||||
|
||||
bool LuaCoreStack::trynil(LuaSlot s) const {
|
||||
return lua_isnil(L_, s);
|
||||
}
|
||||
|
||||
bool LuaCoreStack::tryfunction(LuaSlot s) const {
|
||||
return lua_isfunction(L_, s);
|
||||
}
|
||||
|
||||
bool LuaCoreStack::trycfunction(LuaSlot s) const {
|
||||
return lua_iscfunction(L_, s);
|
||||
}
|
||||
|
||||
bool LuaCoreStack::trytangible(LuaSlot s) const {
|
||||
return (lua_istable(L_, s) && gettabletype(s) == LUA_TT_TANGIBLE);
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool LuaCoreStack::ckboolean(LuaSlot s, const char *argname) const {
|
||||
auto result = tryboolean(s);
|
||||
if (!result) argerr(argname, "boolean");
|
||||
return *result;
|
||||
}
|
||||
|
||||
lua_Integer LuaCoreStack::ckinteger(LuaSlot s, const char *argname) const {
|
||||
auto result = tryinteger(s);
|
||||
if (!result) argerr(argname, "integer");
|
||||
return *result;
|
||||
}
|
||||
|
||||
int LuaCoreStack::ckint(LuaSlot s, const char *argname) const {
|
||||
auto result = tryint(s);
|
||||
if (!result) argerr(argname, "int");
|
||||
return *result;
|
||||
}
|
||||
|
||||
lua_Number LuaCoreStack::cknumber(LuaSlot s, const char *argname) const {
|
||||
auto result = trynumber(s);
|
||||
if (!result) argerr(argname, "number");
|
||||
return *result;
|
||||
}
|
||||
|
||||
eng::string LuaCoreStack::ckstring(LuaSlot s, const char *argname) const {
|
||||
auto result = trystring(s);
|
||||
if (!result) argerr(argname, "string");
|
||||
return *result;
|
||||
}
|
||||
|
||||
std::string_view LuaCoreStack::ckstringview(LuaSlot s, const char *argname) const {
|
||||
auto result = trystringview(s);
|
||||
if (!result) argerr(argname, "string");
|
||||
return *result;
|
||||
}
|
||||
|
||||
lua_State * LuaCoreStack::ckthread(LuaSlot s, const char *argname) const {
|
||||
auto result = trythread(s);
|
||||
if (!result) argerr(argname, "thread");
|
||||
return *result;
|
||||
}
|
||||
|
||||
LuaToken LuaCoreStack::cktoken(LuaSlot s, const char *argname) const {
|
||||
auto result = trytoken(s);
|
||||
if (!result) argerr(argname, "token");
|
||||
return *result;
|
||||
}
|
||||
|
||||
util::DXYZ LuaCoreStack::ckxyz(LuaSlot s, const char *argname) const {
|
||||
auto result = tryxyz(s);
|
||||
if (!result) argerr(argname, "xyz");
|
||||
return *result;
|
||||
}
|
||||
|
||||
void LuaCoreStack::cktable(LuaSlot s, const char *argname) const {
|
||||
if (!trytable(s)) argerr(argname, "table");
|
||||
}
|
||||
|
||||
void LuaCoreStack::cknil(LuaSlot s, const char *argname) const {
|
||||
if (!trynil(s)) argerr(argname, "nil");
|
||||
}
|
||||
|
||||
void LuaCoreStack::ckfunction(LuaSlot s, const char *argname) const {
|
||||
if (!tryfunction(s)) argerr(argname, "function");
|
||||
}
|
||||
|
||||
void LuaCoreStack::ckcfunction(LuaSlot s, const char *argname) const {
|
||||
if (!trycfunction(s)) argerr(argname, "c-function");
|
||||
}
|
||||
|
||||
void LuaCoreStack::cktangible(LuaSlot s, const char *argname) const {
|
||||
if (!trytangible(s)) argerr(argname, "tangible");
|
||||
}
|
||||
|
||||
|
||||
void LuaCoreStack::clearmetatable(LuaSlot tab) const {
|
||||
lua_pushnil(L_);
|
||||
lua_setmetatable(L_, tab);
|
||||
}
|
||||
|
||||
void LuaCoreStack::setmetatable(LuaSlot tab, LuaSlot mt) const {
|
||||
lua_pushvalue(L_, mt);
|
||||
lua_setmetatable(L_, tab);
|
||||
}
|
||||
|
||||
bool LuaCoreStack::getmetatable(LuaSlot mt, LuaSlot tab) const {
|
||||
if (lua_getmetatable(L_, tab)) {
|
||||
lua_replace(L_, mt);
|
||||
return true;
|
||||
} else {
|
||||
lua_pushnil(L_);
|
||||
lua_replace(L_, mt);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool LuaCoreStack::next(LuaSlot tab, LuaSlot key, LuaSlot value) const {
|
||||
lua_pushvalue(L_, key);
|
||||
int ret = lua_next(L_, tab);
|
||||
if (ret != 0) {
|
||||
lua_replace(L_, value);
|
||||
lua_replace(L_, key);
|
||||
}
|
||||
return (ret != 0);
|
||||
}
|
||||
|
||||
void LuaCoreStack::getglobaltable(LuaSlot target) const {
|
||||
lua_pushglobaltable(L_);
|
||||
lua_replace(L_, target);
|
||||
}
|
||||
|
||||
void LuaCoreStack::newtable(LuaSlot target) const {
|
||||
lua_newtable(L_);
|
||||
lua_replace(L_, target);
|
||||
}
|
||||
|
||||
void LuaCoreStack::createtable(LuaSlot target, int narr, int nrec) const {
|
||||
lua_createtable(L_, narr, nrec);
|
||||
lua_replace(L_, target);
|
||||
}
|
||||
|
||||
lua_State *LuaCoreStack::newthread(LuaSlot target) const {
|
||||
lua_State *result = lua_newthread(L_);
|
||||
lua_replace(L_, target);
|
||||
return result;
|
||||
}
|
||||
|
||||
eng::string LuaCoreStack::classname(LuaSlot input) const {
|
||||
LuaVar lookup, classtab, classname;
|
||||
LuaExtStack LS(L_, lookup, classtab, classname);
|
||||
|
||||
int xt = xtype(input);
|
||||
if (xt == LUA_TSTRING) {
|
||||
LS.rawget(lookup, LuaRegistry, "classes");
|
||||
LS.rawget(classtab, lookup, input);
|
||||
if (xtype(classtab) != LUA_TT_CLASS) {
|
||||
return "";
|
||||
}
|
||||
return LS.ckstring(input);
|
||||
} else if (xt == LUA_TT_TANGIBLE) {
|
||||
if (!LS.getmetatable(lookup, input)) {
|
||||
return "";
|
||||
}
|
||||
LS.rawget(classtab, lookup, "__index");
|
||||
if (xtype(classtab) != LUA_TT_CLASS) {
|
||||
return "";
|
||||
}
|
||||
LS.rawget(lookup, LuaRegistry, "classnames");
|
||||
LS.rawget(classname, lookup, classtab);
|
||||
if (!LS.isstring(classname)) {
|
||||
return "";
|
||||
}
|
||||
return LS.ckstring(classname);
|
||||
} else if (xt == LUA_TT_CLASS) {
|
||||
LS.rawget(lookup, LuaRegistry, "classnames");
|
||||
LS.rawget(classname, lookup, input);
|
||||
if (!LS.isstring(classname)) {
|
||||
return "";
|
||||
}
|
||||
return LS.ckstring(classname);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
static eng::string nonexistentclass(eng::string name) {
|
||||
eng::string err;
|
||||
if (!sv::is_lua_classname(name)) {
|
||||
err = "invalid class name: " + name;
|
||||
} else {
|
||||
err = "not a class: " + name;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
eng::string LuaCoreStack::getclass(LuaSlot classtab, LuaSlot input) const {
|
||||
LuaVar lookup;
|
||||
LuaExtStack LS(L_, lookup);
|
||||
|
||||
int xt = xtype(input);
|
||||
if (xt == LUA_TSTRING) {
|
||||
LS.rawget(lookup, LuaRegistry, "classes");
|
||||
LS.rawget(classtab, lookup, input);
|
||||
if (xtype(classtab) != LUA_TT_CLASS) {
|
||||
eng::string classname = LS.ckstring(input);
|
||||
return nonexistentclass(LS.ckstring(input));
|
||||
}
|
||||
return "";
|
||||
} else if (xt == LUA_TT_TANGIBLE) {
|
||||
if (!LS.getmetatable(lookup, input)) {
|
||||
eng::string err = "inactive tangible has no class";
|
||||
return err;
|
||||
}
|
||||
LS.rawget(classtab, lookup, "__index");
|
||||
if (xtype(classtab) != LUA_TT_CLASS) {
|
||||
eng::string err = "tangible has invalid class";
|
||||
}
|
||||
return "";
|
||||
} else if (xt == LUA_TT_CLASS) {
|
||||
LS.set(classtab, input);
|
||||
return "";
|
||||
} else {
|
||||
eng::string err = "getclass must be passed a string, class, or tangible";
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
eng::string LuaCoreStack::getclass(LuaSlot classtab, std::string_view classname) const {
|
||||
LuaVar lookup;
|
||||
LuaExtStack LS(L_, lookup);
|
||||
LS.rawget(lookup, LuaRegistry, "classes");
|
||||
LS.rawget(classtab, lookup, classname);
|
||||
if (xtype(classtab) != LUA_TT_CLASS) {
|
||||
return nonexistentclass(eng::string(classname));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void LuaCoreStack::makeclass(LuaSlot classtab, LuaSlot classname) const {
|
||||
LuaVar classes, classnames, globtab, cname;
|
||||
LuaExtStack LS(L_, classes, classnames, globtab, cname);
|
||||
|
||||
// Validate the class name.
|
||||
assert(LS.isstring(classname));
|
||||
assert(sv::is_lua_classname(LS.ckstring(classname)));
|
||||
|
||||
// Fetch the classes table from the registry.
|
||||
LS.rawget(classes, LuaRegistry, "classes");
|
||||
assert(LS.istable(classes));
|
||||
|
||||
// Fetch the classnames table from the registry.
|
||||
LS.rawget(classnames, LuaRegistry, "classnames");
|
||||
assert(LS.istable(classnames));
|
||||
|
||||
// Fetch the global environment from the registry.
|
||||
LS.getglobaltable(globtab);
|
||||
|
||||
// Get the classtab from the classes table.
|
||||
LS.rawget(classtab, classes, classname);
|
||||
|
||||
// Make a new table if necessary.
|
||||
if (!LS.istable(classtab)) {
|
||||
LS.newtable(classtab);
|
||||
LS.rawset(classes, classname, classtab);
|
||||
LS.rawset(classnames, classtab, classname);
|
||||
}
|
||||
|
||||
// Put the table into the global environment.
|
||||
LS.rawset(globtab, classname, classtab);
|
||||
|
||||
// Repair the special fields.
|
||||
LS.settabletype(classtab, LUA_TT_CLASS);
|
||||
LS.rawset(classtab, "__index", classtab);
|
||||
}
|
||||
|
||||
void LuaCoreStack::makeclass(LuaSlot tab, std::string_view name) const {
|
||||
push_any_value(name);
|
||||
LuaSpecial classname(lua_gettop(L_));
|
||||
makeclass(tab, classname);
|
||||
lua_pop(L_, 1);
|
||||
}
|
||||
|
||||
void LuaCoreStack::maketan(LuaSlot tab, int64_t id) const {
|
||||
assert(validpositiveinteger(id));
|
||||
|
||||
LuaVar tangibles, metatab;
|
||||
LuaExtStack LS(L_, tangibles, metatab);
|
||||
|
||||
// Try to get the existing tangible.
|
||||
LS.rawget(tangibles, LuaRegistry, "tangibles");
|
||||
LS.rawget(tab, tangibles, id);
|
||||
|
||||
// If we succeeded, return it.
|
||||
if (LS.istable(tab)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the tangible's database and metatable.
|
||||
LS.set(tab, LuaNewTable);
|
||||
LS.set(metatab, LuaNewTable);
|
||||
LS.setmetatable(tab, metatab);
|
||||
|
||||
// Mark the tangible using the tabletype field.
|
||||
LS.settabletype(tab, LUA_TT_TANGIBLE);
|
||||
LS.settabletype(metatab, LUA_TT_TANGIBLEMETA);
|
||||
|
||||
// Store the tangible ID and lock the metatable.
|
||||
LS.rawset(metatab, "id", id);
|
||||
LS.rawset(metatab, "__metatable", false);
|
||||
|
||||
// Store the database into the tangibles table.
|
||||
LS.rawset(tangibles, id, tab);
|
||||
}
|
||||
|
||||
|
||||
bool LuaCoreStack::tanblank(LuaSlot tab) const {
|
||||
bool result = true;
|
||||
if (istable(tab) && gettabletype(tab) == LUA_TT_TANGIBLE) {
|
||||
if (lua_getmetatable(L_, tab.index())) {
|
||||
lua_pushstring(L_, "threads");
|
||||
lua_rawget(L_, -2);
|
||||
if (lua_type(L_, -1) == LUA_TTABLE) {
|
||||
result = false;
|
||||
}
|
||||
lua_pop(L_, 2);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int64_t LuaCoreStack::tanid(LuaSlot tab) const {
|
||||
int64_t result = 0;
|
||||
if (istable(tab) && gettabletype(tab) == LUA_TT_TANGIBLE) {
|
||||
if (lua_getmetatable(L_, tab.index())) {
|
||||
lua_pushstring(L_, "id");
|
||||
lua_rawget(L_, -2);
|
||||
if (lua_type(L_, -1) == LUA_TNUMBER) {
|
||||
result = int64_t(lua_tonumber(L_, -1));
|
||||
}
|
||||
lua_pop(L_, 2);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool LuaCoreStack::tangetclass(LuaSlot classobj, LuaSlot tab) {
|
||||
if (istable(tab) && (gettabletype(tab) == LUA_TT_TANGIBLE) && lua_getmetatable(L_, tab.index())) {
|
||||
lua_pushstring(L_, "__index");
|
||||
lua_rawget(L_, -2);
|
||||
lua_replace(L_, classobj);
|
||||
lua_pop(L_, 1);
|
||||
if (istable(classobj) && (gettabletype(classobj) == LUA_TT_CLASS)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
set(classobj, LuaNil);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LuaCoreStack::issortablekey(LuaSlot s) const {
|
||||
int type = lua_type(L_, s);
|
||||
return (type == LUA_TBOOLEAN) || (type == LUA_TNUMBER) || (type == LUA_TSTRING);
|
||||
}
|
||||
|
||||
void LuaCoreStack::movesortablekey(LuaSlot key, LuaCoreStack &otherstack, LuaSlot otherslot) {
|
||||
int type = lua_type(L_, key);
|
||||
switch (type) {
|
||||
case LUA_TBOOLEAN:
|
||||
lua_pushboolean(otherstack.L_, lua_toboolean(L_, key));
|
||||
lua_replace(otherstack.L_, otherslot);
|
||||
break;
|
||||
case LUA_TNUMBER:
|
||||
lua_pushnumber(otherstack.L_, lua_tonumber(L_, key));
|
||||
lua_replace(otherstack.L_, otherslot);
|
||||
break;
|
||||
case LUA_TSTRING: {
|
||||
size_t len;
|
||||
const char *str = lua_tolstring(L_, key, &len);
|
||||
lua_pushlstring(otherstack.L_, str, len);
|
||||
lua_replace(otherstack.L_, otherslot);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
assert(false && "movesortablekey: not a sortable key");
|
||||
}
|
||||
}
|
||||
|
||||
void LuaCoreStack::cleartable(LuaSlot tab, bool clearmeta) const {
|
||||
assert(istable(tab));
|
||||
lua_pushnil(L_);
|
||||
while (lua_next(L_, tab.index()) != 0) {
|
||||
lua_pop(L_, 1); // Pop the old value.
|
||||
lua_pushvalue(L_, -1); // Clone the key
|
||||
lua_pushnil(L_); // Push the new value.
|
||||
lua_rawset(L_, tab.index());
|
||||
}
|
||||
if (clearmeta) {
|
||||
lua_pushnil(L_);
|
||||
lua_setmetatable(L_, tab.index());
|
||||
}
|
||||
}
|
||||
|
||||
int LuaCoreStack::rawlen(LuaSlot obj) const {
|
||||
return lua_rawlen(L_, obj.index());
|
||||
}
|
||||
|
||||
int LuaCoreStack::nkeys(LuaSlot obj) const {
|
||||
return lua_nkeys(L_, obj.index());
|
||||
}
|
||||
|
||||
int LuaCoreStack::gettabletype(LuaSlot tab) const {
|
||||
uint16_t bits = lua_getflagbits(L_, tab.index());
|
||||
return LUA_TT_GENERAL + (bits & 0x000F);
|
||||
}
|
||||
|
||||
void LuaCoreStack::settabletype(LuaSlot tab, int t) const {
|
||||
assert((t >= LUA_TT_GENERAL) && (t <= LUA_TT_CLASS));
|
||||
int offset = (t - LUA_TT_GENERAL);
|
||||
lua_modflagbits(L_, tab.index(), 0x000F, offset);
|
||||
}
|
||||
|
||||
int LuaCoreStack::xtype(LuaSlot slot) const {
|
||||
int t = lua_type(L_, slot);
|
||||
if (t != LUA_TTABLE) return t;
|
||||
uint16_t bits = lua_getflagbits(L_, slot);
|
||||
return LUA_TT_GENERAL + (bits & 0x000F);
|
||||
}
|
||||
|
||||
bool LuaCoreStack::getvisited(LuaSlot tab) const {
|
||||
uint16_t bits = lua_getflagbits(L_, tab.index());
|
||||
return (bits & 0x0010);
|
||||
}
|
||||
|
||||
void LuaCoreStack::setvisited(LuaSlot tab, bool visited) const {
|
||||
lua_modflagbits(L_, tab.index(), 0x0010, visited ? 0x0010 : 0);
|
||||
}
|
||||
|
||||
WorldType LuaCoreStack::get_world_type() const {
|
||||
lua_pushstring(L_, "worldtype");
|
||||
lua_rawget(L_, LUA_REGISTRYINDEX);
|
||||
lua_Integer n = lua_tointeger(L_, -1);
|
||||
lua_pop(L_, 1);
|
||||
assert(n != 0);
|
||||
return (WorldType)n;
|
||||
}
|
||||
|
||||
void LuaCoreStack::set_world_type(WorldType t) const {
|
||||
lua_pushstring(L_, "worldtype");
|
||||
lua_pushnumber(L_, (int)t);
|
||||
lua_rawset(L_, LUA_REGISTRYINDEX);
|
||||
}
|
||||
|
||||
void LuaCoreStack::guard_nopredict(const char *fn) {
|
||||
if (lua_isyieldable(L_)) {
|
||||
if (!is_authoritative()) {
|
||||
lua_yield(L_, 0);
|
||||
luaL_error(L_, "unexplained nopredict failure in %s", fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LuaKeywordParser::LuaKeywordParser(lua_State *L, int slot) {
|
||||
L_ = L;
|
||||
slot_ = slot;
|
||||
not_table_ = !lua_istable(L_, slot_);
|
||||
if (not_table_) {
|
||||
lua_newtable(L_);
|
||||
lua_replace(L_, slot_);
|
||||
}
|
||||
}
|
||||
|
||||
bool LuaKeywordParser::parse(LuaSlot out, const char *kw) {
|
||||
lua_pushstring(L_, kw);
|
||||
lua_rawget(L_, slot_);
|
||||
lua_replace(L_, out.index());
|
||||
if (!lua_isnil(L_, out.index())) {
|
||||
parsed_.insert(kw);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
eng::string LuaKeywordParser::final_check() {
|
||||
if (not_table_) {
|
||||
return "expected a keyword table";
|
||||
}
|
||||
lua_pushnil(L_);
|
||||
while (lua_next(L_, slot_) != 0) {
|
||||
lua_pop(L_, 1); // Don't need the value.
|
||||
if (!lua_isstring(L_, -1)) {
|
||||
return "keyword table contains non-string key";
|
||||
}
|
||||
const char *kw = lua_tostring(L_, -1);
|
||||
if (parsed_.find(kw) == parsed_.end()) {
|
||||
eng::ostringstream oss;
|
||||
oss << "keyword " << kw << " not known";
|
||||
return oss.str();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void LuaKeywordParser::final_check_throw() {
|
||||
eng::string err = final_check();
|
||||
if (!err.empty()) {
|
||||
luaL_error(L_, "%s", err.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
const char *LuaByteReader::lua_reader(lua_State *L, void *ud, size_t *size) {
|
||||
LuaByteReader *reader = (LuaByteReader*)ud;
|
||||
*size = reader->size_;
|
||||
const char *data = reader->data_;
|
||||
reader->data_ = 0;
|
||||
reader->size_ = 0;
|
||||
return data;
|
||||
}
|
||||
1353
luprex/cpp/core/luastack.hpp
Normal file
1353
luprex/cpp/core/luastack.hpp
Normal file
File diff suppressed because it is too large
Load Diff
1354
luprex/cpp/core/planemap.cpp
Normal file
1354
luprex/cpp/core/planemap.cpp
Normal file
File diff suppressed because it is too large
Load Diff
260
luprex/cpp/core/planemap.hpp
Normal file
260
luprex/cpp/core/planemap.hpp
Normal file
@@ -0,0 +1,260 @@
|
||||
//////////////////////////////////////////////////////////////
|
||||
//
|
||||
// PLANEMAP
|
||||
//
|
||||
// This module defines two classes: PlaneMap, and PlaneItem. A
|
||||
// PlaneItem is an object that has a plane (a string) and an
|
||||
// XYZ position. A PlaneMap keeps track of potentially thousands
|
||||
// of PlaneItem objects, allowing you to search for PlaneItems
|
||||
// by scanning a geographic region.
|
||||
//
|
||||
// CLASS SPRITE DERIVES FROM PLANEITEM
|
||||
//
|
||||
// A PlaneMap records the positions of PlaneItems. The intent is
|
||||
// that class Sprite should derive from PlaneItem. That way, the
|
||||
// PlaneMap can record the positions of sprites.
|
||||
//
|
||||
// When you put derived types like Sprite into a PlaneMap, the
|
||||
// PlaneMap will work fine. However, when you scan the PlaneMap,
|
||||
// it will give return PlaneItem pointers, not Sprite pointers.
|
||||
//
|
||||
// If you're sure that the PlaneMap contains only sprites, then
|
||||
// you can use static_cast to convert those PlaneItem pointers to
|
||||
// Sprite pointers. Sadly, there's no simple way to avoid the casting.
|
||||
//
|
||||
// THE SCANNABLE REGION IS FINITE
|
||||
//
|
||||
// A plane is a rectangle, with a finite size: if you stray more
|
||||
// than 80,000,000 meters from the origin, then that PlaneItem
|
||||
// is beyond the scannable region.
|
||||
//
|
||||
// There's nothing stopping you from moving a PlaneItem outside
|
||||
// the scannable region. It is not an error to do so.
|
||||
// However, if you do move a PlaneItem outside the scannable
|
||||
// region, then that PlaneItem will not show up for scan_radius.
|
||||
//
|
||||
// PLANES CANNOT BE DESTROYED BUT THEY CAN BE UNINHABITED
|
||||
//
|
||||
// You can't "create" or "destroy" planes. Instead, we say
|
||||
// that a plane is "uninhabited" until you put a PlaneItem
|
||||
// there. Initially, all possible planes exist, but they're
|
||||
// all uninhabited until you put a PlaneItem there. Planes
|
||||
// that are uninhabited take no memory.
|
||||
//
|
||||
// THE NOWHERE PLANE
|
||||
//
|
||||
// If you use the literal "nowhere" as a plane name, a special case is
|
||||
// triggered: you're "nowhere." Radius scans can never find items
|
||||
// whose plane is "nowhere." The same also applies when you use the
|
||||
// empty string as a plane name.
|
||||
//
|
||||
// THE Z COORDINATE IS IGNORED
|
||||
//
|
||||
// Class PlaneItem stores X, Y, and Z. The Z coordinate is
|
||||
// ignored for all plane-scanning operations. In other words,
|
||||
// planes are 2D. The only reason we store a Z coordinate
|
||||
// is for the consistency of storing the model's X, Y, and Z
|
||||
// all in the same place.
|
||||
//
|
||||
// MEMORY MANAGEMENT
|
||||
//
|
||||
// PlaneMaps do not own PlaneItems. This is a deliberate choice.
|
||||
// We assume that sprites will be owned by the sprite ID table.
|
||||
// So therefore, the PlaneMaps shouldn't own the sprites.
|
||||
//
|
||||
// So instead, we use this rule: a PlaneMap has a function 'track'
|
||||
// that causes it to start tracking the location of a PlaneItem.
|
||||
// However, the PlaneMap still doesn't own the PlaneItem.
|
||||
// If you destroy a PlaneMap, all the PlaneItems
|
||||
// will automatically be untracked, but they won't be deleted.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef PLANEMAP_HPP
|
||||
#define PLANEMAP_HPP
|
||||
|
||||
#include "wrap-vector.hpp"
|
||||
#include "wrap-map.hpp"
|
||||
#include "wrap-string.hpp"
|
||||
#include "wrap-bytell-hash-map.hpp"
|
||||
#include "util.hpp"
|
||||
#include "luastack.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
class PlaneMap;
|
||||
class PlaneTree;
|
||||
|
||||
|
||||
class PlaneScan : public eng::nevernew {
|
||||
public:
|
||||
friend class PlaneMap;
|
||||
friend class PlaneTree;
|
||||
enum Shape { BOX, CYLINDER, SPHERE };
|
||||
private:
|
||||
// The plane to scan.
|
||||
//
|
||||
// Note: the reason this uses std::string instead of eng::string
|
||||
// is that we want plane scans to not touch the engine heap.
|
||||
//
|
||||
std::string plane_;
|
||||
|
||||
// The bounding box of the scan.
|
||||
util::XYZ center_, radius_;
|
||||
|
||||
// If you scan a cylinder or SPHERE, it actually
|
||||
// scans the bounding box first, then clips out
|
||||
// the parts that aren't correct.
|
||||
Shape shape_;
|
||||
|
||||
// When true, the items are sorted by ID.
|
||||
// WARNING: setting this to false can create
|
||||
// nondeterminism. Scans by lua should always be sorted.
|
||||
// When sorting and 'include_near' are both specified,
|
||||
// the near item is returned in its sorted order.
|
||||
bool sorted_;
|
||||
|
||||
// The near ID is omitted from the results if include_near is false.
|
||||
// The near ID is included in the results if include_near is true.
|
||||
// The near ID is ignored if near is zero.
|
||||
int64_t near_;
|
||||
bool include_near_;
|
||||
|
||||
// If this is true, items on the nowhere plane are not scanned.
|
||||
bool omit_nowhere_;
|
||||
|
||||
public:
|
||||
void clear() {
|
||||
plane_.clear();
|
||||
shape_ = BOX;
|
||||
sorted_ = true;
|
||||
near_ = 0;
|
||||
include_near_ = false;
|
||||
omit_nowhere_ = false;
|
||||
radius_ = util::XYZ();
|
||||
center_ = util::XYZ();
|
||||
}
|
||||
PlaneScan() { clear(); }
|
||||
|
||||
// Convert a lua table into a scan configuration.
|
||||
//
|
||||
// If there is an error in the configuration,
|
||||
// throws a lua error message.
|
||||
//
|
||||
// Caution: if this detects the configuration flag 'near',
|
||||
// then it stores the near ID. However, it doesn't fetch
|
||||
// the center and plane. It is the caller's responsibility
|
||||
// to check if 'near' has been set, and if so, store a center
|
||||
// and plane. This is admittedly ugly, but it eliminates
|
||||
// a dependency on the world module.
|
||||
//
|
||||
void configure(LuaKeywordParser &kw);
|
||||
|
||||
void set_center_and_radius(const util::XYZ ¢er, float r) {
|
||||
set_center(center);
|
||||
set_radius(r);
|
||||
}
|
||||
|
||||
void set_whole_plane() {
|
||||
shape_ = BOX;
|
||||
center_ = util::XYZ();
|
||||
radius_.x = std::numeric_limits<float>::infinity();
|
||||
radius_.y = radius_.z = radius_.x;
|
||||
}
|
||||
|
||||
void set_center(const util::XYZ ¢er) { center_ = center; }
|
||||
void set_radius(const util::XYZ &radius) { radius_ = radius; }
|
||||
void set_radius(float f) { radius_.x = radius_.y = radius_.z = f; }
|
||||
void set_plane(std::string_view p) { plane_ = p; }
|
||||
void set_shape(Shape s) { shape_ = s; }
|
||||
void set_sorted(bool s) { sorted_ = s; }
|
||||
void set_near(int64_t id, bool inc) { near_ = id; include_near_ = inc; }
|
||||
void set_omit_nowhere(bool b) { omit_nowhere_ = b; }
|
||||
|
||||
int64_t near() const { return near_; }
|
||||
|
||||
// Reveal the contents of the PlaneScan as a string.
|
||||
eng::string debug_string() const;
|
||||
};
|
||||
|
||||
class PlaneItem : public eng::nevernew {
|
||||
friend class PlaneTree;
|
||||
friend class PlaneMap;
|
||||
private:
|
||||
PlaneTree *tree_;
|
||||
PlaneItem *prev_;
|
||||
PlaneItem *next_;
|
||||
int64_t id_;
|
||||
eng::string plane_;
|
||||
float x_, y_, z_;
|
||||
|
||||
public:
|
||||
PlaneItem();
|
||||
~PlaneItem();
|
||||
|
||||
// You may modify the ID at any time.
|
||||
void set_id(int64_t id) { id_ = id; }
|
||||
|
||||
int64_t id() const { return id_; }
|
||||
const eng::string &plane() const { return plane_; }
|
||||
const float x() const { return x_; }
|
||||
const float y() const { return y_; }
|
||||
const float z() const { return z_; }
|
||||
|
||||
void track(PlaneMap *pmap);
|
||||
void untrack() { track(nullptr); }
|
||||
|
||||
void set_pos(const eng::string &plane, float x, float y, float z);
|
||||
void set_xyz(float x, float y, float z);
|
||||
};
|
||||
|
||||
class PlaneMap : public eng::nevernew {
|
||||
friend class PlaneItem;
|
||||
friend class PlaneTree;
|
||||
private:
|
||||
float default_radius_;
|
||||
|
||||
// Plane Trees table.
|
||||
//
|
||||
eng::map<eng::string, std::unique_ptr<PlaneTree>, std::less<>> planes_;
|
||||
|
||||
public:
|
||||
// No special code is needed for construction or destruction.
|
||||
PlaneMap();
|
||||
~PlaneMap();
|
||||
|
||||
// The 'insert' and 'remove' operators are inside class
|
||||
// PlaneItem. See PlaneItem::track and PlaneItem::untrack.
|
||||
|
||||
// Scan the PlaneMap for items, return their IDs.
|
||||
//
|
||||
// Note: the reason this uses IdVector instead of IdVector
|
||||
// is that we want scans to not touch the engine heap.
|
||||
//
|
||||
void scan(const PlaneScan &sc, util::IdVector *into) const;
|
||||
|
||||
// Set the default radius for all planes.
|
||||
// Maybe we'll make it adaptive some day.
|
||||
void set_default_radius(float r) { default_radius_ = r; }
|
||||
|
||||
// Return a debug string for the specified plane.
|
||||
// This is for unit testing.
|
||||
eng::string tree_debug_string(const eng::string &plane);
|
||||
eng::string outliers_debug_string(const eng::string &plane);
|
||||
eng::string search_bboxes_debug_string(const PlaneScan &scan);
|
||||
eng::string scan_steps_debug_string(const PlaneScan &scan);
|
||||
|
||||
// Untrack all planeitems. This is for unit testing.
|
||||
void untrack_all();
|
||||
|
||||
private:
|
||||
// unit testing stuff.
|
||||
friend int lfn_unittests_planemap(lua_State *L);
|
||||
};
|
||||
|
||||
|
||||
#endif // PLANEMAP_HPP
|
||||
|
||||
|
||||
352
luprex/cpp/core/pprint.cpp
Normal file
352
luprex/cpp/core/pprint.cpp
Normal file
@@ -0,0 +1,352 @@
|
||||
|
||||
#include <ostream>
|
||||
#include "pprint.hpp"
|
||||
#include "util.hpp"
|
||||
#include "table.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
class PrintMachine {
|
||||
public:
|
||||
LuaVar tabchpos_;
|
||||
LuaExtStack LS_;
|
||||
int next_id_;
|
||||
bool indent_;
|
||||
std::ostream *output_;
|
||||
eng::map<int, int> chpos_to_tabnum_;
|
||||
|
||||
void atomic_print(int xtype, LuaSlot val, bool quote) {
|
||||
switch (xtype) {
|
||||
case LUA_TNIL: {
|
||||
(*output_) << "nil";
|
||||
return;
|
||||
}
|
||||
case LUA_TSTRING: {
|
||||
if (quote) {
|
||||
util::quote_string(LS_.ckstring(val), output_);
|
||||
} else {
|
||||
// TODO: this could be more efficient.
|
||||
(*output_) << LS_.ckstring(val);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case LUA_TNUMBER: {
|
||||
double value = LS_.cknumber(val);
|
||||
if (std::isnan(value)) {
|
||||
(*output_) << "nan";
|
||||
} else {
|
||||
int64_t ivalue = int64_t(value);
|
||||
if (double(ivalue) == value) {
|
||||
(*output_) << ivalue;
|
||||
} else {
|
||||
(*output_) << value;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
case LUA_TBOOLEAN: {
|
||||
(*output_) << (LS_.ckboolean(val) ? "true" : "false");
|
||||
return;
|
||||
}
|
||||
case LUA_TFUNCTION: {
|
||||
(*output_) << "<function>";
|
||||
return;
|
||||
}
|
||||
case LUA_TTHREAD: {
|
||||
(*output_) << "<thread>";
|
||||
return;
|
||||
}
|
||||
case LUA_TLIGHTUSERDATA: {
|
||||
LuaToken token = LS_.cktoken(val);
|
||||
(*output_) << "[" << token.str() << "]";
|
||||
return;
|
||||
}
|
||||
case LUA_TT_GENERAL: {
|
||||
(*output_) << "<table>";
|
||||
return;
|
||||
}
|
||||
case LUA_TT_TANGIBLE: {
|
||||
(*output_) << "<tangible " << LS_.tanid(val) << ">";
|
||||
return;
|
||||
}
|
||||
case LUA_TT_CLASS: {
|
||||
(*output_) << "<class " << LS_.classname(val) << ">";
|
||||
return;
|
||||
}
|
||||
case LUA_TT_GLOBALENV: {
|
||||
(*output_) << "<global-env>";
|
||||
return;
|
||||
}
|
||||
case LUA_TT_TANGIBLEMETA: {
|
||||
(*output_) << "<tangible-metatable>";
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
(*output_) << "<unknown type #" << xtype << ">";
|
||||
return;
|
||||
}}
|
||||
}
|
||||
|
||||
void tabify(int level) {
|
||||
if (indent_) {
|
||||
(*output_) << std::endl;
|
||||
for (int i = 0; i < level; i++) {
|
||||
(*output_) << " ";
|
||||
}
|
||||
} else {
|
||||
(*output_) << " ";
|
||||
}
|
||||
}
|
||||
|
||||
void pprint_r(int level, bool expand, LuaSlot value) {
|
||||
lua_State *L = LS_.state();
|
||||
lua_checkstack(L, 20);
|
||||
LuaVar loffset, pairs, key, val, lchpos, nextseq;
|
||||
LuaExtStack LS(L, loffset, pairs, key, val, lchpos, nextseq);
|
||||
|
||||
// Determine the extended type of the object. If the
|
||||
// expand flag is true, try to coerce it to a general table.
|
||||
int xtype = LS.xtype(value);
|
||||
|
||||
// Print the atomic portion.
|
||||
if (xtype != LUA_TT_GENERAL) {
|
||||
atomic_print(xtype, value, true);
|
||||
if ((xtype < LUA_TT_GENERAL) || (!expand)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Find out whether it has a table number. If necessary,
|
||||
// assign one.
|
||||
int tabnum = 0;
|
||||
LS.rawget(lchpos, tabchpos_, value);
|
||||
auto chpos = LS.tryint(lchpos);
|
||||
if (!chpos) {
|
||||
// First time. Record the character position where the
|
||||
// table first appears in the output stream.
|
||||
LS.rawset(tabchpos_, value, int((*output_).tellp()));
|
||||
} else {
|
||||
tabnum = chpos_to_tabnum_[*chpos];
|
||||
if (tabnum == 0) {
|
||||
// Second time. The table is already in the output,
|
||||
// but it hasn't been assigned a number. Assign one.
|
||||
tabnum = next_id_++;
|
||||
chpos_to_tabnum_[*chpos] = tabnum;
|
||||
}
|
||||
}
|
||||
|
||||
// If the table has a number, that means we're visiting
|
||||
// it for the second time. Just print an abbreviated version
|
||||
// and return.
|
||||
if (tabnum > 0) {
|
||||
(*output_) << "<table " << tabnum << ">";
|
||||
if (lua_nkeys(L, value.index())==0) {
|
||||
(*output_) << "{}";
|
||||
} else {
|
||||
(*output_) << "{...}";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// State variables.
|
||||
bool needcomma = false;
|
||||
bool multiline = false;
|
||||
LS.set(nextseq, 1);
|
||||
|
||||
// Open the brackets.
|
||||
(*output_) << "{";
|
||||
|
||||
// Output the table keys.
|
||||
table_getpairs(LS, value, pairs, true);
|
||||
for (int i = 2; ; i+=2) {
|
||||
LS.rawget(key, pairs, i);
|
||||
if (LS.isnil(key)) break;
|
||||
LS.rawget(val, pairs, i+1);
|
||||
if (needcomma) (*output_) << ",";
|
||||
needcomma = true;
|
||||
if (LS.rawequal(key, nextseq)) {
|
||||
(*output_) << " ";
|
||||
pprint_r(level + 1, false, val);
|
||||
LS.set(nextseq, LS.ckinteger(nextseq) + 1);
|
||||
} else {
|
||||
multiline = true;
|
||||
tabify(level + 1);
|
||||
if (LS.isstring(key) && sv::is_lua_id(LS.ckstring(key))) {
|
||||
(*output_) << LS.ckstring(key);
|
||||
} else {
|
||||
(*output_) << "[";
|
||||
pprint_r(level + 1, false, key);
|
||||
(*output_) << "]";
|
||||
}
|
||||
if (indent_) {
|
||||
(*output_) << " = ";
|
||||
} else {
|
||||
(*output_) << "=";
|
||||
}
|
||||
pprint_r(level + 1, false, val);
|
||||
}
|
||||
}
|
||||
|
||||
// Output the metatable.
|
||||
LS.getmetatable(val, value);
|
||||
if (LS.istable(val) && (LS.gettabletype(val) != LUA_TT_TANGIBLEMETA)) {
|
||||
multiline = true;
|
||||
if (needcomma) (*output_) << ",";
|
||||
needcomma = true;
|
||||
tabify(level + 1);
|
||||
(*output_) << "<meta> = ";
|
||||
pprint_r(level + 1, false, val);
|
||||
}
|
||||
|
||||
// Close the brackets.
|
||||
if (multiline) {
|
||||
tabify(level);
|
||||
} else if (LS.ckinteger(nextseq) > 1) {
|
||||
(*output_) << " ";
|
||||
}
|
||||
(*output_) << "}";
|
||||
}
|
||||
|
||||
// Atomic print interface.
|
||||
PrintMachine(LuaCoreStack &LS0, LuaSlot root, bool quote, std::ostream *os) :
|
||||
LS_(LS0.state(), tabchpos_) {
|
||||
output_ = os;
|
||||
atomic_print(LS_.xtype(root), root, quote);
|
||||
}
|
||||
|
||||
// Pretty print interface.
|
||||
PrintMachine(LuaCoreStack &LS0, LuaSlot root, bool indent, int level, bool expand, std::ostream *os) :
|
||||
LS_(LS0.state(), tabchpos_) {
|
||||
next_id_ = 1;
|
||||
indent_ = indent;
|
||||
LS_.newtable(tabchpos_);
|
||||
util::ostringstream preoutput;
|
||||
output_ = &preoutput;
|
||||
pprint_r(level, expand, root);
|
||||
std::string_view pre = preoutput.view();
|
||||
|
||||
// Output the results. We would just copy the characters
|
||||
// one by one to the target stream, except that we have to
|
||||
// insert <table XX> in front of all tables that got referenced.
|
||||
chpos_to_tabnum_.emplace(0x7FFFFFFF, 0);
|
||||
auto iter = chpos_to_tabnum_.begin();
|
||||
for (int i = 0; i < int(pre.size()); i++) {
|
||||
if (i == iter->first) {
|
||||
(*os) << "<table " << iter->second << ">";
|
||||
iter++;
|
||||
}
|
||||
(*os).put(pre[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void PrettyPrintOptions::parse(LuaKeywordParser &kp) {
|
||||
LuaVar option;
|
||||
LuaExtStack LS(kp.state(), option);
|
||||
if (kp.parse(option, "indent")) {
|
||||
indent = LS.ckboolean(option);
|
||||
}
|
||||
if (kp.parse(option, "level")) {
|
||||
level = LS.ckint(option);
|
||||
}
|
||||
if (kp.parse(option, "expand")) {
|
||||
expand = LS.ckboolean(option);
|
||||
}
|
||||
}
|
||||
|
||||
void atomic_print(LuaCoreStack &LS, LuaSlot val, bool quote, std::ostream *os) {
|
||||
PrintMachine pm(LS, val, quote, os);
|
||||
}
|
||||
|
||||
void pprint(LuaCoreStack &LS, LuaSlot val, const PrettyPrintOptions &opts, std::ostream *os) {
|
||||
PrintMachine pm(LS, val, opts.indent, opts.level, opts.expand, os);
|
||||
}
|
||||
|
||||
LuaDefine(string_pprint, "obj1, obj2, ...",
|
||||
"|Pretty-print the specified objects into a string."
|
||||
"|"
|
||||
"|See also: string.pprintx, which has a lot more options."
|
||||
"|This function uses the default options: pretty print indented,"
|
||||
"|start at indentation level zero, and always expand the"
|
||||
"|top-level table."
|
||||
"|") {
|
||||
LuaRet result;
|
||||
LuaExtraArgs extra;
|
||||
LuaDefStack LS(L, result, extra);
|
||||
|
||||
util::ostringstream oss;
|
||||
for (int i = 0; i < extra.size(); i++) {
|
||||
pprint(LS, extra[i], PrettyPrintOptions(), &oss);
|
||||
oss << "\n";
|
||||
}
|
||||
LS.set(result, oss.view());
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(string_pprintx, "options",
|
||||
"|Pretty-print the specified object into a string, with options"
|
||||
"|"
|
||||
"|Options is a table with these fields:"
|
||||
"|"
|
||||
"| value - the object to pretty-print"
|
||||
"| indent - if false, suppress newlines and indentation (default: true)"
|
||||
"| level - base level of indentation (default: zero)"
|
||||
"| expand - if true, force expansion of top-level table (default: false)"
|
||||
"|"
|
||||
"|About the expand flag: normally, when you print a class, it just "
|
||||
"|prints '<class name>', and when you print a tangible, it just"
|
||||
"|prints '<tangible id>'. But sometimes, you want to see the details."
|
||||
"|The expand flag forces it to expand the top-level table, even if the"
|
||||
"|top-level table is a tangible or class."
|
||||
"|") {
|
||||
LuaArg loptions;
|
||||
LuaRet result;
|
||||
LuaVar value;
|
||||
LuaDefStack LS(L, loptions, result, value);
|
||||
PrettyPrintOptions options;
|
||||
LuaKeywordParser kp(LS, loptions);
|
||||
options.parse(kp);
|
||||
if (!kp.parse(value, "value")) {
|
||||
LS.set(value, LuaNil);
|
||||
}
|
||||
kp.final_check_throw();
|
||||
util::ostringstream oss;
|
||||
pprint(LS, value, options, &oss);
|
||||
LS.set(result, oss.view());
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(string_print, "obj",
|
||||
"|Concise print the specified object into a string"
|
||||
"|"
|
||||
"|This prints a concise representation of obj into a string. Tables"
|
||||
"|are not expanded: for that, use string.pprint or string.pprintx."
|
||||
"|"
|
||||
"|The functions string.print and tostring are identical."
|
||||
"|") {
|
||||
LuaArg val;
|
||||
LuaRet result;
|
||||
LuaDefStack LS(L, val, result);
|
||||
eng::ostringstream oss;
|
||||
atomic_print(LS, val, false, &oss);
|
||||
LS.set(result, oss.str());
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefineAlias(tostring, string_print);
|
||||
|
||||
LuaDefine(string_isidentifier, "str", "return true if the string is a valid lua identifier") {
|
||||
LuaArg str;
|
||||
LuaRet result;
|
||||
LuaDefStack LS(L, str, result);
|
||||
if (LS.isstring(str)) {
|
||||
eng::string s = LS.ckstring(str);
|
||||
LS.set(result, sv::is_lua_id(s));
|
||||
} else {
|
||||
LS.set(result, false);
|
||||
}
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
|
||||
|
||||
45
luprex/cpp/core/pprint.hpp
Normal file
45
luprex/cpp/core/pprint.hpp
Normal file
@@ -0,0 +1,45 @@
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// print, pprint, and tostring
|
||||
//
|
||||
// This module implements the heart of the lua 'print', lua 'pprint', and lua
|
||||
// 'tostring' functions. Note that we have to override the lua builtins 'print'
|
||||
// and 'tostring' for two reasons:
|
||||
//
|
||||
// * We need to suppress the printing of table addresses, for determinism.
|
||||
// * We need to channel the output to a PrintBuffer in the world model.
|
||||
//
|
||||
// Note that the actual lua 'print' and 'pprint' routines aren't defined in this
|
||||
// module, they're in the World module, because they send their output into the
|
||||
// PrintBuffer of the world model. But all the tricky code to implement 'print'
|
||||
// and 'pprint' are in this module.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef PPRINT_HPP
|
||||
#define PPRINT_HPP
|
||||
|
||||
#include "luastack.hpp"
|
||||
#include <ostream>
|
||||
|
||||
struct PrettyPrintOptions {
|
||||
bool indent;
|
||||
int level;
|
||||
bool expand;
|
||||
PrettyPrintOptions() : indent(true), level(0), expand(true) {}
|
||||
void parse(LuaKeywordParser &kp);
|
||||
};
|
||||
|
||||
// Atomic print to a stream.
|
||||
//
|
||||
// This prints an atomic value to a stream. If you give it a table,
|
||||
// it just prints "<table>". This routine is the heart of the lua
|
||||
// primitives 'print' and 'tostring'.
|
||||
//
|
||||
void atomic_print(LuaCoreStack &LS, LuaSlot val, bool quote, std::ostream *os);
|
||||
|
||||
// Pretty print to a stream.
|
||||
//
|
||||
void pprint(LuaCoreStack &LS, LuaSlot val, const PrettyPrintOptions &opts, std::ostream *os);
|
||||
|
||||
#endif // PPRINT_HPP
|
||||
278
luprex/cpp/core/printbuffer.cpp
Normal file
278
luprex/cpp/core/printbuffer.cpp
Normal file
@@ -0,0 +1,278 @@
|
||||
#include "wrap-sstream.hpp"
|
||||
|
||||
#include "printbuffer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cinttypes>
|
||||
|
||||
struct PrintBufferCore : public eng::opnew {
|
||||
// The most recent lines printed.
|
||||
eng::deque<eng::string> lines_;
|
||||
|
||||
// Line number of the first line in the buffer. From this, all other
|
||||
// line numbers can be inferred.
|
||||
int first_line_;
|
||||
|
||||
// Line number of the first unchecked line in the buffer. All line
|
||||
// numbers including this one and beyond are unchecked.
|
||||
// "Unchecked" lines are lines in a non-authoritative model
|
||||
// that haven't been verified through difference transmission.
|
||||
|
||||
int first_unchecked_;
|
||||
|
||||
// Constructor.
|
||||
PrintBufferCore() : first_line_(0), first_unchecked_(0) {}
|
||||
};
|
||||
|
||||
static PrintBufferCore shared_core;
|
||||
|
||||
PrintBuffer::PrintBuffer() {
|
||||
core_ = &shared_core;
|
||||
}
|
||||
|
||||
PrintBuffer::~PrintBuffer() {
|
||||
if (core_ != &shared_core) delete core_;
|
||||
}
|
||||
|
||||
int PrintBuffer::first_line() const {
|
||||
return core_->first_line_;
|
||||
}
|
||||
|
||||
int PrintBuffer::last_line() const {
|
||||
return core_->first_line_ + core_->lines_.size();
|
||||
}
|
||||
|
||||
int PrintBuffer::first_unchecked() const {
|
||||
return core_->first_unchecked_;
|
||||
}
|
||||
|
||||
bool PrintBuffer::never_printed() const {
|
||||
return (core_->first_line_==0) && (core_->first_unchecked_==0) && (core_->lines_.size()==0);
|
||||
}
|
||||
|
||||
const eng::string &PrintBuffer::nth(int n) const {
|
||||
return core_->lines_[n - core_->first_line_];
|
||||
}
|
||||
|
||||
eng::string PrintBuffer::debug_string() const {
|
||||
eng::ostringstream oss;
|
||||
oss << core_->first_line_ << "," << core_->first_unchecked_ << ":";
|
||||
for (int i = 0; i < int(core_->lines_.size()); i++) {
|
||||
oss << core_->lines_[i] << ";";
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
void PrintBuffer::clear() {
|
||||
if (core_ != &shared_core) {
|
||||
delete core_;
|
||||
core_ = &shared_core;
|
||||
}
|
||||
}
|
||||
|
||||
static int first_line_len(const char *text, int len) {
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (text[i] == '\n') return i;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
static void add_line(PrintBufferCore *core, const char *text, int len, bool auth) {
|
||||
assert(core != &shared_core);
|
||||
core->lines_.emplace_back(text, len);
|
||||
if (auth) {
|
||||
core->first_unchecked_ = core->first_line_ + int(core->lines_.size());
|
||||
}
|
||||
}
|
||||
|
||||
void PrintBuffer::add_string(const eng::string &s, bool auth) {
|
||||
const char *text = s.c_str();
|
||||
int len = s.size();
|
||||
if (core_ == &shared_core) core_ = new PrintBufferCore;
|
||||
while (true) {
|
||||
int fll = first_line_len(text, len);
|
||||
if (fll == len) {
|
||||
if (len > 0) {
|
||||
add_line(core_, text, len, auth);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
add_line(core_, text, fll, auth);
|
||||
text += (fll + 1);
|
||||
len -= (fll + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PrintBuffer::discard_upto(int n) {
|
||||
if (core_ == &shared_core) return;
|
||||
while ((!core_->lines_.empty()) && (core_->first_line_ < n)) {
|
||||
core_->lines_.pop_front();
|
||||
core_->first_line_ += 1;
|
||||
}
|
||||
if (core_->first_unchecked_ < core_->first_line_) {
|
||||
core_->first_unchecked_ = core_->first_line_;
|
||||
}
|
||||
}
|
||||
|
||||
void PrintBuffer::serialize(StreamBuffer *sb) const {
|
||||
if (never_printed()) {
|
||||
sb->write_bool(true);
|
||||
} else {
|
||||
sb->write_bool(false);
|
||||
sb->write_uint32(core_->first_line_);
|
||||
sb->write_uint32(core_->first_unchecked_);
|
||||
sb->write_uint32(core_->lines_.size());
|
||||
for (const eng::string &s : core_->lines_) {
|
||||
sb->write_string(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PrintBuffer::deserialize(StreamBuffer *sb) {
|
||||
bool never_printed = sb->read_bool();
|
||||
if (never_printed) {
|
||||
clear();
|
||||
} else {
|
||||
if (core_ == &shared_core) core_ = new PrintBufferCore;
|
||||
core_->first_line_ = sb->read_uint32();
|
||||
core_->first_unchecked_ = sb->read_uint32();
|
||||
int nlines = sb->read_uint32();
|
||||
core_->lines_.clear();
|
||||
for (int i = 0; i < nlines; i++) {
|
||||
core_->lines_.push_back(sb->read_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PrintBuffer::diff(const PrintBuffer &auth, StreamBuffer *sb) const {
|
||||
// Currently, the implementation is simple. The synchronous model discards
|
||||
// all prediction lines from its buffer, then the server retransmits all
|
||||
// those lines. So this barely counts as difference transmission - it's
|
||||
// just transmission, regardless of what was in the synchronous model. I
|
||||
// think that's okay for the text console.
|
||||
// Ask the client to discard anything that precedes auth_first.
|
||||
sb->write_int32(auth.first_line());
|
||||
sb->write_int32(auth.last_line());
|
||||
for (int i = core_->first_unchecked_; i < auth.last_line(); i++) {
|
||||
eng::string line;
|
||||
if (i >= auth.first_line()) line = auth.nth(i);
|
||||
sb->write_string(line);
|
||||
}
|
||||
}
|
||||
|
||||
void PrintBuffer::patch(StreamBuffer *sb, DebugCollector *dbc) {
|
||||
DebugBlock dbb(dbc, "PrintBuffer::patch");
|
||||
if (core_ == &shared_core) core_ = new PrintBufferCore;
|
||||
int auth_first = sb->read_int32();
|
||||
int auth_last = sb->read_int32();
|
||||
core_->lines_.resize(core_->first_unchecked_ - core_->first_line_);
|
||||
int nlines = auth_last - core_->first_unchecked_;
|
||||
if (nlines > 0) DebugLine(dbc) << "PrintBuffer received " << nlines << " lines.";
|
||||
for (int i = core_->first_unchecked_; i < auth_last; i++) {
|
||||
core_->lines_.emplace_back(sb->read_string());
|
||||
}
|
||||
core_->first_unchecked_ = core_->first_line_ + core_->lines_.size();
|
||||
discard_upto(auth_first);
|
||||
if (core_->first_line_ < auth_first) {
|
||||
core_->first_line_ = auth_first;
|
||||
}
|
||||
}
|
||||
|
||||
bool PrintChanneler::channel(const PrintBuffer *printbuffer, std::ostream &ostream) {
|
||||
if (printbuffer == nullptr) return false;
|
||||
if (printbuffer->first_line() > line_) {
|
||||
line_ = printbuffer->first_line();
|
||||
}
|
||||
while (line_ < printbuffer->first_unchecked()) {
|
||||
ostream << "|" << printbuffer->nth(line_) << std::endl;
|
||||
line_ += 1;
|
||||
}
|
||||
return line_ > printbuffer->first_line();
|
||||
}
|
||||
|
||||
Invocation PrintChanneler::invocation(int64_t actor_id) {
|
||||
char buf[80];
|
||||
sprintf(buf, "%" PRId64, line_);
|
||||
return Invocation(InvocationKind::FLUSH_PRINTS, actor_id, actor_id, buf);
|
||||
}
|
||||
|
||||
LuaDefine(unittests_printbuffer, "", "some unit tests") {
|
||||
PrintBuffer pbm;
|
||||
PrintBuffer pbs;
|
||||
StreamBuffer sb;
|
||||
|
||||
// Test authoritative add_string and discard_upto.
|
||||
LuaAssertStrEq(L, pbm.debug_string(), "0,0:");
|
||||
pbm.add_string("foo\nbar\nbaz\n", true);
|
||||
LuaAssertStrEq(L, pbm.debug_string(), "0,3:foo;bar;baz;");
|
||||
pbm.add_string("a\nb\nc", true);
|
||||
LuaAssertStrEq(L, pbm.debug_string(), "0,6:foo;bar;baz;a;b;c;");
|
||||
pbm.discard_upto(2);
|
||||
LuaAssertStrEq(L, pbm.debug_string(), "2,6:baz;a;b;c;");
|
||||
pbm.discard_upto(8);
|
||||
LuaAssertStrEq(L, pbm.debug_string(), "6,6:");
|
||||
|
||||
// Test nonauthoritative add_string and discard_upto.
|
||||
LuaAssertStrEq(L, pbs.debug_string(), "0,0:");
|
||||
pbs.add_string("foo\nbar\nbaz\n", false);
|
||||
LuaAssertStrEq(L, pbs.debug_string(), "0,0:foo;bar;baz;");
|
||||
pbs.add_string("a\nb\nc", false);
|
||||
LuaAssertStrEq(L, pbs.debug_string(), "0,0:foo;bar;baz;a;b;c;");
|
||||
pbs.discard_upto(2);
|
||||
LuaAssertStrEq(L, pbs.debug_string(), "2,2:baz;a;b;c;");
|
||||
pbs.discard_upto(8);
|
||||
LuaAssertStrEq(L, pbs.debug_string(), "6,6:");
|
||||
|
||||
// Test serialization and deserialization of a nonempty printbuffer.
|
||||
pbm.clear();
|
||||
sb.clear();
|
||||
pbm.add_string("boy\nhowdy\nyeah\n", true);
|
||||
pbm.add_string("baby\nget\ndown\n", false);
|
||||
LuaAssertStrEq(L, pbm.debug_string(), "0,3:boy;howdy;yeah;baby;get;down;");
|
||||
pbm.serialize(&sb);
|
||||
pbs.deserialize(&sb);
|
||||
LuaAssertStrEq(L, pbs.debug_string(), "0,3:boy;howdy;yeah;baby;get;down;");
|
||||
|
||||
// Test serialization and deserialization of an empty printbuffer.
|
||||
pbm.clear();
|
||||
sb.clear();
|
||||
LuaAssert(L, pbm.never_printed());
|
||||
pbm.serialize(&sb);
|
||||
pbs.deserialize(&sb);
|
||||
LuaAssert(L, pbs.never_printed());
|
||||
|
||||
pbm.clear();
|
||||
pbs.clear();
|
||||
sb.clear();
|
||||
pbm.add_string("foo\nbar\n", true);
|
||||
pbs.diff(pbm, &sb);
|
||||
pbs.patch(&sb, nullptr);
|
||||
LuaAssertStrEq(L, pbs.debug_string(), "0,2:foo;bar;");
|
||||
pbm.clear();
|
||||
pbm.add_string("foo\nyow\nding\ndong\n", true);
|
||||
pbs.diff(pbm, &sb);
|
||||
pbs.patch(&sb, nullptr);
|
||||
LuaAssertStrEq(L, pbs.debug_string(), "0,4:foo;bar;ding;dong;");
|
||||
pbs.discard_upto(2);
|
||||
LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;");
|
||||
pbs.diff(pbm, &sb);
|
||||
pbs.patch(&sb, nullptr);
|
||||
LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;");
|
||||
pbs.add_string("boy\nhowdy\n", false);
|
||||
LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;boy;howdy;");
|
||||
pbs.diff(pbm, &sb);
|
||||
pbs.patch(&sb, nullptr);
|
||||
LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;");
|
||||
pbs.add_string("boy\nhowdy\nyeah\nbaby\nget\ndown\n", false);
|
||||
LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;boy;howdy;yeah;baby;get;down;");
|
||||
pbs.discard_upto(5);
|
||||
LuaAssertStrEq(L, pbs.debug_string(), "5,5:howdy;yeah;baby;get;down;");
|
||||
pbs.diff(pbm, &sb);
|
||||
pbs.patch(&sb, nullptr);
|
||||
LuaAssertStrEq(L, pbs.debug_string(), "5,5:");
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
164
luprex/cpp/core/printbuffer.hpp
Normal file
164
luprex/cpp/core/printbuffer.hpp
Normal file
@@ -0,0 +1,164 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// PRINTBUFFER
|
||||
//
|
||||
// Lua has a 'print' statement - in client-server mode, where does the output of
|
||||
// the 'print' statement go? We need to be able to handle print-statements on
|
||||
// the server (and forward them to the client), and we need for predictive
|
||||
// prints to be handled gracefully.
|
||||
//
|
||||
// Class PrintBuffer is a buffer for storing the output of the print statement.
|
||||
// It is part of the actor tangible. When a lua thread calls 'print', the print
|
||||
// goes into stringstream lthread_prints. When the thread stops or yields, the
|
||||
// contents of lthread_prints are converted to lines and transferred to the
|
||||
// PrintBuffer of the actor. From there, it gets difference transmitted, and the
|
||||
// client can probe it.
|
||||
//
|
||||
// Suppose, for example, that the player logs in and invokes a plan that prints
|
||||
// six lines from a Robert Frost poem. That plan is executed by both the master
|
||||
// model and the synchronous model. When the thread finishes, the PrintBuffer
|
||||
// in the actor in the master model will contain this:
|
||||
//
|
||||
// * Line 0: Whose woods these are I think I know. [authoritative]
|
||||
// * Line 1: His house is in the village though; [authoritative]
|
||||
// * Line 2: He will not see me stopping here [authoritative]
|
||||
// * Line 3: To watch his woods fill up with snow. [authoritative]
|
||||
// * Line 4: My little horse must think it queer [authoritative]
|
||||
// * Line 5: To stop without a farmhouse near. [authoritative]
|
||||
//
|
||||
// Note that the buffer stores line numbers, which start from zero the moment
|
||||
// the player logs in. In the master model, all lines are always authoritative
|
||||
// because everything in the master model is authoritative. In the synchronous
|
||||
// model, the PrintBuffer is likely to contain the same strings, but the lines
|
||||
// will all be marked as [prediction] instead of [authoritative].
|
||||
//
|
||||
// When the server difference transmits, the difference transmission will fix up
|
||||
// any mistakes in the PrintBuffer in the synchronous model. In the process, it
|
||||
// will also fix up any [prediction] changing it to [authoritative].
|
||||
//
|
||||
// Periodically, the oldest lines in the buffer will get discarded. More on
|
||||
// this later. When lines get discarded, the line numbers don't change. For
|
||||
// example, if we were to discard three lines from the buffer above, this is
|
||||
// what would remain:
|
||||
//
|
||||
// * Line 3: To watch his woods fill up with snow. [authoritative]
|
||||
// * Line 4: My little horse must think it queer [authoritative]
|
||||
// * Line 5: To stop without a farmhouse near. [authoritative]
|
||||
//
|
||||
// The client will periodically probe the PrintBuffer. Suppose it sees all six
|
||||
// lines of the Robert Frost poem. The next time it probes the buffer, it will
|
||||
// still see those same six lines. The client will continue to see those six
|
||||
// lines until it takes explicit steps.
|
||||
//
|
||||
// Here's how we keep the buffer from growing forever. The client probes the
|
||||
// PrintBuffer, and sees some authoritative lines. The client downloads and
|
||||
// stores those lines locally. Once the lines are stored locally in the client,
|
||||
// the client injects a command into the command stream: "delete from
|
||||
// PrintBuffer up to line N" into the command stream. This will cause the
|
||||
// engine to discard the lines that the client no longer needs.
|
||||
//
|
||||
// Note that when the client injects a "delete from PrintBuffer" into the
|
||||
// command stream, that's an invoke-command that has to follow the same process
|
||||
// as any other client invoke command. It can take time for it to get
|
||||
// processed. Therefore, the client must be prepared that it might see some
|
||||
// redundant lines for a little while.
|
||||
//
|
||||
// MEMORY EFFICIENCY.
|
||||
//
|
||||
// Every tangible will contain a printbuffer. To avoid memory waste, the
|
||||
// implementation of PrintBuffer stores everything inside a dynamically
|
||||
// allocated "core" struct. All printbuffers that contain nothing share a
|
||||
// common empty core. That way, the empty printbuffers that will exist in 99% of
|
||||
// all tangibles will take up very little space.
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
#ifndef PRINTBUFFER_HPP
|
||||
#define PRINTBUFFER_HPP
|
||||
|
||||
#include "wrap-deque.hpp"
|
||||
#include "wrap-string.hpp"
|
||||
#include <ostream>
|
||||
#include "streambuffer.hpp"
|
||||
#include "util.hpp"
|
||||
#include "invocation.hpp"
|
||||
#include "debugcollector.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
struct PrintBufferCore;
|
||||
|
||||
class PrintBuffer : public eng::nevernew {
|
||||
private:
|
||||
PrintBufferCore *core_;
|
||||
|
||||
public:
|
||||
PrintBuffer();
|
||||
~PrintBuffer();
|
||||
|
||||
// Get the current first line.
|
||||
int first_line() const;
|
||||
|
||||
// Return the line number beyond the end of the buffer.
|
||||
int last_line() const;
|
||||
|
||||
// Get the current first unchecked line.
|
||||
// "Unchecked" lines are lines in a non-authoritative model
|
||||
// that haven't been verified through difference transmission.
|
||||
// Guaranteed to be between first_line and last_line inclusive.
|
||||
int first_unchecked() const;
|
||||
|
||||
// Return true if the printbuffer is in the initial state.
|
||||
// Note: if you clear the buffer, it's back in the initial state.
|
||||
bool never_printed() const;
|
||||
|
||||
// Get the specified line number.
|
||||
const eng::string &nth(int n) const;
|
||||
|
||||
// Print the entire contents of the buffer to a string (for unit testing).
|
||||
eng::string debug_string() const;
|
||||
|
||||
// Clear the buffer
|
||||
void clear();
|
||||
|
||||
// Add a string. If the string doesn't end in a newline, a newline
|
||||
// is added. The string is broken into lines, and the lines are added
|
||||
// to the PrintBuffer.
|
||||
void add_string(const eng::string &s, bool auth);
|
||||
|
||||
// Discard lines up to but not including line N.
|
||||
void discard_upto(int n);
|
||||
|
||||
// Serialization and deserialization
|
||||
void serialize(StreamBuffer *sb) const;
|
||||
void deserialize(StreamBuffer *sb);
|
||||
|
||||
// Difference transmission
|
||||
void diff(const PrintBuffer &auth, StreamBuffer *sb) const;
|
||||
void patch(StreamBuffer *sb, DebugCollector *dbc);
|
||||
};
|
||||
|
||||
|
||||
class PrintChanneler : public eng::nevernew {
|
||||
private:
|
||||
int64_t line_;
|
||||
public:
|
||||
PrintChanneler() { line_ = 0; }
|
||||
|
||||
// Reset the print channeler.
|
||||
void reset() { line_ = 0; }
|
||||
|
||||
// Copy any new lines from the printbuffer to the stdostream.
|
||||
// Update the current line number. Return true if the printbuffer
|
||||
// contains any lines that have already been channeled.
|
||||
bool channel(const PrintBuffer *pb, std::ostream &ostream);
|
||||
|
||||
// Generate an invocation that removes unnecessary lines from the
|
||||
// printbuffer.
|
||||
Invocation invocation(int64_t actor_id);
|
||||
};
|
||||
|
||||
|
||||
#endif // PRINTBUFFER_HPP
|
||||
|
||||
107
luprex/cpp/core/sched.cpp
Normal file
107
luprex/cpp/core/sched.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
|
||||
#include "wrap-sstream.hpp"
|
||||
|
||||
#include "sched.hpp"
|
||||
#include "streambuffer.hpp"
|
||||
#include "luastack.hpp"
|
||||
|
||||
#include <ostream>
|
||||
|
||||
bool SchedEntry::operator < (const SchedEntry &other) const {
|
||||
if (clock_ < other.clock_) return true;
|
||||
if (clock_ > other.clock_) return false;
|
||||
if (thread_id_ < other.thread_id_) return true;
|
||||
if (thread_id_ > other.thread_id_) return false;
|
||||
if (place_id_ < other.place_id_) return true;
|
||||
if (place_id_ > other.place_id_) return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
eng::string SchedEntry::debug_string() const {
|
||||
eng::ostringstream oss;
|
||||
oss << "(" << clock_ << "," << thread_id_ << "," << place_id_ << ")";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
void Schedule::add(int64_t clk, int64_t thid, int64_t plid) {
|
||||
assert(plid != 0);
|
||||
assert(thid != 0);
|
||||
schedule_.insert(SchedEntry(clk, thid, plid));
|
||||
}
|
||||
|
||||
bool Schedule::ready(int64_t clk) const {
|
||||
return (!schedule_.empty()) && (schedule_.begin()->clock() <= clk);
|
||||
}
|
||||
|
||||
SchedEntry Schedule::pop() {
|
||||
assert(!schedule_.empty());
|
||||
SchedEntry result = *schedule_.begin();
|
||||
schedule_.erase(schedule_.begin());
|
||||
return result;
|
||||
}
|
||||
|
||||
eng::string Schedule::debug_string() {
|
||||
eng::ostringstream oss;
|
||||
for (const SchedEntry &se : schedule_) {
|
||||
oss << se.debug_string();
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
void Schedule::serialize(StreamBuffer *sb) {
|
||||
sb->write_uint32(schedule_.size());
|
||||
for (const SchedEntry &entry : schedule_) {
|
||||
sb->write_int64(entry.clock_);
|
||||
sb->write_int64(entry.thread_id_);
|
||||
sb->write_int64(entry.place_id_);
|
||||
}
|
||||
}
|
||||
|
||||
void Schedule::deserialize(StreamBuffer *sb) {
|
||||
schedule_.clear();
|
||||
size_t nentry = sb->read_uint32();
|
||||
for (size_t i = 0; i < nentry; i++) {
|
||||
int64_t clock = sb->read_int64();
|
||||
int64_t thread_id = sb->read_int64();
|
||||
int64_t place_id = sb->read_int64();
|
||||
add(clock, thread_id, place_id);
|
||||
}
|
||||
}
|
||||
|
||||
LuaDefine(unittests_scheduler, "", "some unit tests") {
|
||||
Schedule s, xs;
|
||||
StreamBuffer sb;
|
||||
|
||||
// Put some stuff into a scheduler.
|
||||
s.add(1, 5, 3);
|
||||
s.add(4, 3, 2);
|
||||
s.add(1, 3, 2);
|
||||
s.add(4, 2, 3);
|
||||
|
||||
// Test serialize and deserialize.
|
||||
LuaAssert(L, s.debug_string() == "(1,3,2)(1,5,3)(4,2,3)(4,3,2)");
|
||||
s.serialize(&sb);
|
||||
xs.deserialize(&sb);
|
||||
LuaAssert(L, xs.debug_string() == "(1,3,2)(1,5,3)(4,2,3)(4,3,2)");
|
||||
|
||||
// Verify that pop, ready, and empty do the right thing.
|
||||
LuaAssert(L, s.ready(0) == false)
|
||||
LuaAssert(L, s.ready(1) == true)
|
||||
LuaAssert(L, s.pop().debug_string() == "(1,3,2)");
|
||||
LuaAssert(L, s.ready(0) == false)
|
||||
LuaAssert(L, s.ready(1) == true)
|
||||
LuaAssert(L, s.pop().debug_string() == "(1,5,3)");
|
||||
LuaAssert(L, s.debug_string() == "(4,2,3)(4,3,2)");
|
||||
LuaAssert(L, s.pop().debug_string() == "(4,2,3)");
|
||||
LuaAssert(L, s.pop().debug_string() == "(4,3,2)");
|
||||
LuaAssert(L, s.ready(10000) == false);
|
||||
LuaAssert(L, s.empty());
|
||||
LuaAssert(L, s.debug_string() == "");
|
||||
|
||||
// Test serialization of an empty streambuffer.
|
||||
s.serialize(&sb);
|
||||
xs.deserialize(&sb);
|
||||
LuaAssert(L, xs.debug_string() == "");
|
||||
|
||||
return 0;
|
||||
}
|
||||
48
luprex/cpp/core/sched.hpp
Normal file
48
luprex/cpp/core/sched.hpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#ifndef SCHED_HPP
|
||||
#define SCHED_HPP
|
||||
|
||||
#include "wrap-set.hpp"
|
||||
|
||||
#include "streambuffer.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class SchedEntry : public eng::nevernew {
|
||||
private:
|
||||
friend class Schedule;
|
||||
int64_t clock_;
|
||||
int64_t thread_id_;
|
||||
int64_t place_id_;
|
||||
|
||||
public:
|
||||
int64_t clock() const { return clock_; }
|
||||
int64_t thread_id() const { return thread_id_; }
|
||||
int64_t place_id() const { return place_id_; }
|
||||
|
||||
SchedEntry(int64_t clk, int64_t thid, int64_t plid) {
|
||||
clock_ = clk;
|
||||
thread_id_ = thid;
|
||||
place_id_ = plid;
|
||||
}
|
||||
|
||||
bool operator < (const SchedEntry &other) const;
|
||||
|
||||
eng::string debug_string() const;
|
||||
};
|
||||
|
||||
class Schedule : public eng::nevernew {
|
||||
private:
|
||||
eng::set<SchedEntry> schedule_;
|
||||
public:
|
||||
void add(int64_t clk, int64_t thid, int64_t plid);
|
||||
bool ready(int64_t clk) const;
|
||||
bool empty() const { return schedule_.empty(); }
|
||||
SchedEntry pop();
|
||||
eng::string debug_string();
|
||||
|
||||
void serialize(StreamBuffer *sb);
|
||||
void deserialize(StreamBuffer *sb);
|
||||
};
|
||||
|
||||
#endif // SCHED_HPP
|
||||
|
||||
362
luprex/cpp/core/serializelua.cpp
Normal file
362
luprex/cpp/core/serializelua.cpp
Normal file
@@ -0,0 +1,362 @@
|
||||
#include "serializelua.hpp"
|
||||
|
||||
enum PackCodes {
|
||||
LUA_PK_TRUE = LUA_TT_SENTINEL,
|
||||
LUA_PK_FALSE,
|
||||
LUA_PK_REFERENCE,
|
||||
LUA_PK_ENDTABLE
|
||||
};
|
||||
|
||||
class DeserializeError {
|
||||
};
|
||||
|
||||
class Deserializer {
|
||||
LuaVar id_to_value_;
|
||||
LuaExtStack LS_;
|
||||
eng::string &error_;
|
||||
StreamBuffer *sb_;
|
||||
int next_id_;
|
||||
|
||||
void deserialize_table_r(LuaSlot target) {
|
||||
LuaVar key, val;
|
||||
LuaExtStack LS(LS_.state(), key, val);
|
||||
LS.newtable(target);
|
||||
LS.rawset(id_to_value_, next_id_++, target);
|
||||
bool hasmeta = sb_->read_bool();
|
||||
if (hasmeta) {
|
||||
uint8_t vtype = sb_->read_uint8();
|
||||
deserialize_r(vtype, val);
|
||||
if (!LS.istable(val)) {
|
||||
error_ = "serialized data contains invalid metatable";
|
||||
throw DeserializeError();
|
||||
}
|
||||
LS.setmetatable(target, val);
|
||||
}
|
||||
while (true) {
|
||||
uint8_t ktype = sb_->read_uint8();
|
||||
if (ktype == LUA_PK_ENDTABLE) {
|
||||
break;
|
||||
}
|
||||
deserialize_r(ktype, key);
|
||||
uint8_t vtype = sb_->read_uint8();
|
||||
deserialize_r(vtype, val);
|
||||
LS.rawset(target, key, val);
|
||||
}
|
||||
}
|
||||
|
||||
void deserialize_r(uint8_t kind, LuaSlot target) {
|
||||
switch (kind) {
|
||||
case LUA_TNIL: {
|
||||
LS_.set(target, LuaNil);
|
||||
return;
|
||||
}
|
||||
case LUA_PK_TRUE: {
|
||||
LS_.set(target, true);
|
||||
return;
|
||||
}
|
||||
case LUA_PK_FALSE: {
|
||||
LS_.set(target, false);
|
||||
return;
|
||||
}
|
||||
case LUA_TLIGHTUSERDATA: {
|
||||
LS_.set(target, LuaToken(sb_->read_uint64()));
|
||||
return;
|
||||
}
|
||||
case LUA_TNUMBER: {
|
||||
LS_.set(target, sb_->read_double());
|
||||
return;
|
||||
}
|
||||
case LUA_TSTRING: {
|
||||
LS_.set(target, sb_->read_string());
|
||||
LS_.rawset(id_to_value_, next_id_++, target);
|
||||
return;
|
||||
}
|
||||
case LUA_TT_GENERAL: {
|
||||
deserialize_table_r(target);
|
||||
return;
|
||||
}
|
||||
case LUA_TT_GLOBALENV: {
|
||||
LS_.getglobaltable(target);
|
||||
return;
|
||||
}
|
||||
case LUA_TT_TANGIBLE: {
|
||||
int64_t tanid = sb_->read_int64();
|
||||
if (!LS_.validpositiveinteger(tanid)) {
|
||||
throw DeserializeError();
|
||||
}
|
||||
LS_.maketan(target, tanid);
|
||||
return;
|
||||
}
|
||||
case LUA_TT_CLASS: {
|
||||
eng::string name = sb_->read_string();
|
||||
if (!sv::is_lua_classname(name)) {
|
||||
throw DeserializeError();
|
||||
}
|
||||
LS_.makeclass(target, name);
|
||||
return;
|
||||
}
|
||||
case LUA_PK_REFERENCE: {
|
||||
int32_t key = sb_->read_int32();
|
||||
LS_.rawget(target, id_to_value_, key);
|
||||
if (LS_.isnil(target)) {
|
||||
error_ = "invalid backward reference in serialized data";
|
||||
throw DeserializeError();
|
||||
}
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
error_ = util::ss("unrecognized type token in serialized data: ", kind);
|
||||
throw DeserializeError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
Deserializer(LuaCoreStack &LS0, LuaSlot val, StreamBuffer *sb, eng::string &error) :
|
||||
LS_(LS0.state(), id_to_value_), error_(error), sb_(sb), next_id_(1) {
|
||||
LS_.newtable(id_to_value_);
|
||||
int top = lua_gettop(LS_.state());
|
||||
try {
|
||||
uint16_t v = sb_->read_uint16();
|
||||
if (v != 0xD096) {
|
||||
error_ = "This is not a serialized data structure";
|
||||
return;
|
||||
}
|
||||
uint8_t b = sb_->read_uint8();
|
||||
deserialize_r(b, val);
|
||||
} catch (const StreamException &e) {
|
||||
error_ = "EOF reached while deserializing data";
|
||||
lua_settop(LS_.state(), top);
|
||||
LS_.set(val, LuaNil);
|
||||
} catch (DeserializeError e) {
|
||||
lua_settop(LS_.state(), top);
|
||||
LS_.set(val, LuaNil);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// If the serialize routine encounters an error, it must set the
|
||||
// error message string. In that case, it is assumed that the contents
|
||||
// of the serialization buffer is no longer valid and there is no
|
||||
// requirement to try to keep it valid.
|
||||
|
||||
class Serializer {
|
||||
LuaVar lookup_;
|
||||
LuaVar value_to_id_;
|
||||
LuaExtStack LS_;
|
||||
eng::string &error_;
|
||||
StreamBuffer *sb_;
|
||||
int next_id_;
|
||||
|
||||
void serialize_table_r(LuaSlot tab) {
|
||||
LuaVar key, val;
|
||||
LuaExtStack SLS(LS_.state(), key, val);
|
||||
sb_->write_uint8(LUA_TT_GENERAL);
|
||||
SLS.getmetatable(val, tab);
|
||||
if (SLS.isnil(val)) {
|
||||
sb_->write_bool(false);
|
||||
} else {
|
||||
sb_->write_bool(true);
|
||||
serialize_r(val);
|
||||
}
|
||||
SLS.set(key, LuaNil);
|
||||
while (SLS.next(tab, key, val)) {
|
||||
serialize_r(key);
|
||||
if (!error_.empty()) {
|
||||
return;
|
||||
}
|
||||
serialize_r(val);
|
||||
if (!error_.empty()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
sb_->write_uint8(LUA_PK_ENDTABLE);
|
||||
}
|
||||
|
||||
void serialize_r(LuaSlot val) {
|
||||
int tt = LS_.xtype(val);
|
||||
switch (tt) {
|
||||
case LUA_TNIL: {
|
||||
sb_->write_uint8(LUA_TNIL);
|
||||
return;
|
||||
}
|
||||
case LUA_TBOOLEAN: {
|
||||
if (LS_.ckboolean(val)) {
|
||||
sb_->write_uint8(LUA_PK_TRUE);
|
||||
} else {
|
||||
sb_->write_uint8(LUA_PK_FALSE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case LUA_TLIGHTUSERDATA: {
|
||||
sb_->write_uint8(LUA_TLIGHTUSERDATA);
|
||||
sb_->write_uint64(LS_.cktoken(val).value);
|
||||
return;
|
||||
}
|
||||
case LUA_TNUMBER: {
|
||||
sb_->write_uint8(LUA_TNUMBER);
|
||||
sb_->write_double(LS_.cknumber(val));
|
||||
return;
|
||||
}
|
||||
case LUA_TSTRING: {
|
||||
LS_.rawget(lookup_, value_to_id_, val);
|
||||
if (!LS_.isnil(lookup_)) {
|
||||
sb_->write_uint8(LUA_PK_REFERENCE);
|
||||
sb_->write_int32(LS_.ckint(lookup_));
|
||||
} else {
|
||||
LS_.rawset(value_to_id_, val, next_id_++);
|
||||
sb_->write_uint8(LUA_TSTRING);
|
||||
sb_->write_string(LS_.ckstring(val));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case LUA_TTABLE: {
|
||||
// LS.xtype should never return LUA_TTABLE.
|
||||
error_ = "Bad xtype in serialization";
|
||||
return;
|
||||
}
|
||||
case LUA_TFUNCTION: {
|
||||
error_ = "Cannot serialize closures";
|
||||
return;
|
||||
}
|
||||
case LUA_TUSERDATA: {
|
||||
error_ = "Cannot serialize userdata";
|
||||
return;
|
||||
}
|
||||
case LUA_TTHREAD: {
|
||||
error_ = "Cannot serialize coroutines";
|
||||
return;
|
||||
}
|
||||
case LUA_TT_GENERAL: {
|
||||
LS_.rawget(lookup_, value_to_id_, val);
|
||||
if (!LS_.isnil(lookup_)) {
|
||||
sb_->write_uint8(LUA_PK_REFERENCE);
|
||||
sb_->write_int32(LS_.ckint(lookup_));
|
||||
} else {
|
||||
LS_.rawset(value_to_id_, val, next_id_++);
|
||||
serialize_table_r(val);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case LUA_TT_REGISTRY: {
|
||||
error_ = "Pointer to registry found in serialization, shouldn't happen";
|
||||
return;
|
||||
}
|
||||
case LUA_TT_GLOBALENV: {
|
||||
sb_->write_uint8(LUA_TT_GLOBALENV);
|
||||
return;
|
||||
}
|
||||
case LUA_TT_TANGIBLE: {
|
||||
sb_->write_uint8(LUA_TT_TANGIBLE);
|
||||
sb_->write_int64(LS_.tanid(val));
|
||||
return;
|
||||
}
|
||||
case LUA_TT_TANGIBLEMETA: {
|
||||
error_ = "Pointer to a tangible metatable found in serialization, shouldn't happen";
|
||||
return;
|
||||
}
|
||||
case LUA_TT_CLASS: {
|
||||
sb_->write_uint8(LUA_TT_CLASS);
|
||||
sb_->write_string(LS_.classname(val));
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
error_ = "Unrecognized xtype in serialization";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
Serializer(LuaCoreStack &LS0, LuaSlot val, StreamBuffer *sb, eng::string &error) :
|
||||
LS_(LS0.state(), lookup_, value_to_id_), error_(error), sb_(sb), next_id_(1) {
|
||||
LS_.newtable(value_to_id_);
|
||||
sb_->write_uint16(0xD096);
|
||||
serialize_r(val);
|
||||
}
|
||||
};
|
||||
|
||||
eng::string serialize_lua(LuaCoreStack &LS0, LuaSlot val, StreamBuffer *sb) {
|
||||
eng::string error;
|
||||
Serializer sz(LS0, val, sb, error);
|
||||
return error;
|
||||
};
|
||||
|
||||
eng::string deserialize_lua(LuaCoreStack &LS0, LuaSlot val, StreamBuffer *sb) {
|
||||
eng::string error;
|
||||
Deserializer dsz(LS0, val, sb, error);
|
||||
if (!error.empty()) {
|
||||
LS0.set(val, LuaNil);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
LuaDefine(table_serialize, "value",
|
||||
"|Serialize any lua value, returning a string"
|
||||
"|"
|
||||
"|Converts lua values into a binary string. The string"
|
||||
"|can then be deserialized to produce a copy of the value."
|
||||
"|"
|
||||
"|Supports these atomic types:"
|
||||
"|"
|
||||
"| nil, string, number, boolean, token"
|
||||
"|"
|
||||
"|Does not support these types at all:"
|
||||
"|"
|
||||
"| function, thread"
|
||||
"|"
|
||||
"|Supports simple tables. When it finds a simple table, it recurses"
|
||||
"|into the table making a deep copy. Metatables are also supported."
|
||||
"|"
|
||||
"|Cycles are supported: if you have two tables which point to each other,"
|
||||
"|they can be serialized, and the deserialized copy will contain two"
|
||||
"|tables that point to each other."
|
||||
"|"
|
||||
"|Duplicate tables and duplicate strings are handled efficiently."
|
||||
"|For example, if the data contains two copies of a 10KB string, the"
|
||||
"|serialized data will only contain one copy of the string."
|
||||
"|"
|
||||
"|Tangibles are tables, but they are handled specially. When the"
|
||||
"|serialize routine finds a tangible, it just stores the tangible's ID."
|
||||
"|The deserialized copy will then contain the same tangible by ID."
|
||||
"|"
|
||||
"|Classes are also tables, but they too are handled specially. When"
|
||||
"|the serialize routine finds a class, it stores the class name, and"
|
||||
"|the deserialized copy will then contain the same class."
|
||||
"|"
|
||||
"|The global environment table is also treated specially: if you"
|
||||
"|serialize it, it will not recurse into it, instead, the deserialized"
|
||||
"|copy will just contain a pointer to the global environment."
|
||||
"|") {
|
||||
LuaArg value;
|
||||
LuaRet str;
|
||||
LuaDefStack LS(L, value, str);
|
||||
StreamBuffer sb;
|
||||
eng::string error = serialize_lua(LS, value, &sb);
|
||||
if (!error.empty()) {
|
||||
luaL_error(L, "%s", error.c_str());
|
||||
return LS.result();
|
||||
}
|
||||
LS.set(str, sb.view());
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(table_deserialize, "binary",
|
||||
"|Deserialize a serialized block, returning a value"
|
||||
"|"
|
||||
"|See doc(table.serialize) for more information about what"
|
||||
"|kind of data can be serialized and how it is deserialized."
|
||||
"|") {
|
||||
LuaArg str;
|
||||
LuaRet value;
|
||||
LuaDefStack LS(L, value, str);
|
||||
std::string_view s = LS.ckstringview(str);
|
||||
StreamBuffer sb(s);
|
||||
eng::string error = deserialize_lua(LS, value, &sb);
|
||||
if (!error.empty()) {
|
||||
luaL_error(L, "%s", error.c_str());
|
||||
return LS.result();
|
||||
}
|
||||
return LS.result();
|
||||
}
|
||||
40
luprex/cpp/core/serializelua.hpp
Normal file
40
luprex/cpp/core/serializelua.hpp
Normal file
@@ -0,0 +1,40 @@
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// This module contains routines to serialize and deserialize
|
||||
// lua data structures. These routines can handle cyclic data
|
||||
// structures.
|
||||
//
|
||||
// These routines are used for some specific difference
|
||||
// transmission cases, but they're not the heart of the lua
|
||||
// difference transmission system.
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
#ifndef SERIALIZELUA_HPP
|
||||
#define SERIALIZELUA_HPP
|
||||
|
||||
#include "luastack.hpp"
|
||||
#include "streambuffer.hpp"
|
||||
|
||||
// serialize_lua
|
||||
//
|
||||
// Serialize a value from the LuaSlot and store it in the StreamBuffer.
|
||||
// On success, returns empty string.
|
||||
//
|
||||
// If there is an error, returns an error message. In this case, the
|
||||
// streambuffer contains garbage.
|
||||
//
|
||||
eng::string serialize_lua(LuaCoreStack &LS0, LuaSlot val, StreamBuffer *sb);
|
||||
|
||||
// deserialize_lua
|
||||
//
|
||||
// Deserialize a value from the StreamBuffer and store it in the LuaSlot.
|
||||
// On success, returns empty string.
|
||||
//
|
||||
// If there is an error, returns an error message. In this case, the
|
||||
// streambuffer is likely only partially consumed.
|
||||
//
|
||||
eng::string deserialize_lua(LuaCoreStack &LS0, LuaSlot val, StreamBuffer *sb);
|
||||
|
||||
#endif // SERIALIZELUA_HPP
|
||||
859
luprex/cpp/core/source.cpp
Normal file
859
luprex/cpp/core/source.cpp
Normal file
@@ -0,0 +1,859 @@
|
||||
|
||||
#define _USE_MATH_DEFINES
|
||||
#include <cmath>
|
||||
|
||||
#include "wrap-string.hpp"
|
||||
#include "wrap-vector.hpp"
|
||||
#include "wrap-map.hpp"
|
||||
#include "wrap-set.hpp"
|
||||
#include "wrap-sstream.hpp"
|
||||
#include "util.hpp"
|
||||
#include "luastack.hpp"
|
||||
#include "traceback.hpp"
|
||||
#include "table.hpp"
|
||||
#include "source.hpp"
|
||||
#include "luasnap.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
|
||||
LuaDefine(makeclass, "classname", "create a class if it doesn't already exist") {
|
||||
LuaArg classname;
|
||||
LuaRet classtab;
|
||||
LuaDefStack LS(L, classname, classtab);
|
||||
if (!sv::is_lua_classname(LS.ckstring(classname))) {
|
||||
luaL_error(L, "invalid class name: %s", LS.ckstring(classname).c_str());
|
||||
};
|
||||
LS.makeclass(classtab, classname);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(getclass, "classname", "get the classtab with the specified name") {
|
||||
LuaArg classname;
|
||||
LuaRet classtab;
|
||||
LuaDefStack LS(L, classname, classtab);
|
||||
eng::string err = LS.getclass(classtab, classname);
|
||||
if (err != "") {
|
||||
luaL_error(L, "%s", err.c_str());
|
||||
}
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(classname, "classtable", "get the class name from a class table") {
|
||||
LuaArg table;
|
||||
LuaRet result;
|
||||
LuaDefStack LS(L, table, result);
|
||||
eng::string rstr = LS.classname(table);
|
||||
if (rstr == "") {
|
||||
LS.set(result, LuaNil);
|
||||
} else {
|
||||
LS.set(result, rstr);
|
||||
}
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
static void get_reg_name(std::string_view name, std::string_view &classname, std::string_view &funcname) {
|
||||
size_t upos = name.find('_');
|
||||
if (upos == std::string_view::npos) {
|
||||
funcname = name;
|
||||
classname = "";
|
||||
} else {
|
||||
funcname = name.substr(upos + 1);
|
||||
classname = name.substr(0, upos);
|
||||
}
|
||||
}
|
||||
|
||||
static void get_info_table(LuaCoreStack &LS, LuaSlot db, LuaSlot info, const eng::string &fn) {
|
||||
LS.rawget(info, db, fn);
|
||||
if (!LS.istable(info)) {
|
||||
LS.set(info, LuaNewTable);
|
||||
LS.rawset(db, fn, info);
|
||||
}
|
||||
LS.rawset(info, "name", fn);
|
||||
}
|
||||
|
||||
static void calculate_loadresult(LuaCoreStack &LS0, LuaSlot info, const eng::string &fn, const eng::string &code) {
|
||||
LuaVar loadresult;
|
||||
LuaExtStack LS(LS0.state(), loadresult);
|
||||
if (code == "") {
|
||||
LS.rawset(info, "loadresult", "missing or empty source file");
|
||||
} else {
|
||||
eng::string chunk = "=" + fn;
|
||||
luaL_loadbuffer(LS.state(), code.c_str(), code.size(), chunk.c_str());
|
||||
lua_replace(LS.state(), loadresult.index());
|
||||
LS.rawset(info, "loadresult", loadresult);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void SourceDB::diff(const SourceDB &auth, StreamBuffer *sb) {
|
||||
LuaVar sdb, sfn, sinfo, shash, sseq;
|
||||
LuaVar mdb, mfn, minfo, mhash, mseq, mcode;
|
||||
LuaExtStack SLS(lua_state_, sdb, sfn, sinfo, shash, sseq);
|
||||
LuaExtStack MLS(auth.lua_state_, mdb, mfn, minfo, mhash, mseq, mcode);
|
||||
sb->write_int32(0);
|
||||
int wc_after = sb->total_writes();
|
||||
int nupdates = 0;
|
||||
// Fetch the two source databases.
|
||||
SLS.rawget(sdb, LuaRegistry, "sourcedb");
|
||||
MLS.rawget(mdb, LuaRegistry, "sourcedb");
|
||||
// Loop over the master database.
|
||||
MLS.set(mfn, LuaNil);
|
||||
while (MLS.next(mdb, mfn, minfo) != 0) {
|
||||
eng::string fn = MLS.ckstring(mfn);
|
||||
assert(MLS.istable(minfo));
|
||||
MLS.rawget(mseq, minfo, "sequence");
|
||||
MLS.rawget(mhash, minfo, "hash");
|
||||
bool diffhash = true;
|
||||
bool diffseq = true;
|
||||
SLS.set(sfn, fn);
|
||||
SLS.rawget(sinfo, sdb, sfn);
|
||||
if (SLS.istable(sinfo)) {
|
||||
SLS.rawget(shash, sinfo, "hash");
|
||||
SLS.rawget(sseq, sinfo, "sequence");
|
||||
diffhash = MLS.ckstring(mhash) != SLS.ckstring(shash);
|
||||
diffseq = MLS.ckinteger(mseq) != SLS.ckinteger(sseq);
|
||||
}
|
||||
if (diffhash || diffseq) {
|
||||
sb->write_string(MLS.ckstring(mfn));
|
||||
sb->write_int32(MLS.ckinteger(mseq));
|
||||
if (diffhash) {
|
||||
MLS.rawget(mcode, minfo, "code");
|
||||
sb->write_string(MLS.ckstring(mcode));
|
||||
} else {
|
||||
sb->write_string("\001");
|
||||
}
|
||||
nupdates += 1;
|
||||
}
|
||||
}
|
||||
// Loop over synch database.
|
||||
SLS.set(sfn, LuaNil);
|
||||
while (SLS.next(sdb, sfn, sinfo) != 0) {
|
||||
eng::string fn = SLS.ckstring(sfn);
|
||||
assert(SLS.istable(sinfo));
|
||||
MLS.set(mfn, fn);
|
||||
MLS.rawget(minfo, mdb, mfn);
|
||||
if (!MLS.istable(minfo)) {
|
||||
sb->write_string(SLS.ckstring(sfn));
|
||||
sb->write_int32(-1);
|
||||
sb->write_string("");
|
||||
nupdates += 1;
|
||||
}
|
||||
}
|
||||
sb->overwrite_int32(wc_after, nupdates);
|
||||
}
|
||||
|
||||
bool SourceDB::patch(StreamBuffer *sb, DebugCollector *dbc) {
|
||||
lua_State *L = lua_state_;
|
||||
LuaVar db, info;
|
||||
LuaExtStack LS(L, db, info);
|
||||
LS.rawget(db, LuaRegistry, "sourcedb");
|
||||
int nupdates = sb->read_int32();
|
||||
for (int i = 0; i < nupdates; i++) {
|
||||
eng::string fn = sb->read_string();
|
||||
int sequence = sb->read_int32();
|
||||
eng::string code = sb->read_string();
|
||||
DebugLine(dbc) << "Source file " << fn << " updated";
|
||||
if (sequence < 0) {
|
||||
LS.rawset(db, fn, LuaNil);
|
||||
} else {
|
||||
get_info_table(LS, db, info, fn);
|
||||
LS.rawset(info, "sequence", sequence);
|
||||
if (code != "\001") {
|
||||
LS.rawset(info, "code", code);
|
||||
LS.rawset(info, "hash", util::hash_to_hex(util::hash_string(code)));
|
||||
calculate_loadresult(LS, info, fn, code);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (nupdates > 0);
|
||||
}
|
||||
|
||||
void SourceDB::set(const eng::string &fn, const eng::string &code, int sequence) {
|
||||
lua_State *L = lua_state_;
|
||||
LuaVar db, info;
|
||||
LuaExtStack LS(L, db, info);
|
||||
LS.rawget(db, LuaRegistry, "sourcedb");
|
||||
get_info_table(LS, db, info, fn);
|
||||
LS.rawset(info, "sequence", sequence);
|
||||
LS.rawset(info, "code", code);
|
||||
LS.rawset(info, "fingerprint", "");
|
||||
LS.rawset(info, "hash", util::hash_to_hex(util::hash_string(code)));
|
||||
calculate_loadresult(LS, info, fn, code);
|
||||
}
|
||||
|
||||
eng::string SourceDB::get(const eng::string &fn) {
|
||||
lua_State *L = lua_state_;
|
||||
LuaVar db, info, code, sequence, loadresult;
|
||||
LuaExtStack LS(L, db, info, code, sequence, loadresult);
|
||||
LS.rawget(db, LuaRegistry, "sourcedb");
|
||||
LS.rawget(info, db, fn);
|
||||
if (!LS.istable(info)) {
|
||||
return "<nonexistent>";
|
||||
}
|
||||
LS.rawget(code, info, "code");
|
||||
LS.rawget(sequence, info, "sequence");
|
||||
LS.rawget(loadresult, info, "loadresult");
|
||||
eng::string ccode = LS.ckstring(code);
|
||||
int seqno = LS.ckint(sequence);
|
||||
eng::string cloadresult;
|
||||
if (LS.isfunction(loadresult)) {
|
||||
cloadresult = "<function>";
|
||||
} else {
|
||||
cloadresult = LS.ckstring(loadresult);
|
||||
}
|
||||
return util::ss(seqno, ":", ccode, ":", cloadresult);
|
||||
}
|
||||
|
||||
void SourceDB::update(const util::LuaSourceVec &source) {
|
||||
lua_State *L = lua_state_;
|
||||
LuaVar sourcedb, info;
|
||||
LuaExtStack LS(L, sourcedb, info);
|
||||
|
||||
// Get and clear the source database.
|
||||
LS.rawget(sourcedb, LuaRegistry, "sourcedb");
|
||||
if (!LS.istable(sourcedb)) {
|
||||
LS.newtable(sourcedb);
|
||||
LS.rawset(LuaRegistry, "sourcedb", sourcedb);
|
||||
}
|
||||
LS.cleartable(sourcedb, true);
|
||||
|
||||
for (int i = 0; i < int(source.size()); i++) {
|
||||
const eng::string &file = source[i].first;
|
||||
const eng::string &code = source[i].second;
|
||||
util::dprint("Compiling ", file);
|
||||
LS.newtable(info);
|
||||
LS.rawset(info, "name", file);
|
||||
LS.rawset(info, "code", code);
|
||||
LS.rawset(info, "hash", util::hash_to_hex(util::hash_string(code)));
|
||||
LS.rawset(info, "sequence", i + 1);
|
||||
calculate_loadresult(LS, info, file, code);
|
||||
LS.rawset(sourcedb, file, info);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete everything from the global environment.
|
||||
// Clear all the classes in the registry classes table.
|
||||
//
|
||||
static void source_clear_globals(lua_State *L) {
|
||||
LuaVar classname, classtab, key, globtab, classes;
|
||||
LuaExtStack LS(L, classname, classtab, key, globtab, classes);
|
||||
|
||||
LS.getglobaltable(globtab);
|
||||
LS.cleartable(globtab, true);
|
||||
LS.rawset(globtab, "_G", globtab);
|
||||
|
||||
LS.rawget(classes, LuaRegistry, "classes");
|
||||
assert(LS.istable(classes));
|
||||
LS.set(classname, LuaNil);
|
||||
while (LS.next(classes, classname, classtab) != 0) {
|
||||
assert(LS.istable(classtab));
|
||||
LS.cleartable(classtab, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Load all the 'LuaDefine' C functions into the lua state.
|
||||
//
|
||||
static void source_load_cfunctions(lua_State *L) {
|
||||
LuaVar classobj;
|
||||
LuaExtStack LS(L, classobj);
|
||||
for (auto r = LuaFunctionReg::All; r != nullptr; r=r->next()) {
|
||||
lua_CFunction func = r->get_func();
|
||||
if ((func != nullptr) && (!r->get_sandbox())) {
|
||||
std::string_view classname;
|
||||
std::string_view funcname;
|
||||
get_reg_name(r->get_name(), classname, funcname);
|
||||
if (classname.empty()) {
|
||||
LS.getglobaltable(classobj);
|
||||
LS.rawset(classobj, funcname, func);
|
||||
} else {
|
||||
LS.makeclass(classobj, classname);
|
||||
LS.rawset(classobj, funcname, func);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load all the 'LuaConstant' constants into the lua state.
|
||||
//
|
||||
static void source_load_cconstants(lua_State *L) {
|
||||
LuaVar classobj, value;
|
||||
LuaExtStack LS(L, classobj, value);
|
||||
for (auto r = LuaConstantReg::All; r != nullptr; r=r->next()) {
|
||||
if (r->get_tokenvalue().empty()) {
|
||||
LS.set(value, r->get_numbervalue());
|
||||
} else {
|
||||
LS.set(value, r->get_tokenvalue());
|
||||
}
|
||||
std::string_view classname;
|
||||
std::string_view funcname;
|
||||
get_reg_name(r->get_name(), classname, funcname);
|
||||
if (classname.empty()) {
|
||||
LS.getglobaltable(classobj);
|
||||
LS.rawset(classobj, funcname, value);
|
||||
} else {
|
||||
LS.makeclass(classobj, classname);
|
||||
LS.rawset(classobj, funcname, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eng::vector<eng::string> SourceDB::modules() {
|
||||
eng::vector<eng::string> result;
|
||||
LuaVar sourcedb, key, info, seq;
|
||||
LuaExtStack LS(lua_state_, sourcedb, key, info, seq);
|
||||
|
||||
result.push_back("CORE");
|
||||
|
||||
// Get the source database.
|
||||
LS.rawget(sourcedb, LuaRegistry, "sourcedb");
|
||||
if (!LS.istable(sourcedb)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Sort the keys by sequence number.
|
||||
eng::map<int, eng::string> indices;
|
||||
LS.set(key, LuaNil);
|
||||
while (LS.next(sourcedb, key, info) != 0) {
|
||||
LS.rawget(seq, info, "sequence");
|
||||
indices[LS.ckinteger(seq)] = LS.ckstring(key);
|
||||
}
|
||||
|
||||
for (const auto &pair : indices) {
|
||||
result.push_back(pair.second);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void SourceDB::rebuild_core() {
|
||||
source_clear_globals(lua_state_);
|
||||
source_load_cfunctions(lua_state_);
|
||||
source_load_cconstants(lua_state_);
|
||||
}
|
||||
|
||||
eng::string SourceDB::rebuild_module(const eng::string &mod) {
|
||||
if (mod == "CORE") {
|
||||
rebuild_core();
|
||||
return "";
|
||||
} else {
|
||||
LuaVar sourcedb, info, closure;
|
||||
LuaExtStack LS(lua_state_, sourcedb, info, closure);
|
||||
LS.rawget(sourcedb, LuaRegistry, "sourcedb");
|
||||
if (!LS.istable(sourcedb)) {
|
||||
return "SourceDB not initialized";
|
||||
}
|
||||
LS.rawget(info, sourcedb, mod);
|
||||
if (!LS.istable(info)) {
|
||||
return util::ss("No such module: ", mod);
|
||||
}
|
||||
LS.rawget(closure, info, "loadresult");
|
||||
if (!LS.isfunction(closure)) {
|
||||
return util::ss(mod, ":", LS.ckstring(closure));
|
||||
}
|
||||
lua_pushvalue(lua_state_, closure.index());
|
||||
return traceback_pcall(lua_state_, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void SourceDB::run_unittests() {
|
||||
lua_State *L = lua_state_;
|
||||
LuaVar unittests, name, func, err, globtab;
|
||||
LuaExtStack LS(L, unittests, name, func, err, globtab);
|
||||
|
||||
LS.getglobaltable(globtab);
|
||||
LS.rawget(unittests, globtab, "unittests");
|
||||
|
||||
// Sort the unit test names.
|
||||
eng::set<eng::string> names;
|
||||
LS.set(name, LuaNil);
|
||||
while (LS.next(unittests, name, func) != 0) {
|
||||
if (LS.isfunction(func) && LS.isstring(name)) {
|
||||
names.insert(LS.ckstring(name));
|
||||
}
|
||||
}
|
||||
|
||||
// Run the functions in order
|
||||
bool any = false;
|
||||
for (const eng::string &name : names) {
|
||||
util::dprint("Running unittests: ", name);
|
||||
LS.rawget(func, unittests, name);
|
||||
|
||||
lua_pushvalue(L, func.index());
|
||||
eng::string msg = traceback_pcall(L, 0, 0);
|
||||
if (!msg.empty()) {
|
||||
LS.set(err, msg);
|
||||
util::dprint(msg);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (any) {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void SourceDB::init(lua_State *L) {
|
||||
lua_state_ = L;
|
||||
LuaVar globtab, persist, unpersist, classname, classtab, funcname, funcp, rawfunc, nullstring;
|
||||
LuaExtStack LS(L, globtab, persist, unpersist, classname, classtab, funcname, funcp, rawfunc, nullstring);
|
||||
LS.getglobaltable(globtab);
|
||||
LS.rawset(LuaRegistry, "sourcedb", LuaNewTable);
|
||||
|
||||
// Set the metatable for strings.
|
||||
LS.makeclass(classtab, "string");
|
||||
LS.set(nullstring, "");
|
||||
LS.setmetatable(nullstring, classtab);
|
||||
|
||||
// Rebuild the global environment.
|
||||
rebuild_core();
|
||||
|
||||
// We need to register all C functions with the eris permanents tables.
|
||||
LS.rawget(persist, LuaRegistry, "persist");
|
||||
LS.rawget(unpersist, LuaRegistry, "unpersist");
|
||||
LS.set(classname, LuaNil);
|
||||
while (LS.next(globtab, classname, classtab) != 0) {
|
||||
if (LS.isstring(classname) && LS.istable(classtab)) {
|
||||
LS.set(funcname, LuaNil);
|
||||
while (LS.next(classtab, funcname, funcp) != 0) {
|
||||
if (LS.isstring(funcname) && LS.iscfunction(funcp)) {
|
||||
eng::string full = "cfunc:";
|
||||
full += LS.ckstring(classname);
|
||||
full += ".";
|
||||
full += LS.ckstring(funcname);
|
||||
lua_pushcfunction(L, lua_tocfunction(L, funcp.index()));
|
||||
lua_replace(L, rawfunc.index());
|
||||
LS.rawset(persist, rawfunc, full);
|
||||
LS.rawset(unpersist, full, rawfunc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SourceDB::serialize_source(const util::LuaSourceVec &sv, StreamBuffer *sb) {
|
||||
sb->write_int32(sv.size());
|
||||
for (const auto &pair : sv) {
|
||||
sb->write_string(pair.first);
|
||||
sb->write_string(pair.second);
|
||||
}
|
||||
}
|
||||
|
||||
void SourceDB::deserialize_source(util::LuaSourceVec *sv, StreamBuffer *sb) {
|
||||
sv->clear();
|
||||
int count = sb->read_int32();
|
||||
if ((count < 0) || (count > 10000)) throw StreamCorruption();
|
||||
for (int i = 0; i < count; i++) {
|
||||
eng::string fn = sb->read_string();
|
||||
eng::string code = sb->read_string();
|
||||
sv->emplace_back(fn, code);
|
||||
}
|
||||
}
|
||||
|
||||
// Register lua builtins.
|
||||
//
|
||||
// To find the lua builtins, we use this approach: we create a new lua state
|
||||
// (the 'prototype', we call it), and we call luaL_openlibs on it. The
|
||||
// prototype should now contain all the builtins.
|
||||
//
|
||||
// Then, we iterate over our function registry, looking for LuaDefineBuiltin
|
||||
// records. These records don't have a function pointer yet. For each such
|
||||
// record, we look into the prototype, and look up the function in the global
|
||||
// environment. If it's not found, we print an error. If we find it, we extract
|
||||
// the C function pointer from it. Then, we remove the function from the
|
||||
// prototype, to indicate that we already processed that function.
|
||||
//
|
||||
// At the end, we scan the prototype to make sure all functions have been
|
||||
// removed. If not, we know our function registry is missing something and we
|
||||
// print out an error.
|
||||
//
|
||||
void SourceDB::register_lua_builtins() {
|
||||
lua_State *L = LuaCoreStack::newstate(nullptr);
|
||||
luaL_openlibs(L);
|
||||
{
|
||||
LuaVar globals, lclassname, lfuncname, classtab, func;
|
||||
LuaExtStack LS(L, globals, lclassname, lfuncname, classtab, func);
|
||||
LS.getglobaltable(globals);
|
||||
|
||||
// Iterate over the function registry, copying function pointers from
|
||||
// the prototype lua state into the registry, then remove the closure
|
||||
// from the prototype.
|
||||
for (auto reg = LuaFunctionReg::All; reg != nullptr; reg=reg->next()) {
|
||||
std::string_view funcname;
|
||||
std::string_view classname;
|
||||
get_reg_name(reg->get_name(), classname, funcname);
|
||||
if (classname.empty()) {
|
||||
LS.getglobaltable(classtab);
|
||||
} else {
|
||||
LS.rawget(classtab, globals, classname);
|
||||
}
|
||||
lua_CFunction builtin = nullptr;
|
||||
if (LS.istable(classtab)) {
|
||||
LS.rawget(func, classtab, funcname);
|
||||
if (LS.iscfunction(func)) {
|
||||
builtin = lua_tocfunction(L, func.index());
|
||||
LS.rawset(classtab, funcname, LuaNil);
|
||||
}
|
||||
}
|
||||
if (reg->get_func() == nullptr) {
|
||||
if (builtin == nullptr) {
|
||||
if (!reg->get_sandbox()) {
|
||||
util::dprint("No such builtin function: ", classname, " ", funcname);
|
||||
}
|
||||
} else {
|
||||
reg->set_func(builtin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over the prototype. All cfunctions should have been removed.
|
||||
LS.set(lclassname, LuaNil);
|
||||
while (LS.next(globals, lclassname, classtab)) {
|
||||
if (LS.isstring(lclassname)) {
|
||||
if (LS.istable(classtab)) {
|
||||
LS.set(lfuncname, LuaNil);
|
||||
while (LS.next(classtab, lfuncname, func)) {
|
||||
if (LS.iscfunction(func)) {
|
||||
util::dprint("Failed to declare builtin: ", LS.ckstring(lclassname), ".", LS.ckstring(lfuncname));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lua_close(L);
|
||||
}
|
||||
|
||||
|
||||
eng::string SourceDB::function_docs(const LuaCoreStack &LS0, LuaSlot fn) {
|
||||
lua_State *L = LS0.state();
|
||||
LuaVar sourcedb, fname, finfo, code;
|
||||
LuaExtStack LS(L, sourcedb, fname, finfo, code);
|
||||
|
||||
if (LS.iscfunction(fn)) {
|
||||
lua_CFunction cfn = lua_tocfunction(L, fn.index());
|
||||
const LuaFunctionReg *reg = LuaFunctionReg::lookup(cfn);
|
||||
if (reg == nullptr) {
|
||||
return "";
|
||||
}
|
||||
std::string_view classname;
|
||||
std::string_view funcname;
|
||||
get_reg_name(reg->get_name(), classname, funcname);
|
||||
eng::ostringstream oss;
|
||||
util::StringVec docs = util::split_docstring(reg->get_docs());
|
||||
oss << "function ";
|
||||
if (!classname.empty()) {
|
||||
oss << classname << ".";
|
||||
}
|
||||
oss << funcname << "(" << reg->get_args() << ")" << std::endl;
|
||||
oss << "--" << std::endl;
|
||||
for (const eng::string &line : docs) {
|
||||
oss << "-- " << line << std::endl;
|
||||
}
|
||||
oss << "--" << std::endl;
|
||||
return oss.str();
|
||||
} else if (LS.isfunction(fn)) {
|
||||
lua_Debug ar;
|
||||
lua_pushvalue(L, fn.index());
|
||||
int status = lua_getinfo(L, ">S", &ar);
|
||||
if (status == 0) return "";
|
||||
|
||||
// Get the source database.
|
||||
LS.rawget(sourcedb, LuaRegistry, "sourcedb");
|
||||
if (!LS.istable(sourcedb)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Get the finfo table from the source db.
|
||||
LS.set(fname, eng::string(ar.short_src));
|
||||
LS.rawget(finfo, sourcedb, fname);
|
||||
if (!LS.istable(finfo)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Get the code from the finfo table.
|
||||
LS.rawget(code, finfo, "code");
|
||||
if (!LS.isstring(code)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Split the code into lines.
|
||||
util::StringVec lines = util::split_lines(LS.ckstring(code));
|
||||
int linehi = ar.linedefined - 1;
|
||||
if ((linehi < 0) || (linehi >= int(lines.size()))) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Incorporate the function comment.
|
||||
int linelo = linehi;
|
||||
while ((linelo > 0) && (sv::is_lua_comment(lines[linelo-1]))) linelo -= 1;
|
||||
|
||||
// Output the docs.
|
||||
eng::ostringstream result;
|
||||
result << lines[linehi] << std::endl;
|
||||
result << "--" << std::endl;
|
||||
for (int i = linelo; i < linehi; i++) {
|
||||
result << lines[i] << std::endl;
|
||||
}
|
||||
result << "--" << std::endl;
|
||||
return result.str();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// These should go away eventually. They're for debugging.
|
||||
LuaDefine(coroutine_setnextid, "thread,id", "set the next id of a thread (debugging only)") {
|
||||
LuaArg co, lid;
|
||||
LuaDefStack LS(L, co, lid);
|
||||
lua_State *CO = LS.ckthread(co);
|
||||
lua_Number id = LS.ckinteger(lid);
|
||||
lua_setnextid(CO, id);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(coroutine_getnextid, "thread", "get the next id of a thread (debugging only)") {
|
||||
LuaArg co;
|
||||
LuaRet lid;
|
||||
LuaDefStack LS(L, co, lid);
|
||||
lua_State *CO = LS.ckthread(co);
|
||||
LS.set(lid, lua_getnextid(CO));
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(unittests_sourcedb, "", "some unit tests") {
|
||||
LuaSnap msnap;
|
||||
LuaSnap ssnap;
|
||||
SourceDB mdb;
|
||||
SourceDB sdb;
|
||||
mdb.init(msnap.state());
|
||||
sdb.init(ssnap.state());
|
||||
StreamBuffer sb;
|
||||
|
||||
// Create a code database using 'set'.
|
||||
mdb.set("foo", "function foo() print('foo') end", 1);
|
||||
mdb.set("bar", "function bar() print('bar') end", 2);
|
||||
mdb.set("zoo", "funcjdshja mxooso yowza!", 3);
|
||||
|
||||
// Verify that get works.
|
||||
LuaAssertStrEq(L, mdb.get("foo"), "1:function foo() print('foo') end:<function>");
|
||||
LuaAssertStrEq(L, mdb.get("bar"), "2:function bar() print('bar') end:<function>");
|
||||
LuaAssertStrEq(L, mdb.get("zoo"), "3:funcjdshja mxooso yowza!:zoo:1: syntax error near 'mxooso'");
|
||||
LuaAssertStrEq(L, mdb.get("baz"), "<nonexistent>");
|
||||
|
||||
// Difference transmit.
|
||||
sdb.diff(mdb, &sb);
|
||||
|
||||
// There should still be nothing in the sdb.
|
||||
LuaAssertStrEq(L, sdb.get("foo"), "<nonexistent>");
|
||||
LuaAssertStrEq(L, sdb.get("bar"), "<nonexistent>");
|
||||
LuaAssertStrEq(L, sdb.get("zoo"), "<nonexistent>");
|
||||
LuaAssertStrEq(L, sdb.get("baz"), "<nonexistent>");
|
||||
|
||||
// Apply the diffs.
|
||||
sdb.patch(&sb, nullptr);
|
||||
|
||||
// Everything should now be copied to sdb.
|
||||
LuaAssertStrEq(L, sdb.get("foo"), "1:function foo() print('foo') end:<function>");
|
||||
LuaAssertStrEq(L, sdb.get("bar"), "2:function bar() print('bar') end:<function>");
|
||||
LuaAssertStrEq(L, sdb.get("zoo"), "3:funcjdshja mxooso yowza!:zoo:1: syntax error near 'mxooso'");
|
||||
LuaAssertStrEq(L, sdb.get("baz"), "<nonexistent>");
|
||||
|
||||
// Make a single change to the code.
|
||||
mdb.set("bar", "function bar1() print('bar1') end", 2);
|
||||
|
||||
// Diff and patch
|
||||
sdb.diff(mdb, &sb);
|
||||
sdb.patch(&sb, nullptr);
|
||||
|
||||
// Verify that it's been updated.
|
||||
LuaAssertStrEq(L, sdb.get("foo"), "1:function foo() print('foo') end:<function>");
|
||||
LuaAssertStrEq(L, sdb.get("bar"), "2:function bar1() print('bar1') end:<function>");
|
||||
LuaAssertStrEq(L, sdb.get("zoo"), "3:funcjdshja mxooso yowza!:zoo:1: syntax error near 'mxooso'");
|
||||
LuaAssertStrEq(L, sdb.get("baz"), "<nonexistent>");
|
||||
|
||||
// Make a single change to just a sequence number.
|
||||
mdb.set("foo", "function foo() print('foo') end", 6);
|
||||
|
||||
// Diff
|
||||
sdb.diff(mdb, &sb);
|
||||
|
||||
// The only thing we've updated is a single sequence number. Make
|
||||
// sure the number of bytes transmitted is reasonable.
|
||||
// 4 bytes for the count of changes
|
||||
// 4 bytes for "foo"
|
||||
// 4 bytes for the sequence number
|
||||
// 2 bytes for unmodified code
|
||||
// This verifies that the hash comparisons are working.
|
||||
LuaAssert(L, sb.fill() == 14);
|
||||
|
||||
// Patch.
|
||||
sdb.patch(&sb, nullptr);
|
||||
|
||||
// Verify that it's been updated.
|
||||
LuaAssertStrEq(L, sdb.get("foo"), "6:function foo() print('foo') end:<function>");
|
||||
LuaAssertStrEq(L, sdb.get("bar"), "2:function bar1() print('bar1') end:<function>");
|
||||
LuaAssertStrEq(L, sdb.get("zoo"), "3:funcjdshja mxooso yowza!:zoo:1: syntax error near 'mxooso'");
|
||||
LuaAssertStrEq(L, sdb.get("baz"), "<nonexistent>");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
LuaDefineBuiltin(table_concat, "vector1, vector2", "concatenate two vectors");
|
||||
LuaDefineBuiltin(table_insert, "vector, pos, value", "insert an element into a vector");
|
||||
LuaDefineBuiltin(table_remove, "vector, pos", "remove an element from a vector");
|
||||
LuaDefineBuiltin(table_sort, "vector [,comparefn]", "sort a vector");
|
||||
LuaDefineBuiltin(table_pack, "v1, v2, v3...", "turn a sequence of arguments into a vector");
|
||||
LuaDefineBuiltin(table_unpack, "vector", "turn a vector into a sequence of return values");
|
||||
LuaSandboxBuiltin(table_maxn, "", "");
|
||||
|
||||
LuaDefineBuiltin(string_byte, "str [,index]", "get a single byte from a string");
|
||||
LuaDefineBuiltin(string_char, "byte, byte,...", "convert sequence of bytes to a string");
|
||||
LuaDefineBuiltin(string_find, "str, pattern [,index]", "return start and end of pattern in a string");
|
||||
LuaDefineBuiltin(string_len, "str", "return the length of the string, in bytes");
|
||||
LuaDefineBuiltin(string_rep, "str, count", "repeat the string some number of times");
|
||||
LuaDefineBuiltin(string_reverse, "str", "reverse the bytes of the string");
|
||||
LuaDefineBuiltin(string_lower, "str", "convert string to lowercase");
|
||||
LuaDefineBuiltin(string_upper, "str", "convert string to uppercase");
|
||||
LuaDefineBuiltin(string_format, "formatstr, v1,v2,v3...", "generate formatted output string");
|
||||
LuaDefineBuiltin(string_gmatch, "str, pattern", "iterate over pattern-matched substrings");
|
||||
LuaDefineBuiltin(string_gsub, "str, pattern, replace", "global replace pattern in string");
|
||||
LuaDefineBuiltin(string_match, "str, pattern", "return start and end of pattern in string");
|
||||
LuaDefineBuiltin(string_sub, "str, pos1, pos2", "return substring of str from pos1 to pos2");
|
||||
LuaSandboxBuiltin(string_dump, "func", "convert a function to a string");
|
||||
|
||||
LuaDefineBuiltin(bit32_arshift, "n, shift", "shift 32-bit number to the right, keeping high bit unchanged");
|
||||
LuaDefineBuiltin(bit32_band, "n, n, n...", "return the bitwise and of all 32-bit numbers");
|
||||
LuaDefineBuiltin(bit32_bnot, "n", "return the bitwise negation of n, ie, (-1 - n) % 2^32");
|
||||
LuaDefineBuiltin(bit32_bor, "n, n, n...", "return the bitwise or of all 32-bit arguments");
|
||||
LuaDefineBuiltin(bit32_bxor, "n, n, n...", "return the bitwise exclusive or of all 32-bit arguments");
|
||||
LuaDefineBuiltin(bit32_btest, "n, n, n...", "compute bitwise and of all 32-bit arguments, return true if nonzero");
|
||||
LuaDefineBuiltin(bit32_extract, "n, field, width", "return value from extracted bitfield of 32-bit number");
|
||||
LuaDefineBuiltin(bit32_lrotate, "n, shift", "rotate 32-bit number to the left");
|
||||
LuaDefineBuiltin(bit32_lshift, "n, shift", "shift 32-bit number to the left, padding with zeros");
|
||||
LuaDefineBuiltin(bit32_replace, "n, v, field, width", "change value of extracted bitfield in 32-bit number");
|
||||
LuaDefineBuiltin(bit32_rrotate, "n, shift", "rotate 32-bit number to the right");
|
||||
LuaDefineBuiltin(bit32_rshift, "n, shift", "shift 32-bit number to the right, padding with zeros");
|
||||
|
||||
LuaDefineBuiltin(math_abs, "x", "return absolute value of x");
|
||||
LuaDefineBuiltin(math_acos, "x", "return arc-cosine of x in radians");
|
||||
LuaDefineBuiltin(math_asin, "x", "return arc-sine of x in radians");
|
||||
LuaDefineBuiltin(math_atan, "x", "return the arc-tangent of x in radians");
|
||||
LuaDefineBuiltin(math_atan2, "y, x", "return arc-tangent of y/x, using signs to compute quadrant");
|
||||
LuaDefineBuiltin(math_ceil, "x", "return the smallest integer larger than or equal to x");
|
||||
LuaDefineBuiltin(math_cos, "x", "return the cosine of x in radians");
|
||||
LuaDefineBuiltin(math_cosh, "x", "return the hyperbolic cosine of x in radians");
|
||||
LuaDefineBuiltin(math_deg, "rad", "convert radians to degrees");
|
||||
LuaDefineBuiltin(math_exp, "x", "returns the value e^x");
|
||||
LuaDefineBuiltin(math_floor, "x", "returns the smallest integer less than or equal to x");
|
||||
LuaDefineBuiltin(math_fmod, "x, y", "return the remainder of x/y that rounds the quotient towards zero");
|
||||
LuaDefineBuiltin(math_frexp, "x", "given x, returns mantissa and exponent");
|
||||
LuaDefineBuiltin(math_ldexp, "x", "return unnormalized mantissa and exponent");
|
||||
LuaDefineBuiltin(math_log, "x [, base]", "return the log of x in base, default base is e");
|
||||
LuaDefineBuiltin(math_max, "x, x, x...", "return the largest argument");
|
||||
LuaDefineBuiltin(math_min, "x, x, x...", "return the smallest argument");
|
||||
LuaDefineBuiltin(math_modf, "x", "returns the integral and fractional part of x");
|
||||
LuaDefineBuiltin(math_pow, "x, y", "returns x ^ y, equivalent to the operator");
|
||||
LuaDefineBuiltin(math_rad, "deg", "convert degrees to radians");
|
||||
LuaDefineBuiltin(math_sin, "x", "return the sine of x in radians");
|
||||
LuaDefineBuiltin(math_sinh, "x", "return the hyperbolic sine of x in radians");
|
||||
LuaDefineBuiltin(math_sqrt, "x", "return the square root of x");
|
||||
LuaDefineBuiltin(math_tan, "x", "return the tangent of x in radians");
|
||||
LuaDefineBuiltin(math_tanh, "x", "return the hyperbolic tangent of x in radians");
|
||||
LuaSandboxBuiltin(math_log10, "", "");
|
||||
LuaNumberConstant(math_pi, M_PI, "");
|
||||
LuaNumberConstant(math_huge, HUGE_VAL, "");
|
||||
LuaNumberConstant(math_nan, NAN, "");
|
||||
LuaNumberConstant(math_maxint, LuaCoreStack::MAXINT, "");
|
||||
LuaTokenConstant(math_auto, "auto", "A value used to request that a value should be automatically computed");
|
||||
|
||||
// math.random and math.randomseed are in world-accessor.cpp, because
|
||||
// generating random numbers must manipulate global state which is
|
||||
// stored in the world model.
|
||||
|
||||
LuaDefineBuiltin(assert, "flag [,message]", "assert that flag is true, if not, raise error");
|
||||
LuaDefineBuiltin(error, "message", "raise an error");
|
||||
LuaDefineBuiltin(getmetatable, "table", "get the metatable of a table");
|
||||
LuaDefineBuiltin(ipairs, "vector", "meant to be used in loops, iterates over a vector");
|
||||
LuaDefineBuiltin(next, "table, k", "returns the key after k and the corresponding value");
|
||||
LuaDefineBuiltin(pairs, "table", "meant to be used in loops, iterates over a table");
|
||||
LuaDefineBuiltin(rawpairs, "table", "meant to be used in loops, iterates over a table");
|
||||
LuaDefineBuiltin(pcall, "function, arg1, arg2, ...", "call the function, catching any errors");
|
||||
LuaDefineBuiltin(rawequal, "val1, val2", "return true if the two values are the same object");
|
||||
LuaDefineBuiltin(rawlen, "obj", "return the length of a vector or string");
|
||||
LuaDefineBuiltin(rawget, "table, key", "get a table entry, ignoring metamethods");
|
||||
LuaDefineBuiltin(rawset, "table, key, value", "set a table entry, ignoring metamethods");
|
||||
LuaDefineBuiltin(select, "n, arg1, arg2, ...", "return the nth argument");
|
||||
LuaDefineBuiltin(setmetatable, "table, meta", "set the metatable of the specified table");
|
||||
LuaDefineBuiltin(tonumber, "str", "convert a string to a number");
|
||||
LuaDefineBuiltin(type, "obj", "return the type of obj as a string");
|
||||
// print is redefined in world.cpp (because it prints into the world model)
|
||||
// tostring is redefined in pprint.cpp
|
||||
|
||||
LuaSandboxBuiltin(collectgarbage, "", "");
|
||||
LuaSandboxBuiltin(dofile, "", "");
|
||||
LuaSandboxBuiltin(xpcall, "", "");
|
||||
LuaSandboxBuiltin(loadfile, "", "");
|
||||
LuaSandboxBuiltin(load, "", "");
|
||||
LuaSandboxBuiltin(require, "", "");
|
||||
LuaSandboxBuiltin(module, "", "");
|
||||
LuaSandboxBuiltin(loadstring, "", "");
|
||||
LuaSandboxBuiltin(unpack, "", "");
|
||||
|
||||
|
||||
LuaSandboxBuiltin(debug_debug, "", "");
|
||||
LuaSandboxBuiltin(debug_getuservalue, "", "");
|
||||
LuaSandboxBuiltin(debug_gethook, "", "");
|
||||
LuaSandboxBuiltin(debug_getinfo, "", "");
|
||||
LuaSandboxBuiltin(debug_getlocal, "", "");
|
||||
LuaSandboxBuiltin(debug_getregistry, "", "");
|
||||
LuaSandboxBuiltin(debug_getmetatable, "", "");
|
||||
LuaSandboxBuiltin(debug_getupvalue, "", "");
|
||||
LuaSandboxBuiltin(debug_upvaluejoin, "", "");
|
||||
LuaSandboxBuiltin(debug_upvalueid, "", "");
|
||||
LuaSandboxBuiltin(debug_setuservalue, "", "");
|
||||
LuaSandboxBuiltin(debug_sethook, "", "");
|
||||
LuaSandboxBuiltin(debug_setlocal, "", "");
|
||||
LuaSandboxBuiltin(debug_setmetatable, "", "");
|
||||
LuaSandboxBuiltin(debug_setupvalue, "", "");
|
||||
LuaSandboxBuiltin(debug_traceback, "", "");
|
||||
|
||||
LuaSandboxBuiltin(eris_persist, "", "");
|
||||
LuaSandboxBuiltin(eris_unpersist, "", "");
|
||||
LuaSandboxBuiltin(eris_settings, "", "");
|
||||
|
||||
LuaSandboxBuiltin(package_loadlib, "", "");
|
||||
LuaSandboxBuiltin(package_searchpath, "", "");
|
||||
LuaSandboxBuiltin(package_seeall, "", "");
|
||||
|
||||
LuaSandboxBuiltin(coroutine_create, "", "");
|
||||
LuaSandboxBuiltin(coroutine_resume, "", "");
|
||||
LuaSandboxBuiltin(coroutine_running, "", "");
|
||||
LuaSandboxBuiltin(coroutine_status, "", "");
|
||||
LuaSandboxBuiltin(coroutine_wrap, "", "");
|
||||
LuaSandboxBuiltin(coroutine_yield, "", "");
|
||||
|
||||
LuaSandboxBuiltin(io_close, "", "");
|
||||
LuaSandboxBuiltin(io_flush, "", "");
|
||||
LuaSandboxBuiltin(io_input, "", "");
|
||||
LuaSandboxBuiltin(io_lines, "", "");
|
||||
LuaSandboxBuiltin(io_open, "", "");
|
||||
LuaSandboxBuiltin(io_output, "", "");
|
||||
LuaSandboxBuiltin(io_popen, "", "");
|
||||
LuaSandboxBuiltin(io_read, "", "");
|
||||
LuaSandboxBuiltin(io_tmpfile, "", "");
|
||||
LuaSandboxBuiltin(io_type, "", "");
|
||||
LuaSandboxBuiltin(io_write, "", "");
|
||||
|
||||
LuaSandboxBuiltin(os_clock, "", "");
|
||||
LuaSandboxBuiltin(os_date, "", "");
|
||||
LuaSandboxBuiltin(os_difftime, "", "");
|
||||
LuaSandboxBuiltin(os_execute, "", "");
|
||||
LuaSandboxBuiltin(os_exit, "", "");
|
||||
LuaSandboxBuiltin(os_getenv, "", "");
|
||||
LuaSandboxBuiltin(os_remove, "", "");
|
||||
LuaSandboxBuiltin(os_rename, "", "");
|
||||
LuaSandboxBuiltin(os_setlocale, "", "");
|
||||
LuaSandboxBuiltin(os_time, "", "");
|
||||
LuaSandboxBuiltin(os_tmpname, "", "");
|
||||
|
||||
209
luprex/cpp/core/source.hpp
Normal file
209
luprex/cpp/core/source.hpp
Normal file
@@ -0,0 +1,209 @@
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// SOURCEDB
|
||||
//
|
||||
// This module manages the loading of lua source files into the Lua environment.
|
||||
// Since the source files can be reloaded over and over, this module doesn't
|
||||
// just load the source files. Instead, it does "source rebuilds" in which it
|
||||
// purges everything from the global environment, then it reinstalls everything
|
||||
// that should be there. That way, if you delete something from a lua source
|
||||
// file, it gets removed from the lua global environment.
|
||||
//
|
||||
// THE MAKECLASS OPERATOR
|
||||
//
|
||||
// This module provides a new lua 'builtin' operator: "makeclass". This creates
|
||||
// a table and stores it in the global environment. The new table is meant to be
|
||||
// used as a class. Three fields are initially created:
|
||||
//
|
||||
// __index --> points back to the class. Makes it convenient to use the
|
||||
// class as a metatable.
|
||||
//
|
||||
// action --> a subtable of additional methods.
|
||||
//
|
||||
// If you invoke 'makeclass' on a class that already exists, the existing table
|
||||
// is "repaired" - __index field is restored and the action
|
||||
// subtable, if not present, is recreated. If there are already functions or
|
||||
// constants inside the class, they are not affected.
|
||||
//
|
||||
//
|
||||
// THE LUA SOURCE DATABASE
|
||||
//
|
||||
// Class SourceDB only contains a single pointer to the lua environment. That's
|
||||
// because all the data for SourceDB is stored in the lua registry.
|
||||
// Specifically, the registry contains these keys:
|
||||
//
|
||||
// 1. The source database proper (registry key "sourcedb")
|
||||
// 2. The snapshot of builtins (registry key "source_snapshot_builtins")
|
||||
//
|
||||
// The source database proper is a table where the keys are filenames, and the
|
||||
// values are records containing information about that file:
|
||||
//
|
||||
// "foo.lua" : {
|
||||
// "name": "foo.lua",
|
||||
// "fingerprint": "12893129385854",
|
||||
// "code": "function xyz ...",
|
||||
// "loadresult": <function-23>,
|
||||
// "sequence": 13
|
||||
// }
|
||||
//
|
||||
// Let's break that down a little. The "name" field is just the filename. The
|
||||
// "fingerprint" is an encoding of the file modification date and the file
|
||||
// length, it is used to detect whether the file has been altered on disk
|
||||
// without having to reread the file. The "code" is the entire content of the
|
||||
// source file. The "loadresult" is the closure that results from calling the
|
||||
// lua 'load' function on the code. If load fails, then the "loadresult" is an
|
||||
// error message. Finally, "sequence" indicates the order in which the source
|
||||
// files are meant to be loaded.
|
||||
//
|
||||
// The operation SourceDB::update refreshes the source database from disk. It
|
||||
// doesn't reread files whose fingerprints have not changed. In a synchronous
|
||||
// model, we don't call SourceDB::update - instead, we update the source
|
||||
// database by difference transmission.
|
||||
//
|
||||
// Note that updating the source database has *no effect* on the lua global
|
||||
// variables (or lua function definitions). The SourceDB::update operation
|
||||
// calls lua's "load" on the code, but all that does is return a loaded closure.
|
||||
// Nothing happens to the lua invironment until you *invoke* the loaded closure.
|
||||
// That doesn't happen until you do a SourceDB::rebuild operation, described
|
||||
// below.
|
||||
//
|
||||
//
|
||||
// SOURCE REBUILDS, IN DEPTH
|
||||
//
|
||||
// The function SourceDB::rebuild clears and then rebuilds the entire contents
|
||||
// of the lua global environment. The reason to clear the environment is that
|
||||
// if we didn't clear it, then removing a function from the lua source would not
|
||||
// remove it from the lua environment. Rebuilding the lua environment is a
|
||||
// multi-step process:
|
||||
//
|
||||
// * Delete everything from the global environment except class tables.
|
||||
//
|
||||
// - Class tables are kept, but the contents are cleared.
|
||||
// - If a class has an "actions" subtable, that is kept and cleared.
|
||||
// - Anything else is deleted.
|
||||
//
|
||||
// * Lua Builtin functions are reinstalled in the global environment.
|
||||
//
|
||||
// - To make this possible, the snapshot of builtins is used.
|
||||
//
|
||||
// * C++ functions registered with "LuaDefine" are reinstalled.
|
||||
//
|
||||
// - LuaDefine creates a registry, that registry is iterated over.
|
||||
//
|
||||
// * Lua code is reinstalled by running the closures in the source DB.
|
||||
//
|
||||
// - Simply call the "loadresult" closure to reinstall functions into the
|
||||
// global environment.
|
||||
//
|
||||
// Note that if you've stored any global data in the lua environment, it's gone.
|
||||
// So therefore, we have to provide a separate "safe" space for global data
|
||||
// structures. That is provided elsewhere, in the module "globaldb".
|
||||
//
|
||||
//
|
||||
// UNITTESTS
|
||||
//
|
||||
// We reserve the lua class name 'unittests' for storing unit tests. Any
|
||||
// function placed into this class is considered a unit test. We don't separate
|
||||
// unit tests from the rest of the code - they're compiled right into the main
|
||||
// binary.
|
||||
//
|
||||
// Unit tests can be either lua functions, or Lua-registered C functions. Unit
|
||||
// tests are executed by calling 'source_run_unittests'.
|
||||
//
|
||||
// Each unit test is run in a protected 'pcall' environment. Any errors are
|
||||
// printed out to console. (At least for now).
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef SOURCE_HPP
|
||||
#define SOURCE_HPP
|
||||
|
||||
#include "wrap-string.hpp"
|
||||
|
||||
#include "util.hpp"
|
||||
#include "luastack.hpp"
|
||||
#include "streambuffer.hpp"
|
||||
#include "debugcollector.hpp"
|
||||
|
||||
class SourceDB : public eng::nevernew {
|
||||
private:
|
||||
lua_State *lua_state_;
|
||||
|
||||
public:
|
||||
void init(lua_State *L);
|
||||
|
||||
// Update
|
||||
//
|
||||
// Update the database using the specified lua source code.
|
||||
// Compiles these files using lua's "load" function.
|
||||
//
|
||||
void update(const util::LuaSourceVec &source);
|
||||
|
||||
// modules
|
||||
//
|
||||
// Returns a list of all the modules. The first item in the list
|
||||
// is always the string "CORE" which represents the lua core
|
||||
// functionality with all the builtins. This is then followed by
|
||||
// all the lua sourcefiles in the correct order.
|
||||
//
|
||||
eng::vector<eng::string> modules();
|
||||
|
||||
// rebuild_module
|
||||
//
|
||||
// To rebuild the lua environment, fetch the module list, then
|
||||
// call rebuild_module on each module in turn. This will return
|
||||
// an error message for the module, or empty string if no error.
|
||||
//
|
||||
// This is a thin wrapper around traceback_pcall. The return
|
||||
// value is the return value of traceback_pcall.
|
||||
//
|
||||
eng::string rebuild_module(const eng::string &mod);
|
||||
|
||||
// rebuild_core
|
||||
//
|
||||
// This is equivalent to rebuild_module("CORE"). Clears the environment
|
||||
// and installs all the builtins. No error conditions.
|
||||
//
|
||||
void rebuild_core();
|
||||
|
||||
// Difference transmission.
|
||||
//
|
||||
// Note: The patch routine applies the differences to the source
|
||||
// database, and if there are any changes, it does a source rebuild.
|
||||
// The patch routine returns true if anything was modified.
|
||||
//
|
||||
void diff(const SourceDB &auth, StreamBuffer *sb);
|
||||
bool patch(StreamBuffer *sb, DebugCollector *dbc);
|
||||
|
||||
// run_unittests
|
||||
//
|
||||
// Run all the lua unit tests. Print any errors to console. If there
|
||||
// are any errors, exits the program.
|
||||
//
|
||||
void run_unittests();
|
||||
|
||||
// Get/Set code (for unit testing).
|
||||
//
|
||||
// These functions are direct getters/setters for values in the source
|
||||
// database. They are intended only for unit testing.
|
||||
//
|
||||
void set(const eng::string &fn, const eng::string &code, int sequence);
|
||||
eng::string get(const eng::string &fn);
|
||||
|
||||
// Add builtins to the global function registry.
|
||||
//
|
||||
static void register_lua_builtins();
|
||||
|
||||
// Get function documentation.
|
||||
//
|
||||
static eng::string function_docs(const LuaCoreStack &LS, LuaSlot slot);
|
||||
|
||||
// Serialize and unserialize a source vector.
|
||||
//
|
||||
static void serialize_source(const util::LuaSourceVec &sv, StreamBuffer *sb);
|
||||
static void deserialize_source(util::LuaSourceVec *sv, StreamBuffer *sb);
|
||||
};
|
||||
|
||||
#endif // SOURCE_HPP
|
||||
|
||||
|
||||
360
luprex/cpp/core/spookyv2.cpp
Normal file
360
luprex/cpp/core/spookyv2.cpp
Normal file
@@ -0,0 +1,360 @@
|
||||
// Spooky Hash
|
||||
// A 128-bit noncryptographic hash, for checksums and table lookup
|
||||
// By Bob Jenkins. Public domain.
|
||||
// Oct 31 2010: published framework, disclaimer ShortHash isn't right
|
||||
// Nov 7 2010: disabled ShortHash
|
||||
// Oct 31 2011: replace End, ShortMix, ShortEnd, enable ShortHash again
|
||||
// April 10 2012: buffer overflow on platforms without unaligned reads
|
||||
// July 12 2012: was passing out variables in final to in/out in short
|
||||
// July 30 2012: I reintroduced the buffer overflow
|
||||
// August 5 2012: SpookyV2: d = should be d += in short hash, and remove extra mix from long hash
|
||||
|
||||
#include <memory.h>
|
||||
#include "spookyv2.hpp"
|
||||
|
||||
#define ALLOW_UNALIGNED_READS 1
|
||||
|
||||
#define INLINE inline
|
||||
|
||||
// number of uint64_t's in internal state
|
||||
static const size_t sc_numVars = 12;
|
||||
|
||||
// size of the internal state
|
||||
static const size_t sc_blockSize = sc_numVars*8;
|
||||
|
||||
// size of buffer of unhashed data, in bytes
|
||||
static const size_t sc_bufSize = 2*sc_blockSize;
|
||||
|
||||
//
|
||||
// sc_const: a constant which:
|
||||
// * is not zero
|
||||
// * is odd
|
||||
// * is a not-very-regular mix of 1's and 0's
|
||||
// * does not need any other special mathematical properties
|
||||
//
|
||||
static const uint64_t sc_const = 0xdeadbeefdeadbeefLL;
|
||||
|
||||
//
|
||||
// left rotate a 64-bit value by k bytes
|
||||
//
|
||||
static INLINE uint64_t Rot64(uint64_t x, int k)
|
||||
{
|
||||
return (x << k) | (x >> (64 - k));
|
||||
}
|
||||
|
||||
//
|
||||
// This is used if the input is 96 bytes long or longer.
|
||||
//
|
||||
// The internal state is fully overwritten every 96 bytes.
|
||||
// Every input bit appears to cause at least 128 bits of entropy
|
||||
// before 96 other bytes are combined, when run forward or backward
|
||||
// For every input bit,
|
||||
// Two inputs differing in just that input bit
|
||||
// Where "differ" means xor or subtraction
|
||||
// And the base value is random
|
||||
// When run forward or backwards one Mix
|
||||
// I tried 3 pairs of each; they all differed by at least 212 bits.
|
||||
//
|
||||
static INLINE void Mix(
|
||||
const uint64_t *data,
|
||||
uint64_t &s0, uint64_t &s1, uint64_t &s2, uint64_t &s3,
|
||||
uint64_t &s4, uint64_t &s5, uint64_t &s6, uint64_t &s7,
|
||||
uint64_t &s8, uint64_t &s9, uint64_t &s10,uint64_t &s11)
|
||||
{
|
||||
s0 += data[0]; s2 ^= s10; s11 ^= s0; s0 = Rot64(s0,11); s11 += s1;
|
||||
s1 += data[1]; s3 ^= s11; s0 ^= s1; s1 = Rot64(s1,32); s0 += s2;
|
||||
s2 += data[2]; s4 ^= s0; s1 ^= s2; s2 = Rot64(s2,43); s1 += s3;
|
||||
s3 += data[3]; s5 ^= s1; s2 ^= s3; s3 = Rot64(s3,31); s2 += s4;
|
||||
s4 += data[4]; s6 ^= s2; s3 ^= s4; s4 = Rot64(s4,17); s3 += s5;
|
||||
s5 += data[5]; s7 ^= s3; s4 ^= s5; s5 = Rot64(s5,28); s4 += s6;
|
||||
s6 += data[6]; s8 ^= s4; s5 ^= s6; s6 = Rot64(s6,39); s5 += s7;
|
||||
s7 += data[7]; s9 ^= s5; s6 ^= s7; s7 = Rot64(s7,57); s6 += s8;
|
||||
s8 += data[8]; s10 ^= s6; s7 ^= s8; s8 = Rot64(s8,55); s7 += s9;
|
||||
s9 += data[9]; s11 ^= s7; s8 ^= s9; s9 = Rot64(s9,54); s8 += s10;
|
||||
s10 += data[10]; s0 ^= s8; s9 ^= s10; s10 = Rot64(s10,22); s9 += s11;
|
||||
s11 += data[11]; s1 ^= s9; s10 ^= s11; s11 = Rot64(s11,46); s10 += s0;
|
||||
}
|
||||
|
||||
//
|
||||
// Mix all 12 inputs together so that h0, h1 are a hash of them all.
|
||||
//
|
||||
// For two inputs differing in just the input bits
|
||||
// Where "differ" means xor or subtraction
|
||||
// And the base value is random, or a counting value starting at that bit
|
||||
// The final result will have each bit of h0, h1 flip
|
||||
// For every input bit,
|
||||
// with probability 50 +- .3%
|
||||
// For every pair of input bits,
|
||||
// with probability 50 +- 3%
|
||||
//
|
||||
// This does not rely on the last Mix() call having already mixed some.
|
||||
// Two iterations was almost good enough for a 64-bit result, but a
|
||||
// 128-bit result is reported, so End() does three iterations.
|
||||
//
|
||||
static INLINE void EndPartial(
|
||||
uint64_t &h0, uint64_t &h1, uint64_t &h2, uint64_t &h3,
|
||||
uint64_t &h4, uint64_t &h5, uint64_t &h6, uint64_t &h7,
|
||||
uint64_t &h8, uint64_t &h9, uint64_t &h10,uint64_t &h11)
|
||||
{
|
||||
h11+= h1; h2 ^= h11; h1 = Rot64(h1,44);
|
||||
h0 += h2; h3 ^= h0; h2 = Rot64(h2,15);
|
||||
h1 += h3; h4 ^= h1; h3 = Rot64(h3,34);
|
||||
h2 += h4; h5 ^= h2; h4 = Rot64(h4,21);
|
||||
h3 += h5; h6 ^= h3; h5 = Rot64(h5,38);
|
||||
h4 += h6; h7 ^= h4; h6 = Rot64(h6,33);
|
||||
h5 += h7; h8 ^= h5; h7 = Rot64(h7,10);
|
||||
h6 += h8; h9 ^= h6; h8 = Rot64(h8,13);
|
||||
h7 += h9; h10^= h7; h9 = Rot64(h9,38);
|
||||
h8 += h10; h11^= h8; h10= Rot64(h10,53);
|
||||
h9 += h11; h0 ^= h9; h11= Rot64(h11,42);
|
||||
h10+= h0; h1 ^= h10; h0 = Rot64(h0,54);
|
||||
}
|
||||
|
||||
static INLINE void End(
|
||||
const uint64_t *data,
|
||||
uint64_t &h0, uint64_t &h1, uint64_t &h2, uint64_t &h3,
|
||||
uint64_t &h4, uint64_t &h5, uint64_t &h6, uint64_t &h7,
|
||||
uint64_t &h8, uint64_t &h9, uint64_t &h10,uint64_t &h11)
|
||||
{
|
||||
h0 += data[0]; h1 += data[1]; h2 += data[2]; h3 += data[3];
|
||||
h4 += data[4]; h5 += data[5]; h6 += data[6]; h7 += data[7];
|
||||
h8 += data[8]; h9 += data[9]; h10 += data[10]; h11 += data[11];
|
||||
EndPartial(h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11);
|
||||
EndPartial(h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11);
|
||||
EndPartial(h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11);
|
||||
}
|
||||
|
||||
//
|
||||
// The goal is for each bit of the input to expand into 128 bits of
|
||||
// apparent entropy before it is fully overwritten.
|
||||
// n trials both set and cleared at least m bits of h0 h1 h2 h3
|
||||
// n: 2 m: 29
|
||||
// n: 3 m: 46
|
||||
// n: 4 m: 57
|
||||
// n: 5 m: 107
|
||||
// n: 6 m: 146
|
||||
// n: 7 m: 152
|
||||
// when run forwards or backwards
|
||||
// for all 1-bit and 2-bit diffs
|
||||
// with diffs defined by either xor or subtraction
|
||||
// with a base of all zeros plus a counter, or plus another bit, or random
|
||||
//
|
||||
static INLINE void ShortMix(uint64_t &h0, uint64_t &h1, uint64_t &h2, uint64_t &h3)
|
||||
{
|
||||
h2 = Rot64(h2,50); h2 += h3; h0 ^= h2;
|
||||
h3 = Rot64(h3,52); h3 += h0; h1 ^= h3;
|
||||
h0 = Rot64(h0,30); h0 += h1; h2 ^= h0;
|
||||
h1 = Rot64(h1,41); h1 += h2; h3 ^= h1;
|
||||
h2 = Rot64(h2,54); h2 += h3; h0 ^= h2;
|
||||
h3 = Rot64(h3,48); h3 += h0; h1 ^= h3;
|
||||
h0 = Rot64(h0,38); h0 += h1; h2 ^= h0;
|
||||
h1 = Rot64(h1,37); h1 += h2; h3 ^= h1;
|
||||
h2 = Rot64(h2,62); h2 += h3; h0 ^= h2;
|
||||
h3 = Rot64(h3,34); h3 += h0; h1 ^= h3;
|
||||
h0 = Rot64(h0,5); h0 += h1; h2 ^= h0;
|
||||
h1 = Rot64(h1,36); h1 += h2; h3 ^= h1;
|
||||
}
|
||||
|
||||
//
|
||||
// Mix all 4 inputs together so that h0, h1 are a hash of them all.
|
||||
//
|
||||
// For two inputs differing in just the input bits
|
||||
// Where "differ" means xor or subtraction
|
||||
// And the base value is random, or a counting value starting at that bit
|
||||
// The final result will have each bit of h0, h1 flip
|
||||
// For every input bit,
|
||||
// with probability 50 +- .3% (it is probably better than that)
|
||||
// For every pair of input bits,
|
||||
// with probability 50 +- .75% (the worst case is approximately that)
|
||||
//
|
||||
static INLINE void ShortEnd(uint64_t &h0, uint64_t &h1, uint64_t &h2, uint64_t &h3)
|
||||
{
|
||||
h3 ^= h2; h2 = Rot64(h2,15); h3 += h2;
|
||||
h0 ^= h3; h3 = Rot64(h3,52); h0 += h3;
|
||||
h1 ^= h0; h0 = Rot64(h0,26); h1 += h0;
|
||||
h2 ^= h1; h1 = Rot64(h1,51); h2 += h1;
|
||||
h3 ^= h2; h2 = Rot64(h2,28); h3 += h2;
|
||||
h0 ^= h3; h3 = Rot64(h3,9); h0 += h3;
|
||||
h1 ^= h0; h0 = Rot64(h0,47); h1 += h0;
|
||||
h2 ^= h1; h1 = Rot64(h1,54); h2 += h1;
|
||||
h3 ^= h2; h2 = Rot64(h2,32); h3 += h2;
|
||||
h0 ^= h3; h3 = Rot64(h3,25); h0 += h3;
|
||||
h1 ^= h0; h0 = Rot64(h0,63); h1 += h0;
|
||||
}
|
||||
|
||||
//
|
||||
// short hash ... it could be used on any message,
|
||||
// but it's used by Spooky just for short messages.
|
||||
//
|
||||
static void Short(
|
||||
const void *message,
|
||||
size_t length,
|
||||
uint64_t *hash1,
|
||||
uint64_t *hash2)
|
||||
{
|
||||
uint64_t buf[2*sc_numVars];
|
||||
union
|
||||
{
|
||||
const uint8_t *p8;
|
||||
uint32_t *p32;
|
||||
uint64_t *p64;
|
||||
size_t i;
|
||||
} u;
|
||||
|
||||
u.p8 = (const uint8_t *)message;
|
||||
|
||||
if (!ALLOW_UNALIGNED_READS && (u.i & 0x7))
|
||||
{
|
||||
memcpy(buf, message, length);
|
||||
u.p64 = buf;
|
||||
}
|
||||
|
||||
size_t remainder = length%32;
|
||||
uint64_t a=*hash1;
|
||||
uint64_t b=*hash2;
|
||||
uint64_t c=sc_const;
|
||||
uint64_t d=sc_const;
|
||||
|
||||
if (length > 15)
|
||||
{
|
||||
const uint64_t *end = u.p64 + (length/32)*4;
|
||||
|
||||
// handle all complete sets of 32 bytes
|
||||
for (; u.p64 < end; u.p64 += 4)
|
||||
{
|
||||
c += u.p64[0];
|
||||
d += u.p64[1];
|
||||
ShortMix(a,b,c,d);
|
||||
a += u.p64[2];
|
||||
b += u.p64[3];
|
||||
}
|
||||
|
||||
//Handle the case of 16+ remaining bytes.
|
||||
if (remainder >= 16)
|
||||
{
|
||||
c += u.p64[0];
|
||||
d += u.p64[1];
|
||||
ShortMix(a,b,c,d);
|
||||
u.p64 += 2;
|
||||
remainder -= 16;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the last 0..15 bytes, and its length
|
||||
d += ((uint64_t)length) << 56;
|
||||
switch (remainder)
|
||||
{
|
||||
case 15:
|
||||
d += ((uint64_t)u.p8[14]) << 48;
|
||||
case 14:
|
||||
d += ((uint64_t)u.p8[13]) << 40;
|
||||
case 13:
|
||||
d += ((uint64_t)u.p8[12]) << 32;
|
||||
case 12:
|
||||
d += u.p32[2];
|
||||
c += u.p64[0];
|
||||
break;
|
||||
case 11:
|
||||
d += ((uint64_t)u.p8[10]) << 16;
|
||||
case 10:
|
||||
d += ((uint64_t)u.p8[9]) << 8;
|
||||
case 9:
|
||||
d += (uint64_t)u.p8[8];
|
||||
case 8:
|
||||
c += u.p64[0];
|
||||
break;
|
||||
case 7:
|
||||
c += ((uint64_t)u.p8[6]) << 48;
|
||||
case 6:
|
||||
c += ((uint64_t)u.p8[5]) << 40;
|
||||
case 5:
|
||||
c += ((uint64_t)u.p8[4]) << 32;
|
||||
case 4:
|
||||
c += u.p32[0];
|
||||
break;
|
||||
case 3:
|
||||
c += ((uint64_t)u.p8[2]) << 16;
|
||||
case 2:
|
||||
c += ((uint64_t)u.p8[1]) << 8;
|
||||
case 1:
|
||||
c += (uint64_t)u.p8[0];
|
||||
break;
|
||||
case 0:
|
||||
c += sc_const;
|
||||
d += sc_const;
|
||||
}
|
||||
ShortEnd(a,b,c,d);
|
||||
*hash1 = a;
|
||||
*hash2 = b;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// do the whole hash in one call
|
||||
void SpookyHash::ChainHash128(
|
||||
const void *message,
|
||||
size_t length,
|
||||
uint64_t *hash1,
|
||||
uint64_t *hash2)
|
||||
{
|
||||
if ((*hash1 == 0) && (*hash2 == 0)) {
|
||||
*hash1 = 0x9438478934792837;
|
||||
*hash2 = 0x8347848738748378;
|
||||
}
|
||||
|
||||
if (length < sc_bufSize)
|
||||
{
|
||||
Short(message, length, hash1, hash2);
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11;
|
||||
uint64_t buf[sc_numVars];
|
||||
uint64_t *end;
|
||||
union
|
||||
{
|
||||
const uint8_t *p8;
|
||||
uint64_t *p64;
|
||||
size_t i;
|
||||
} u;
|
||||
size_t remainder;
|
||||
|
||||
h0=h3=h6=h9 = *hash1;
|
||||
h1=h4=h7=h10 = *hash2;
|
||||
h2=h5=h8=h11 = sc_const;
|
||||
|
||||
u.p8 = (const uint8_t *)message;
|
||||
end = u.p64 + (length/sc_blockSize)*sc_numVars;
|
||||
|
||||
// handle all whole sc_blockSize blocks of bytes
|
||||
if (ALLOW_UNALIGNED_READS || ((u.i & 0x7) == 0))
|
||||
{
|
||||
while (u.p64 < end)
|
||||
{
|
||||
Mix(u.p64, h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11);
|
||||
u.p64 += sc_numVars;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
while (u.p64 < end)
|
||||
{
|
||||
memcpy(buf, u.p64, sc_blockSize);
|
||||
Mix(buf, h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11);
|
||||
u.p64 += sc_numVars;
|
||||
}
|
||||
}
|
||||
|
||||
// handle the last partial block of sc_blockSize bytes
|
||||
remainder = (length - ((const uint8_t *)end-(const uint8_t *)message));
|
||||
memcpy(buf, end, remainder);
|
||||
memset(((uint8_t *)buf)+remainder, 0, sc_blockSize-remainder);
|
||||
((uint8_t *)buf)[sc_blockSize-1] = remainder;
|
||||
|
||||
// do some final mixing
|
||||
End(buf, h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11);
|
||||
*hash1 = h0;
|
||||
*hash2 = h1;
|
||||
}
|
||||
|
||||
83
luprex/cpp/core/spookyv2.hpp
Normal file
83
luprex/cpp/core/spookyv2.hpp
Normal file
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// SpookyHash: a 128-bit noncryptographic hash function
|
||||
// By Bob Jenkins, public domain
|
||||
// Oct 31 2010: alpha, framework + SpookyHash::Mix appears right
|
||||
// Oct 31 2011: alpha again, Mix only good to 2^^69 but rest appears right
|
||||
// Dec 31 2011: beta, improved Mix, tested it for 2-bit deltas
|
||||
// Feb 2 2012: production, same bits as beta
|
||||
// Feb 5 2012: adjusted definitions of uint* to be more portable
|
||||
// Mar 30 2012: 3 bytes/cycle, not 4. Alpha was 4 but wasn't thorough enough.
|
||||
// August 5 2012: SpookyV2 (different results)
|
||||
//
|
||||
// Up to 3 bytes/cycle for long messages. Reasonably fast for short messages.
|
||||
// All 1 or 2 bit deltas achieve avalanche within 1% bias per output bit.
|
||||
//
|
||||
// This was developed for and tested on 64-bit x86-compatible processors.
|
||||
// It assumes the processor is little-endian. There is a macro
|
||||
// controlling whether unaligned reads are allowed (by default they are).
|
||||
// This should be an equally good hash on big-endian machines, but it will
|
||||
// compute different results on them than on little-endian machines.
|
||||
//
|
||||
// Google's CityHash has similar specs to SpookyHash, and CityHash is faster
|
||||
// on new Intel boxes. MD4 and MD5 also have similar specs, but they are orders
|
||||
// of magnitude slower. CRCs are two or more times slower, but unlike
|
||||
// SpookyHash, they have nice math for combining the CRCs of pieces to form
|
||||
// the CRCs of wholes. There are also cryptographic hashes, but those are even
|
||||
// slower than MD5.
|
||||
//
|
||||
|
||||
#ifndef SPOOKYV2_HPP
|
||||
#define SPOOKYV2_HPP
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
class SpookyHash
|
||||
{
|
||||
public:
|
||||
// A hash is two uint64's.
|
||||
//
|
||||
using HashValue = std::pair<uint64_t, uint64_t>;
|
||||
|
||||
//
|
||||
// SpookyHash: hash a single message in one call, produce 128-bit output
|
||||
//
|
||||
static void ChainHash128(
|
||||
const void *message, // message to hash
|
||||
size_t length, // length of message in bytes
|
||||
uint64_t *hash1, // input seed0, output hash0
|
||||
uint64_t *hash2); // input seed1, output hash1
|
||||
|
||||
|
||||
static inline HashValue QkHash128(const void *message, size_t len) {
|
||||
uint64_t hash1 = 0;
|
||||
uint64_t hash2 = 0;
|
||||
ChainHash128(message, len, &hash1, &hash2);
|
||||
return std::make_pair(hash1, hash2);
|
||||
}
|
||||
|
||||
static inline HashValue QkHash128(std::string_view v) {
|
||||
uint64_t hash1 = 0;
|
||||
uint64_t hash2 = 0;
|
||||
ChainHash128(v.data(), v.size(), &hash1, &hash2);
|
||||
return std::make_pair(hash1, hash2);
|
||||
}
|
||||
|
||||
static inline uint64_t QkHash64(const void *message, size_t len) {
|
||||
uint64_t hash1 = 0;
|
||||
uint64_t hash2 = 0;
|
||||
ChainHash128(message, len, &hash1, &hash2);
|
||||
return hash1;
|
||||
}
|
||||
|
||||
static inline uint64_t QkHash64(std::string_view v) {
|
||||
uint64_t hash1 = 0;
|
||||
uint64_t hash2 = 0;
|
||||
ChainHash128(v.data(), v.size(), &hash1, &hash2);
|
||||
return hash1;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // SPOOKYV2_HPP
|
||||
149
luprex/cpp/core/streambuffer.cpp
Normal file
149
luprex/cpp/core/streambuffer.cpp
Normal file
@@ -0,0 +1,149 @@
|
||||
#include "wrap-string.hpp"
|
||||
|
||||
#include "eng-malloc.hpp"
|
||||
#include "streambuffer.hpp"
|
||||
#include "spookyv2.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
|
||||
|
||||
int lua_writer_into_streambuffer(lua_State *L, const void* bytes, size_t sz, void* sbv) {
|
||||
StreamBuffer *sb = (StreamBuffer *)sbv;
|
||||
memcpy(sb->make_space(sz), bytes, sz);
|
||||
sb->wrote_space(sz);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool streq(const char *str, const char *data) {
|
||||
int len = strlen(str);
|
||||
return memcmp(str, data, len) == 0;
|
||||
}
|
||||
|
||||
LuaDefine(unittests_streambuffer, "", "some unit tests") {
|
||||
// An 11-byte fixed-size stream buffer.
|
||||
StreamBuffer sb11(11, true);
|
||||
|
||||
// Check the initial state.
|
||||
LuaAssert(L, sb11.layout_is(0, 0, 11));
|
||||
|
||||
// Write a few bytes.
|
||||
sb11.write_bytes("abcdef");
|
||||
LuaAssert(L, sb11.layout_is(0, 6, 5));
|
||||
|
||||
// Try reading some bytes.
|
||||
LuaAssert(L, streq("abcd", sb11.read_bytes(4)));
|
||||
LuaAssert(L, sb11.layout_is(4, 2, 5));
|
||||
|
||||
// Put back two bytes.
|
||||
sb11.unread_to(2);
|
||||
LuaAssert(L, sb11.layout_is(2, 4, 5));
|
||||
|
||||
// Read some more bytes.
|
||||
LuaAssert(L, streq("cdef", sb11.read_bytes(4)));
|
||||
LuaAssert(L, sb11.layout_is(6, 0, 5));
|
||||
|
||||
// Reading bytes now should raise an EOF and should not alter layout
|
||||
try {
|
||||
sb11.read_bytes(1);
|
||||
LuaAssert(L, false && "This should have thrown an exception");
|
||||
} catch (const StreamEofOnRead &) {}
|
||||
LuaAssert(L, sb11.layout_is(6, 0, 5));
|
||||
|
||||
// Write some more bytes into the stream, forcing a shift-left
|
||||
sb11.write_bytes("ghijkl");
|
||||
LuaAssert(L, sb11.layout_is(0, 6, 5));
|
||||
|
||||
// Test buffer wrapping a little more.
|
||||
LuaAssert(L, streq("ghi", sb11.read_bytes(3)));
|
||||
LuaAssert(L, sb11.layout_is(3, 3, 5));
|
||||
sb11.write_bytes("mnopqr");
|
||||
LuaAssert(L, sb11.layout_is(0, 9, 2));
|
||||
LuaAssert(L, streq("jklmnopqr", sb11.read_bytes(9)));
|
||||
LuaAssert(L, sb11.layout_is(9, 0, 2));
|
||||
|
||||
// Test 1-byte integer ops.
|
||||
sb11.clear();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
sb11.write_int8(i);
|
||||
sb11.write_int8(i+100);
|
||||
LuaAssert(L, sb11.read_int8() == i);
|
||||
LuaAssert(L, sb11.read_int8() == i+100);
|
||||
}
|
||||
|
||||
// Test 2-byte integer ops.
|
||||
for (int i = 0; i < 10; i++) {
|
||||
sb11.write_int16(i);
|
||||
sb11.write_int16(i+10000);
|
||||
LuaAssert(L, sb11.read_int16() == i);
|
||||
LuaAssert(L, sb11.read_int16() == i+10000);
|
||||
}
|
||||
|
||||
// Test 4-byte integer ops.
|
||||
for (int i = 0; i < 10; i++) {
|
||||
sb11.write_int32(i);
|
||||
sb11.write_int32(i+1000000);
|
||||
LuaAssert(L, sb11.read_int32() == i);
|
||||
LuaAssert(L, sb11.read_int32() == i+1000000);
|
||||
}
|
||||
|
||||
// Test 8-byte integer ops.
|
||||
for (int i = 0; i < 10; i++) {
|
||||
sb11.write_int64(i + 1000000);
|
||||
LuaAssert(L, sb11.read_int64() == i + 1000000);
|
||||
}
|
||||
|
||||
// Check the write count and read count accumulator ability.
|
||||
sb11.clear();
|
||||
LuaAssert(L, sb11.total_writes() == 0);
|
||||
LuaAssert(L, sb11.total_reads() == 0);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
LuaAssert(L, sb11.total_writes() == i * 8);
|
||||
sb11.write_int32(i);
|
||||
sb11.write_int32(i+1000000);
|
||||
LuaAssert(L, sb11.total_reads() == i * 8);
|
||||
LuaAssert(L, sb11.read_int32() == i);
|
||||
LuaAssert(L, sb11.read_int32() == i+1000000);
|
||||
}
|
||||
|
||||
// Try clearing the buffer.
|
||||
sb11.clear();
|
||||
LuaAssert(L, sb11.layout_is(0, 0, 11));
|
||||
|
||||
// Write a number then overwrite.
|
||||
for (int i = 0; i < 2; i++) {
|
||||
sb11.write_int16(12);
|
||||
sb11.write_int16(34);
|
||||
int64_t wc = sb11.total_writes();
|
||||
sb11.write_int16(56);
|
||||
sb11.write_int16(78);
|
||||
sb11.overwrite_int16(wc, 90);
|
||||
LuaAssert(L, sb11.read_int16() == 12);
|
||||
LuaAssert(L, sb11.read_int16() == 90);
|
||||
LuaAssert(L, sb11.read_int16() == 56);
|
||||
LuaAssert(L, sb11.read_int16() == 78);
|
||||
}
|
||||
|
||||
// Try compact string encoding.
|
||||
sb11.clear();
|
||||
sb11.write_string("abc");
|
||||
sb11.write_string("");
|
||||
sb11.write_string("de");
|
||||
LuaAssert(L, sb11.layout_is(0, 8, 3));
|
||||
LuaAssert(L, sb11.read_string() == "abc");
|
||||
LuaAssert(L, sb11.read_string() == "");
|
||||
LuaAssert(L, sb11.read_string() == "de");
|
||||
|
||||
// Make sure that contents_equal is not obviously broken.
|
||||
StreamBuffer eqsb1, eqsb2;
|
||||
eqsb1.write_int32(12);
|
||||
eqsb1.write_int32(34);
|
||||
eqsb2.write_int32(34);
|
||||
LuaAssert(L, !eqsb1.contents_equal(&eqsb2));
|
||||
eqsb1.read_int32();
|
||||
LuaAssert(L, eqsb1.contents_equal(&eqsb2));
|
||||
eqsb1.write_int32(34);
|
||||
LuaAssert(L, !eqsb1.contents_equal(&eqsb2));
|
||||
|
||||
return 0;
|
||||
}
|
||||
311
luprex/cpp/core/streambuffer.hpp
Normal file
311
luprex/cpp/core/streambuffer.hpp
Normal file
@@ -0,0 +1,311 @@
|
||||
//////////////////////////////////////////////////////////////
|
||||
//
|
||||
// STREAMBUFFER
|
||||
//
|
||||
// Serves as a buffer for buffered I/O operations. Has rather sophisticated
|
||||
// methods to help serialize and deserialize data.
|
||||
//
|
||||
// The semantics of this class contain a lot of subtlety! Please read the
|
||||
// documentation carefully.
|
||||
//
|
||||
// TELLING LINUX TO READ A FILE DESCRIPTOR INTO A STREAMBUFFER
|
||||
//
|
||||
// It is possible to read from a linux file descriptor, directly into a stream
|
||||
// buffer. You should do this, it's very efficient. Here is how you do it:
|
||||
//
|
||||
// // With linux read, you have to pick an arbitrary buffer size.
|
||||
// const int bufsize = 16384;
|
||||
//
|
||||
// // Allocate transient space in the streambuffer.
|
||||
// char *space = streambuffer.make_space(bufsize);
|
||||
//
|
||||
// // Call the linux 'read' function.
|
||||
// ssize_t bytes_read = read(fd, space, bufsize);
|
||||
//
|
||||
// // Append the bytes read to the streambuffer.
|
||||
// streambuffer.wrote_space(bytes_read);
|
||||
//
|
||||
// The make_space operation allocates an array of bytes where the data can be
|
||||
// written, and returns a pointer to that array of bytes. The read operation
|
||||
// fills some or all of the allocated bytes. Finally, the wrote_space operation
|
||||
// notifies the StreamBuffer that some of the bytes have been filled with data.
|
||||
// These bytes are appended to the StreamBuffer.
|
||||
//
|
||||
// The pointer returned by 'make_space' is only valid until you mutate the
|
||||
// StreamBuffer. Therefore, you should call 'make_space', then immediately fill
|
||||
// the bytes. It is imperative that 'wrote_space' be the first mutator after
|
||||
// 'make_space.' You should think of 'make_space' followed by 'wrote_space' as
|
||||
// a single two-phase operation.
|
||||
//
|
||||
// THE OVERWRITE_INT METHODS:
|
||||
//
|
||||
// These overwrite methods are meant to help deal with this situation: you want
|
||||
// to write a length followed by some data, but you don't know the length until
|
||||
// after you've written the data. The workaround: write a dummy length, then
|
||||
// write the data, and then overwrite the previously-written length with the
|
||||
// correct length. This is the construction that accomplishes this:
|
||||
//
|
||||
// // Write the dummy length, this will get overwritten.
|
||||
// streambuffer.write_int32(0);
|
||||
//
|
||||
// // Write the data, and calculate its length in bytes.
|
||||
// int64_t write_count_1 = streambuffer.total_writes();
|
||||
// write_data(stream);
|
||||
// int64_t write_count_2 = streambuffer.total_writes();
|
||||
// int64_t data_len = write_count_2 - write_count_1;
|
||||
//
|
||||
// // Overwrite the previously-written dummy length.
|
||||
// streambuffer.overwrite_int32(write_count_1, data_len);
|
||||
//
|
||||
// Almost all of this is self-explanatory, but the last line is interesting. In
|
||||
// order to know what part of the buffer to overwrite, overwrite_int uses
|
||||
// write_count_1 as a pointer into the buffer - it points immedately to the
|
||||
// right of the integer to overwrite.
|
||||
//
|
||||
// OVERWRITE_INT LIMITS
|
||||
//
|
||||
// If you use write_int to write an integer into the buffer, you are allowed to
|
||||
// overwrite that integer UNTIL you do a read from the buffer. Once you do a
|
||||
// read, it is no longer legal to overwrite ints that you wrote BEFORE the read.
|
||||
//
|
||||
// WRITE_STRING STORES THE STRING LENGTH, WRITE_BYTES DOES NOT
|
||||
//
|
||||
// write_string writes a string into the buffer and prepends a length. The
|
||||
// encoding of the length field is designed to be efficient for short strings
|
||||
// but still capable of encoding long lengths.
|
||||
//
|
||||
// write_bytes doesn't store the data length in the buffer. It's just a raw
|
||||
// write of bytes.
|
||||
//
|
||||
// STREAM EXCEPTIONS
|
||||
//
|
||||
// If you do a read_int64, but the buffer doesn't contain the necessary 8 bytes,
|
||||
// it throws a StreamEof exception. In general, during reading, the following
|
||||
// common situations generate StreamEof or StreamCorruption exceptions:
|
||||
//
|
||||
// * not enough bytes to satisfy a 'read' call: StreamEof
|
||||
// * call read_eof, but the buffer is not empty: StreamCorruption
|
||||
// * call read_string, but the string is unreasonably long: StreamCorruption
|
||||
//
|
||||
// Exceptions are only generated when reading from a stream that contains bad
|
||||
// data. Any other error generates a full-blown abort. For example, if you try
|
||||
// to write to a stream that's not open for writing, that's an abort, not an
|
||||
// exception. Write operations never generate exceptions.
|
||||
//
|
||||
// Sometimes, it is convenient to throw StreamCorruption yourself, if you detect
|
||||
// that the data you've read from a stream is invalid. This can make error
|
||||
// handling a little cleaner.
|
||||
//
|
||||
// READ BYTES POINTER VALIDITY
|
||||
//
|
||||
// When you call read_bytes, it returns a pointer to a block of bytes. This
|
||||
// pointer only remains valid until you do a 'write' into the stream.
|
||||
//
|
||||
// UNREADING BYTES
|
||||
//
|
||||
// It's possible to 'unread' bytes that you've already read from a stream. This
|
||||
// makes it possible to read those same bytes again.
|
||||
//
|
||||
// A common situation where this might be useful is: you're decoding a message,
|
||||
// but you discover halfway through the process of decoding the message that you
|
||||
// haven't received the whole message yet. In that case, it may be desirable to
|
||||
// unread the partial message, so that you can wait for the rest of the message
|
||||
// to be received.
|
||||
//
|
||||
// Here is the construction that accomplishes this:
|
||||
//
|
||||
// // Get the stream's read count before parsing the message.
|
||||
// size_t read_count_before = streambuffer.total_reads();
|
||||
//
|
||||
// // Parse the message, but if there's an EOF, deal with it:
|
||||
// try {
|
||||
// // Parse the message.
|
||||
// int32_t value1 = streambuffer.read_int32();
|
||||
// eng::string value2 = streambuffer.read_string(maxlen);
|
||||
// int64_t value3 = streambuffer.read_int64();
|
||||
//
|
||||
// // Great! I got the whole message.
|
||||
// execute_message(value1, value2, value3);
|
||||
// } catch (StreamEof) {
|
||||
// // I ran out of bytes. Unread the message.
|
||||
// streambuffer.unread(read_count_before);
|
||||
// }
|
||||
//
|
||||
// UNREAD LIMITS
|
||||
//
|
||||
// If you read bytes from a stream, that data can be 'unread' until you do a
|
||||
// write. After a write, it is no longer possible to 'unread' data that you
|
||||
// read before the write.
|
||||
//
|
||||
// STREAMBUFFERS THAT DON'T OWN THEIR OWN MEMORY
|
||||
//
|
||||
// If you create a streambuffer using this constructor:
|
||||
//
|
||||
// StreamBuffer(const char *data, uint64_t len);
|
||||
//
|
||||
// This StreamBuffer reads from an external (unowned) block of bytes, which is
|
||||
// not copied! The StreamBuffer saves the pointer that you passed in. This
|
||||
// pointer must remain valid until you're done with the StreamBuffer.
|
||||
//
|
||||
// A StreamBuffer that reads from an external block of bytes is read-only.
|
||||
// Attempts to write to this buffer will be caught and will cause an abort. The
|
||||
// total_writes for such a buffer returns the 'len' value that you initialized
|
||||
// the buffer with.
|
||||
//
|
||||
// NESTED DECODING
|
||||
//
|
||||
// Here is an interesting construct:
|
||||
//
|
||||
// // Read a message from the stream.
|
||||
// size_t len = streambuffer.read_int32()
|
||||
// const char *bytes = streambuffer.read_bytes(len);
|
||||
//
|
||||
// // Construct another stream object to decode the message.
|
||||
// StreamBuffer substream(bytes, len);
|
||||
// decode(substream);
|
||||
//
|
||||
// This is perfectly valid and a potentially convenient way to parse the
|
||||
// contents of a message. Note that the substream contains a pointer to
|
||||
// the parent stream's buffer, and therefore, data corruption will occur
|
||||
// if you mutate the parent stream while reading the substream.
|
||||
//
|
||||
// USING A STREAMBUFFER TO READ AN ENTIRE FILE
|
||||
//
|
||||
// If you wish to read an entire file and store the file contents in a
|
||||
// StreamBuffer, you should probe the size of the file, then allocate a
|
||||
// StreamBuffer of the correct size using this constructor:
|
||||
//
|
||||
// StreamBuffer(int64_t size);
|
||||
//
|
||||
// Then, you can use 'alloc_space' and 'wrote_space' to read the file into the
|
||||
// buffer in a single read call.
|
||||
//
|
||||
// USING A STREAMBUFFER AS A LUA_WRITER OR LUA_READER
|
||||
//
|
||||
// You can use a streambuffer as a lua_Writer, as follows:
|
||||
//
|
||||
// lua_dump(L, lua_writer_into_streambuffer, &sb);
|
||||
//
|
||||
// Anything written to the lua_writer gets appended to the streambuffer, the
|
||||
// same as if it had been written using write_bytes.
|
||||
//
|
||||
// You can't use streambuffer as a lua_Reader directly, but you can get a
|
||||
// string_view out of it and then use that to construct a lua_Reader, as
|
||||
// follows:
|
||||
//
|
||||
// LuaStringViewReader svr(mystreambuffer.view());
|
||||
// lua_load (L, svr.lua_reader(), svr.lua_reader_userdata());
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
#ifndef STREAMBUFFER_HPP
|
||||
#define STREAMBUFFER_HPP
|
||||
|
||||
#include "wrap-string.hpp"
|
||||
#include "wrap-sstream.hpp"
|
||||
|
||||
#include <utility>
|
||||
#include <cstdint>
|
||||
#include <cassert>
|
||||
|
||||
#include "base-buffer.hpp"
|
||||
#include "luastack.hpp"
|
||||
#include "util.hpp"
|
||||
|
||||
class StreamException : public eng::nevernew
|
||||
{
|
||||
public:
|
||||
virtual char const *what() const { return "General stream exception"; }
|
||||
};
|
||||
|
||||
class StreamEofOnRead : public StreamException
|
||||
{
|
||||
public:
|
||||
virtual char const *what() const { return "Stream ran out of data"; }
|
||||
};
|
||||
|
||||
class StreamStringTooLong: public StreamException
|
||||
{
|
||||
public:
|
||||
virtual char const *what() const { return "Stream contained a string that was too long"; }
|
||||
};
|
||||
|
||||
class StreamIntegerTruncated: public StreamException
|
||||
{
|
||||
public:
|
||||
virtual char const *what() const { return "You truncated an integer when writing to a stream"; }
|
||||
};
|
||||
|
||||
class StreamCorruption: public StreamException
|
||||
{
|
||||
public:
|
||||
virtual char const *what() const { return "Stream Corruption"; }
|
||||
};
|
||||
|
||||
using SimpleDynamicValue = SimpleDynamic<eng::string>;
|
||||
|
||||
class StreamBufferCore {
|
||||
protected:
|
||||
void *basebuffer_malloc(size_t size) { return eng::malloc(size); }
|
||||
void basebuffer_free(void *p) { eng::free(p); }
|
||||
void clear_error_flags() { }
|
||||
void raise_eof_on_read() { throw StreamEofOnRead(); }
|
||||
void raise_string_too_long() { throw StreamStringTooLong(); }
|
||||
void raise_integer_truncated() { throw StreamIntegerTruncated(); }
|
||||
};
|
||||
|
||||
class StreamBuffer : public eng::nevernew, public BaseBuffer<StreamBufferCore, eng::string> {
|
||||
public:
|
||||
using BaseBuffer::BaseBuffer;
|
||||
|
||||
void write_xyz(const util::XYZ &xyz) {
|
||||
write_float(xyz.x);
|
||||
write_float(xyz.y);
|
||||
write_float(xyz.z);
|
||||
}
|
||||
|
||||
void write_dxyz(const util::DXYZ &xyz) {
|
||||
write_double(xyz.x);
|
||||
write_double(xyz.y);
|
||||
write_double(xyz.z);
|
||||
}
|
||||
|
||||
void write_hashvalue(const util::HashValue &h) {
|
||||
write_uint64(h.first);
|
||||
write_uint64(h.second);
|
||||
}
|
||||
|
||||
util::XYZ read_xyz() {
|
||||
float x = read_float();
|
||||
float y = read_float();
|
||||
float z = read_float();
|
||||
return util::XYZ(x, y, z);
|
||||
}
|
||||
|
||||
util::DXYZ read_dxyz() {
|
||||
double x = read_double();
|
||||
double y = read_double();
|
||||
double z = read_double();
|
||||
return util::DXYZ(x, y, z);
|
||||
}
|
||||
|
||||
util::HashValue read_hashvalue() {
|
||||
uint64_t f = read_uint64();
|
||||
uint64_t s = read_uint64();
|
||||
return util::HashValue(f, s);
|
||||
}
|
||||
|
||||
bool contents_equal(const StreamBuffer *sb) {
|
||||
return view() == sb->view();
|
||||
}
|
||||
|
||||
void copy_into(StreamBuffer *sb) {
|
||||
sb->write_bytes(view());
|
||||
}
|
||||
};
|
||||
|
||||
// Use a streambuffer as a lua_writer.
|
||||
int lua_writer_into_streambuffer(lua_State *L, const void* bytes, size_t sz, void* sb);
|
||||
|
||||
#endif // STREAMBUFFER_HPP
|
||||
813
luprex/cpp/core/table.cpp
Normal file
813
luprex/cpp/core/table.cpp
Normal file
@@ -0,0 +1,813 @@
|
||||
|
||||
#include "wrap-string.hpp"
|
||||
|
||||
#include "table.hpp"
|
||||
#include "source.hpp"
|
||||
|
||||
// A quick check to see if a table appears to be a vector.
|
||||
// Does not thoroughly verify the vector. Returns the size
|
||||
// of the vector.
|
||||
static int check_vector_quick(LuaCoreStack &LS, LuaSlot vector, LuaSlot tmp) {
|
||||
LS.cktable(vector, "vector");
|
||||
int nkeys = LS.nkeys(vector);
|
||||
if (nkeys > 0) {
|
||||
LS.rawget(tmp, vector, nkeys);
|
||||
if (LS.isnil(tmp)) {
|
||||
luaL_error(LS.state(), "Not a valid vector");
|
||||
}
|
||||
LS.rawget(tmp, vector, nkeys + 1);
|
||||
if (!LS.isnil(tmp)) {
|
||||
luaL_error(LS.state(), "Not a valid vector");
|
||||
}
|
||||
}
|
||||
return nkeys;
|
||||
}
|
||||
|
||||
bool table_equal(LuaCoreStack &LS, LuaSlot t1, LuaSlot t2) {
|
||||
lua_State *L = LS.state();
|
||||
int top = lua_gettop(L);
|
||||
LS.cktable(t1, "table1");
|
||||
LS.cktable(t2, "table2");
|
||||
int nkeys1 = lua_nkeys(L, t1.index());
|
||||
int nkeys2 = lua_nkeys(L, t2.index());
|
||||
if (nkeys1 != nkeys2) return false;
|
||||
int total = 0;
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, t1.index()) != 0) {
|
||||
lua_pushvalue(L, -2); // k v1 k
|
||||
lua_rawget(L, t2.index()); // k v1 v2
|
||||
if (!lua_rawequal(L, -1, -2)) {
|
||||
lua_settop(L, top);
|
||||
return false;
|
||||
}
|
||||
lua_pop(L, 2);
|
||||
total += 1;
|
||||
}
|
||||
assert(total == nkeys1);
|
||||
lua_settop(L, top);
|
||||
return true;
|
||||
}
|
||||
|
||||
LuaDefine(table_equal, "table1,table2",
|
||||
"|Return true if two tables contain the same keys and values."
|
||||
"|"
|
||||
"|This works on arbitrary tables. Metatables are ignored."
|
||||
"|") {
|
||||
LuaArg t1, t2;
|
||||
LuaRet eql;
|
||||
LuaDefStack LS(L, t1, t2, eql);
|
||||
LS.set(eql, table_equal(LS, t1, t2));
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(table_isvector, "table",
|
||||
"|Return true if the table is a valid vector."
|
||||
"|"
|
||||
"|A vector is a table that has keys starting with 1, and no gaps."
|
||||
"|The empty table also counts as a valid vector. This function"
|
||||
"|scans the entire vector to verify its validity, so it takes O(N)"
|
||||
"|time. See also table.isvectorq"
|
||||
"|"
|
||||
"|The functions vector.isvector and table.isvector are identical."
|
||||
"|") {
|
||||
LuaArg table;
|
||||
LuaRet result;
|
||||
LuaVar tmp;
|
||||
LuaDefStack LS(L, table, result, tmp);
|
||||
if (!LS.istable(table)) {
|
||||
LS.set(result, false);
|
||||
return LS.result();
|
||||
}
|
||||
int nkeys = lua_nkeys(L, table.index());
|
||||
for (int i = 1; i <= nkeys; i++) {
|
||||
LS.rawget(tmp, table, i);
|
||||
if (LS.isnil(tmp)) {
|
||||
LS.set(result, false);
|
||||
return LS.result();
|
||||
}
|
||||
}
|
||||
LS.set(result, true);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(table_isvectorq, "table",
|
||||
"|Return true if the table is probably a valid vector."
|
||||
"|"
|
||||
"|A vector is a table that has keys starting with 1, and no gaps."
|
||||
"|The empty table also counts as a valid vector. This function"
|
||||
"|does a constant-time heuristic check: it gets the number of keys"
|
||||
"|in the table, NKEYS. Then it verifies that table[NKEYS] is not"
|
||||
"|nil and that table[NKEYS+1] is nil. If these things are both"
|
||||
"|true, the table is very likely a valid vector."
|
||||
"|"
|
||||
"|The functions vector.isvectorq and table.isvectorq are identical."
|
||||
"|") {
|
||||
LuaArg table;
|
||||
LuaRet result;
|
||||
LuaVar tmp;
|
||||
LuaDefStack LS(L, table, result, tmp);
|
||||
if (!LS.istable(table)) {
|
||||
LS.set(result, false);
|
||||
return LS.result();
|
||||
}
|
||||
int nkeys = LS.nkeys(table);
|
||||
if (nkeys > 0) {
|
||||
LS.rawget(tmp, table, nkeys);
|
||||
if (LS.isnil(tmp)) {
|
||||
LS.set(result, false);
|
||||
return LS.result();
|
||||
}
|
||||
LS.rawget(tmp, table, nkeys + 1);
|
||||
if (!LS.isnil(tmp)) {
|
||||
LS.set(result, false);
|
||||
return LS.result();
|
||||
}
|
||||
}
|
||||
LS.set(result, true);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefineAlias(vector_isvector, table_isvector);
|
||||
|
||||
LuaDefine(vector_removeall, "vector,value",
|
||||
"|Remove all occurrences of value from vector."
|
||||
"|"
|
||||
"|For example, if you remove the number 3 from the vector"
|
||||
"|{1,2,3,4,5,4,3,2,1} you get {1,2,4,5,4,2,1}."
|
||||
"|"
|
||||
"|Returns true if it removed something, false otherwise."
|
||||
"|") {
|
||||
LuaArg vector, value;
|
||||
LuaRet result;
|
||||
LuaVar tmp;
|
||||
LuaDefStack LS(L, vector, value, result, tmp);
|
||||
int nkeys = check_vector_quick(LS, vector, tmp);
|
||||
int dest = 1;
|
||||
for (int i = 1; i <= nkeys; i++) {
|
||||
LS.rawget(tmp, vector, i);
|
||||
if (LS.isnil(tmp)) {
|
||||
luaL_error(L, "not a valid vector");
|
||||
return LS.result();
|
||||
}
|
||||
if (!LS.rawequal(tmp, value)) {
|
||||
if (dest < i) {
|
||||
LS.rawset(vector, dest, tmp);
|
||||
}
|
||||
dest += 1;
|
||||
}
|
||||
}
|
||||
LS.set(result, (dest < nkeys));
|
||||
while (dest <= nkeys) {
|
||||
LS.rawset(vector, dest, LuaNil);
|
||||
dest += 1;
|
||||
}
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(vector_push, "vector,value",
|
||||
"|Push a value onto the end of a vector."
|
||||
"|"
|
||||
"|Argument must be a valid vector. Appends the value to"
|
||||
"|the end of the vector."
|
||||
"|") {
|
||||
LuaArg vector, value;
|
||||
LuaVar tmp;
|
||||
LuaDefStack LS(L, vector, value, tmp);
|
||||
int nkeys = check_vector_quick(LS, vector, tmp);
|
||||
LS.rawset(vector, nkeys + 1, value);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(vector_pop, "vector,value",
|
||||
"|Pop a value from the end of a vector."
|
||||
"|"
|
||||
"|Argument must be a valid vector. Returns the last value"
|
||||
"|from the vector. If the vector is empty, returns nil."
|
||||
"|") {
|
||||
LuaArg vector;
|
||||
LuaRet value;
|
||||
LuaVar tmp;
|
||||
LuaDefStack LS(L, vector, value, tmp);
|
||||
int nkeys = check_vector_quick(LS, vector, tmp);
|
||||
if (nkeys == 0) {
|
||||
LS.set(value, LuaNil);
|
||||
} else {
|
||||
LS.rawget(value, vector, nkeys);
|
||||
LS.rawset(vector, nkeys, LuaNil);
|
||||
}
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(vector_find, "vector,value",
|
||||
"|Find the first occurence of value in vector."
|
||||
"|"
|
||||
"|Argument must be a valid vector. Returns the index of the"
|
||||
"|first occurrence of value in vector. If the value is not"
|
||||
"|found, returns nil."
|
||||
"|"
|
||||
"|Searching for 'nil' in a vector is explicitly disallowed, since"
|
||||
"|a valid vector cannot contain nil."
|
||||
"|") {
|
||||
LuaArg vector, value;
|
||||
LuaRet index;
|
||||
LuaVar tmp;
|
||||
LuaDefStack LS(L, vector, value, index, tmp);
|
||||
int nkeys = check_vector_quick(LS, vector, tmp);
|
||||
if (LS.isnil(value)) {
|
||||
luaL_error(L, "cannot search for nil in a vector");
|
||||
return 0;
|
||||
}
|
||||
for (int i = 1; i <= nkeys; i++) {
|
||||
LS.rawget(tmp, vector, i);
|
||||
if (LS.rawequal(tmp, value)) {
|
||||
LS.set(index, i);
|
||||
return LS.result();
|
||||
}
|
||||
}
|
||||
LS.set(index, LuaNil);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(table_empty, "table",
|
||||
"|Return true if the table is empty."
|
||||
"|"
|
||||
"|Empty means the table has no key-value pairs stored."
|
||||
"|Metatables and metavalues are ignored."
|
||||
"|") {
|
||||
luaL_checktype(L, -1, LUA_TTABLE);
|
||||
int total = lua_nkeys(L, -1);
|
||||
lua_pushboolean(L, (total == 0)?1:0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
LuaDefine(table_count, "table",
|
||||
"|Return the number of key-value pairs in the table."
|
||||
"|"
|
||||
"|Returns the number of key-value pairs that have been"
|
||||
"|explicitly stored. Metatables and metavalues don't count."
|
||||
"|") {
|
||||
luaL_checktype(L, -1, LUA_TTABLE);
|
||||
int total = lua_nkeys(L, -1);
|
||||
lua_pushinteger(L, total);
|
||||
return 1;
|
||||
}
|
||||
|
||||
LuaDefine(table_clear, "table,metaflag",
|
||||
"|Clear the table key-value pairs, and optionally the metatable"
|
||||
"|"
|
||||
"|This removes all key-value pairs from the table. If the metaflag"
|
||||
"|is set, it also removes the metatable. If you try to clear the"
|
||||
"|metatable when the metatable is locked, this will throw an error."
|
||||
"|") {
|
||||
LuaArg tab, clearmeta;
|
||||
LuaVar metatable, metafield;
|
||||
LuaDefStack LS(L, tab, clearmeta, metatable, metafield);
|
||||
LS.cktable(tab, "table");
|
||||
if (LS.ckboolean(clearmeta)) {
|
||||
LS.getmetatable(metatable, tab);
|
||||
if (LS.istable(metatable)) {
|
||||
LS.rawget(metafield, metatable, "__metatable");
|
||||
if (!LS.isnil(metafield)) {
|
||||
luaL_error(L, "Cannot clear metatable.");
|
||||
return LS.result();
|
||||
}
|
||||
}
|
||||
LS.cleartable(tab, true);
|
||||
} else {
|
||||
LS.cleartable(tab, false);
|
||||
}
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(table_getflagbits, "table", "get the table's flag bits (debugging only)") {
|
||||
LuaArg tab;
|
||||
LuaRet bits;
|
||||
LuaDefStack LS(L, tab, bits);
|
||||
uint16_t ubits = lua_getflagbits(L, tab.index());
|
||||
LS.set(bits, ubits);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(table_setflagbits, "table,bits", "set the table's flag bits (debugging only)") {
|
||||
LuaArg tab, bits;
|
||||
LuaDefStack LS(L, tab, bits);
|
||||
uint16_t ubits = LS.ckinteger(bits);
|
||||
lua_setflagbits(L, tab.index(), ubits);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Deque operators.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////
|
||||
|
||||
#define DEQUE_LEFT 1
|
||||
#define DEQUE_FILL 2
|
||||
#define DEQUE_MAX 3
|
||||
#define DEQUE_BASE 4
|
||||
|
||||
void deque_checktype(lua_State *L, int slot, int type) {
|
||||
if (lua_type(L, slot) != type) {
|
||||
luaL_error(L, "object is not a valid deque");
|
||||
}
|
||||
}
|
||||
|
||||
void deque_get_info(lua_State *L, int deque, int *left, int *fill, int *max) {
|
||||
luaL_checktype(L, deque, LUA_TTABLE);
|
||||
if (left) {
|
||||
lua_rawgeti(L, deque, DEQUE_LEFT);
|
||||
deque_checktype(L, -1, LUA_TNUMBER);
|
||||
*left = lua_tointeger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
if (fill) {
|
||||
lua_rawgeti(L, deque, DEQUE_FILL);
|
||||
deque_checktype(L, -1, LUA_TNUMBER);
|
||||
*fill = lua_tointeger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
if (max) {
|
||||
lua_rawgeti(L, deque, DEQUE_MAX);
|
||||
deque_checktype(L, -1, LUA_TNUMBER);
|
||||
*max = lua_tointeger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
|
||||
void deque_put_info(lua_State *L, int deque, int *left, int *fill, int *max) {
|
||||
if (left) {
|
||||
lua_pushinteger(L, *left);
|
||||
lua_rawseti(L, deque, DEQUE_LEFT);
|
||||
}
|
||||
if (fill) {
|
||||
lua_pushinteger(L, *fill);
|
||||
lua_rawseti(L, deque, DEQUE_FILL);
|
||||
}
|
||||
if (max) {
|
||||
lua_pushinteger(L, *max);
|
||||
lua_rawseti(L, deque, DEQUE_MAX);
|
||||
}
|
||||
}
|
||||
|
||||
int deque_make_room(lua_State *L, int deque, int left, int fill, int max) {
|
||||
if (fill == max) {
|
||||
for (int i = 0; i < left; i++) {
|
||||
lua_rawgeti(L, deque, DEQUE_BASE + i);
|
||||
lua_rawseti(L, deque, DEQUE_BASE + i + max);
|
||||
lua_pushinteger(L, 0);
|
||||
lua_rawseti(L, deque, DEQUE_BASE + i);
|
||||
}
|
||||
for (int i = left; i < max; i++) {
|
||||
lua_pushinteger(L, 0);
|
||||
lua_rawseti(L, deque, DEQUE_BASE + i + max);
|
||||
}
|
||||
max *= 2;
|
||||
lua_pushinteger(L, max);
|
||||
lua_rawseti(L, deque, DEQUE_MAX);
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
LuaDefine(deque_create, "", "create a deque") {
|
||||
LuaRet rdeque;
|
||||
LuaVar classobj;
|
||||
LuaDefStack LS(L, rdeque, classobj);
|
||||
const int imax = 4;
|
||||
eng::string err = LS.getclass(classobj, "deque");
|
||||
if (err != "") {
|
||||
luaL_error(L, "Class deque has been corrupted");
|
||||
}
|
||||
LS.createtable(rdeque, DEQUE_BASE + imax - 1, 0);
|
||||
LS.rawset(rdeque, DEQUE_LEFT, 0);
|
||||
LS.rawset(rdeque, DEQUE_FILL, 0);
|
||||
LS.rawset(rdeque, DEQUE_MAX, imax);
|
||||
for (int i = 0; i < imax; i++) {
|
||||
LS.rawset(rdeque, DEQUE_BASE + i, 0);
|
||||
}
|
||||
LS.setmetatable(rdeque, classobj);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(deque_pushl, "deque,value", "push onto the left end of a deque") {
|
||||
LuaArg deque, elt;
|
||||
LuaDefStack LS(L, deque, elt);
|
||||
int left, fill, max;
|
||||
deque_get_info(L, deque.index(), &left, &fill, &max);
|
||||
max = deque_make_room(L, deque.index(), left, fill, max);
|
||||
int target = (left - 1) & (max-1);
|
||||
LS.rawset(deque, DEQUE_BASE + target, elt);
|
||||
fill += 1;
|
||||
left = (left - 1) & (max - 1);
|
||||
deque_put_info(L, deque.index(), &left, &fill, NULL);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(deque_pushr, "deque,value", "push onto the right end of a deque") {
|
||||
LuaArg deque, elt;
|
||||
LuaDefStack LS(L, deque, elt);
|
||||
int left, fill, max;
|
||||
deque_get_info(L, deque.index(), &left, &fill, &max);
|
||||
max = deque_make_room(L, deque.index(), left, fill, max);
|
||||
int target = (left + fill) & (max-1);
|
||||
LS.rawset(deque, DEQUE_BASE + target, elt);
|
||||
fill += 1;
|
||||
deque_put_info(L, deque.index(), NULL, &fill, NULL);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(deque_popl, "deque", "pop the left end of a deque") {
|
||||
LuaArg deque;
|
||||
LuaRet result;
|
||||
LuaDefStack LS(L, deque, result);
|
||||
int left, fill, max;
|
||||
deque_get_info(L, deque.index(), &left, &fill, &max);
|
||||
if (fill == 0) {
|
||||
LS.set(result, LuaNil);
|
||||
return LS.result();
|
||||
}
|
||||
LS.rawget(result, deque, DEQUE_BASE + left);
|
||||
LS.rawset(deque, DEQUE_BASE + left, 0);
|
||||
left = (left + 1) & (max - 1);
|
||||
fill -= 1;
|
||||
deque_put_info(L, deque.index(), &left, &fill, NULL);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(deque_popr, "deque", "pop the right end of a deque") {
|
||||
LuaArg deque;
|
||||
LuaRet result;
|
||||
LuaDefStack LS(L, deque, result);
|
||||
int left, fill, max;
|
||||
deque_get_info(L, deque.index(), &left, &fill, &max);
|
||||
if (fill == 0) {
|
||||
LS.set(result, LuaNil);
|
||||
return LS.result();
|
||||
}
|
||||
int target = (left + fill - 1) & (max - 1);
|
||||
LS.rawget(result, deque, DEQUE_BASE + target);
|
||||
LS.rawset(deque, DEQUE_BASE + target, 0);
|
||||
fill -= 1;
|
||||
deque_put_info(L, deque.index(), NULL, &fill, NULL);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(deque_nthl, "deque,n", "return the nth item from the left end of a deque") {
|
||||
LuaArg deque, nn;
|
||||
LuaRet result;
|
||||
LuaDefStack LS(L, deque, nn, result);
|
||||
int left, fill, max;
|
||||
deque_get_info(L, deque.index(), &left, &fill, &max);
|
||||
int n = LS.ckint(nn);
|
||||
if ((n < 1) || (n > fill)) {
|
||||
LS.set(result, LuaNil);
|
||||
return LS.result();
|
||||
}
|
||||
int target = (left + n - 1) & (max - 1);
|
||||
LS.rawget(result, deque, DEQUE_BASE + target);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(deque_nthr, "deque,n", "return the nth item from the right end of a deque") {
|
||||
LuaArg deque, nn;
|
||||
LuaRet result;
|
||||
LuaDefStack LS(L, deque, nn, result);
|
||||
int left, fill, max;
|
||||
deque_get_info(L, deque.index(), &left, &fill, &max);
|
||||
int n = LS.ckint(nn);
|
||||
if ((n < 1) || (n > fill)) {
|
||||
LS.set(result, LuaNil);
|
||||
return LS.result();
|
||||
}
|
||||
int target = (left + fill - n) & (max - 1);
|
||||
LS.rawget(result, deque, DEQUE_BASE + target);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(deque_setl, "deque,n,value", "set the nth item from the left end of a deque") {
|
||||
LuaArg deque, nn, val;
|
||||
LuaDefStack LS(L, deque, nn, val);
|
||||
int left, fill, max;
|
||||
deque_get_info(L, deque.index(), &left, &fill, &max);
|
||||
int n = LS.ckint(nn);
|
||||
if ((n < 1) || (n > fill)) {
|
||||
luaL_error(L, "invalid index");
|
||||
return LS.result();
|
||||
}
|
||||
int target = (left + n - 1) & (max - 1);
|
||||
LS.rawset(deque, DEQUE_BASE + target, val);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(deque_setr, "deque,n,value", "set the nth item from the right end of a deque") {
|
||||
LuaArg deque, nn, val;
|
||||
LuaDefStack LS(L, deque, nn, val);
|
||||
int left, fill, max;
|
||||
deque_get_info(L, deque.index(), &left, &fill, &max);
|
||||
int n = LS.ckint(nn);
|
||||
if ((n < 1) || (n > fill)) {
|
||||
luaL_error(L, "invalid index");
|
||||
return LS.result();
|
||||
}
|
||||
int target = (left + fill - n) & (max - 1);
|
||||
LS.rawset(deque, DEQUE_BASE + target, val);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(deque_findl, "deque,value", "find the first occurence of value in deque, starting from left") {
|
||||
LuaArg deque, val;
|
||||
LuaRet pos;
|
||||
LuaVar check;
|
||||
LuaDefStack LS(L, deque, val, pos, check);
|
||||
int left, fill, max;
|
||||
deque_get_info(L, deque.index(), &left, &fill, &max);
|
||||
for (int i = 0; i < fill; i++) {
|
||||
int index = (left + i) & (max - 1);
|
||||
LS.rawget(check, deque, DEQUE_BASE + index);
|
||||
if (LS.rawequal(check, val)) {
|
||||
LS.set(pos, i + 1);
|
||||
return LS.result();
|
||||
}
|
||||
}
|
||||
LS.set(pos, LuaNil);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(deque_findr, "deque,value", "find the first occurrence of value in deque, starting from right") {
|
||||
LuaArg deque, val;
|
||||
LuaRet pos;
|
||||
LuaVar check;
|
||||
LuaDefStack LS(L, deque, val, pos, check);
|
||||
int left, fill, max;
|
||||
deque_get_info(L, deque.index(), &left, &fill, &max);
|
||||
int base = left + fill - 1;
|
||||
for (int i = 0; i < fill; i++) {
|
||||
int index = (base - i) & (max - 1);
|
||||
LS.rawget(check, deque, DEQUE_BASE + index);
|
||||
if (LS.rawequal(check, val)) {
|
||||
LS.set(pos, i + 1);
|
||||
return LS.result();
|
||||
}
|
||||
}
|
||||
LS.set(pos, LuaNil);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(deque_size, "deque", "return the number of items in the deque") {
|
||||
LuaArg deque;
|
||||
LuaRet size;
|
||||
LuaDefStack LS(L, deque, size);
|
||||
LS.cktable(deque, "deque");
|
||||
LS.rawget(size, deque, DEQUE_FILL);
|
||||
if (!LS.tryinteger(size)) {
|
||||
luaL_error(L, "deque has been corrupted");
|
||||
}
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// table_getpairs
|
||||
//
|
||||
// Given a table, return all the pairs in the table as a sequence.
|
||||
// The resulting sequence contains a 1 in the first position,
|
||||
// followed by the pairs, like so:
|
||||
//
|
||||
// sortedpairs({a=1,b=2,c=3}) => {1,"a",1,"b",2,"c",3}
|
||||
//
|
||||
// Note that this function is not directly exposed to lua.
|
||||
// It's exposed only through the 'sortedpairs' iterator.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////
|
||||
|
||||
static void pushkey (lua_State *L, int tab, int i) {
|
||||
lua_rawgeti(L, tab, i*2);
|
||||
}
|
||||
|
||||
static void push2keys (lua_State *L, int tab, int i, int j) {
|
||||
lua_rawgeti(L, tab, i*2);
|
||||
lua_rawgeti(L, tab, j*2);
|
||||
}
|
||||
|
||||
static void pop2keys (lua_State *L, int tab, int i, int j) {
|
||||
lua_rawseti(L, tab, i*2);
|
||||
lua_rawseti(L, tab, j*2);
|
||||
}
|
||||
|
||||
static void swap2values (lua_State *L, int tab, int i, int j) {
|
||||
lua_rawgeti(L, tab, i*2+1);
|
||||
lua_rawgeti(L, tab, j*2+1);
|
||||
lua_rawseti(L, tab, i*2+1);
|
||||
lua_rawseti(L, tab, j*2+1);
|
||||
}
|
||||
|
||||
static void auxsort (lua_State *L, int tab, int l, int u) {
|
||||
while (l < u) { /* for tail recursion */
|
||||
int i, j;
|
||||
/* sort elements a[l], a[(l+u)/2] and a[u] */
|
||||
push2keys(L, tab, l, u);
|
||||
if (lua_genlt(L, -1, -2)) {
|
||||
pop2keys(L, tab, l, u);
|
||||
swap2values(L, tab, l, u);
|
||||
} else {
|
||||
lua_pop(L, 2);
|
||||
}
|
||||
if (u - l == 1) break; /* only 2 elements */
|
||||
i = (l + u) / 2;
|
||||
push2keys(L, tab, i, l);
|
||||
if (lua_genlt(L, -2, -1)) {
|
||||
pop2keys(L, tab, i, l);
|
||||
swap2values(L, tab, i, l);
|
||||
} else {
|
||||
lua_pop(L, 1); /* remove a[l] */
|
||||
pushkey(L, tab, u);
|
||||
if (lua_genlt(L, -1, -2)) {
|
||||
pop2keys(L, tab, i, u);
|
||||
swap2values(L, tab, i, u);
|
||||
} else {
|
||||
lua_pop(L, 2);
|
||||
}
|
||||
}
|
||||
if (u - l == 2) break; /* only 3 elements */
|
||||
/* put the pivot value on top of the stack and keep it there */
|
||||
pushkey(L, tab, i);
|
||||
/* move the pivot from i to u-1 */
|
||||
lua_pushvalue(L, -1);
|
||||
pushkey(L, tab, u-1);
|
||||
pop2keys(L, tab, i, u-1);
|
||||
swap2values(L, tab, i, u-1);
|
||||
/* a[l] <= P == a[u-1] <= a[u], only need to sort from l+1 to u-2 */
|
||||
i = l;
|
||||
j = u - 1;
|
||||
for (;;) { /* invariant: a[l..i] <= P <= a[j..u] */
|
||||
/* repeat ++i until a[i] >= P */
|
||||
while (pushkey(L, tab, ++i), lua_genlt(L, -1, -2)) {
|
||||
if (i >= u) luaL_error(L, "invalid order function for sorting");
|
||||
lua_pop(L, 1); /* remove a[i] */
|
||||
}
|
||||
/* repeat --j until a[j] <= P */
|
||||
while (pushkey(L, tab, --j), lua_genlt(L, -3, -1)) {
|
||||
if (j <= l) luaL_error(L, "invalid order function for sorting");
|
||||
lua_pop(L, 1); /* remove a[j] */
|
||||
}
|
||||
if (j < i) {
|
||||
lua_pop(L, 3); /* pop pivot, a[i], a[j] */
|
||||
break;
|
||||
}
|
||||
pop2keys(L, tab, i, j);
|
||||
swap2values(L, tab, i, j);
|
||||
}
|
||||
push2keys(L, tab, u-1, i);
|
||||
pop2keys(L, tab, u-1, i);
|
||||
swap2values(L, tab, u-1, i);
|
||||
/* a[l..i-1] <= a[i] == P <= a[i+1..u] */
|
||||
/* adjust so that smaller half is in [j..i] and larger one in [l..u] */
|
||||
if (i - l < u - i) {
|
||||
j = l;
|
||||
i = i - 1;
|
||||
l = i + 2;
|
||||
} else {
|
||||
j = i + 1;
|
||||
i = u;
|
||||
u = j - 2;
|
||||
}
|
||||
auxsort(L, tab, j, i); /* call recursively the smaller one */
|
||||
} /* repeat the routine for the larger one */
|
||||
}
|
||||
|
||||
bool table_getpairs(LuaCoreStack &LS0, LuaSlot tab, LuaSlot pairs, bool sort) {
|
||||
lua_State *L = LS0.state();
|
||||
LuaVar key, value;
|
||||
LuaExtStack LS(L, key, value);
|
||||
bool sorted = true;
|
||||
// Create the table, store the initial 1.
|
||||
int total = lua_nkeys(L, tab.index());
|
||||
LS.createtable(pairs, total * 2 + 1, 0);
|
||||
LS.rawset(pairs, 1, 1);
|
||||
// Transfer the pairs into the sequence.
|
||||
lua_pushnil(L);
|
||||
int offset = 2;
|
||||
while (lua_next(L, tab.index()) != 0) {
|
||||
int ktype = lua_type(L, -2);
|
||||
if (ktype != LUA_TNUMBER && ktype != LUA_TSTRING && ktype != LUA_TBOOLEAN) {
|
||||
sorted = false;
|
||||
}
|
||||
lua_pushvalue(L, -2); // K V K
|
||||
lua_rawseti(L, pairs.index(), offset++);
|
||||
lua_rawseti(L, pairs.index(), offset++);
|
||||
}
|
||||
if (sort) {
|
||||
auxsort(L, pairs.index(), 1, total);
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Given a sortedpairs vector, return the (key, value) pairs
|
||||
// one by one. The first element of the vector is used as a
|
||||
// counter to keep track of our position.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////
|
||||
|
||||
LuaDefine(table_nextsortedpair, "sortedpairs,dummy", "next function used by sortedpairs") {
|
||||
if (lua_gettop(L) < 2) {
|
||||
luaL_error(L, "Not enough arguments to nextpair");
|
||||
}
|
||||
luaL_checktype(L, 1, LUA_TTABLE);
|
||||
lua_rawgeti(L, 1, 1);
|
||||
lua_Integer i = luaL_checkinteger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_pushinteger(L, i+1);
|
||||
lua_rawseti(L, 1, 1);
|
||||
lua_rawgeti(L, 1, i*2);
|
||||
if (lua_isnil(L, -1)) {
|
||||
return 1;
|
||||
} else {
|
||||
lua_rawgeti(L, 1, i*2+1);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
LuaDefine(table_sortedpairs, "table",
|
||||
"|Iterate over table, sorting all keys"
|
||||
"|"
|
||||
"|Some keys can't be sorted. For example, you can use a closure"
|
||||
"|as a table key. If you try to iterate over a table containing"
|
||||
"|a non-sortable key, the error 'Cannot sort the table keys' will"
|
||||
"|be generated."
|
||||
"|"
|
||||
"|See doc(genlt) for information about the sort order."
|
||||
"|"
|
||||
"|") {
|
||||
LuaArg tab;
|
||||
LuaRet closure, rtab, key;
|
||||
LuaDefStack LS(L, tab, closure, rtab, key);
|
||||
bool sorted = table_getpairs(LS, tab, rtab, true);
|
||||
if (!sorted) {
|
||||
luaL_error(L, "Cannot sort the table keys");
|
||||
}
|
||||
LS.set(closure, lfn_table_nextsortedpair);
|
||||
LS.set(key, LuaNil);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(table_semisortedpairs, "table",
|
||||
"|Iterate over table, sorting all the keys that can be sorted."
|
||||
"|"
|
||||
"|Some keys can't be sorted. For example, you can use a closure"
|
||||
"|as a table key. If you try to iterate over a table containing"
|
||||
"|a non-sortable key, the non-sortable elements will appear at"
|
||||
"|the end of the iteration, after all the sortable elements. The"
|
||||
"|non-sortable elements will be in an arbitrary order."
|
||||
"|"
|
||||
"|See doc(genlt) for information about the sort order."
|
||||
"|") {
|
||||
LuaArg tab;
|
||||
LuaRet closure, rtab, key;
|
||||
LuaDefStack LS(L, tab, closure, rtab, key);
|
||||
table_getpairs(LS, tab, rtab, true);
|
||||
LS.set(closure, lfn_table_nextsortedpair);
|
||||
LS.set(key, LuaNil);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
#define LUA_TNIL 0
|
||||
#define LUA_TBOOLEAN 1
|
||||
#define LUA_TLIGHTUSERDATA 2
|
||||
#define LUA_TNUMBER 3
|
||||
#define LUA_TSTRING 4
|
||||
#define LUA_TTABLE 5
|
||||
#define LUA_TFUNCTION 6
|
||||
#define LUA_TUSERDATA 7
|
||||
#define LUA_TTHREAD 8
|
||||
|
||||
#define LUA_NUMTAGS 9
|
||||
|
||||
LuaDefine(genlt, "obj1,obj2",
|
||||
"|Generalized less-than function"
|
||||
"|"
|
||||
"|This comparison function can compare any two objects. The"
|
||||
"|return value is as follows:"
|
||||
"|"
|
||||
"|* Numbers are compared in the obvious numeric manner."
|
||||
"|* Strings are compared alphabetically."
|
||||
"|* Booleans are compared with false being less than true."
|
||||
"|* Tables are all considered equal to other tables."
|
||||
"|* Functions are all considered equal to other functions."
|
||||
"|* Coroutines are all considered equal to other coroutines."
|
||||
"|"
|
||||
"|* Numbers are less than strings."
|
||||
"|* Strings are less than booleans."
|
||||
"|* Booleans are less than functions."
|
||||
"|* Functions are less than coroutines."
|
||||
"|* Coroutines are less than tables."
|
||||
"|") {
|
||||
LuaArg o1,o2;
|
||||
LuaRet lt;
|
||||
LuaDefStack LS(L, o1, o2, lt);
|
||||
int ltf = lua_genlt(L, o1.index(), o2.index());
|
||||
LS.set(lt, ltf ? true:false);
|
||||
return LS.result();
|
||||
}
|
||||
29
luprex/cpp/core/table.hpp
Normal file
29
luprex/cpp/core/table.hpp
Normal file
@@ -0,0 +1,29 @@
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// This module contains a library of lua functions
|
||||
// for manipulating tables. It also provides a library
|
||||
// of useful classes based on tables.
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
#ifndef TABLE_HPP
|
||||
#define TABLE_HPP
|
||||
|
||||
#include "luastack.hpp"
|
||||
|
||||
// table_equal
|
||||
//
|
||||
// True if two tables contain the same key/value pairs.
|
||||
//
|
||||
bool table_equal(LuaCoreStack &LS0, LuaSlot tab1, LuaSlot tab2);
|
||||
|
||||
// table_getpairs
|
||||
//
|
||||
// Get a table containing the key-value pairs in tab. Optionally sort
|
||||
// the pairs. Return true if all keys were sortable.
|
||||
//
|
||||
bool table_getpairs(LuaCoreStack &LS0, LuaSlot tab, LuaSlot pairs, bool sort);
|
||||
|
||||
|
||||
#endif // TABLE_HPP
|
||||
97
luprex/cpp/core/traceback.cpp
Normal file
97
luprex/cpp/core/traceback.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
#include "traceback.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <cassert>
|
||||
|
||||
#define TRACEBACK_LEVELS1 12
|
||||
#define TRACEBACK_LEVELS2 10
|
||||
|
||||
// Call this with the error message on top of the stack.
|
||||
// The error message is replaced with a traceback.
|
||||
//
|
||||
int traceback_coroutine(lua_State *L) {
|
||||
lua_checkstack(L, 20);
|
||||
int top = lua_gettop(L);
|
||||
|
||||
// Convert message to a string
|
||||
if (!lua_tostring(L, top)) {
|
||||
luaL_callmeta(L, top, "__tostring");
|
||||
// If callmeta didn't produce exactly one string, clear the stack
|
||||
// and push "unknown error"
|
||||
if ((lua_gettop(L) == top + 1) && (lua_tostring(L, -1))) {
|
||||
lua_remove(L, top);
|
||||
} else {
|
||||
lua_settop(L, top - 1);
|
||||
lua_pushstring(L, "unknown error");
|
||||
}
|
||||
}
|
||||
|
||||
// Append the traceback.
|
||||
lua_Debug ar;
|
||||
int firstpart = 1;
|
||||
bool any = false;
|
||||
for (int level = 0; lua_getstack(L, level, &ar); level++) {
|
||||
if (level > TRACEBACK_LEVELS1 && firstpart) {
|
||||
/* no more than `LEVELS2' more levels? */
|
||||
if (!lua_getstack(L, level + TRACEBACK_LEVELS2, &ar))
|
||||
level--; /* keep going */
|
||||
else {
|
||||
lua_pushliteral(L, "\n\t..."); /* too many levels */
|
||||
while (lua_getstack(L, level + TRACEBACK_LEVELS2, &ar)) /* find last levels */
|
||||
level++;
|
||||
}
|
||||
firstpart = 0;
|
||||
continue;
|
||||
}
|
||||
lua_getinfo(L, "Snl", &ar);
|
||||
if ((!any) && (*ar.what == 'C') && (ar.name != 0)) {
|
||||
if (strcmp(ar.name, "__newindex") == 0) continue;
|
||||
}
|
||||
if ((ar.currentline > 0) || (*ar.namewhat != 0) || (*ar.what != 'C')) {
|
||||
any = true;
|
||||
lua_pushliteral(L, "\n\t");
|
||||
lua_pushfstring(L, "%s:", ar.short_src);
|
||||
if (ar.currentline > 0)
|
||||
lua_pushfstring(L, "%d:", ar.currentline);
|
||||
if (*ar.namewhat != '\0') /* is there a name? */
|
||||
lua_pushfstring(L, " in function " LUA_QS, ar.name);
|
||||
else {
|
||||
if (*ar.what == 'm') /* main? */
|
||||
lua_pushfstring(L, " in main chunk");
|
||||
else if (*ar.what == 'C' || *ar.what == 't')
|
||||
lua_pushliteral(L, " ?"); /* C function or tail call */
|
||||
else
|
||||
lua_pushfstring(L, " in function <%s:%d>",
|
||||
ar.short_src, ar.linedefined);
|
||||
}
|
||||
if (1 + lua_gettop(L) - top > 5) {
|
||||
lua_concat(L, 1 + lua_gettop(L) - top);
|
||||
}
|
||||
}
|
||||
}
|
||||
lua_pushstring(L, "\n");
|
||||
if (1 + lua_gettop(L) - top > 1) {
|
||||
lua_concat(L, 1 + lua_gettop(L) - top);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
eng::string traceback_pcall(lua_State *L, int narg, int nret) {
|
||||
int status;
|
||||
int base = lua_gettop(L) - narg; /* function index */
|
||||
lua_pushcfunction(L, traceback_coroutine); /* push traceback function */
|
||||
lua_insert(L, base); /* put it under chunk and args */
|
||||
status = lua_pcall(L, narg, nret, base);
|
||||
lua_remove(L, base); /* remove traceback function */
|
||||
if (status != LUA_OK) {
|
||||
const char *msg = lua_tostring(L, -1);
|
||||
if ((msg == NULL) || (msg[0] == 0)) {
|
||||
msg = "unknown error";
|
||||
}
|
||||
eng::string result = msg;
|
||||
assert(result != "attempt to yield from outside a coroutine");
|
||||
return result;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
33
luprex/cpp/core/traceback.hpp
Normal file
33
luprex/cpp/core/traceback.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
/////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TRACEBACK ROUTINES
|
||||
//
|
||||
// The following routines are meant to help produce good-quality
|
||||
// tracebacks from errors in lua code.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
#ifndef TRACEBACK_HPP
|
||||
#define TRACEBACK_HPP
|
||||
|
||||
#include "luastack.hpp"
|
||||
|
||||
// traceback_coroutine
|
||||
//
|
||||
// Given a coroutine which contains an error message, replace
|
||||
// the error message with a full traceback. Always returns 1.
|
||||
//
|
||||
int traceback_coroutine(lua_State *L);
|
||||
|
||||
// traceback_pcall
|
||||
//
|
||||
// Similar to lua_pcall, except that it automatically supplies
|
||||
// traceback_coroutine as a message handler. It also automatically
|
||||
// returns any error message. Returns empty string if there's
|
||||
// no error. Also checks for a yield inside a pcall, which is
|
||||
// not allowed, and does an assert-fail in that case.
|
||||
//
|
||||
eng::string traceback_pcall(lua_State *L, int narg, int nret);
|
||||
|
||||
#endif // TRACEBACK_HPP
|
||||
985
luprex/cpp/core/util.cpp
Normal file
985
luprex/cpp/core/util.cpp
Normal file
@@ -0,0 +1,985 @@
|
||||
#include "wrap-string.hpp"
|
||||
#include "wrap-vector.hpp"
|
||||
#include "util.hpp"
|
||||
#include "fast-float.hpp"
|
||||
#include "luastack.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <iomanip>
|
||||
#include <cassert>
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
#include <charconv>
|
||||
|
||||
|
||||
namespace sv {
|
||||
|
||||
bool case_insensitive_eq(string_view s1, string_view s2) {
|
||||
if (s1.size() != s2.size()) return false;
|
||||
for (int i = 0; i < int(s1.size()); i++) {
|
||||
char c1 = s1[i];
|
||||
char c2 = s2[i];
|
||||
if (ascii_isupper(c1)) c1 += 'a'-'A';
|
||||
if (ascii_isupper(c2)) c2 += 'a'-'A';
|
||||
if (c1 != c2) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool valid_int64(string_view value) {
|
||||
int64_t result;
|
||||
const char *last = value.data() + value.size();
|
||||
auto r = std::from_chars(value.data(), last, result, 10);
|
||||
if (r.ec != std::errc()) return false;
|
||||
if (r.ptr != last) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool valid_hex64(string_view value) {
|
||||
int64_t result;
|
||||
const char *last = value.data() + value.size();
|
||||
auto r = std::from_chars(value.data(), last, result, 16);
|
||||
if (r.ec != std::errc()) return false;
|
||||
if (r.ptr != last) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool valid_double(string_view value) {
|
||||
double result;
|
||||
const char *last = value.data() + value.size();
|
||||
auto r = fast_float::from_chars(value.data(), last, result);
|
||||
if (r.ec != std::errc()) return false;
|
||||
if (r.ptr != last) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool valid_hostname(string_view value) {
|
||||
bool start_word = true;
|
||||
bool last_dash = false;
|
||||
for (char c : value) {
|
||||
if (c == '.') {
|
||||
if (start_word) return false;
|
||||
if (last_dash) return false;
|
||||
start_word = true;
|
||||
last_dash = false;
|
||||
} else if (c == '-') {
|
||||
if (start_word) return false;
|
||||
start_word = false;
|
||||
last_dash = true;
|
||||
} else if (ascii_isalnum(c)) {
|
||||
start_word = false;
|
||||
last_dash = false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (start_word || last_dash) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
int64_t to_int64(string_view value, int64_t errval) {
|
||||
int64_t result;
|
||||
const char *p = value.data();
|
||||
const char *last = p + value.size();
|
||||
if ((p < last) && (*p == '+')) p++;
|
||||
auto r = std::from_chars(p, last, result, 10);
|
||||
if (r.ec != std::errc()) return errval;
|
||||
if (r.ptr != last) return errval;
|
||||
return result;
|
||||
}
|
||||
|
||||
uint64_t to_hex64(string_view value, uint64_t errval) {
|
||||
uint64_t result;
|
||||
if (sv::zfront(value) == '-') return errval;
|
||||
const char *last = value.data() + value.size();
|
||||
auto r = std::from_chars(value.data(), last, result, 16);
|
||||
if (r.ec != std::errc()) return errval;
|
||||
if (r.ptr != last) return errval;
|
||||
return result;
|
||||
}
|
||||
|
||||
double to_double(string_view value, double errval) {
|
||||
double result;
|
||||
const char *last = value.data() + value.size();
|
||||
auto r = fast_float::from_chars(value.data(), last, result);
|
||||
if (r.ec != std::errc()) return errval;
|
||||
if (r.ptr != last) return errval;
|
||||
return result;
|
||||
}
|
||||
|
||||
string_view ltrim(string_view v) {
|
||||
while ((!v.empty()) && (ascii_isspace(v.front()))) {
|
||||
v.remove_prefix(1);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
string_view rtrim(string_view v) {
|
||||
while ((!v.empty()) && (ascii_isspace(v.back()))) {
|
||||
v.remove_suffix(1);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
string_view trim(string_view v) {
|
||||
while ((!v.empty()) && (ascii_isspace(v.front()))) {
|
||||
v.remove_prefix(1);
|
||||
}
|
||||
while ((!v.empty()) && (ascii_isspace(v.back()))) {
|
||||
v.remove_suffix(1);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
string_view ltrim(string_view v, char c) {
|
||||
while ((!v.empty()) && (v.front() == c)) {
|
||||
v.remove_prefix(1);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
string_view rtrim(string_view v, char c) {
|
||||
while ((!v.empty()) && (v.back() == c)) {
|
||||
v.remove_suffix(1);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
string_view trim(string_view v, char c) {
|
||||
while ((!v.empty()) && (v.front() == c)) {
|
||||
v.remove_prefix(1);
|
||||
}
|
||||
while ((!v.empty()) && (v.back() == c)) {
|
||||
v.remove_suffix(1);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
bool has_prefix(string_view s, string_view prefix) {
|
||||
return 0 == s.compare(0, prefix.size(), prefix);
|
||||
}
|
||||
|
||||
bool has_suffix(string_view s, string_view suffix) {
|
||||
if (s.length() >= suffix.length()) {
|
||||
return (0 == s.compare (s.length() - suffix.length(), suffix.length(), suffix));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int common_prefix_length(string_view a, string_view b) {
|
||||
int minlen = std::min(a.size(), b.size());
|
||||
for (int i = 0; i < minlen; i++) {
|
||||
if (a[i] != b[i]) return i;
|
||||
}
|
||||
return minlen;
|
||||
}
|
||||
|
||||
bool is_lua_id(string_view str) {
|
||||
if (str.size() == 0) return false;
|
||||
char c=str[0];
|
||||
if ((!ascii_isalpha(c)) && (c!='_')) return false;
|
||||
for (int i = 1; i < int(str.size()); i++) {
|
||||
char c = str[i];
|
||||
if ((!ascii_isalnum(c)) && (c!='_')) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool is_lua_classname(string_view s) {
|
||||
if (s == "_G") return false;
|
||||
return is_lua_id(s);
|
||||
}
|
||||
|
||||
bool is_lua_comment(string_view s) {
|
||||
int start = 0;
|
||||
while ((start < int(s.size())) && ((s[start]==' ') || (s[start]=='\t'))) start++;
|
||||
return s.substr(start, 2) == "--";
|
||||
}
|
||||
|
||||
string_view read_to_sep(string_view &source, char sep) {
|
||||
size_t pos = source.find(sep);
|
||||
string_view result;
|
||||
if (pos == string_view::npos) {
|
||||
result = source;
|
||||
source = string_view();
|
||||
} else {
|
||||
result = source.substr(0, pos);
|
||||
source = source.substr(pos + 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
string_view read_to_line(string_view &source) {
|
||||
size_t pos = source.find('\n');
|
||||
string_view result;
|
||||
if (pos == string_view::npos) {
|
||||
result = source;
|
||||
source = string_view();
|
||||
} else {
|
||||
result = source.substr(0, pos);
|
||||
source = source.substr(pos + 1);
|
||||
}
|
||||
if ((!result.empty()) && (result.back() == '\r')) {
|
||||
result.remove_suffix(1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool read_prefix(string_view &source, string_view prefix) {
|
||||
if (0 == source.compare(0, prefix.size(), prefix)) {
|
||||
source.remove_prefix(prefix.size());
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
string_view read_to_space(string_view &source) {
|
||||
size_t pos1 = 0;
|
||||
while ((pos1 < source.size()) && (!ascii_isspace(source[pos1]))) {
|
||||
pos1 += 1;
|
||||
}
|
||||
string_view result = source.substr(0, pos1);
|
||||
if (pos1 == source.size()) {
|
||||
source = string_view();
|
||||
return result;
|
||||
}
|
||||
size_t pos2 = pos1 + 1;
|
||||
while ((pos2 < source.size()) && (ascii_isspace(source[pos2]))) {
|
||||
pos2 += 1;
|
||||
}
|
||||
source = source.substr(pos2);
|
||||
return result;
|
||||
}
|
||||
|
||||
string_view read_nbytes(string_view &source, int nbytes) {
|
||||
if (nbytes < 0) nbytes = 0;
|
||||
if (nbytes > int(source.size())) nbytes = source.size();
|
||||
string_view result = source.substr(0, nbytes);
|
||||
source = source.substr(nbytes);
|
||||
return result;
|
||||
}
|
||||
|
||||
string_view read_ascii_identifier(string_view &source) {
|
||||
size_t len = 0;
|
||||
if ((len < source.size()) && (sv::ascii_isalpha(source[len]))) {
|
||||
len += 1;
|
||||
while ((len < source.size()) && (sv::ascii_isalnum(source[len]))) {
|
||||
len += 1;
|
||||
}
|
||||
}
|
||||
string_view result = source.substr(0, len);
|
||||
source.remove_prefix(len);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string_view read_number(string_view &source, bool plus, bool minus, bool dec, bool exp) {
|
||||
const char *p = source.data();
|
||||
const char *l = p + source.size();
|
||||
if (p == l) return source.substr(0, 0);
|
||||
char sign = *p;
|
||||
if (sign == '+') {
|
||||
if (!plus) return source.substr(0, 0);
|
||||
p++;
|
||||
}
|
||||
if (sign == '-') {
|
||||
if (!minus) return source.substr(0, 0);
|
||||
p++;
|
||||
}
|
||||
if (p == l) return source.substr(0, 0);
|
||||
bool have_digits = false;
|
||||
while ((p < l) && (ascii_isdigit(*p))) {
|
||||
have_digits = true;
|
||||
p++;
|
||||
}
|
||||
if ((p < l) && dec && (*p == '.')) {
|
||||
p++;
|
||||
while ((p < l) && (ascii_isdigit(*p))) {
|
||||
have_digits = true;
|
||||
p++;
|
||||
}
|
||||
}
|
||||
if (!have_digits) return source.substr(0, 0);
|
||||
if ((p < l) && exp && ((*p == 'e')||(*p == 'E'))) {
|
||||
p++;
|
||||
if ((p < l) && ((*p == '+') || (*p == '-'))) {
|
||||
p++;
|
||||
}
|
||||
bool have_exp = false;
|
||||
while ((p < l) && (ascii_isdigit(*p))) {
|
||||
have_exp = true;
|
||||
p++;
|
||||
}
|
||||
if (!have_exp) return source.substr(0, 0);
|
||||
}
|
||||
string_view result = source.substr(0, p - source.data());
|
||||
source.remove_prefix(result.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
int32_t read_ascii_char(string_view &source) {
|
||||
if (source.empty()) return -1;
|
||||
int32_t result = source.front();
|
||||
source.remove_prefix(1);
|
||||
return result;
|
||||
}
|
||||
|
||||
int32_t read_codepoint_utf8(std::string_view &source) {
|
||||
size_t size = source.size();
|
||||
if (size == 0) return -1;
|
||||
|
||||
const unsigned char *bytes = (const unsigned char *)source.data();
|
||||
int codepoint;
|
||||
size_t seqlen;
|
||||
if ((bytes[0] & 0x80) == 0x00) {
|
||||
// U+0000 to U+007F
|
||||
codepoint = (bytes[0] & 0x7F);
|
||||
seqlen = 1;
|
||||
} else if ((bytes[0] & 0xE0) == 0xC0) {
|
||||
// U+0080 to U+07FF
|
||||
codepoint = (bytes[0] & 0x1F);
|
||||
seqlen = 2;
|
||||
} else if ((bytes[0] & 0xF0) == 0xE0) {
|
||||
// U+0800 to U+FFFF
|
||||
codepoint = (bytes[0] & 0x0F);
|
||||
seqlen = 3;
|
||||
} else if ((bytes[0] & 0xF8) == 0xF0) {
|
||||
// U+10000 to U+10FFFF
|
||||
codepoint = (bytes[0] & 0x07);
|
||||
seqlen = 4;
|
||||
} else {
|
||||
// Bad character. return invalid CP.
|
||||
return -2;
|
||||
}
|
||||
|
||||
if (seqlen > size) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (size_t i = 1; i < seqlen; ++i) {
|
||||
if ((bytes[i] & 0xC0) != 0x80) {
|
||||
// Bad character. return invalid CP.
|
||||
return -2;
|
||||
}
|
||||
codepoint = (codepoint << 6) | (bytes[i] & 0x3F);
|
||||
}
|
||||
|
||||
if ((codepoint > 0x10FFFF) ||
|
||||
((codepoint >= 0xD800) && (codepoint <= 0xDFFF)) ||
|
||||
((codepoint <= 0x007F) && (seqlen != 1)) ||
|
||||
((codepoint >= 0x0080) && (codepoint <= 0x07FF) && (seqlen != 2)) ||
|
||||
((codepoint >= 0x0800) && (codepoint <= 0xFFFF) && (seqlen != 3)) ||
|
||||
((codepoint >= 0x10000) && (codepoint <= 0x1FFFFF) && (seqlen != 4))) {
|
||||
// Bad character. return invalid CP.
|
||||
return -2;
|
||||
}
|
||||
|
||||
source.remove_prefix(seqlen);
|
||||
return codepoint;
|
||||
}
|
||||
|
||||
bool valid_utf8(string_view s) {
|
||||
while (!s.empty()) {
|
||||
int32_t codepoint = read_codepoint_utf8(s);
|
||||
if (codepoint < 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool valid_number(string_view s, bool plus, bool minus, bool dec, bool exp) {
|
||||
read_number(s, plus, minus, dec, exp);
|
||||
return s.empty();
|
||||
}
|
||||
|
||||
} // namespace sv
|
||||
|
||||
|
||||
namespace util {
|
||||
|
||||
eng::string ascii_tolower(std::string_view s) {
|
||||
eng::string mod(s);
|
||||
for (int i = 0; i < int(mod.size()); i++) {
|
||||
if (sv::ascii_isupper(mod[i])) {
|
||||
mod[i] += 'a' - 'A';
|
||||
}
|
||||
}
|
||||
return mod;
|
||||
}
|
||||
|
||||
eng::string ascii_toupper(std::string_view s) {
|
||||
eng::string mod(s);
|
||||
for (int i = 0; i < int(mod.size()); i++) {
|
||||
if (sv::ascii_islower(mod[i])) {
|
||||
mod[i] += 'A' - 'a';
|
||||
}
|
||||
}
|
||||
return mod;
|
||||
}
|
||||
|
||||
void quote_string(const eng::string &s, std::ostream *os) {
|
||||
bool anysq = false;
|
||||
bool anydq = false;
|
||||
for (char c : s) {
|
||||
if (c == '\'') anysq = true;
|
||||
if (c == '"') anydq = true;
|
||||
}
|
||||
bool usesinglequote = (!anysq)||(anydq);
|
||||
(*os) << (usesinglequote ? '\'' : '"');
|
||||
std::string_view str(s);
|
||||
while (!str.empty()) {
|
||||
unsigned char c0 = (unsigned char)(str[0]);
|
||||
int cp = sv::read_codepoint_utf8(str);
|
||||
if (cp < 0) {
|
||||
(*os) << "\\" << dec.width(3).fill('0').val(c0);
|
||||
str.remove_prefix(1);
|
||||
} else if (cp < 32) {
|
||||
c0 = ((unsigned char)cp);
|
||||
switch (c0) {
|
||||
case '\n': (*os) << "\\n"; break;
|
||||
case '\t': (*os) << "\\t"; break;
|
||||
case '\r': (*os) << "\\r"; break;
|
||||
case '\b': (*os) << "\\b"; break;
|
||||
default:
|
||||
(*os) << "\\" << dec.width(3).fill('0').val(c0);
|
||||
break;
|
||||
}
|
||||
} else if (cp == '"') {
|
||||
(*os) << (usesinglequote ? "\"" : "\\\"");
|
||||
} else if (cp == '\'') {
|
||||
(*os) << (usesinglequote ? "\\'" : "'");
|
||||
} else if (cp == '\\') {
|
||||
(*os) << "\\\\";
|
||||
} else {
|
||||
write_codepoint_utf8(cp, os);
|
||||
}
|
||||
}
|
||||
(*os) << (usesinglequote ? '\'' : '"');
|
||||
}
|
||||
|
||||
void base64_encode(std::string_view str, std::ostream *oss) {
|
||||
const char *encode_tab =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
const char *s = str.data();
|
||||
size_t size = str.size();
|
||||
for (size_t i = 0; i < size; i += 3) {
|
||||
uint32_t block = ((unsigned char)(s[i])) << 16;
|
||||
if (i + 1 < size) block |= ((unsigned char)(s[i + 1])) << 8;
|
||||
if (i + 2 < size) block |= ((unsigned char)(s[i + 2]));
|
||||
(*oss) << encode_tab[(block>>18)&0x3F];
|
||||
(*oss) << encode_tab[(block>>12)&0x3F];
|
||||
(*oss) << ((i + 1 < size) ? encode_tab[(block>>6)&0x3F] : '=');
|
||||
(*oss) << ((i + 2 < size) ? encode_tab[(block>>0)&0x3F] : '=');
|
||||
}
|
||||
}
|
||||
|
||||
bool base64_decode(std::string_view str, std::ostream *oss) {
|
||||
uint32_t chunk = 0;
|
||||
int fill = 0;
|
||||
int skip = 0;
|
||||
bool clean = true;
|
||||
for (int i = 0; i < int(str.size()); i++) {
|
||||
char c = str[i];
|
||||
uint32_t value;
|
||||
|
||||
if ((c >= 'A') && (c <= 'Z')) value = c - 'A';
|
||||
else if ((c >= 'a') && (c <= 'z')) value = c - 'a' + 26;
|
||||
else if ((c >= '0') && (c <= '9')) value = c - '0' + 52;
|
||||
else if (c == '+') value = 62;
|
||||
else if (c == '/') value = 63;
|
||||
else if (c == '=') { value = 0; skip ++; }
|
||||
else { clean=false; continue; }
|
||||
|
||||
chunk = (chunk << 6) | value;
|
||||
fill ++;
|
||||
if (fill == 4) {
|
||||
oss->put((chunk>>16) & 0xFF);
|
||||
if (skip < 2) oss->put((chunk>>8) & 0xFF);
|
||||
if (skip < 1) oss->put(chunk & 0xFF);
|
||||
chunk = 0; fill = 0; skip = 0;
|
||||
}
|
||||
}
|
||||
if (fill != 0) clean = false;
|
||||
return clean;
|
||||
}
|
||||
|
||||
IdVector id_vector_create(int64_t id1, int64_t id2, int64_t id3, int64_t id4) {
|
||||
IdVector result;
|
||||
if (id1 >= 0) result.push_back(id1);
|
||||
if (id2 >= 0) result.push_back(id2);
|
||||
if (id3 >= 0) result.push_back(id3);
|
||||
if (id4 >= 0) result.push_back(id4);
|
||||
return result;
|
||||
}
|
||||
|
||||
void print_id_vector(const IdVector &idv, std::ostream *os) {
|
||||
bool first = true;
|
||||
for (int64_t id : idv) {
|
||||
if (!first) (*os) << ",";
|
||||
(*os) << id;
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
void print_id_vector(const std::vector<uint64_t> &idv, std::ostream *os) {
|
||||
bool first = true;
|
||||
for (int64_t id : idv) {
|
||||
if (!first) (*os) << ",";
|
||||
(*os) << id;
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
eng::string id_vector_debug_string(const IdVector &idv) {
|
||||
eng::ostringstream oss;
|
||||
print_id_vector(idv, &oss);
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
IdVector sort_union_id_vectors(const IdVector &v1, const IdVector &v2) {
|
||||
IdVector result(v1.size() + v2.size());
|
||||
int next = 0;
|
||||
for (int64_t id : v1) result[next++] = id;
|
||||
for (int64_t id : v2) result[next++] = id;
|
||||
std::sort(result.begin(), result.end());
|
||||
int64_t prev = -1;
|
||||
int64_t count = 0;
|
||||
for (int64_t id : result) {
|
||||
if (id != prev) {
|
||||
prev = id;
|
||||
result[count++] = id;
|
||||
}
|
||||
}
|
||||
result.resize(count);
|
||||
return result;
|
||||
}
|
||||
|
||||
HashValue hash_string(std::string_view s) {
|
||||
uint64_t hash1 = 0;
|
||||
uint64_t hash2 = 0;
|
||||
SpookyHash::ChainHash128(s.data(), s.size(), &hash1, &hash2);
|
||||
return util::HashValue(hash1, hash2);
|
||||
}
|
||||
|
||||
HashValue hash_string(HashValue prev, std::string_view s) {
|
||||
uint64_t hash1 = prev.first;
|
||||
uint64_t hash2 = prev.second;
|
||||
SpookyHash::ChainHash128(s.data(), s.size(), &hash1, &hash2);
|
||||
return util::HashValue(hash1, hash2);
|
||||
}
|
||||
|
||||
HashValue hash_id_vector(const IdVector &idv) {
|
||||
uint64_t hash1 = 0;
|
||||
uint64_t hash2 = 0;
|
||||
SpookyHash::ChainHash128(&idv[0], idv.size() * sizeof(int64_t), &hash1, &hash2);
|
||||
return util::HashValue(hash1, hash2);
|
||||
}
|
||||
|
||||
eng::string hash_to_hex(const HashValue &hv) {
|
||||
eng::ostringstream oss;
|
||||
oss << hex64.val(hv.first) << hex64.val(hv.second);
|
||||
return oss.str();
|
||||
}
|
||||
static inline uint64_t Rot64(uint64_t x, int k)
|
||||
{
|
||||
return (x << k) | (x >> (64 - k));
|
||||
}
|
||||
|
||||
uint64_t hash_ints(uint64_t a, uint64_t b, uint64_t c, uint64_t d) {
|
||||
uint64_t h0 = c ^ 0xc548cebf3714dbb9;
|
||||
uint64_t h1 = d ^ 0xd23a7edd44383f8d;
|
||||
uint64_t h2 = a ^ 0x7356f92e4b154df7;
|
||||
uint64_t h3 = b ^ 0x55ce09295766838d;
|
||||
|
||||
h3 ^= h2; h2 = Rot64(h2,15); h3 += h2;
|
||||
h0 ^= h3; h3 = Rot64(h3,52); h0 += h3;
|
||||
h1 ^= h0; h0 = Rot64(h0,26); h1 += h0;
|
||||
h2 ^= h1; h1 = Rot64(h1,51); h2 += h1;
|
||||
h3 ^= h2; h2 = Rot64(h2,28); h3 += h2;
|
||||
h0 ^= h3; h3 = Rot64(h3,9); h0 += h3;
|
||||
h1 ^= h0; h0 = Rot64(h0,47); h1 += h0;
|
||||
h2 ^= h1; h1 = Rot64(h1,54); h2 += h1;
|
||||
h3 ^= h2; h2 = Rot64(h2,32); h3 += h2;
|
||||
h0 ^= h3; h3 = Rot64(h3,25); h0 += h3;
|
||||
h1 ^= h0; h0 = Rot64(h0,63); h1 += h0;
|
||||
|
||||
return h1;
|
||||
}
|
||||
|
||||
double hash_to_double(uint64_t hash) {
|
||||
return (hash >> (64-53)) * 0x1p-53;
|
||||
}
|
||||
|
||||
StringVec split(const eng::string &s, char sep) {
|
||||
StringVec result;
|
||||
int start = 0;
|
||||
for (int i = 0; i < int(s.size()); i++) {
|
||||
if (s[i] == sep) {
|
||||
result.push_back(s.substr(start, i-start));
|
||||
start = i+1;
|
||||
}
|
||||
}
|
||||
if (start < int(s.size())) {
|
||||
result.push_back(s.substr(start));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static eng::string substr_nocr(const eng::string &s, int start, int len) {
|
||||
if ((len > 0) && (s[start + len - 1] == '\r')) {
|
||||
len -= 1;
|
||||
}
|
||||
return s.substr(start, len);
|
||||
}
|
||||
|
||||
StringVec split_lines(const eng::string &s) {
|
||||
StringVec result;
|
||||
int start = 0;
|
||||
for (int i = 0; i < int(s.size()); i++) {
|
||||
if (s[i]=='\n') {
|
||||
result.push_back(substr_nocr(s, start, i-start));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
if (start < int(s.size())) {
|
||||
result.push_back(substr_nocr(s, start, s.size()-start));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
StringVec split_docstring(const eng::string &s) {
|
||||
StringVec result;
|
||||
int start = 0;
|
||||
for (int i = 0; i < int(s.size()); i++) {
|
||||
if (s[i]=='|') {
|
||||
int len = i-start;
|
||||
if ((len > 0)||(start > 0)) {
|
||||
result.push_back(s.substr(start, i-start));
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
if (start < int(s.size())) {
|
||||
result.push_back(s.substr(start, s.size()-start));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
eng::string join(const StringVec &strs, const eng::string &sep) {
|
||||
if (strs.empty()) return "";
|
||||
eng::ostringstream oss;
|
||||
oss << strs[0];
|
||||
for (int i = 1; i < int(strs.size()); i++) {
|
||||
oss << sep << strs[i];
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
eng::string repeat_string(const eng::string &a, int n) {
|
||||
int len = a.size();
|
||||
eng::string result(len * n, ' ');
|
||||
for (int i = 0; i < n; i++) {
|
||||
for (int j = 0; j < len; j++) {
|
||||
result[i*len + j] = a[j];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
eng::string tolower(eng::string input) {
|
||||
for (int i = 0; i < int(input.size()); i++) {
|
||||
input[i] = std::tolower(input[i]);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
eng::string toupper(eng::string input) {
|
||||
for (int i = 0; i < int(input.size()); i++) {
|
||||
input[i] = std::toupper(input[i]);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
static int buffer_codepoint_utf8(char32_t scp, char *buffer) {
|
||||
uint32_t cp = (uint32_t)scp;
|
||||
unsigned char *c = (unsigned char *)buffer;
|
||||
if (cp < 0) {
|
||||
return 0;
|
||||
}
|
||||
else if (cp <= 0x7F) {
|
||||
c[0] = cp;
|
||||
return 1;
|
||||
}
|
||||
else if (cp <= 0x7FF) {
|
||||
c[0] = (cp>>6)+192;
|
||||
c[1] = (cp&63)+128;
|
||||
return 2;
|
||||
}
|
||||
else if (cp <= 0xFFFF) {
|
||||
if ((cp >= 0xD800) && (cp <= 0xDFFF)) {
|
||||
return 0;
|
||||
}
|
||||
c[0] = (cp>>12)+224;
|
||||
c[1] = ((cp>>6)&63)+128;
|
||||
c[2] = (cp&63)+128;
|
||||
return 3;
|
||||
}
|
||||
else if (cp <= 0x10FFFF) {
|
||||
c[0] = (cp>>18)+240;
|
||||
c[1] = ((cp>>12)&63)+128;
|
||||
c[2] = ((cp>>6)&63)+128;
|
||||
c[3] = (cp&63)+128;
|
||||
return 4;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
eng::string get_codepoint_utf8(uint32_t cp) {
|
||||
char buffer[4];
|
||||
int len = buffer_codepoint_utf8(cp, buffer);
|
||||
return eng::string(buffer, len);
|
||||
}
|
||||
|
||||
bool write_codepoint_utf8(int32_t cp, std::ostream *s) {
|
||||
char buffer[4];
|
||||
int len = buffer_codepoint_utf8(cp, buffer);
|
||||
(*s) << std::string_view(buffer, len);
|
||||
return (len > 0);
|
||||
}
|
||||
|
||||
double distance_squared(double x1, double y1, double x2, double y2) {
|
||||
double dx = x1 - x2;
|
||||
double dy = y1 - y2;
|
||||
return dx*dx + dy*dy;
|
||||
}
|
||||
|
||||
LuaSourcePtr make_lua_source(const eng::string &code) {
|
||||
LuaSourcePtr result(new LuaSourceVec);
|
||||
eng::string fn = "file.lua";
|
||||
result->push_back(std::make_pair(fn, code));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void (*dprint_hook)(const char *oneline, size_t size);
|
||||
|
||||
void hook_dprint(void (*func)(const char *oneline, size_t size)) {
|
||||
dprint_hook = func;
|
||||
}
|
||||
|
||||
void dprintview(std::string_view view) {
|
||||
// Drop the final newline, if any. We're going
|
||||
// to automatically end with a newline and we don't
|
||||
// want to double up.
|
||||
if ((view.size() > 0) && (view.back() == '\n')) {
|
||||
view.remove_suffix(1);
|
||||
}
|
||||
|
||||
// Chop it up into lines and call the hook one line
|
||||
// at a time.
|
||||
const char *buffer = view.data();
|
||||
const char *base = buffer;
|
||||
for (int i = 0; i < int(view.size()); i++) {
|
||||
if ((buffer[i] == '\n') || (buffer[i] == 0)) {
|
||||
size_t sz = buffer + i - base;
|
||||
if (dprint_hook == nullptr) {
|
||||
fwrite(base, 1, sz, stderr);
|
||||
fwrite("\n", 1, 1, stderr);
|
||||
} else {
|
||||
dprint_hook(base, sz);
|
||||
}
|
||||
base = buffer + i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Output the final line.
|
||||
size_t sz = buffer + view.size() - base;
|
||||
if (sz > 0) {
|
||||
if (dprint_hook == nullptr) {
|
||||
fwrite(base, 1, sz, stderr);
|
||||
fwrite("\n", 1, 1, stderr);
|
||||
} else {
|
||||
dprint_hook(base, sz);
|
||||
}
|
||||
}
|
||||
|
||||
// If we're sending to stderr, flush. If not, then
|
||||
// the hook routine is responsible for flushing its own
|
||||
// output.
|
||||
if (dprint_hook == nullptr) {
|
||||
fflush(stderr);
|
||||
}
|
||||
}
|
||||
|
||||
void dprintf(const char *format, ...) {
|
||||
char buffer[256];
|
||||
va_list args;
|
||||
va_start (args, format);
|
||||
int n = vsnprintf(buffer, 256, format, args);
|
||||
if (n <= 255) {
|
||||
dprintview(std::string_view(buffer, n));
|
||||
} else {
|
||||
char *lbuffer = (char *)malloc(n + 1);
|
||||
vsnprintf(lbuffer, n+1, format, args);
|
||||
dprintview(std::string_view(lbuffer, n));
|
||||
free(lbuffer);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
|
||||
|
||||
static std::string_view read_number_x(const char *p, bool plus, bool minus, bool dec, bool exp) {
|
||||
std::string_view source = p;
|
||||
return sv::read_number(source, plus, minus, dec, exp);
|
||||
}
|
||||
|
||||
LuaDefine(unittests_util, "", "some unit tests") {
|
||||
// Test valid_hostname
|
||||
LuaAssert(L, sv::valid_hostname("foo123"));
|
||||
LuaAssert(L, !sv::valid_hostname("foo%123"));
|
||||
LuaAssert(L, sv::valid_hostname("foo-bar"));
|
||||
LuaAssert(L, !sv::valid_hostname("-foo"));
|
||||
LuaAssert(L, !sv::valid_hostname("foo-"));
|
||||
LuaAssert(L, sv::valid_hostname("foo.bar.baz"));
|
||||
LuaAssert(L, sv::valid_hostname("foo-bar.baz"));
|
||||
LuaAssert(L, !sv::valid_hostname("foo-bar-.baz"));
|
||||
LuaAssert(L, !sv::valid_hostname("foo.-bar-baz"));
|
||||
|
||||
// test str_to_int64, str_to_double
|
||||
LuaAssert(L, sv::to_int64("123") == 123);
|
||||
LuaAssert(L, sv::to_int64("123.4") == INT64_MAX);
|
||||
LuaAssert(L, sv::to_int64("12ab") == INT64_MAX);
|
||||
LuaAssert(L, sv::to_int64("") == INT64_MAX);
|
||||
LuaAssert(L, sv::to_double("123.5") == 123.5);
|
||||
LuaAssert(L, std::isnan(sv::to_double("12ab")));
|
||||
LuaAssert(L, std::isnan(sv::to_double("")));
|
||||
|
||||
// Test trim, ltrim, rtrim
|
||||
LuaAssert(L, sv::ltrim(" foo ") == "foo ");
|
||||
LuaAssert(L, sv::rtrim(" foo ") == " foo");
|
||||
LuaAssert(L, sv::trim(" foo ") == "foo");
|
||||
LuaAssert(L, sv::trim("foo") == "foo");
|
||||
LuaAssert(L, sv::trim("") == "");
|
||||
LuaAssert(L, sv::ltrim("**foo**", '*') == "foo**");
|
||||
LuaAssert(L, sv::rtrim("**foo**", '*') == "**foo");
|
||||
LuaAssert(L, sv::trim("**foo**", '*') == "foo");
|
||||
LuaAssert(L, sv::trim("foo", '*') == "foo");
|
||||
LuaAssert(L, sv::trim("", '*') == "");
|
||||
|
||||
// Test read_to_line
|
||||
std::string_view v = "foo\nbar\r\nbaz";
|
||||
|
||||
std::string_view v1 = sv::read_to_line(v);
|
||||
LuaAssertStrEq(L, v1, "foo");
|
||||
LuaAssertStrEq(L, v, "bar\r\nbaz");
|
||||
|
||||
std::string_view v2 = sv::read_to_line(v);
|
||||
LuaAssertStrEq(L, v2, "bar");
|
||||
LuaAssertStrEq(L, v, "baz");
|
||||
|
||||
std::string_view v3 = sv::read_to_line(v);
|
||||
LuaAssertStrEq(L, v3, "baz");
|
||||
LuaAssert(L, sv::isnull(v));
|
||||
|
||||
// Test read_to_space
|
||||
v = "foo bar baz";
|
||||
|
||||
std::string_view s1 = sv::read_to_space(v);
|
||||
LuaAssertStrEq(L, s1, "foo");
|
||||
LuaAssertStrEq(L, v, "bar baz");
|
||||
|
||||
std::string_view s2 = sv::read_to_space(v);
|
||||
LuaAssertStrEq(L, s2, "bar");
|
||||
LuaAssertStrEq(L, v, "baz");
|
||||
|
||||
std::string_view s3 = sv::read_to_space(v);
|
||||
LuaAssertStrEq(L, s3, "baz");
|
||||
LuaAssert(L, sv::isnull(v));
|
||||
|
||||
// Test the unioning of ID vectors.
|
||||
util::IdVector idv1,idv2;
|
||||
idv1.push_back(1);
|
||||
idv1.push_back(6);
|
||||
idv1.push_back(4);
|
||||
idv2.push_back(5);
|
||||
idv2.push_back(1);
|
||||
idv2.push_back(6);
|
||||
util::IdVector joined = util::sort_union_id_vectors(idv1, idv2);
|
||||
LuaAssert(L, joined.size() == 4);
|
||||
LuaAssert(L, joined[0] == 1);
|
||||
LuaAssert(L, joined[1] == 4);
|
||||
LuaAssert(L, joined[2] == 5);
|
||||
LuaAssert(L, joined[3] == 6);
|
||||
|
||||
// Test the string split routine.
|
||||
util::StringVec sv1 = util::split("foo,bar,baz", ',');
|
||||
LuaAssert(L, sv1.size() == 3);
|
||||
LuaAssert(L, sv1[0] == "foo");
|
||||
LuaAssert(L, sv1[1] == "bar");
|
||||
LuaAssert(L, sv1[2] == "baz");
|
||||
util::StringVec sv2 = util::split(",foo,,bar", ',');
|
||||
LuaAssert(L, sv2.size() == 4);
|
||||
LuaAssert(L, sv2[0]=="");
|
||||
LuaAssert(L, sv2[1]=="foo");
|
||||
LuaAssert(L, sv2[2]=="");
|
||||
LuaAssert(L, sv2[3]=="bar");
|
||||
|
||||
// Test the split_lines routine.
|
||||
util::StringVec sv3 = util::split_lines("foo\n\nbar\r\nbaz\r\n\r\n");
|
||||
LuaAssert(L, sv3.size() == 5);
|
||||
LuaAssert(L, sv3[0] == "foo");
|
||||
LuaAssert(L, sv3[1] == "");
|
||||
LuaAssert(L, sv3[2] == "bar");
|
||||
LuaAssert(L, sv3[3] == "baz");
|
||||
LuaAssert(L, sv3[4] == "");
|
||||
|
||||
// Test the repeat string routine.
|
||||
LuaAssertStrEq(L, util::repeat_string("abc", 3), "abcabcabc");
|
||||
|
||||
// test toupper and tolower
|
||||
LuaAssert(L, util::toupper("fooBar") == "FOOBAR");
|
||||
LuaAssert(L, util::tolower("fooBar") == "foobar");
|
||||
|
||||
// Test distance_squared
|
||||
LuaAssert(L, util::distance_squared(1, 1, 5, 4) == 25.0);
|
||||
LuaAssert(L, util::distance_squared(5, 4, 1, 1) == 25.0);
|
||||
|
||||
// Test XYZ.
|
||||
util::XYZ xyza(3,4,5), xyzb(3,4,5), xyzc(3,4,6);
|
||||
LuaAssert(L, xyza.x == 3);
|
||||
LuaAssert(L, xyza.y == 4);
|
||||
LuaAssert(L, xyza.z == 5);
|
||||
LuaAssert(L, xyza == xyzb);
|
||||
LuaAssert(L, xyza != xyzc);
|
||||
LuaAssert(L, xyza.debug_string() == "(3,4,5)");
|
||||
|
||||
// Test hash_to_string
|
||||
LuaAssertStrEq(L, util::hash_to_hex(util::HashValue(0x1234,0x789a)),
|
||||
"0000000000001234000000000000789a");
|
||||
|
||||
// Test hash_to_double
|
||||
LuaAssert(L, util::hash_to_double(0x1000000000000000) == 1.0/16.0);
|
||||
LuaAssert(L, util::hash_to_double(0x7000000000000000) == 7.0/16.0);
|
||||
LuaAssert(L, util::hash_to_double(0xF000000000000000) == 15.0/16.0);
|
||||
|
||||
// Test read_number allowing everything.
|
||||
LuaAssert(L, read_number_x("123x", true, true, true, true) == "123");
|
||||
LuaAssert(L, read_number_x("123.3x", true, true, true, true) == "123.3");
|
||||
LuaAssert(L, read_number_x("123.x", true, true, true, true) == "123.");
|
||||
LuaAssert(L, read_number_x("123..x", true, true, true, true) == "123.");
|
||||
LuaAssert(L, read_number_x("-123x", true, true, true, true) == "-123");
|
||||
LuaAssert(L, read_number_x("+123x", true, true, true, true) == "+123");
|
||||
LuaAssert(L, read_number_x("+-123x", true, true, true, true) == "");
|
||||
LuaAssert(L, read_number_x("-123.02e05x", true, true, true, true) == "-123.02e05");
|
||||
LuaAssert(L, read_number_x("-123e-5x", true, true, true, true) == "-123e-5");
|
||||
LuaAssert(L, read_number_x("-123e+5x", true, true, true, true) == "-123e+5");
|
||||
LuaAssert(L, read_number_x("-123e+x", true, true, true, true) == "");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
504
luprex/cpp/core/util.hpp
Normal file
504
luprex/cpp/core/util.hpp
Normal file
@@ -0,0 +1,504 @@
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// NAMESPACE SV
|
||||
//
|
||||
// * Operate on string_view or just characters.
|
||||
// * Do not allocate memory.
|
||||
// * Do not copy strings.
|
||||
//
|
||||
// NAMESPACE UTIL
|
||||
//
|
||||
// * General purpose utility functions.
|
||||
// * Sort of a catch-all.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef UTIL_HPP
|
||||
#define UTIL_HPP
|
||||
|
||||
#include "wrap-string.hpp"
|
||||
#include "wrap-set.hpp"
|
||||
#include "wrap-map.hpp"
|
||||
#include "wrap-vector.hpp"
|
||||
#include "wrap-sstream.hpp"
|
||||
#include <ostream>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <algorithm>
|
||||
#include <string_view>
|
||||
#include <limits>
|
||||
#include <iomanip>
|
||||
// #include <cstdint>
|
||||
#include <cstdarg>
|
||||
#include "spookyv2.hpp"
|
||||
|
||||
|
||||
enum WorldType {
|
||||
WORLD_TYPE_MASTER = 1,
|
||||
WORLD_TYPE_PREDICTIVE = 2,
|
||||
};
|
||||
|
||||
|
||||
namespace sv {
|
||||
|
||||
// Bring this into our namespace.
|
||||
using string_view = std::string_view;
|
||||
|
||||
// Test character class, ignoring current locale and unicode issues.
|
||||
inline bool ascii_isupper(char c) { return (c >= 'A') && (c <= 'Z'); }
|
||||
inline bool ascii_islower(char c) { return (c >= 'a') && (c <= 'z'); }
|
||||
inline bool ascii_isdigit(char c) { return (c >= '0') && (c <= '9'); }
|
||||
inline bool ascii_isalpha(char c) { return ascii_isupper(c) || ascii_islower(c); }
|
||||
inline bool ascii_isalnum(char c) { return ascii_isalpha(c) || ascii_isdigit(c); }
|
||||
inline bool ascii_isspace(char c) { return (c==' ')||(c=='\t')||(c=='\r')||(c=='\n')||(c=='\f')||(c=='\v'); }
|
||||
|
||||
// Check for the null string_view
|
||||
//
|
||||
// Note that the null string view is an empty string,
|
||||
// but not every empty string is the null string view.
|
||||
//
|
||||
inline bool isnull(string_view v) { return v.data() == nullptr; }
|
||||
|
||||
// Return true if the two strings are equal, ignoring case.
|
||||
//
|
||||
bool case_insensitive_eq(std::string_view s1, std::string_view s2);
|
||||
|
||||
// Check if numbers can be parsed as int64/double
|
||||
bool valid_double(string_view v);
|
||||
bool valid_int64(string_view v);
|
||||
bool valid_hex64(string_view v);
|
||||
|
||||
// Check if a hostname is a valid DNS (ascii) hostname.
|
||||
bool valid_hostname(string_view v);
|
||||
|
||||
// Convert strings to numbers. Returns errval on failure.
|
||||
//
|
||||
// The integer parser accepts a sequence of digits,
|
||||
// with or without a + or - sign. The hex parser
|
||||
// does not allow a + or - sign. For both the int64
|
||||
// and hex64 parser, it is a failure if the number
|
||||
// does not fit in 64 bits. The double parser does
|
||||
// not accept the strings 'nan' or 'inf'.
|
||||
//
|
||||
double to_double(string_view v, double errval = std::numeric_limits<double>::quiet_NaN());
|
||||
int64_t to_int64(string_view v, int64_t errval = std::numeric_limits<int64_t>::max());
|
||||
uint64_t to_hex64(string_view v, uint64_t errval = std::numeric_limits<uint64_t>::max());
|
||||
|
||||
// Trim whitspace from a string_view.
|
||||
string_view ltrim(string_view v);
|
||||
string_view rtrim(string_view v);
|
||||
string_view trim(string_view v);
|
||||
|
||||
// Trim specific character (all occurrences) from a string_view.
|
||||
string_view ltrim(string_view v, char c);
|
||||
string_view rtrim(string_view v, char c);
|
||||
string_view trim(string_view v, char c);
|
||||
|
||||
// Return true if the string has the specified prefix or suffix.
|
||||
bool has_prefix(string_view s, string_view prefix);
|
||||
bool has_suffix(string_view s, string_view suffix);
|
||||
|
||||
// Return the length of the common prefix of A and B.
|
||||
int common_prefix_length(string_view a, string_view b);
|
||||
|
||||
// Return true if the string is a lua identifier.
|
||||
bool is_lua_id(string_view s);
|
||||
|
||||
// Return true if the string is a valid lua classname.
|
||||
bool is_lua_classname(string_view s);
|
||||
|
||||
// Return true if the line of code is a lua comment.
|
||||
bool is_lua_comment(string_view s);
|
||||
|
||||
// Return the first character, but if the view is empty,
|
||||
// return zero.
|
||||
inline char zfront(string_view &s) {
|
||||
return s.empty() ? char(0) : s.front();
|
||||
}
|
||||
|
||||
// Read from a string_view until separator is reached.
|
||||
//
|
||||
// If the separator appears in the source, returns everything
|
||||
// before the separator, and updates the source to everything
|
||||
// after the separator.
|
||||
//
|
||||
// If the separator doesn't appear in the source, returns
|
||||
// the entire source, and replaces source with the null string_view.
|
||||
//
|
||||
string_view read_to_sep(string_view &source, char sep);
|
||||
|
||||
// Read from a string_view until newline is reached.
|
||||
//
|
||||
// If there's a line-break in the source (newline or CRLF),
|
||||
// returns the text before the line-break, and updates the
|
||||
// source to the text after the line-break.
|
||||
//
|
||||
// If there's no line-break in the source, returns the entire source,
|
||||
// and updates source to the null string_view.
|
||||
//
|
||||
string_view read_to_line(string_view &source);
|
||||
|
||||
// Read a prefix string from a string_view.
|
||||
//
|
||||
// Returns false if the string view doesn't start with
|
||||
// the specified prefix.
|
||||
//
|
||||
bool read_prefix(string_view &source, string_view prefix);
|
||||
|
||||
// Read from a string_view until whitespace is reached.
|
||||
//
|
||||
// If there's any whitespace in the source, returns the text
|
||||
// before the whitespace, and update the source to the text
|
||||
// after the whitespace.
|
||||
//
|
||||
// If there's no whitespace in the source, returns the entire
|
||||
// source, and updates the source to the null string_view.
|
||||
//
|
||||
string_view read_to_space(string_view &source);
|
||||
|
||||
// Read up to nbytes from a string_view.
|
||||
//
|
||||
string_view read_nbytes(string_view &source, int nbytes);
|
||||
|
||||
// Read an ascii identifier from a string_view
|
||||
//
|
||||
// If there's no valid identifier, returns empty string.
|
||||
//
|
||||
string_view read_ascii_identifier(string_view &source);
|
||||
|
||||
// Read a number from a string view
|
||||
//
|
||||
// This is basically a regex pattern matching routine
|
||||
// hardwired with the regex for numbers. You must
|
||||
// specify which of the following parts of the regex
|
||||
// are allowed or not:
|
||||
//
|
||||
// * plus sign
|
||||
// * minus sign
|
||||
// * decimal point
|
||||
// * scientific notation exponents
|
||||
//
|
||||
// Returns the number as a string_view. There is
|
||||
// no guarantee that the number is small enough to
|
||||
// fit into any particular number of bits. This
|
||||
// always uses base 10.
|
||||
//
|
||||
std::string_view read_number(string_view &source, bool plus, bool minus, bool dec, bool exp);
|
||||
|
||||
// Read an ascii character from a string.
|
||||
//
|
||||
// Returns -1 if the string is empty.
|
||||
//
|
||||
int32_t read_ascii_char(string_view &source);
|
||||
|
||||
// Read a UTF8 codepoint from a string_view.
|
||||
//
|
||||
// If the string_view is empty, returns -1 and doesn't update
|
||||
// the string_view.
|
||||
//
|
||||
// If the string_view contains an unfinished but possibly valid
|
||||
// codepoint, returns -1 and doesn't update the string_view.
|
||||
//
|
||||
// If the next thing in the string_view is an invalid codepoint,
|
||||
// returns -2 and doesn't update the string_view.
|
||||
//
|
||||
int32_t read_codepoint_utf8(string_view &source);
|
||||
|
||||
// Return true if the string is valid utf-8.
|
||||
bool valid_utf8(string_view s);
|
||||
|
||||
// Return true if the number conforms to the spec.
|
||||
// See read_number for more information.
|
||||
//
|
||||
bool valid_number(string_view v, bool plus, bool minus, bool dec, bool exp);
|
||||
|
||||
} // namespace sv
|
||||
|
||||
namespace util {
|
||||
|
||||
enum MessageType {
|
||||
MSG_NULL,
|
||||
MSG_DIFF,
|
||||
MSG_ACK,
|
||||
MSG_INVOKE,
|
||||
};
|
||||
|
||||
// Note: IdVector is weird in that it deliberately uses std::vector
|
||||
// instead of eng::vector. This is because we want plane scans
|
||||
// to not touch the engine heap.
|
||||
//
|
||||
using IdVector = std::vector<int64_t>;
|
||||
|
||||
using StringVec = eng::vector<eng::string>;
|
||||
using StringPair = std::pair<eng::string, eng::string>;
|
||||
using StringSet = eng::set<eng::string>;
|
||||
using LuaSourceVec = eng::vector<StringPair>;
|
||||
using LuaSourcePtr = std::unique_ptr<LuaSourceVec>;
|
||||
using HashValue = std::pair<uint64_t, uint64_t>;
|
||||
using SharedStdString = std::shared_ptr<std::string>;
|
||||
using SharedStdStringVec = std::vector<SharedStdString>;
|
||||
|
||||
// Ascii uppercase and lowercase.
|
||||
eng::string ascii_tolower(std::string_view c);
|
||||
eng::string ascii_toupper(std::string_view c);
|
||||
|
||||
// Output a string to a stream using Lua string escaping and quoting.
|
||||
void quote_string(const eng::string &str, std::ostream *os);
|
||||
|
||||
// base64 encode.
|
||||
void base64_encode(std::string_view v, std::ostream *oss);
|
||||
|
||||
// base64 decode.
|
||||
//
|
||||
// Returns true if the base64 was 'clean' base64, as
|
||||
// opposed to base64 with extraneous characters.
|
||||
//
|
||||
bool base64_decode(std::string_view v, std::ostream *oss);
|
||||
|
||||
// ID vector quick create.
|
||||
IdVector id_vector_create(int64_t id1=-1, int64_t id2=-1, int64_t id3=-1, int64_t id4=-1);
|
||||
|
||||
// Print an ID vector to a stream.
|
||||
void print_id_vector(const IdVector &idv, std::ostream *os);
|
||||
void print_id_vector(const std::vector<uint64_t> &idv, std::ostream *os);
|
||||
|
||||
// ID vector debug string.
|
||||
eng::string id_vector_debug_string(const IdVector &idv);
|
||||
|
||||
// Unions and sorts two ID vectors.
|
||||
IdVector sort_union_id_vectors(const IdVector &v1, const IdVector &v2);
|
||||
|
||||
// Get a 128-bit hashvalue for a string.
|
||||
HashValue hash_string(std::string_view str);
|
||||
|
||||
// Get a 128-bit hashvalue for a string, with a previous value.
|
||||
HashValue hash_string(HashValue prev, std::string_view str);
|
||||
|
||||
// Get a 128-bit hashvalue for an ID vector.
|
||||
HashValue hash_id_vector(const IdVector &idv);
|
||||
|
||||
// Convert a 128-bit hash to a hexadecimal string.
|
||||
eng::string hash_to_hex(const HashValue &hash);
|
||||
|
||||
// Hash four integers together to 64 bits.
|
||||
// This is a good hash, but not cryptographically good.
|
||||
uint64_t hash_ints(uint64_t n1, uint64_t n2, uint64_t n3, uint64_t n4);
|
||||
|
||||
// Hash a single 64-bit integer.
|
||||
// This is a good hash, but not cryptographically good.
|
||||
// Published by David Stafford in his article 'Better Bit Mixing'.
|
||||
inline uint64_t hash_int(uint64_t x) {
|
||||
x = (x ^ (x >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
|
||||
x = (x ^ (x >> 27)) * UINT64_C(0x94d049bb133111eb);
|
||||
x = x ^ (x >> 31);
|
||||
return x;
|
||||
}
|
||||
|
||||
// Convert a 64-bit hash value into a floating point number between 0 and 1.
|
||||
double hash_to_double(uint64_t hash);
|
||||
|
||||
// Split a string into multiple strings
|
||||
StringVec split(const eng::string &s, char sep);
|
||||
|
||||
// Split a string into multiple strings using \r or \n
|
||||
StringVec split_lines(const eng::string &s);
|
||||
|
||||
// Split a string into multiple lines using |, remove any leading blank line.
|
||||
StringVec split_docstring(const eng::string &s);
|
||||
|
||||
// Join multiple strings into one string
|
||||
eng::string join(const StringVec &strs, eng::string sep);
|
||||
|
||||
// Return N repetitions of string A
|
||||
eng::string repeat_string(const eng::string &a, int n);
|
||||
|
||||
// String to lowercase/uppercase. Ascii only, no unicode.
|
||||
eng::string tolower(eng::string input);
|
||||
eng::string toupper(eng::string input);
|
||||
|
||||
// Convert a codepoint number into a utf8 string.
|
||||
// If the codepoint is invalid, returns empty string.
|
||||
eng::string get_codepoint_utf8(int32_t cp);
|
||||
|
||||
// Write a codepoint in utf8 to a stream.
|
||||
// If the codepoint is invalid, writes nothing and returns false.
|
||||
bool write_codepoint_utf8(int32_t cp, std::ostream *out);
|
||||
|
||||
// Calculate distance between two points
|
||||
double distance_squared(double x1, double y1, double x2, double y2);
|
||||
|
||||
// Make a LuaSourceVec with one element, for unit testing.
|
||||
LuaSourcePtr make_lua_source(const eng::string &code);
|
||||
|
||||
// Return true if the worldtype is authoritative.
|
||||
inline bool is_authoritative(WorldType t) { return (t == WORLD_TYPE_MASTER); }
|
||||
|
||||
// Remove items from a vector that are nullptr.
|
||||
template<class T>
|
||||
void remove_nullptrs(T &vec) {
|
||||
auto iter = std::partition(vec.begin(), vec.end(), [] (const auto &x) { return x != nullptr; });
|
||||
vec.erase(iter, vec.end());
|
||||
}
|
||||
|
||||
// Remove items from a vector that are marked for deletion.
|
||||
template<class T>
|
||||
void remove_marked_items(T &vec) {
|
||||
auto iter = std::partition(vec.begin(), vec.end(), [] (const auto &x) { return !x.marked_for_deletion(); });
|
||||
vec.erase(iter, vec.end());
|
||||
}
|
||||
|
||||
// An XYZ coordinate, general purpose.
|
||||
template <typename NUMBER>
|
||||
struct NumXYZ {
|
||||
using Number = NUMBER;
|
||||
Number x, y, z;
|
||||
NumXYZ() { x=0; y=0; z=0; }
|
||||
NumXYZ(Number ix, Number iy, Number iz) { x=ix; y=iy; z=iz; }
|
||||
void operator =(const NumXYZ<double> &other) { x = other.x; y = other.y; z = other.z; }
|
||||
void operator =(const NumXYZ<float> &other) { x = other.x; y = other.y; z = other.z; }
|
||||
void operator =(Number n) { x = n; y = n; z = n; }
|
||||
bool operator ==(const NumXYZ &o) const { return x==o.x && y == o.y && z==o.z; }
|
||||
bool operator !=(const NumXYZ &o) const { return x!=o.x || y != o.y || z!=o.z; }
|
||||
NumXYZ operator -(const NumXYZ &o) const { return NumXYZ(x-o.x, y-o.y, z-o.z); }
|
||||
NumXYZ operator +(const NumXYZ &o) const { return NumXYZ(x+o.x, y+o.y, z+o.z); }
|
||||
NumXYZ operator *(float scale) const { return NumXYZ(x*scale, y*scale, z*scale); }
|
||||
template<typename ONUMBER>
|
||||
const NumXYZ<ONUMBER> convert() const { NumXYZ<ONUMBER> r; r.x=ONUMBER(x); r.y=ONUMBER(y); r.z=ONUMBER(z); return r; }
|
||||
|
||||
eng::string debug_string() const {
|
||||
eng::ostringstream oss;
|
||||
oss << "(" << x << "," << y << "," << z << ")";
|
||||
return oss.str();
|
||||
}
|
||||
};
|
||||
|
||||
using XYZ=NumXYZ<float>;
|
||||
using DXYZ=NumXYZ<double>;
|
||||
|
||||
// util::ostringstream
|
||||
//
|
||||
// This is a variant of ostringstream in which it is possible
|
||||
// to get the contents without copying. To get the contents
|
||||
// without copying, use oss.view().
|
||||
//
|
||||
class ostringstream : public eng::ostringstream {
|
||||
class rstringbuf : public std::basic_stringbuf<char_type, traits_type, allocator_type> {
|
||||
public:
|
||||
char *eback() const { return std::streambuf::eback(); }
|
||||
char *pptr() const { return std::streambuf::pptr(); }
|
||||
};
|
||||
rstringbuf rstringbuf_;
|
||||
public:
|
||||
ostringstream() {
|
||||
std::basic_ostream<char>::rdbuf(&rstringbuf_);
|
||||
}
|
||||
char *data() const {
|
||||
return rstringbuf_.eback();
|
||||
}
|
||||
size_t size() const {
|
||||
return rstringbuf_.pptr() - rstringbuf_.eback();
|
||||
}
|
||||
std::string_view view() const {
|
||||
return std::string_view(data(), size());
|
||||
}
|
||||
eng::string str() const {
|
||||
return rstringbuf_.str();
|
||||
}
|
||||
};
|
||||
|
||||
// send_to_stream: send all arguments to the specified stream.
|
||||
inline void send_to_stream(std::ostream &os) {}
|
||||
template <typename ARG, typename... REST>
|
||||
inline void send_to_stream(std::ostream &os, const ARG &arg, const REST & ... rest) {
|
||||
os << arg;
|
||||
send_to_stream(os, rest...);
|
||||
}
|
||||
|
||||
// ss: convert all arguments to a string by sending them to a stringstream.
|
||||
template <typename... ARGS>
|
||||
inline eng::string ss(const ARGS & ... args) {
|
||||
eng::ostringstream oss;
|
||||
send_to_stream(oss, args...);
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
// dprintf / dprint
|
||||
//
|
||||
// Send a debugging message to somewhere that it can be seen. This routine
|
||||
// initially just sends output to stderr. But it can be hooked to send output
|
||||
// somewhere else, like to a debug output window.
|
||||
//
|
||||
// The hook function must be a function that accepts a single line of text. The
|
||||
// hook function will always be passed one line, consisting of printable
|
||||
// characters only. There will be no control characters. The newline is
|
||||
// implied.
|
||||
//
|
||||
void dprintview(std::string_view view);
|
||||
void dprintf(const char *format, ...);
|
||||
void hook_dprint(void (*func)(const char *oneline, size_t size));
|
||||
|
||||
template <typename... ARGS>
|
||||
inline void dprint(const ARGS & ... args) {
|
||||
util::ostringstream oss;
|
||||
send_to_stream(oss, args...);
|
||||
dprintview(oss.view());
|
||||
}
|
||||
|
||||
// A better API than std::setfill, std::hex, std::setw, std::setprecision
|
||||
//
|
||||
// Usage examples:
|
||||
// std::cout << util::hex.width(5).fill('0').val(123)
|
||||
// std::cout << util::dec.fill('$').precision(val(123)
|
||||
//
|
||||
// The reason that other API is bad is that it can leave std::cout
|
||||
// in an unpredictable state. This API always leaves the stream clean.
|
||||
//
|
||||
template <class VALUE>
|
||||
class FormattedNumber {
|
||||
public:
|
||||
VALUE value_;
|
||||
bool hex_;
|
||||
int width_;
|
||||
char fill_;
|
||||
int precision_;
|
||||
|
||||
constexpr FormattedNumber(VALUE v, bool h, int w, char f, int p)
|
||||
: value_(v), hex_(h), width_(w), fill_(f), precision_(p) {}
|
||||
|
||||
constexpr FormattedNumber width(int w) const { return FormattedNumber(value_, hex_, w, fill_, precision_); }
|
||||
constexpr FormattedNumber fill(char f) const { return FormattedNumber(value_, hex_, width_, f, precision_); }
|
||||
constexpr FormattedNumber precision(int p) const { return FormattedNumber(value_, hex_, width_, fill_, p); }
|
||||
|
||||
template <class NVALUE>
|
||||
constexpr FormattedNumber val(NVALUE v) const { return FormattedNumber(v, hex_, width_, fill_, precision_); }
|
||||
};
|
||||
|
||||
constexpr auto hex = FormattedNumber<int>(0, true, 0, '0', 6);
|
||||
constexpr auto hex8 = FormattedNumber<int>(0, true, 2, '0', 6);
|
||||
constexpr auto hex16 = FormattedNumber<int>(0, true, 4, '0', 6);
|
||||
constexpr auto hex32 = FormattedNumber<int>(0, true, 8, '0', 6);
|
||||
constexpr auto hex64 = FormattedNumber<int>(0, true, 16, '0', 6);
|
||||
constexpr auto dec = FormattedNumber<int>(0, false, 0, ' ', 6);
|
||||
|
||||
} // namespace util
|
||||
|
||||
template<class VALUE>
|
||||
inline std::ostream &operator<<(std::ostream &oss, util::FormattedNumber<VALUE> n) {
|
||||
if (n.hex_) oss << std::hex;
|
||||
else oss << std::dec;
|
||||
oss << std::setprecision(n.precision_) << std::setfill(n.fill_) << std::setw(n.width_) << n.value_;
|
||||
oss << std::dec << std::setfill(' ') << std::setprecision(6);
|
||||
return oss;
|
||||
}
|
||||
|
||||
inline std::ostream &operator<<(std::ostream &oss, const util::XYZ &xyz) {
|
||||
oss << xyz.x << "," << xyz.y << "," << xyz.z;
|
||||
return oss;
|
||||
}
|
||||
|
||||
inline std::ostream &operator<<(std::ostream &oss, const util::DXYZ &xyz) {
|
||||
oss << xyz.x << "," << xyz.y << "," << xyz.z;
|
||||
return oss;
|
||||
}
|
||||
|
||||
#endif // UTIL_HPP
|
||||
1073
luprex/cpp/core/world-accessor.cpp
Normal file
1073
luprex/cpp/core/world-accessor.cpp
Normal file
File diff suppressed because it is too large
Load Diff
1226
luprex/cpp/core/world-core.cpp
Normal file
1226
luprex/cpp/core/world-core.cpp
Normal file
File diff suppressed because it is too large
Load Diff
521
luprex/cpp/core/world-difftab.cpp
Normal file
521
luprex/cpp/core/world-difftab.cpp
Normal file
@@ -0,0 +1,521 @@
|
||||
////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// This file contains the code to compare the contents of tables.
|
||||
// The top level functions in this file are:
|
||||
//
|
||||
// World::diff_numbered_tables
|
||||
// World::diff_tangible_databases
|
||||
// World::patch_numbered_tables
|
||||
// World::patch_tangible_databases
|
||||
//
|
||||
// It also contains these unit testing support routines:
|
||||
//
|
||||
// table.diffcompare
|
||||
// table.diffapply
|
||||
//
|
||||
// This file also contains all the support code needed to implement
|
||||
// this stuff.
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "luastack.hpp"
|
||||
#include "streambuffer.hpp"
|
||||
#include "table.hpp"
|
||||
#include "world.hpp"
|
||||
|
||||
// Given a table and an tnmap, return the table number of the table.
|
||||
// Returns zero if the table doesn't have a table number.
|
||||
//
|
||||
static int get_table_number(LuaCoreStack &MLS, LuaSlot mval, LuaSlot mtnmap) {
|
||||
lua_State *L = MLS.state();
|
||||
lua_pushvalue(L, mval.index());
|
||||
lua_rawget(L, mtnmap.index());
|
||||
int result = 0;
|
||||
if (lua_type(L, -1) == LUA_TNUMBER) {
|
||||
result = lua_tointeger(L, -1);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
static bool equivalent_values(LuaCoreStack &MLS, LuaSlot mval, LuaSlot mtnmap,
|
||||
LuaCoreStack &SLS, LuaSlot sval, LuaSlot stnmap) {
|
||||
switch (MLS.xtype(mval)) {
|
||||
case LUA_TBOOLEAN: {
|
||||
if (SLS.type(sval) != LUA_TBOOLEAN) return false;
|
||||
return MLS.tryboolean(mval) == SLS.tryboolean(sval);
|
||||
}
|
||||
case LUA_TNUMBER: {
|
||||
if (SLS.type(sval) != LUA_TNUMBER) return false;
|
||||
return MLS.trynumber(mval) == SLS.trynumber(sval);
|
||||
}
|
||||
case LUA_TSTRING: {
|
||||
if (SLS.type(sval) != LUA_TSTRING) return false;
|
||||
return MLS.trystring(mval) == SLS.trystring(sval);
|
||||
}
|
||||
case LUA_TLIGHTUSERDATA: {
|
||||
if (SLS.type(sval) != LUA_TLIGHTUSERDATA) return false;
|
||||
return MLS.trytoken(mval) == SLS.trytoken(sval);
|
||||
}
|
||||
case LUA_TFUNCTION: {
|
||||
// Cannot really compare. Just return true if the types match.
|
||||
return SLS.type(sval) == MLS.type(mval);
|
||||
}
|
||||
case LUA_TT_GENERAL: {
|
||||
int midx = get_table_number(MLS, mval, mtnmap);
|
||||
if (midx == 0) {
|
||||
return SLS.isnil(sval);
|
||||
}
|
||||
int sidx = get_table_number(SLS, sval, stnmap);
|
||||
return midx == sidx;
|
||||
}
|
||||
case LUA_TT_CLASS: {
|
||||
if (SLS.xtype(sval) != LUA_TT_CLASS) return false;
|
||||
// What if it's an ill-formed class?
|
||||
return MLS.classname(mval) == SLS.classname(sval);
|
||||
}
|
||||
case LUA_TT_TANGIBLE: {
|
||||
if (SLS.xtype(sval) != LUA_TT_TANGIBLE) return false;
|
||||
// What if it's an ill-formed tangible?
|
||||
return MLS.tanid(mval) == SLS.tanid(sval);
|
||||
}
|
||||
case LUA_TT_GLOBALENV: {
|
||||
return (SLS.xtype(sval) == LUA_TT_GLOBALENV);
|
||||
}
|
||||
default:
|
||||
// We're forcing anything else to go to NIL,
|
||||
// and once it's NIL, we consider it 'equal'.
|
||||
return SLS.type(sval) == LUA_TNIL;
|
||||
}
|
||||
}
|
||||
|
||||
static void transmit_value(LuaCoreStack &MLS, LuaSlot mval, LuaSlot mtnmap, StreamBuffer *sb) {
|
||||
switch (MLS.xtype(mval)) {
|
||||
case LUA_TBOOLEAN: {
|
||||
sb->write_uint8(LUA_TBOOLEAN);
|
||||
sb->write_bool(*MLS.tryboolean(mval));
|
||||
return;
|
||||
}
|
||||
case LUA_TNUMBER: {
|
||||
sb->write_uint8(LUA_TNUMBER);
|
||||
sb->write_double(*MLS.trynumber(mval));
|
||||
return;
|
||||
}
|
||||
case LUA_TSTRING: {
|
||||
sb->write_uint8(LUA_TSTRING);
|
||||
sb->write_string(*MLS.trystring(mval));
|
||||
return;
|
||||
}
|
||||
case LUA_TLIGHTUSERDATA: {
|
||||
sb->write_uint8(LUA_TLIGHTUSERDATA);
|
||||
sb->write_uint64((*MLS.trytoken(mval)).value);
|
||||
return;
|
||||
}
|
||||
case LUA_TT_GENERAL: {
|
||||
int midx = get_table_number(MLS, mval, mtnmap);
|
||||
if (midx == 0) {
|
||||
sb->write_uint8(LUA_TNIL);
|
||||
} else {
|
||||
sb->write_uint8(LUA_TT_GENERAL);
|
||||
sb->write_uint32(midx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case LUA_TT_CLASS: {
|
||||
sb->write_uint8(LUA_TT_CLASS);
|
||||
sb->write_string(MLS.classname(mval));
|
||||
return;
|
||||
}
|
||||
case LUA_TT_TANGIBLE: {
|
||||
sb->write_uint8(LUA_TT_TANGIBLE);
|
||||
sb->write_int64(MLS.tanid(mval));
|
||||
return;
|
||||
}
|
||||
case LUA_TT_GLOBALENV: {
|
||||
sb->write_uint8(LUA_TT_GLOBALENV);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
sb->write_uint8(LUA_TNIL);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void transmit_value_debug_string(StreamBuffer *sb, eng::ostringstream &oss) {
|
||||
int kind = sb->read_uint8();
|
||||
switch (kind) {
|
||||
case LUA_TBOOLEAN: {
|
||||
bool b = sb->read_bool();
|
||||
oss << (b ? "true":"false");
|
||||
return;
|
||||
}
|
||||
case LUA_TNUMBER: {
|
||||
oss << sb->read_double();
|
||||
return;
|
||||
}
|
||||
case LUA_TSTRING: {
|
||||
oss << sb->read_string();
|
||||
return;
|
||||
}
|
||||
case LUA_TLIGHTUSERDATA: {
|
||||
LuaToken token(sb->read_uint64());
|
||||
oss << "[" << token.str() << "]";
|
||||
}
|
||||
case LUA_TT_GENERAL: {
|
||||
oss << "table " << sb->read_int32();
|
||||
return;
|
||||
}
|
||||
case LUA_TT_CLASS: {
|
||||
oss << "class " << sb->read_string();
|
||||
return;
|
||||
}
|
||||
case LUA_TT_TANGIBLE: {
|
||||
oss << "tan " << sb->read_int64();
|
||||
return;
|
||||
}
|
||||
case LUA_TT_GLOBALENV: {
|
||||
oss << "globals";
|
||||
return;
|
||||
}
|
||||
case LUA_TNIL: {
|
||||
oss << "nil";
|
||||
return;
|
||||
}
|
||||
default:
|
||||
assert(false); // Should not get here.
|
||||
}
|
||||
}
|
||||
|
||||
static bool diff_tables(LuaCoreStack &SLS0, LuaSlot stnmap, LuaSlot stab,
|
||||
LuaCoreStack &MLS0, LuaSlot mtnmap, LuaSlot mtab,
|
||||
bool cmeta, StreamBuffer *sb) {
|
||||
LuaVar skey, mkey, sval, mval, mnil;
|
||||
LuaExtStack SLS(SLS0.state(), skey, sval);
|
||||
LuaExtStack MLS(MLS0.state(), mkey, mval, mnil);
|
||||
assert(MLS.istable(mtab));
|
||||
assert(SLS.istable(stab));
|
||||
MLS.set(mnil, LuaNil);
|
||||
int nupdates = 0;
|
||||
|
||||
sb->write_int32(0);
|
||||
int wc = sb->total_writes();
|
||||
|
||||
MLS.set(mkey, LuaNil);
|
||||
while (MLS.next(mtab, mkey, mval)) {
|
||||
if (!MLS.issortablekey(mkey)) continue;
|
||||
MLS.movesortablekey(mkey, SLS, skey);
|
||||
SLS.rawget(sval, stab, skey);
|
||||
if (!equivalent_values(MLS, mval, mtnmap, SLS, sval, stnmap)) {
|
||||
transmit_value(MLS, mkey, mtnmap, sb);
|
||||
transmit_value(MLS, mval, mtnmap, sb);
|
||||
nupdates += 1;
|
||||
}
|
||||
}
|
||||
|
||||
SLS.set(skey, LuaNil);
|
||||
while (SLS.next(stab, skey, sval)) {
|
||||
if (!SLS.issortablekey(skey)) continue;
|
||||
SLS.movesortablekey(skey, MLS, mkey);
|
||||
MLS.rawget(mval, mtab, mkey);
|
||||
if (MLS.isnil(mval)) {
|
||||
transmit_value(MLS, mkey, mtnmap, sb);
|
||||
transmit_value(MLS, mval, mtnmap, sb);
|
||||
nupdates += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (cmeta) {
|
||||
SLS.getmetatable(sval, stab);
|
||||
MLS.getmetatable(mval, mtab);
|
||||
if (!equivalent_values(MLS, mval, mtnmap, SLS, sval, stnmap)) {
|
||||
transmit_value(MLS, mnil, mtnmap, sb);
|
||||
transmit_value(MLS, mval, mtnmap, sb);
|
||||
nupdates += 1;
|
||||
}
|
||||
}
|
||||
|
||||
sb->overwrite_int32(wc, nupdates);
|
||||
return (nupdates > 0);
|
||||
}
|
||||
|
||||
static eng::string diff_tables_debug_string(StreamBuffer *sb) {
|
||||
eng::vector<eng::string> sorted;
|
||||
eng::ostringstream oss;
|
||||
int ndiffs = sb->read_int32();
|
||||
for (int i = 0; i < ndiffs; i++) {
|
||||
transmit_value_debug_string(sb, oss);
|
||||
oss << "=";
|
||||
transmit_value_debug_string(sb, oss);
|
||||
sorted.push_back(oss.str());
|
||||
oss.str("");
|
||||
}
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
for (const eng::string &s : sorted) {
|
||||
oss << s << ";";
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
static void set_transmitted_value(LuaCoreStack &LS, LuaSlot tangibles, LuaSlot ntmap, LuaSlot target, StreamBuffer *sb, const char *dbinfo, DebugCollector *dbc) {
|
||||
int kind = sb->read_uint8();
|
||||
switch (kind) {
|
||||
case LUA_TBOOLEAN: {
|
||||
bool value = sb->read_bool();
|
||||
DebugLine(dbc) << dbinfo << (value ? "true" : "false");
|
||||
LS.set(target, value);
|
||||
return;
|
||||
}
|
||||
case LUA_TNUMBER: {
|
||||
double value = sb->read_double();
|
||||
DebugLine(dbc) << dbinfo << value;
|
||||
LS.set(target, value);
|
||||
return;
|
||||
}
|
||||
case LUA_TSTRING: {
|
||||
eng::string value = sb->read_string();
|
||||
DebugLine(dbc) << dbinfo << "'" << value << "'";
|
||||
LS.set(target, value);
|
||||
return;
|
||||
}
|
||||
case LUA_TLIGHTUSERDATA: {
|
||||
LuaToken value(sb->read_uint64());
|
||||
DebugLine(dbc) << dbinfo << "[" << value.str() << "]";
|
||||
LS.set(target, value);
|
||||
return;
|
||||
}
|
||||
case LUA_TT_GENERAL: {
|
||||
int index = sb->read_int32();
|
||||
DebugLine(dbc) << dbinfo << "table " << index;
|
||||
LS.rawget(target, ntmap, index);
|
||||
return;
|
||||
}
|
||||
case LUA_TT_CLASS: {
|
||||
eng::string value = sb->read_string();
|
||||
DebugLine(dbc) << dbinfo << "class " << value;
|
||||
LS.makeclass(target, value);
|
||||
return;
|
||||
}
|
||||
case LUA_TT_TANGIBLE: {
|
||||
int64_t id = sb->read_int64();
|
||||
DebugLine(dbc) << dbinfo << "tan " << id;
|
||||
LS.maketan(target, id);
|
||||
return;
|
||||
}
|
||||
case LUA_TT_GLOBALENV: {
|
||||
DebugLine(dbc) << dbinfo << "global env";
|
||||
LS.getglobaltable(target);
|
||||
return;
|
||||
}
|
||||
case LUA_TNIL: {
|
||||
DebugLine(dbc) << dbinfo << "nil";
|
||||
LS.set(target, LuaNil);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
assert(false); // Should not get here.
|
||||
}
|
||||
}
|
||||
|
||||
static void patch_table(LuaCoreStack &LS0, LuaSlot tangibles, LuaSlot ntmap, LuaSlot tab, StreamBuffer *sb, DebugCollector *dbc) {
|
||||
LuaVar key, val;
|
||||
LuaExtStack LS(LS0.state(), key, val);
|
||||
int ndiffs = sb->read_int32();
|
||||
for (int i = 0; i < ndiffs; i++) {
|
||||
set_transmitted_value(LS, tangibles, ntmap, key, sb, "key=", dbc);
|
||||
set_transmitted_value(LS, tangibles, ntmap, val, sb, "val=", dbc);
|
||||
if (LS.isnil(key)) {
|
||||
LS.setmetatable(tab, val);
|
||||
} else {
|
||||
LS.rawset(tab, key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void World::patch_numbered_tables(StreamBuffer *sb, DebugCollector *dbc) {
|
||||
lua_State *L = state();
|
||||
LuaVar tangibles, ntmap, tab;
|
||||
LuaExtStack LS(L, tangibles, ntmap, tab);
|
||||
LS.rawget(tangibles, LuaRegistry, "tangibles");
|
||||
LS.rawget(ntmap, LuaRegistry, "ntmap");
|
||||
assert(LS.istable(tangibles));
|
||||
assert(LS.istable(ntmap));
|
||||
|
||||
int nmodified = sb->read_int32();
|
||||
for (int i = 0; i < nmodified; i++) {
|
||||
int index = sb->read_int32();
|
||||
LS.rawget(tab, ntmap, index);
|
||||
assert(LS.istable(tab));
|
||||
DebugHeader(dbc) << "Lua Table " << index << ":";
|
||||
patch_table(LS, tangibles, ntmap, tab, sb, dbc);
|
||||
}
|
||||
}
|
||||
|
||||
void World::diff_numbered_tables(lua_State *master, StreamBuffer *sb) {
|
||||
lua_State *synch = state();
|
||||
LuaVar sntmap, mntmap, stnmap, mtnmap, stab, mtab;
|
||||
LuaExtStack SLS(synch, sntmap, stnmap, stab);
|
||||
LuaExtStack MLS(master, mntmap, mtnmap, mtab);
|
||||
SLS.rawget(sntmap, LuaRegistry, "ntmap");
|
||||
MLS.rawget(mntmap, LuaRegistry, "ntmap");
|
||||
SLS.rawget(stnmap, LuaRegistry, "tnmap");
|
||||
MLS.rawget(mtnmap, LuaRegistry, "tnmap");
|
||||
int m_ntables = MLS.rawlen(mntmap);
|
||||
int s_ntables = SLS.rawlen(sntmap);
|
||||
assert(m_ntables == s_ntables);
|
||||
|
||||
sb->write_int32(0);
|
||||
int write_count_after = sb->total_writes();
|
||||
int nmodified = 0;
|
||||
int s_top = lua_gettop(synch);
|
||||
int m_top = lua_gettop(master);
|
||||
for (int id = 1; id <= m_ntables; id++) {
|
||||
MLS.rawget(mtab, mntmap, id);
|
||||
if (MLS.istable(mtab)) {
|
||||
SLS.rawget(stab, sntmap, id);
|
||||
assert(SLS.istable(stab));
|
||||
int tw = sb->total_writes();
|
||||
sb->write_int32(id);
|
||||
nmodified += 1;
|
||||
if (!diff_tables(SLS, stnmap, stab, MLS, mtnmap, mtab, true, sb)) {
|
||||
sb->unwrite_to(tw);
|
||||
nmodified -= 1;
|
||||
}
|
||||
}
|
||||
assert(lua_gettop(synch) == s_top);
|
||||
assert(lua_gettop(master) == m_top);
|
||||
}
|
||||
sb->overwrite_int32(write_count_after, nmodified);
|
||||
}
|
||||
|
||||
void World::patch_tangible_databases(StreamBuffer *sb, DebugCollector *dbc) {
|
||||
lua_State *L = state();
|
||||
LuaVar tangibles, ntmap, tab;
|
||||
LuaExtStack LS(L, tangibles, ntmap, tab);
|
||||
LS.rawget(tangibles, LuaRegistry, "tangibles");
|
||||
LS.rawget(ntmap, LuaRegistry, "ntmap");
|
||||
assert(LS.istable(tangibles));
|
||||
assert(LS.istable(ntmap));
|
||||
|
||||
int nmodified = sb->read_int32();
|
||||
for (int i = 0; i < nmodified; i++) {
|
||||
int64_t id = sb->read_int64();
|
||||
LS.rawget(tab, tangibles, id);
|
||||
assert(LS.istable(tab));
|
||||
DebugHeader(dbc) << "Tangible DB " << id << ":";
|
||||
patch_table(LS, tangibles, ntmap, tab, sb, dbc);
|
||||
}
|
||||
}
|
||||
|
||||
void World::diff_tangible_databases(const IdVector &basis, lua_State *master, StreamBuffer *sb) {
|
||||
lua_State *synch = state();
|
||||
LuaVar stnmap, mtnmap, stangibles, mtangibles, stab, mtab;
|
||||
LuaExtStack SLS(synch, stnmap, stangibles, stab);
|
||||
LuaExtStack MLS(master, mtnmap, mtangibles, mtab);
|
||||
SLS.rawget(stnmap, LuaRegistry, "tnmap");
|
||||
MLS.rawget(mtnmap, LuaRegistry, "tnmap");
|
||||
SLS.rawget(stangibles, LuaRegistry, "tangibles");
|
||||
MLS.rawget(mtangibles, LuaRegistry, "tangibles");
|
||||
|
||||
sb->write_int32(0);
|
||||
int write_count_after = sb->total_writes();
|
||||
int nmodified = 0;
|
||||
int s_top = lua_gettop(synch);
|
||||
int m_top = lua_gettop(master);
|
||||
for (int64_t id : basis) {
|
||||
MLS.rawget(mtab, mtangibles, id);
|
||||
SLS.rawget(stab, stangibles, id);
|
||||
assert(MLS.istable(mtab));
|
||||
assert(SLS.istable(stab));
|
||||
int tw = sb->total_writes();
|
||||
sb->write_int64(id);
|
||||
nmodified += 1;
|
||||
if (!diff_tables(SLS, stnmap, stab, MLS, mtnmap, mtab, false, sb)) {
|
||||
sb->unwrite_to(tw);
|
||||
nmodified -= 1;
|
||||
}
|
||||
assert(lua_gettop(synch) == s_top);
|
||||
assert(lua_gettop(master) == m_top);
|
||||
}
|
||||
sb->overwrite_int32(write_count_after, nmodified);
|
||||
}
|
||||
|
||||
LuaDefine(table_diffcompare, "mtnmap,mtab,stnmap,stab", "for unit testing only") {
|
||||
LuaArg mtnmap, mtab, mstnmap, mstab;
|
||||
LuaRet dbgstring;
|
||||
LuaVar tthread;
|
||||
LuaDefStack MLS(L, mtnmap, mtab, mstnmap, mstab, dbgstring, tthread);
|
||||
// Check the arguments.
|
||||
MLS.cktable(mtnmap, "mtnmap");
|
||||
MLS.cktable(mstnmap, "mstnmap");
|
||||
MLS.cktable(mtab, "mtab");
|
||||
MLS.cktable(mstab, "mstab");
|
||||
|
||||
// Create a temporary thread to be the 'synch model'. We'll use the
|
||||
// existing thread as the 'master model'. Move two tables to the synch thread.
|
||||
lua_State *synch = lua_newthread(L);
|
||||
lua_replace(L, tthread.index());
|
||||
lua_pushvalue(L, mstnmap.index());
|
||||
lua_pushvalue(L, mstab.index());
|
||||
lua_xmove(L, synch, 2);
|
||||
LuaArg stnmap,stab;
|
||||
LuaDefStack SLS(synch, stnmap, stab);
|
||||
|
||||
// Call tablecmp_diff.
|
||||
StreamBuffer sb;
|
||||
diff_tables(SLS, stnmap, stab, MLS, mtnmap, mtab, true, &sb);
|
||||
|
||||
// Convert the output to a debug string.
|
||||
MLS.set(dbgstring, diff_tables_debug_string(&sb));
|
||||
return MLS.result();
|
||||
}
|
||||
|
||||
LuaDefine(table_diffapply, "mtnmap,mtab,mstab", "for unit testing only") {
|
||||
LuaArg mtnmap, mtab, mstab;
|
||||
LuaRet eql, eqlstr, rtab;
|
||||
LuaVar tthread, tangibles, mntmap, key, val;
|
||||
LuaDefStack MLS(L, mtnmap, mtab, mstab, eql, eqlstr, rtab, tthread, tangibles, mntmap, key, val);
|
||||
// Check the arguments.
|
||||
MLS.cktable(mtnmap, "mtnmap");
|
||||
MLS.cktable(mtab, "mtab");
|
||||
MLS.cktable(mstab, "mstab");
|
||||
|
||||
// Get the tangibles map.
|
||||
MLS.rawget(tangibles, LuaRegistry, "tangibles");
|
||||
|
||||
// Invert the tnmap to make the ntmap.
|
||||
MLS.set(mntmap, LuaNewTable);
|
||||
MLS.set(key, LuaNil);
|
||||
while (MLS.next(mtnmap, key, val)) {
|
||||
MLS.rawset(mntmap, val, key);
|
||||
}
|
||||
|
||||
// Create a temporary thread to be the 'synch model'. We'll use the
|
||||
// existing thread as the 'master model'.
|
||||
lua_State *synch = lua_newthread(L);
|
||||
lua_replace(L, tthread.index());
|
||||
lua_pushvalue(L, mtnmap.index());
|
||||
lua_pushvalue(L, mstab.index());
|
||||
lua_xmove(L, synch, 2);
|
||||
LuaArg stnmap, stab;
|
||||
LuaDefStack SLS(synch, stnmap, stab);
|
||||
|
||||
// Call diff_tables and patch_tables
|
||||
StreamBuffer sb;
|
||||
diff_tables(SLS, stnmap, stab, MLS, mtnmap, mtab, true, &sb);
|
||||
|
||||
patch_table(MLS, tangibles, mntmap, mstab, &sb, nullptr);
|
||||
|
||||
bool eq = table_equal(MLS, mstab, mtab);
|
||||
MLS.set(eql, eq);
|
||||
if (eq) {
|
||||
MLS.set(eqlstr, "tables equal");
|
||||
} else {
|
||||
MLS.set(eqlstr, "tables were supposed to be equal");
|
||||
}
|
||||
MLS.set(rtab, stab);
|
||||
|
||||
return MLS.result();
|
||||
}
|
||||
|
||||
|
||||
400
luprex/cpp/core/world-diffxmit.cpp
Normal file
400
luprex/cpp/core/world-diffxmit.cpp
Normal file
@@ -0,0 +1,400 @@
|
||||
|
||||
#include "world.hpp"
|
||||
#include "serializelua.hpp"
|
||||
|
||||
util::IdVector World::get_visible_union(int64_t actor_id, World *master) {
|
||||
util::IdVector v1, v2;
|
||||
master->get_near(actor_id, RadiusVisibility, true, false, false, &v1);
|
||||
get_near(actor_id, RadiusVisibility, true, false, false, &v2);
|
||||
return util::sort_union_id_vectors(v1, v2);
|
||||
}
|
||||
|
||||
int64_t World::patch_actor(StreamBuffer *sb, DebugCollector *dbc) {
|
||||
DebugBlock dbb(dbc, "patch_actor");
|
||||
int64_t actor_id = sb->read_int64();
|
||||
Tangible *s_actor = tangible_get(actor_id);
|
||||
if (s_actor == nullptr) {
|
||||
DebugLine(dbc) << "create new actor " << actor_id;
|
||||
s_actor = tangible_make(actor_id);
|
||||
s_actor->id_player_pool_.deserialize(sb);
|
||||
s_actor->anim_queue_.deserialize(sb);
|
||||
s_actor->print_buffer_.deserialize(sb);
|
||||
} else {
|
||||
DebugHeader(dbc) << "patching actor " << actor_id << ":";
|
||||
s_actor->id_player_pool_.patch(sb, dbc);
|
||||
s_actor->anim_queue_.patch(sb, dbc);
|
||||
s_actor->print_buffer_.patch(sb, dbc);
|
||||
}
|
||||
s_actor->update_plane_item();
|
||||
return actor_id;
|
||||
}
|
||||
|
||||
void World::diff_actor(int64_t actor_id, World *master, StreamBuffer *xsb) {
|
||||
StreamBuffer tsb;
|
||||
|
||||
// Get the actor in both models. s_actor may be nil.
|
||||
const Tangible *s_actor = tangible_get(actor_id);
|
||||
const Tangible *m_actor = master->tangible_get(actor_id);
|
||||
assert(m_actor != nullptr);
|
||||
|
||||
// Calculate diffs.
|
||||
tsb.write_int64(actor_id);
|
||||
if (s_actor == nullptr) {
|
||||
m_actor->id_player_pool_.serialize(&tsb);
|
||||
m_actor->anim_queue_.serialize(&tsb);
|
||||
m_actor->print_buffer_.serialize(&tsb);
|
||||
} else {
|
||||
s_actor->id_player_pool_.diff(m_actor->id_player_pool_, &tsb);
|
||||
s_actor->anim_queue_.diff(m_actor->anim_queue_, &tsb);
|
||||
s_actor->print_buffer_.diff(m_actor->print_buffer_, &tsb);
|
||||
}
|
||||
|
||||
// Forward to client, and apply to server-synchronous.
|
||||
tsb.copy_into(xsb);
|
||||
patch_actor(&tsb, nullptr);
|
||||
assert(tsb.empty());
|
||||
}
|
||||
|
||||
void World::patch_visible(StreamBuffer *sb, DebugCollector *dbc) {
|
||||
DebugBlock dbb(dbc, "patch_visible");
|
||||
// Receive create messages.
|
||||
int count = sb->read_int32();
|
||||
for (int i = 0; i < count; i++) {
|
||||
int64_t id = sb->read_int64();
|
||||
DebugLine(dbc) << "patch_visible create tan " << id;
|
||||
Tangible *t = tangible_make(id);
|
||||
t->anim_queue_.deserialize(sb);
|
||||
t->update_plane_item();
|
||||
}
|
||||
|
||||
// Receive delete messages
|
||||
count = sb->read_int32();
|
||||
for (int i = 0; i < count; i++) {
|
||||
int64_t id = sb->read_int64();
|
||||
DebugLine(dbc) << "patch_visible delete tan " << id;
|
||||
tangible_delete(id);
|
||||
}
|
||||
|
||||
// Receive update messages
|
||||
count = sb->read_int32();
|
||||
for (int i = 0; i < count; i++) {
|
||||
int64_t id = sb->read_int64();
|
||||
DebugLine(dbc) << "patch_visible animqueue tan " << id;
|
||||
Tangible *t = tangible_get(id);
|
||||
assert(t != nullptr);
|
||||
t->anim_queue_.patch(sb, dbc);
|
||||
t->update_plane_item();
|
||||
}
|
||||
}
|
||||
|
||||
void World::diff_visible(const util::IdVector &visible, World *master,
|
||||
StreamBuffer *xsb) {
|
||||
StreamBuffer tsb;
|
||||
|
||||
// Get the specified tangibles in both models.
|
||||
// Some tangibles may be missing in the master, some may be missing in the
|
||||
// sync.
|
||||
TanVector mvis = master->tangible_get_all(visible);
|
||||
TanVector svis = tangible_get_all(visible);
|
||||
assert(mvis.size() == svis.size());
|
||||
|
||||
// For each tangible that exists in the master, but not
|
||||
// in the synchronous model, send a create message.
|
||||
tsb.write_int32(0);
|
||||
int64_t count_pos = tsb.total_writes();
|
||||
int count = 0;
|
||||
for (int i = 0; i < int(svis.size()); i++) {
|
||||
const Tangible *mt = mvis[i];
|
||||
const Tangible *st = svis[i];
|
||||
if ((st == nullptr) && (mt != nullptr)) {
|
||||
count += 1;
|
||||
tsb.write_int64(mt->id());
|
||||
mt->anim_queue_.serialize(&tsb);
|
||||
}
|
||||
}
|
||||
tsb.overwrite_int32(count_pos, count);
|
||||
|
||||
// For each tangible present in the synchronous model that doesn't
|
||||
// exist in the master model, send command to delete it.
|
||||
tsb.write_int32(0);
|
||||
count_pos = tsb.total_writes();
|
||||
count = 0;
|
||||
for (int i = 0; i < int(svis.size()); i++) {
|
||||
const Tangible *mt = mvis[i];
|
||||
const Tangible *st = svis[i];
|
||||
if ((mt == nullptr) && (st != nullptr)) {
|
||||
count += 1;
|
||||
tsb.write_int64(st->id());
|
||||
}
|
||||
}
|
||||
tsb.overwrite_int32(count_pos, count);
|
||||
|
||||
// For each tangible present in both models, compare
|
||||
// the animation queues.
|
||||
tsb.write_int32(0);
|
||||
count_pos = tsb.total_writes();
|
||||
count = 0;
|
||||
for (int i = 0; i < int(svis.size()); i++) {
|
||||
const Tangible *mt = mvis[i];
|
||||
const Tangible *st = svis[i];
|
||||
if ((mt != nullptr) && (st != nullptr)) {
|
||||
int64_t unwind = tsb.total_writes();
|
||||
tsb.write_int64(st->id());
|
||||
if (st->anim_queue_.diff(mt->anim_queue_, &tsb)) {
|
||||
count++;
|
||||
} else {
|
||||
tsb.unwrite_to(unwind);
|
||||
}
|
||||
}
|
||||
}
|
||||
tsb.overwrite_int32(count_pos, count);
|
||||
|
||||
// Forward to client, and apply to server-synchronous.
|
||||
tsb.copy_into(xsb);
|
||||
patch_visible(&tsb, nullptr);
|
||||
assert(tsb.empty());
|
||||
|
||||
// Copy the version number from master animqueue to synch animqueue.
|
||||
for (int i = 0; i < int(mvis.size()); i++) {
|
||||
const Tangible *m_tan = mvis[i];
|
||||
if (m_tan != nullptr) {
|
||||
Tangible *s_tan = const_cast<Tangible *>(svis[i]);
|
||||
if (s_tan == nullptr) {
|
||||
s_tan = tangible_get(m_tan->id());
|
||||
assert(s_tan != nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void World::patch_luatabs(StreamBuffer *sb, DebugCollector *dbc) {
|
||||
DebugBlock dbb(dbc, "patch_luatabs");
|
||||
int64_t actor_id = sb->read_int64();
|
||||
util::HashValue closehash = sb->read_hashvalue();
|
||||
int ncreate = sb->read_int32();
|
||||
util::IdVector closetans;
|
||||
get_near(actor_id, RadiusClose, true, false, true, &closetans);
|
||||
assert(closehash == util::hash_id_vector(closetans));
|
||||
number_lua_tables(closetans);
|
||||
create_new_tables(ncreate);
|
||||
// DebugLine(dbc) << "lua tables: " << nt << " existing " << ncreate << "
|
||||
// new";
|
||||
patch_tangible_databases(sb, dbc);
|
||||
patch_numbered_tables(sb, dbc);
|
||||
unnumber_lua_tables();
|
||||
}
|
||||
|
||||
void World::diff_luatabs(int64_t actor_id, World *master, StreamBuffer *xsb) {
|
||||
StreamBuffer tsb;
|
||||
|
||||
// Calculate the set of close tangibles.
|
||||
util::IdVector mclosetans, sclosetans;
|
||||
master->get_near(actor_id, RadiusClose, true, false, true, &mclosetans);
|
||||
get_near(actor_id, RadiusClose, true, false, true, &sclosetans);
|
||||
assert(mclosetans == sclosetans);
|
||||
util::HashValue closehash = util::hash_id_vector(mclosetans);
|
||||
|
||||
// Number and pair tables in the synchronous and master model.
|
||||
number_lua_tables(mclosetans);
|
||||
pair_lua_tables(mclosetans, master->state());
|
||||
int ncreate = number_remaining_tables(mclosetans, master->state());
|
||||
create_new_tables(ncreate);
|
||||
|
||||
// Difference transmit.
|
||||
tsb.write_int64(actor_id);
|
||||
tsb.write_hashvalue(closehash);
|
||||
tsb.write_int32(ncreate);
|
||||
diff_tangible_databases(mclosetans, master->state(), &tsb);
|
||||
diff_numbered_tables(master->state(), &tsb);
|
||||
|
||||
// Forward to client, and apply to server-synchronous.
|
||||
tsb.copy_into(xsb);
|
||||
assert(tsb.read_int64() == actor_id);
|
||||
assert(tsb.read_hashvalue() == closehash);
|
||||
assert(tsb.read_int32() == ncreate);
|
||||
patch_tangible_databases(&tsb, nullptr);
|
||||
patch_numbered_tables(&tsb, nullptr);
|
||||
assert(tsb.empty());
|
||||
|
||||
// Unnumber tables in both models.
|
||||
unnumber_lua_tables();
|
||||
master->unnumber_lua_tables();
|
||||
}
|
||||
|
||||
void World::patch_tanclass(StreamBuffer *sb, DebugCollector *dbc) {
|
||||
DebugBlock dbb(dbc, "patch_tanclass");
|
||||
lua_State *L = state();
|
||||
LuaVar tangibles, tab, meta, sclass;
|
||||
LuaExtStack LS(L, tangibles, tab, meta, sclass);
|
||||
LS.rawget(tangibles, LuaRegistry, "tangibles");
|
||||
|
||||
int nmodified = sb->read_int32();
|
||||
for (int i = 0; i < nmodified; i++) {
|
||||
int64_t id = sb->read_int64();
|
||||
LS.rawget(tab, tangibles, id);
|
||||
assert(LS.istable(tab));
|
||||
LS.getmetatable(meta, tab);
|
||||
eng::string name = sb->read_string();
|
||||
DebugLine(dbc) << "tanclass " << id << "=" << name;
|
||||
if (name == "") {
|
||||
LS.rawset(meta, "__index", LuaNil);
|
||||
} else {
|
||||
LS.makeclass(sclass, name);
|
||||
LS.rawset(meta, "__index", sclass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void World::diff_tanclass(int64_t actor_id, World *master, StreamBuffer *xsb) {
|
||||
StreamBuffer tsb;
|
||||
|
||||
{
|
||||
LuaVar stangibles, mtangibles, stab, mtab, smeta, mmeta, sclass, mclass;
|
||||
LuaExtStack SLS(state(), stangibles, stab, smeta, sclass);
|
||||
LuaExtStack MLS(master->state(), mtangibles, mtab, mmeta, mclass);
|
||||
SLS.rawget(stangibles, LuaRegistry, "tangibles");
|
||||
MLS.rawget(mtangibles, LuaRegistry, "tangibles");
|
||||
|
||||
// Calculate the set of close tangibles.
|
||||
// TODO: we've already calculated this in an earlier function. This is
|
||||
// wasteful.
|
||||
util::IdVector closetans;
|
||||
master->get_near(actor_id, RadiusClose, true, false, true, &closetans);
|
||||
|
||||
tsb.write_int32(0);
|
||||
int write_count_after = tsb.total_writes();
|
||||
int nmodified = 0;
|
||||
for (int64_t id : closetans) {
|
||||
MLS.rawget(mtab, mtangibles, id);
|
||||
SLS.rawget(stab, stangibles, id);
|
||||
MLS.getmetatable(mmeta, mtab);
|
||||
SLS.getmetatable(smeta, stab);
|
||||
MLS.rawget(mclass, mmeta, "__index");
|
||||
SLS.rawget(sclass, smeta, "__index");
|
||||
eng::string mname = MLS.classname(mclass);
|
||||
eng::string sname = SLS.classname(sclass);
|
||||
if (mname != sname) {
|
||||
tsb.write_int64(id);
|
||||
tsb.write_string(mname);
|
||||
nmodified += 1;
|
||||
}
|
||||
}
|
||||
tsb.overwrite_int32(write_count_after, nmodified);
|
||||
}
|
||||
|
||||
// Forward to client, and apply to server-synchronous.
|
||||
tsb.copy_into(xsb);
|
||||
patch_tanclass(&tsb, nullptr);
|
||||
assert(tsb.empty());
|
||||
}
|
||||
|
||||
void World::patch_source(StreamBuffer *sb, DebugCollector *dbc) {
|
||||
DebugBlock dbb(dbc, "patch_source");
|
||||
bool modified = source_db_.patch(sb, dbc);
|
||||
if (modified) {
|
||||
rebuild_sourcedb();
|
||||
DebugLine(dbc) << "Source DB rebuilt";
|
||||
}
|
||||
}
|
||||
|
||||
void World::diff_source(World *master, StreamBuffer *sb) {
|
||||
StreamBuffer tsb;
|
||||
source_db_.diff(master->source_db_, &tsb);
|
||||
tsb.copy_into(sb);
|
||||
patch_source(&tsb, nullptr);
|
||||
assert(tsb.empty());
|
||||
}
|
||||
|
||||
const eng::string &World::get_gvname_serial(const eng::string &gvar) {
|
||||
static eng::string empty;
|
||||
auto iter = gvname_to_serial_.find(gvar);
|
||||
if (iter == gvname_to_serial_.end()) {
|
||||
return empty;
|
||||
} else {
|
||||
return iter->second;
|
||||
}
|
||||
}
|
||||
|
||||
void World::patch_globals(StreamBuffer *sb, DebugCollector *dbc) {
|
||||
DebugBlock dbb(dbc, "patch_globals");
|
||||
int64_t seqno = sb->read_int64();
|
||||
int32_t total = sb->read_int32();
|
||||
if (total > 0) {
|
||||
lua_State *L = state();
|
||||
LuaVar globaldb, copy;
|
||||
LuaExtStack LS(L, globaldb, copy);
|
||||
LS.rawget(globaldb, LuaRegistry, "globaldb");
|
||||
for (int i = 0; i < total; i++) {
|
||||
eng::string gvar = sb->read_string();
|
||||
eng::string serial = sb->read_string();
|
||||
gvname_to_serial_[gvar] = serial;
|
||||
StreamBuffer sb(serial);
|
||||
eng::string error = deserialize_lua(LS, copy, &sb);
|
||||
if (error.empty()) {
|
||||
LS.rawset(globaldb, gvar, copy);
|
||||
} else {
|
||||
DebugLine(dbc) << "Invalid global serialized data: " << gvar << ":" << error;
|
||||
}
|
||||
}
|
||||
}
|
||||
assign_seqno_ = seqno;
|
||||
gvname_to_seqno_.clear();
|
||||
seqno_to_gvname_.clear();
|
||||
gvname_modified_.clear();
|
||||
}
|
||||
|
||||
void World::diff_globals(World *master, StreamBuffer *sb) {
|
||||
StreamBuffer tsb;
|
||||
tsb.write_int64(master->assign_seqno_);
|
||||
tsb.write_int32(0);
|
||||
int64_t count_pos = tsb.total_writes();
|
||||
int32_t total_mods = 0;
|
||||
for (const eng::string &gvar : gvname_modified_) {
|
||||
const eng::string &mval = master->get_gvname_serial(gvar);
|
||||
const eng::string &sval = get_gvname_serial(gvar);
|
||||
if (mval != sval) {
|
||||
total_mods += 1;
|
||||
tsb.write_string(gvar);
|
||||
tsb.write_string(mval);
|
||||
}
|
||||
}
|
||||
auto iter = master->seqno_to_gvname_.lower_bound(assign_seqno_);
|
||||
while (iter != master->seqno_to_gvname_.end()) {
|
||||
const auto &gvar = iter->second;
|
||||
if (gvname_modified_.find(gvar) == gvname_modified_.end()) {
|
||||
const eng::string &mval = master->get_gvname_serial(gvar);
|
||||
const eng::string &sval = get_gvname_serial(gvar);
|
||||
if (mval != sval) {
|
||||
total_mods += 1;
|
||||
tsb.write_string(gvar);
|
||||
tsb.write_string(mval);
|
||||
}
|
||||
}
|
||||
iter++;
|
||||
}
|
||||
tsb.overwrite_int32(count_pos, total_mods);
|
||||
tsb.copy_into(sb);
|
||||
patch_globals(&tsb, nullptr);
|
||||
assert(tsb.empty());
|
||||
}
|
||||
|
||||
int64_t World::patch_everything(StreamBuffer *sb, DebugCollector *dbc) {
|
||||
DebugBlock dbb(dbc, "patch_everything");
|
||||
int64_t actor_id = patch_actor(sb, dbc);
|
||||
patch_visible(sb, dbc);
|
||||
patch_luatabs(sb, dbc);
|
||||
patch_tanclass(sb, dbc);
|
||||
patch_source(sb, dbc);
|
||||
patch_globals(sb, dbc);
|
||||
return actor_id;
|
||||
}
|
||||
|
||||
void World::diff_everything(int64_t actor_id, World *master, StreamBuffer *sb) {
|
||||
diff_actor(actor_id, master, sb);
|
||||
util::IdVector visible = get_visible_union(actor_id, master);
|
||||
diff_visible(visible, master, sb);
|
||||
diff_luatabs(actor_id, master, sb);
|
||||
diff_tanclass(actor_id, master, sb);
|
||||
diff_source(master, sb);
|
||||
diff_globals(master, sb);
|
||||
}
|
||||
268
luprex/cpp/core/world-pairtab.cpp
Normal file
268
luprex/cpp/core/world-pairtab.cpp
Normal file
@@ -0,0 +1,268 @@
|
||||
////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// This file contains the code to pair up tables in the synchronous
|
||||
// model with tables in the master model. Here are the top-level
|
||||
// routines in this file:
|
||||
//
|
||||
// World::number_lua_tables
|
||||
// World::pair_lua_tables
|
||||
// World::number_remaining_tables
|
||||
// World::create_new_tables
|
||||
// World::unnumber_lua_tables
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "luastack.hpp"
|
||||
#include "streambuffer.hpp"
|
||||
#include "world.hpp"
|
||||
|
||||
int World::number_lua_tables(const IdVector &basis) {
|
||||
// This is conceptually recursive, but we're going to use an
|
||||
// explicit stack (the lua stack).
|
||||
lua_State *L = state();
|
||||
int nextid = 1;
|
||||
{
|
||||
LuaVar tnmap, ntmap, tangibles, tab, key, val, xid;
|
||||
LuaExtStack LS(L, tnmap, ntmap, tangibles, tab, key, val, xid);
|
||||
LS.set(tnmap, LuaNewTable);
|
||||
LS.set(ntmap, LuaNewTable);
|
||||
LS.rawset(LuaRegistry, "tnmap", tnmap);
|
||||
LS.rawset(LuaRegistry, "ntmap", ntmap);
|
||||
LS.rawget(tangibles, LuaRegistry, "tangibles");
|
||||
int top = lua_gettop(L);
|
||||
|
||||
// Push all subtables onto the stack. Note that we may push
|
||||
// the same table twice, that's OK.
|
||||
for (int64_t id : basis) {
|
||||
LS.rawget(tab, tangibles, id);
|
||||
assert(LS.istable(tab));
|
||||
// Traverse subtables.
|
||||
LS.set(key, LuaNil);
|
||||
while (LS.next(tab, key, val)) {
|
||||
if (LS.istable(val) && LS.gettabletype(val)==LUA_TT_GENERAL) {
|
||||
lua_checkstack(L, 10);
|
||||
lua_pushvalue(L, val.index());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pop tables from the stack one by one. If the table is not
|
||||
// already numbered, number it and push subtables onto the stack.
|
||||
while (lua_gettop(L) > top) {
|
||||
lua_replace(L, tab.index());
|
||||
LS.rawget(xid, tnmap, tab);
|
||||
if (LS.isnil(xid)) {
|
||||
int id = nextid++;
|
||||
LS.rawset(tnmap, tab, id);
|
||||
LS.rawset(ntmap, id, tab);
|
||||
// Traverse the metatable.
|
||||
LS.getmetatable(val, tab);
|
||||
if (LS.istable(val) && LS.gettabletype(val)==LUA_TT_GENERAL) {
|
||||
lua_checkstack(L, 10);
|
||||
lua_pushvalue(L, val.index());
|
||||
}
|
||||
// Traverse the subtables.
|
||||
LS.set(key, LuaNil);
|
||||
while (LS.next(tab, key, val)) {
|
||||
if (LS.istable(val) && LS.gettabletype(val)==LUA_TT_GENERAL) {
|
||||
lua_checkstack(L, 10);
|
||||
lua_pushvalue(L, val.index());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert(stack_is_clear());
|
||||
return nextid - 1;
|
||||
}
|
||||
|
||||
void World::pair_lua_tables(const IdVector &basis, lua_State *master) {
|
||||
lua_State *synch = state();
|
||||
LuaVar stangibles, mtangibles, sntmap, mntmap, stnmap, mtnmap, stab, mtab, skey, mkey, sval, mval, sidx, midx;
|
||||
LuaExtStack SLS(synch, stangibles, stab, skey, sval, sntmap, stnmap, sidx);
|
||||
LuaExtStack MLS(master, mtangibles, mtab, mkey, mval, mntmap, mtnmap, midx);
|
||||
|
||||
// Fetch the tangible databases
|
||||
SLS.rawget(stangibles, LuaRegistry, "tangibles");
|
||||
MLS.rawget(mtangibles, LuaRegistry, "tangibles");
|
||||
|
||||
// Fetch the synchronous model tnmap and ntmap
|
||||
SLS.rawget(stnmap, LuaRegistry, "tnmap");
|
||||
SLS.rawget(sntmap, LuaRegistry, "ntmap");
|
||||
assert(SLS.istable(stnmap));
|
||||
assert(SLS.istable(sntmap));
|
||||
|
||||
// Initialize the master model tnmap and ntmap
|
||||
MLS.set(mtnmap, LuaNewTable);
|
||||
MLS.set(mntmap, LuaNewTable);
|
||||
MLS.rawset(LuaRegistry, "tnmap", mtnmap);
|
||||
MLS.rawset(LuaRegistry, "ntmap", mntmap);
|
||||
int s_ntables = SLS.rawlen(sntmap);
|
||||
for (int i = 1; i <= s_ntables; i++) {
|
||||
MLS.rawset(mntmap, i, 0);
|
||||
}
|
||||
|
||||
// Keep track of which tables are already paired
|
||||
eng::vector<bool> paired;
|
||||
paired.assign(s_ntables + 1, false);
|
||||
|
||||
// This records the top of the stack.
|
||||
int mtop = lua_gettop(master);
|
||||
|
||||
for (int64_t id : basis) {
|
||||
MLS.rawget(mtab, mtangibles, id);
|
||||
SLS.rawget(stab, stangibles, id);
|
||||
assert(MLS.istable(mtab));
|
||||
assert(SLS.istable(stab));
|
||||
MLS.set(mkey, LuaNil);
|
||||
while (MLS.next(mtab, mkey, mval)) {
|
||||
if (!MLS.issortablekey(mkey)) continue;
|
||||
if (!MLS.istable(mval)) continue;
|
||||
MLS.movesortablekey(mkey, SLS, skey);
|
||||
SLS.rawget(sval, stab, skey);
|
||||
if (!SLS.istable(sval)) continue;
|
||||
lua_checkstack(master, 20);
|
||||
lua_checkstack(synch, 20);
|
||||
lua_pushvalue(master, mval.index());
|
||||
lua_pushvalue(synch, sval.index());
|
||||
}
|
||||
}
|
||||
|
||||
while (lua_gettop(master) > mtop) {
|
||||
lua_replace(master, mtab.index());
|
||||
lua_replace(synch, stab.index());
|
||||
// If the master table is not a general table, skip.
|
||||
if (MLS.gettabletype(mtab) != LUA_TT_GENERAL) continue;
|
||||
// If the master table is already paired, skip.
|
||||
MLS.rawget(midx, mtnmap, mtab);
|
||||
if (MLS.isnumber(midx)) continue;
|
||||
// If the synch table doesn't have a number, skip.
|
||||
SLS.rawget(sidx, stnmap, stab);
|
||||
auto idx = SLS.trynumber(sidx);
|
||||
if (!idx) continue;
|
||||
assert((*idx >= 1) && (*idx <= s_ntables));
|
||||
// Pair the tables.
|
||||
MLS.rawset(mtnmap, mtab, *idx);
|
||||
MLS.rawset(mntmap, *idx, mtab);
|
||||
paired[*idx] = true;
|
||||
// Potentially pair the metatables.
|
||||
MLS.getmetatable(mval, mtab);
|
||||
if (MLS.istable(mval)) {
|
||||
SLS.getmetatable(sval, stab);
|
||||
if (SLS.istable(sval)) {
|
||||
lua_pushvalue(master, mval.index());
|
||||
lua_pushvalue(synch, sval.index());
|
||||
}
|
||||
}
|
||||
// Pair the subtables.
|
||||
MLS.set(mkey, LuaNil);
|
||||
while (MLS.next(mtab, mkey, mval)) {
|
||||
if (!MLS.issortablekey(mkey)) continue;
|
||||
if (!MLS.istable(mval)) continue;
|
||||
MLS.movesortablekey(mkey, SLS, skey);
|
||||
SLS.rawget(sval, stab, skey);
|
||||
if (!SLS.istable(sval)) continue;
|
||||
lua_checkstack(master, 20);
|
||||
lua_checkstack(synch, 20);
|
||||
lua_pushvalue(master, mval.index());
|
||||
lua_pushvalue(synch, sval.index());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int World::number_remaining_tables(const IdVector &basis, lua_State *master) {
|
||||
// This is conceptually recursive, but we're going to use an
|
||||
// explicit stack (the lua stack).
|
||||
eng::vector<bool> visited;
|
||||
int ntables;
|
||||
{
|
||||
lua_State *L = master;
|
||||
LuaVar tnmap, ntmap, tangibles, tab, key, val, xid;
|
||||
LuaExtStack LS(L, tnmap, ntmap, tangibles, tab, key, val, xid);
|
||||
LS.rawget(tnmap, LuaRegistry, "tnmap");
|
||||
LS.rawget(ntmap, LuaRegistry, "ntmap");
|
||||
LS.rawget(tangibles, LuaRegistry, "tangibles");
|
||||
ntables = LS.rawlen(ntmap);
|
||||
visited.assign(ntables + 1, false);
|
||||
int top = lua_gettop(L);
|
||||
|
||||
// Push all subtables onto the stack. Note that we may push
|
||||
// the same table twice, that's OK.
|
||||
for (int64_t id : basis) {
|
||||
LS.rawget(tab, tangibles, id);
|
||||
assert(LS.istable(tab));
|
||||
LS.set(key, LuaNil);
|
||||
while (LS.next(tab, key, val)) {
|
||||
if (LS.istable(val) && LS.gettabletype(val)==LUA_TT_GENERAL) {
|
||||
lua_checkstack(L, 10);
|
||||
lua_pushvalue(L, val.index());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pop tables from the stack one by one. If the table is not
|
||||
// numbered, number it. If it is not visited, visit it.
|
||||
while (lua_gettop(L) > top) {
|
||||
lua_replace(L, tab.index());
|
||||
int id = 0;
|
||||
LS.rawget(xid, tnmap, tab);
|
||||
if (!LS.isnumber(xid)) {
|
||||
id = visited.size();
|
||||
LS.rawset(tnmap, tab, id);
|
||||
LS.rawset(ntmap, id, tab);
|
||||
visited.push_back(false);
|
||||
} else {
|
||||
id = LS.cknumber(xid);
|
||||
assert((id >= 0) && (id < int(visited.size())));
|
||||
}
|
||||
if (!visited[id]) {
|
||||
visited[id] = true;
|
||||
// Traverse the metatable.
|
||||
LS.getmetatable(val, tab);
|
||||
if (LS.istable(val) && LS.gettabletype(val)==LUA_TT_GENERAL) {
|
||||
lua_checkstack(L, 10);
|
||||
lua_pushvalue(L, val.index());
|
||||
}
|
||||
// Traverse the subtables.
|
||||
LS.set(key, LuaNil);
|
||||
while (LS.next(tab, key, val)) {
|
||||
if (LS.istable(val) && LS.gettabletype(val)==LUA_TT_GENERAL) {
|
||||
lua_checkstack(L, 10);
|
||||
lua_pushvalue(L, val.index());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert(stack_is_clear());
|
||||
return visited.size() - 1 - ntables;
|
||||
}
|
||||
|
||||
void World::create_new_tables(int n) {
|
||||
{
|
||||
LuaVar tnmap, ntmap, tab;
|
||||
LuaExtStack LS(state(), tnmap, ntmap, tab);
|
||||
LS.rawget(tnmap, LuaRegistry, "tnmap");
|
||||
LS.rawget(ntmap, LuaRegistry, "ntmap");
|
||||
assert(LS.istable(tnmap));
|
||||
assert(LS.istable(ntmap));
|
||||
int ntables = LS.rawlen(ntmap);
|
||||
int nextid = ntables + 1;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int id = nextid++;
|
||||
LS.newtable(tab);
|
||||
LS.rawset(ntmap, id, tab);
|
||||
LS.rawset(tnmap, tab, id);
|
||||
}
|
||||
}
|
||||
assert(stack_is_clear());
|
||||
}
|
||||
|
||||
void World::unnumber_lua_tables() {
|
||||
// All we have to do is remove these tables from the registry.
|
||||
LuaExtStack LS(state());
|
||||
LS.rawset(LuaRegistry, "tnmap", LuaNil);
|
||||
LS.rawset(LuaRegistry, "ntmap", LuaNil);
|
||||
}
|
||||
|
||||
519
luprex/cpp/core/world-testing.cpp
Normal file
519
luprex/cpp/core/world-testing.cpp
Normal file
@@ -0,0 +1,519 @@
|
||||
#include "world.hpp"
|
||||
#include "pprint.hpp"
|
||||
#include "json.hpp"
|
||||
#include <cassert>
|
||||
|
||||
|
||||
void World::tangible_clear_anim_queue_to_empty(int64_t id) {
|
||||
Tangible *t = tangible_get(id);
|
||||
assert(t != nullptr);
|
||||
AnimState state;
|
||||
t->anim_queue_.clear(state);
|
||||
t->update_plane_item();
|
||||
}
|
||||
|
||||
|
||||
void World::tangible_clear_plane_and_xyz(int64_t id, const eng::string &plane, const util::DXYZ &xyz) {
|
||||
Tangible *t = tangible_get(id);
|
||||
assert(t != nullptr);
|
||||
AnimState state;
|
||||
state.set_string("plane", plane);
|
||||
state.set_dxyz("xyz", xyz);
|
||||
state.set_persistent("plane");
|
||||
state.set_persistent("xyz");
|
||||
t->anim_queue_.clear(state);
|
||||
t->update_plane_item();
|
||||
}
|
||||
|
||||
void World::tangible_walkto(int64_t id, float x, float y) {
|
||||
Tangible *t = tangible_get(id);
|
||||
assert(t != nullptr);
|
||||
AnimState state = t->anim_queue_.get_final_persistent();
|
||||
state.set_string("action", "walkto");
|
||||
state.set_dxyz("xyz", util::DXYZ(x,y,0.0));
|
||||
t->anim_queue_.add(state);
|
||||
}
|
||||
|
||||
eng::string World::tangible_anim_debug_string(int64_t id) const {
|
||||
const Tangible *t = tangible_get(id);
|
||||
if (t == 0) return "no such tangible";
|
||||
return t->anim_queue_.steps_debug_string();
|
||||
}
|
||||
|
||||
|
||||
eng::string World::tangible_id_pool_debug_string(int64_t id) const {
|
||||
const Tangible *t = tangible_get(id);
|
||||
if (t == 0) return "no such tangible";
|
||||
return t->id_player_pool_.debug_string();
|
||||
}
|
||||
|
||||
|
||||
eng::string World::tangible_ids_debug_string() const {
|
||||
util::IdVector idv;
|
||||
for (const auto &pair : tangibles_) {
|
||||
idv.push_back(pair.first);
|
||||
}
|
||||
std::sort(idv.begin(), idv.end());
|
||||
return util::id_vector_debug_string(idv);
|
||||
}
|
||||
|
||||
eng::string World::tangibles_near_debug_string(int64_t actor, int64_t distance) {
|
||||
assert(stack_is_clear());
|
||||
lua_State *L = state();
|
||||
LuaVar tangibles, tanobj, classtab;
|
||||
LuaExtStack LS(L, tangibles, tanobj, classtab);
|
||||
LS.rawget(tangibles, LuaRegistry, "tangibles");
|
||||
eng::ostringstream result;
|
||||
util::IdVector tans;
|
||||
get_near(actor, distance, true, false, true, &tans);
|
||||
for (int64_t id : tans) {
|
||||
const Tangible *tan = tangible_get(id);
|
||||
LS.rawget(tanobj, tangibles, id);
|
||||
LS.tangetclass(classtab, tanobj);
|
||||
eng::string cname = LS.classname(classtab);
|
||||
AnimState state = tan->anim_queue_.get_final_persistent();
|
||||
result << id << " (" << cname << "): " << state.debug_string() << std::endl;
|
||||
}
|
||||
return result.str();
|
||||
}
|
||||
|
||||
eng::string World::tangible_pprint(int64_t id) const {
|
||||
lua_State *L = state();
|
||||
LuaVar tangibles, tan, meta;
|
||||
LuaExtStack LS(L, tangibles, tan, meta);
|
||||
LS.rawget(tangibles, LuaRegistry, "tangibles");
|
||||
LS.rawget(tan, tangibles, id);
|
||||
eng::ostringstream oss;
|
||||
if (LS.istable(tan)) {
|
||||
PrettyPrintOptions opts;
|
||||
opts.indent = false;
|
||||
pprint(LS, tan, opts, &oss);
|
||||
} else {
|
||||
oss << "<no such tangible " << id << ">{}";
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
eng::string World::numbered_tables_debug_string() const {
|
||||
lua_State *L = state();
|
||||
LuaVar ntmap, tab, tid;
|
||||
LuaExtStack LS(L, ntmap, tab, tid);
|
||||
eng::vector<eng::string> result;
|
||||
eng::ostringstream oss;
|
||||
|
||||
// Fetch the numbered tables map.
|
||||
LS.rawget(ntmap, LuaRegistry, "ntmap");
|
||||
|
||||
// Iterate over the map. For each table, if it has
|
||||
// a TID, output that. Otherwise, output "unknown".
|
||||
for (int i = 1; i < 10000; i++) {
|
||||
LS.rawget(tab, ntmap, i);
|
||||
if (!LS.istable(tab)) break;
|
||||
LS.rawget(tid, tab, "TID");
|
||||
if (LS.isstring(tid)) {
|
||||
result.push_back(LS.ckstring(tid));
|
||||
} else {
|
||||
result.push_back("unknown");
|
||||
}
|
||||
}
|
||||
std::sort(result.begin(), result.end());
|
||||
for (const eng::string &s : result) {
|
||||
oss << s << ";";
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
eng::string World::paired_tables_debug_string(lua_State *master) const {
|
||||
lua_State *synch = state();
|
||||
LuaVar mntmap, sntmap, mtab, stab, mtid, stid;
|
||||
LuaExtStack MLS(master, mntmap, mtab, mtid);
|
||||
LuaExtStack SLS(synch, sntmap, stab, stid);
|
||||
eng::vector<std::pair<eng::string, eng::string>> result;
|
||||
eng::ostringstream oss;
|
||||
|
||||
// Fetch the numbered tables map.
|
||||
MLS.rawget(mntmap, LuaRegistry, "ntmap");
|
||||
SLS.rawget(sntmap, LuaRegistry, "ntmap");
|
||||
int m_ntables = MLS.rawlen(mntmap);
|
||||
int s_ntables = MLS.rawlen(sntmap);
|
||||
assert(m_ntables == s_ntables);
|
||||
|
||||
for (int i = 1; i <= m_ntables; i++) {
|
||||
MLS.rawget(mtab, mntmap, i);
|
||||
SLS.rawget(stab, sntmap, i);
|
||||
if (MLS.istable(mtab) && SLS.istable(stab)) {
|
||||
eng::string mname = "unknown";
|
||||
eng::string sname = "unknown";
|
||||
MLS.rawget(mtid, mtab, "TID");
|
||||
if (MLS.isstring(mtid)) {
|
||||
mname = MLS.ckstring(mtid);
|
||||
}
|
||||
SLS.rawget(stid, stab, "TID");
|
||||
if (SLS.isstring(stid)) {
|
||||
sname = SLS.ckstring(stid);
|
||||
}
|
||||
result.push_back(std::make_pair(mname, sname));
|
||||
}
|
||||
}
|
||||
std::sort(result.begin(), result.end());
|
||||
for (const auto &pair : result) {
|
||||
oss << pair.first << "=" << pair.second << ";";
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
void World::tangible_set_string(int64_t id, const eng::string &path, const eng::string &value) {
|
||||
lua_State *L = state();
|
||||
LuaVar tangibles, tab, subtab;
|
||||
LuaExtStack LS(L, tangibles, tab, subtab);
|
||||
|
||||
// Fetch the lua side of the tangible.
|
||||
LS.rawget(tangibles, LuaRegistry, "tangibles");
|
||||
LS.rawget(tab, tangibles, id);
|
||||
assert(LS.istable(tab));
|
||||
|
||||
// Split the path parts into the table names and final part.
|
||||
util::StringVec pathparts = util::split(path, '.');
|
||||
assert(pathparts.size() >= 1);
|
||||
eng::string finalpart = pathparts.back();
|
||||
pathparts.pop_back();
|
||||
|
||||
// Create subtables as necessary.
|
||||
for (const eng::string &subname : pathparts) {
|
||||
LS.rawget(subtab, tab, subname);
|
||||
if (LS.isnil(subtab)) {
|
||||
LS.set(subtab, LuaNewTable);
|
||||
LS.rawset(tab, subname, subtab);
|
||||
}
|
||||
assert(LS.istable(subtab));
|
||||
LS.set(tab, subtab);
|
||||
}
|
||||
|
||||
// Set the string value.
|
||||
LS.rawset(tab, finalpart, value);
|
||||
}
|
||||
|
||||
void World::tangible_copy_global(int64_t id, const eng::string &path, const eng::string &global) {
|
||||
lua_State *L = state();
|
||||
LuaVar tangibles, tab, subtab, globtab, value;
|
||||
LuaExtStack LS(L, tangibles, tab, subtab, globtab, value);
|
||||
|
||||
// Fetch the lua side of the tangible.
|
||||
LS.rawget(tangibles, LuaRegistry, "tangibles");
|
||||
LS.rawget(tab, tangibles, id);
|
||||
assert(LS.istable(tab));
|
||||
|
||||
// Split the path parts into the table names and final part.
|
||||
util::StringVec pathparts = util::split(path, '.');
|
||||
assert(pathparts.size() >= 1);
|
||||
eng::string finalpart = pathparts.back();
|
||||
pathparts.pop_back();
|
||||
|
||||
// Create subtables as necessary.
|
||||
for (const eng::string &subname : pathparts) {
|
||||
LS.rawget(subtab, tab, subname);
|
||||
if (LS.isnil(subtab)) {
|
||||
LS.set(subtab, LuaNewTable);
|
||||
LS.rawset(tab, subname, subtab);
|
||||
}
|
||||
assert(LS.istable(subtab));
|
||||
LS.set(tab, subtab);
|
||||
}
|
||||
|
||||
// Copy the global value.
|
||||
LS.getglobaltable(globtab);
|
||||
LS.rawget(value, globtab, global);
|
||||
LS.rawset(tab, finalpart, value);
|
||||
}
|
||||
|
||||
void World::tangible_set_class(int64_t id, const eng::string &c) const {
|
||||
LuaVar tangibles, tan, meta, sclass;
|
||||
LuaExtStack LS(state(), tangibles, tan, meta, sclass);
|
||||
LS.rawget(tangibles, LuaRegistry, "tangibles");
|
||||
LS.rawget(tan, tangibles, id);
|
||||
assert(LS.istable(tan));
|
||||
LS.getmetatable(meta, tan);
|
||||
if (c == "") {
|
||||
LS.set(sclass, LuaNil);
|
||||
} else {
|
||||
LS.makeclass(sclass, c);
|
||||
}
|
||||
LS.rawset(meta, "__index", sclass);
|
||||
}
|
||||
|
||||
void World::set_global_json(const eng::string &gvar, const eng::string &json) {
|
||||
LuaVar decoded;
|
||||
LuaExtStack LS(state(), decoded);
|
||||
bool ok = json::decode(LS, decoded, json);
|
||||
if (!ok) {
|
||||
luaL_error(state(), "invalid json");
|
||||
return;
|
||||
}
|
||||
set_global(LS, gvar, decoded);
|
||||
}
|
||||
|
||||
eng::string World::get_global_json(const eng::string &gvar) {
|
||||
LuaVar value, globaldb;
|
||||
LuaExtStack LS(state(), globaldb, value);
|
||||
LS.rawget(globaldb, LuaRegistry, "globaldb");
|
||||
LS.rawget(value, globaldb, gvar);
|
||||
eng::string out;
|
||||
eng::string error = json::encode(LS, value, out, false, 10000);
|
||||
if (!error.empty()) {
|
||||
luaL_error(state(), "%s", error.c_str());
|
||||
return "";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
eng::string World::tangible_get_class(int64_t id) const {
|
||||
LuaVar tangibles, tan, meta, sclass;
|
||||
LuaExtStack LS(state(), tangibles, tan, meta, sclass);
|
||||
LS.rawget(tangibles, LuaRegistry, "tangibles");
|
||||
LS.rawget(tan, tangibles, id);
|
||||
assert(LS.istable(tan));
|
||||
LS.getmetatable(meta, tan);
|
||||
LS.rawget(sclass, meta, "__index");
|
||||
eng::string result = LS.classname(sclass);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
static bool worlds_identical(const UniqueWorld &w1, const UniqueWorld &w2) {
|
||||
StreamBuffer sbw1, sbw2;
|
||||
w1->serialize(&sbw1);
|
||||
w2->serialize(&sbw2);
|
||||
return sbw1.contents_equal(&sbw2);
|
||||
}
|
||||
|
||||
LuaDefine(unittests_world1animdiff, "", "some unit tests") {
|
||||
UniqueWorld m(new World(WORLD_TYPE_MASTER));
|
||||
UniqueWorld ss(new World(WORLD_TYPE_PREDICTIVE));
|
||||
UniqueWorld cs(new World(WORLD_TYPE_PREDICTIVE));
|
||||
StreamBuffer sb;
|
||||
util::IdVector ids = util::id_vector_create(123, 345);
|
||||
|
||||
// Create some tangibles, and add some animations.
|
||||
m->tangible_make(123);
|
||||
m->tangible_make(345);
|
||||
m->tangible_clear_anim_queue_to_empty(123);
|
||||
m->tangible_clear_anim_queue_to_empty(345);
|
||||
m->tangible_walkto(123, 3, 4);
|
||||
m->tangible_walkto(345, 6, 2);
|
||||
LuaAssertStrEq(L, m->tangible_ids_debug_string(), "123,345");
|
||||
LuaAssertStrEq(L, m->tangible_anim_debug_string(123), "[empty]; action:walkto xyz:3,4,0");
|
||||
LuaAssertStrEq(L, m->tangible_anim_debug_string(345), "[empty]; action:walkto xyz:6,2,0");
|
||||
|
||||
// Now difference transmit all that to the client.
|
||||
ss->diff_visible(ids, m.get(), &sb);
|
||||
cs->patch_visible(&sb, nullptr);
|
||||
LuaAssertStrEq(L, ss->tangible_ids_debug_string(), "123,345");
|
||||
LuaAssertStrEq(L, ss->tangible_anim_debug_string(123), "[empty]; action:walkto xyz:3,4,0");
|
||||
LuaAssertStrEq(L, ss->tangible_anim_debug_string(345), "[empty]; action:walkto xyz:6,2,0");
|
||||
LuaAssert(L, worlds_identical(ss, cs));
|
||||
|
||||
// Now add some more animation records to the master.
|
||||
m->tangible_walkto(123, 7, 3);
|
||||
m->tangible_walkto(345, 2, 5);
|
||||
LuaAssertStrEq(L, m->tangible_anim_debug_string(123), "[empty]; action:walkto xyz:3,4,0; action:walkto xyz:7,3,0");
|
||||
LuaAssertStrEq(L, m->tangible_anim_debug_string(345), "[empty]; action:walkto xyz:6,2,0; action:walkto xyz:2,5,0");
|
||||
|
||||
// Now difference transmit all that to the client again.
|
||||
ss->diff_visible(ids, m.get(), &sb);
|
||||
cs->patch_visible(&sb, nullptr);
|
||||
LuaAssertStrEq(L, ss->tangible_anim_debug_string(123), "[empty]; action:walkto xyz:3,4,0; action:walkto xyz:7,3,0");
|
||||
LuaAssertStrEq(L, ss->tangible_anim_debug_string(345), "[empty]; action:walkto xyz:6,2,0; action:walkto xyz:2,5,0");
|
||||
LuaAssert(L, worlds_identical(ss, cs));
|
||||
|
||||
// Delete tangible 345.
|
||||
m->tangible_delete(345);
|
||||
LuaAssertStrEq(L, m->tangible_ids_debug_string(), "123");
|
||||
|
||||
// And difference transmit
|
||||
ss->diff_visible(ids, m.get(), &sb);
|
||||
cs->patch_visible(&sb, nullptr);
|
||||
LuaAssertStrEq(L, ss->tangible_ids_debug_string(), "123");
|
||||
LuaAssert(L, worlds_identical(ss, cs));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
LuaDefine(unittests_world2pairtab, "", "some unit tests") {
|
||||
UniqueWorld m(new World(WORLD_TYPE_MASTER));
|
||||
UniqueWorld ss(new World(WORLD_TYPE_PREDICTIVE));
|
||||
StreamBuffer sb;
|
||||
int ncreate;
|
||||
|
||||
// Create a master model containing some general tables, and
|
||||
// some specialty tables (not numberable).
|
||||
m->tangible_make(123);
|
||||
m->tangible_set_string(123, "inventory.TID", "inventory");
|
||||
m->tangible_set_string(123, "transactions.TID", "transactions");
|
||||
m->tangible_set_string(123, "skills.TID", "skills");
|
||||
m->tangible_set_string(123, "skills.leet.TID", "skills.leet");
|
||||
m->tangible_set_string(123, "inventory.cplx.TID", "inventory.cplx");
|
||||
m->tangible_copy_global(123, "math", "math");
|
||||
m->tangible_copy_global(123, "gltab", "_G");
|
||||
|
||||
// Now we're going to create a synchronous model that's similar to, but not
|
||||
// exactly the same as that master model.
|
||||
ss->tangible_make(123);
|
||||
ss->tangible_set_string(123, "inventory.TID", "inventory");
|
||||
ss->tangible_set_string(123, "skills.TID", "skills");
|
||||
ss->tangible_set_string(123, "skills.crap.TID", "skills.crap");
|
||||
ss->tangible_set_string(123, "skills.leet.TID", "skills.leet");
|
||||
ss->tangible_set_string(123, "math.TID", "math");
|
||||
ss->tangible_set_string(123, "gltab.TID", "gltab");
|
||||
|
||||
// Now we're going to test the numbering and pairing of tables.
|
||||
// Only these tables should pair: inventory, skills, and skills.leet
|
||||
ss->number_lua_tables(util::id_vector_create(123));
|
||||
LuaAssertStrEq(L, ss->numbered_tables_debug_string(),
|
||||
"gltab;inventory;math;skills;skills.crap;skills.leet;");
|
||||
ss->pair_lua_tables(util::id_vector_create(123), m->state());
|
||||
LuaAssertStrEq(L, ss->paired_tables_debug_string(m->state()),
|
||||
"inventory=inventory;skills=skills;skills.leet=skills.leet;");
|
||||
|
||||
// Test the creation of new tables during difference transmission.
|
||||
// The master world model above has two tables that couldn't be paired
|
||||
// to the client: inventory.cplx, and transactions. These two tables
|
||||
// should be paired to new, created tables.
|
||||
ncreate = m->number_remaining_tables(util::id_vector_create(123), m->state());
|
||||
LuaAssert(L, ncreate == 2);
|
||||
ss->create_new_tables(ncreate);
|
||||
LuaAssertStrEq(L, ss->paired_tables_debug_string(m->state()),
|
||||
"inventory=inventory;inventory.cplx=unknown;skills=skills;skills.leet=skills.leet;transactions=unknown;");
|
||||
return 0;
|
||||
}
|
||||
|
||||
LuaDefine(unittests_world3diffluatab, "", "some unit tests") {
|
||||
UniqueWorld m(new World(WORLD_TYPE_MASTER));
|
||||
UniqueWorld ss(new World(WORLD_TYPE_PREDICTIVE));
|
||||
UniqueWorld cs(new World(WORLD_TYPE_PREDICTIVE));
|
||||
StreamBuffer sb;
|
||||
|
||||
// Initialize all three models so that a tangible exists.
|
||||
m->tangible_make(123);
|
||||
ss->tangible_make(123);
|
||||
cs->tangible_make(123);
|
||||
m->tangible_make(345);
|
||||
ss->tangible_make(345);
|
||||
cs->tangible_make(345);
|
||||
|
||||
m->tangible_clear_plane_and_xyz(123, "earth", util::DXYZ(0,0,0));
|
||||
ss->tangible_clear_plane_and_xyz(123, "earth", util::DXYZ(0,0,0));
|
||||
cs->tangible_clear_plane_and_xyz(123, "earth", util::DXYZ(0,0,0));
|
||||
m->tangible_clear_plane_and_xyz(345, "earth", util::DXYZ(0,0,0));
|
||||
ss->tangible_clear_plane_and_xyz(345, "earth", util::DXYZ(0,0,0));
|
||||
cs->tangible_clear_plane_and_xyz(345, "earth", util::DXYZ(0,0,0));
|
||||
|
||||
// Put some data into the master model.
|
||||
m->tangible_set_string(123, "bacon", "crispy");
|
||||
m->tangible_set_string(123, "inventory.gold", "wealthy");
|
||||
m->tangible_set_string(123, "skills.hunting", "leet");
|
||||
m->tangible_set_string(123, "skills.magic.fireball", "weak");
|
||||
m->tangible_set_string(345, "inventory.gold", "poor");
|
||||
m->tangible_set_string(345, "phone", "867-5309");
|
||||
|
||||
// The data in the master model should now look like this:
|
||||
const char *expect_123 =
|
||||
"<tangible 123>{ "
|
||||
"bacon='crispy', "
|
||||
"inventory={ gold='wealthy' }, "
|
||||
"skills={ "
|
||||
"hunting='leet', "
|
||||
"magic={ fireball='weak' } "
|
||||
"} "
|
||||
"}";
|
||||
const char *expect_345 =
|
||||
"<tangible 345>{ "
|
||||
"inventory={ gold='poor' }, "
|
||||
"phone='867-5309' "
|
||||
"}";
|
||||
LuaAssertStrEq(L, m->tangible_pprint(123), expect_123);
|
||||
LuaAssertStrEq(L, m->tangible_pprint(345), expect_345);
|
||||
|
||||
// Difference transmit.
|
||||
ss->diff_luatabs(123, m.get(), &sb);
|
||||
cs->patch_luatabs(&sb, nullptr);
|
||||
|
||||
// Verify that the data was transmitted.
|
||||
LuaAssertStrEq(L, ss->tangible_pprint(123), expect_123);
|
||||
LuaAssertStrEq(L, cs->tangible_pprint(123), expect_123);
|
||||
LuaAssertStrEq(L, ss->tangible_pprint(345), expect_345);
|
||||
LuaAssertStrEq(L, cs->tangible_pprint(345), expect_345);
|
||||
LuaAssert(L, worlds_identical(ss, cs));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
LuaDefine(unittests_world4difftanclass, "", "some unit tests") {
|
||||
UniqueWorld m(new World(WORLD_TYPE_MASTER));
|
||||
UniqueWorld ss(new World(WORLD_TYPE_PREDICTIVE));
|
||||
UniqueWorld cs(new World(WORLD_TYPE_PREDICTIVE));
|
||||
StreamBuffer sb;
|
||||
|
||||
// Initialize all three models so that a tangible exists.
|
||||
m->tangible_make(123);
|
||||
ss->tangible_make(123);
|
||||
cs->tangible_make(123);
|
||||
|
||||
// Change the lua class of the tangible.
|
||||
m->tangible_set_class(123, "chicken");
|
||||
LuaAssertStrEq(L, m->tangible_get_class(123), "chicken");
|
||||
|
||||
// Difference transmit.
|
||||
ss->diff_tanclass(123, m.get(), &sb);
|
||||
cs->patch_tanclass(&sb, nullptr);
|
||||
|
||||
// Verify that the data was transmitted.
|
||||
LuaAssertStrEq(L, ss->tangible_get_class(123), "chicken");
|
||||
LuaAssertStrEq(L, cs->tangible_get_class(123), "chicken");
|
||||
LuaAssert(L, worlds_identical(ss, cs));
|
||||
|
||||
// Change the class again.
|
||||
m->tangible_set_class(123, "");
|
||||
LuaAssertStrEq(L, m->tangible_get_class(123), "");
|
||||
|
||||
// Difference transmit.
|
||||
ss->diff_tanclass(123, m.get(), &sb);
|
||||
cs->patch_tanclass(&sb, nullptr);
|
||||
|
||||
// Verify that the data was transmitted.
|
||||
LuaAssertStrEq(L, ss->tangible_get_class(123), "");
|
||||
LuaAssertStrEq(L, cs->tangible_get_class(123), "");
|
||||
LuaAssert(L, worlds_identical(ss, cs));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
LuaDefine(unittests_world5diffglobals, "", "some unit tests") {
|
||||
UniqueWorld m(new World(WORLD_TYPE_MASTER));
|
||||
UniqueWorld ss(new World(WORLD_TYPE_PREDICTIVE));
|
||||
UniqueWorld cs(new World(WORLD_TYPE_PREDICTIVE));
|
||||
StreamBuffer sb;
|
||||
|
||||
m->set_global_json("x", "3");
|
||||
ss->diff_globals(m.get(), &sb);
|
||||
cs->patch_globals(&sb, nullptr);
|
||||
LuaAssertStrEq(L, cs->get_global_json("x"), "3");
|
||||
|
||||
m->set_global_json("x", "4");
|
||||
m->set_global_json("x", "5");
|
||||
m->set_global_json("y", "6");
|
||||
ss->diff_globals(m.get(), &sb);
|
||||
cs->patch_globals(&sb, nullptr);
|
||||
LuaAssertStrEq(L, cs->get_global_json("x"), "5");
|
||||
LuaAssertStrEq(L, cs->get_global_json("y"), "6");
|
||||
|
||||
cs->set_global_json("x", "2");
|
||||
ss->set_global_json("x", "2");
|
||||
ss->diff_globals(m.get(), &sb);
|
||||
cs->patch_globals(&sb, nullptr);
|
||||
LuaAssertStrEq(L, cs->get_global_json("x"), "5");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
696
luprex/cpp/core/world.hpp
Normal file
696
luprex/cpp/core/world.hpp
Normal file
@@ -0,0 +1,696 @@
|
||||
|
||||
#ifndef WORLD_HPP
|
||||
#define WORLD_HPP
|
||||
|
||||
#include "wrap-set.hpp"
|
||||
#include "wrap-unordered-map.hpp"
|
||||
#include "wrap-map.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "luastack.hpp"
|
||||
#include "planemap.hpp"
|
||||
#include "idalloc.hpp"
|
||||
#include "animqueue.hpp"
|
||||
#include "invocation.hpp"
|
||||
#include "streambuffer.hpp"
|
||||
#include "debugcollector.hpp"
|
||||
#include "printbuffer.hpp"
|
||||
#include "sched.hpp"
|
||||
#include "http.hpp"
|
||||
#include "source.hpp"
|
||||
#include "luasnap.hpp"
|
||||
|
||||
class World;
|
||||
|
||||
class Tangible : public eng::opnew {
|
||||
private:
|
||||
friend class World;
|
||||
|
||||
// Serialize and deserialize
|
||||
//
|
||||
// The tangible's ID is not serialized. When you serialize a tangible, you
|
||||
// should probably serialize the ID separately.
|
||||
//
|
||||
// The Lua portion of the tangible is not serialized here. Instead, the lua
|
||||
// portion is serialized when you serialize the lua state as a whole.
|
||||
//
|
||||
// PlaneItem is not serialized. The deserialize routine rebuilds the
|
||||
// PlaneItem from the AnimQueue.
|
||||
//
|
||||
// World pointer is not serialized.
|
||||
//
|
||||
void serialize(StreamBuffer *sb);
|
||||
void deserialize(StreamBuffer *sb);
|
||||
|
||||
public:
|
||||
// Always points back to the world model.
|
||||
World *world_;
|
||||
|
||||
// Animation queue.
|
||||
//
|
||||
AnimQueue anim_queue_;
|
||||
|
||||
// Plane Item.
|
||||
//
|
||||
// The PlaneItem also contains this tangible's ID.
|
||||
// To move this PlaneItem, update the anim_queue first, then call
|
||||
// update_plane_item, which copies the data from the anim_queue.
|
||||
//
|
||||
PlaneItem plane_item_;
|
||||
|
||||
// Player ID pool
|
||||
//
|
||||
// This is present in every tangible, whether a player or not.
|
||||
// However, the fifo is only enabled in logged-in players.
|
||||
//
|
||||
IdPlayerPool id_player_pool_;
|
||||
|
||||
// Print Buffer
|
||||
//
|
||||
// Stores the console output for this actor until it can be
|
||||
// probed by the client. Most tangibles have empty printbuffers,
|
||||
// which are stored as just a null pointer internally.
|
||||
//
|
||||
PrintBuffer print_buffer_;
|
||||
|
||||
// Can-Be-Controlled flag.
|
||||
//
|
||||
// This flag indicates whether the tangible can be controlled
|
||||
// by a client. Clients will not be allowed to attach to tangibles
|
||||
// who don't have this flag. If this flag is true, the
|
||||
// tangible cannot be deleted using a mere 'tangible.delete', instead,
|
||||
// you have to use 'tangible.deleteplayer'.
|
||||
//
|
||||
bool can_be_controlled_;
|
||||
|
||||
// Is Controlled Flag.
|
||||
//
|
||||
// This flag is set to true when a client is controlling this player.
|
||||
// It gets set back to false when the client logs out or attaches
|
||||
// to a different player. This can only be set in master models.
|
||||
//
|
||||
bool is_controlled_;
|
||||
|
||||
// Force disconnect flag.
|
||||
//
|
||||
// This flag is used to force the client to log out ASAP. This flag
|
||||
// can only be set in master models.
|
||||
//
|
||||
bool force_disconnect_;
|
||||
|
||||
// Delete on Logout Flag.
|
||||
//
|
||||
// This flag can be set on a controlled player. When the player
|
||||
// disconnects, their character will be deleted. This flag can only
|
||||
// be set if the is_controlled_ flag is true.
|
||||
//
|
||||
bool delete_on_disconnect_;
|
||||
|
||||
// constructor.
|
||||
//
|
||||
Tangible(World *w, int64_t id);
|
||||
|
||||
// Get the ID
|
||||
//
|
||||
int64_t id() const { return plane_item_.id(); }
|
||||
|
||||
void update_plane_item();
|
||||
bool is_an_actor() { return (id_player_pool_.get_fifo_capacity() > 0); }
|
||||
void configure_id_pool_for_actor() { id_player_pool_.set_fifo_capacity(3); id_player_pool_.refill(); }
|
||||
};
|
||||
|
||||
using UniqueTangible = std::unique_ptr<Tangible>;
|
||||
|
||||
class World : public eng::opnew {
|
||||
public:
|
||||
using IdVector = util::IdVector;
|
||||
using TanVector = eng::vector<const Tangible*>;
|
||||
using Redirects = eng::map<int64_t, int64_t>;
|
||||
const float RadiusVisibility = 1000.0;
|
||||
const float RadiusClose = 1000.0;
|
||||
|
||||
// Constructor.
|
||||
//
|
||||
// The constructor also calls 'lua_open' to create a new
|
||||
// lua interpreter for this world model.
|
||||
//
|
||||
World(WorldType wt);
|
||||
|
||||
// Destructor.
|
||||
//
|
||||
// Not currently functional.
|
||||
//
|
||||
~World();
|
||||
|
||||
// get_lua_state
|
||||
//
|
||||
// Get the lua interpreter associated with this world model.
|
||||
//
|
||||
lua_State *state() const { return lua_snap_.state(); }
|
||||
|
||||
// get_near
|
||||
//
|
||||
// Get a list of tangibles in any arbitrary region.
|
||||
//
|
||||
void get_near(PlaneScan &sc, IdVector *into) const;
|
||||
|
||||
// get_near
|
||||
//
|
||||
// Get a list of the tangibles that are near the player. If
|
||||
// 'exclude_nowhere' is true, exclude any tangibles on the nowhere plane
|
||||
// (but still include the player himself). The unsorted version returns the
|
||||
// tangibles in an unpredictable order. If sorted is false, return them in
|
||||
// an unpredictable order. This is a thin wrapper around the more general
|
||||
// form of 'get_near', above.
|
||||
//
|
||||
void get_near(int64_t player_id, float radius, bool exclude_nowhere, bool omit_player, bool sorted, IdVector *into) const;
|
||||
|
||||
// get encoded animation queues.
|
||||
//
|
||||
// This is used by the graphics engine to get the animation queues.
|
||||
//
|
||||
void get_encoded_animation_queues(uint32_t count, const int64_t *ids, util::SharedStdStringVec &into);
|
||||
|
||||
// Make a tangible.
|
||||
//
|
||||
// You must provide a valid previously-unused ID. Otherwise, leaves the lua
|
||||
// stack untouched. Returns a pointer to the C++ part of the tangible, and
|
||||
// optionally stores the Lua part in a stack slot.
|
||||
//
|
||||
Tangible *tangible_make(const LuaCoreStack &LS0, LuaSlot tan, int64_t id);
|
||||
Tangible *tangible_make(int64_t id);
|
||||
|
||||
// Get a pointer to the specified tangible.
|
||||
//
|
||||
// If there's no such tangible, or if the tangible is deleted,
|
||||
// returns nullptr.
|
||||
//
|
||||
Tangible *tangible_get(int64_t id);
|
||||
const Tangible *tangible_get(int64_t id) const;
|
||||
|
||||
// Get a pointer to the specified tangible.
|
||||
//
|
||||
// The value on the lua stack should be a lua tangible.
|
||||
//
|
||||
// If the 'allowdel' flag is true, then it is valid to pass in
|
||||
// a deleted tangible. In that case, this function returns nullptr,
|
||||
// but this is not a Lua error.
|
||||
//
|
||||
Tangible *tangible_get(const LuaCoreStack &LS, LuaSlot slot, bool allowdel);
|
||||
|
||||
// Get pointers to many tangibles.
|
||||
//
|
||||
TanVector tangible_get_all(const IdVector &ids) const;
|
||||
|
||||
// Delete the specified tangible.
|
||||
//
|
||||
// If there's no such tangible, this is a no-op.
|
||||
//
|
||||
void tangible_delete(int64_t id);
|
||||
|
||||
// Create a login actor.
|
||||
//
|
||||
// Creates a tangible of class 'login' and returns its ID.
|
||||
// This is used to create a temporary actor which is used during
|
||||
// the login process.
|
||||
//
|
||||
// If this is a master model, The function 'login.initialize'
|
||||
// called. Then, the following login flags are set:
|
||||
// can_be_controlled, is_controlled, and delete_on_disconnect.
|
||||
//
|
||||
// In a client model, 'login.initialize' is not called,
|
||||
// and the login flags are not used in client models.
|
||||
//
|
||||
int64_t create_login_actor();
|
||||
|
||||
// Log out a connected player.
|
||||
//
|
||||
// This is to be called after a client disconnects.
|
||||
//
|
||||
void disconnected(int64_t actor_id);
|
||||
|
||||
// Fetch all redirects and clear the redirects table.
|
||||
//
|
||||
Redirects fetch_redirects();
|
||||
|
||||
// Probe an arbitrary lua expression.
|
||||
//
|
||||
// Any print-statements in the lua code are sent into
|
||||
// a stringstream. The return value of probe_lua is the string
|
||||
// from the stringstream. If the lua expression returns a
|
||||
// value, that is also printed to the stringstream.
|
||||
//
|
||||
eng::string probe_lua_expr(int64_t actor_id, std::string_view lua);
|
||||
|
||||
// Probe that calls lua function, passing arguments.
|
||||
//
|
||||
// Print statements are discarded. The lua function may return a vector
|
||||
// of values. If so, the values are packed into a StreamBuffer.
|
||||
//
|
||||
void probe_lua_call(int64_t place_id, int64_t actor_id, std::string_view datapack, StreamBuffer *retvals);
|
||||
|
||||
// Invoke an Invocation object.
|
||||
//
|
||||
// This is the primary dispatcher for all operations that mutate a world model.
|
||||
// To mutate a world model, create an invocation, then invoke it.
|
||||
//
|
||||
// It is legal to mutate a world model without using 'Invoke', but
|
||||
// only in authoritative world models.
|
||||
//
|
||||
void invoke(const Invocation &inv);
|
||||
|
||||
// Get the PrintBuffer of the actor.
|
||||
//
|
||||
const PrintBuffer *get_printbuffer(int64_t actor_id);
|
||||
|
||||
// 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::LuaSourceVec &source);
|
||||
void update_source(const util::LuaSourcePtr &source);
|
||||
void update_source(std::string_view sourcepk);
|
||||
|
||||
// Rebuild the source database.
|
||||
//
|
||||
void rebuild_sourcedb();
|
||||
|
||||
// Supply an HTTP response to an outstanding HTTP request.
|
||||
//
|
||||
void http_response(const HttpParser &response);
|
||||
void http_responses(const HttpParserVec &responses);
|
||||
|
||||
// Abort all HTTP requests. This is typically used after
|
||||
// reloading a world from a save-game. The http requests that
|
||||
// were in progress are long-since dead.
|
||||
//
|
||||
void abort_all_http_requests(int status_code, std::string_view error);
|
||||
|
||||
// Serve an HTTP query coming in from outside.
|
||||
//
|
||||
// Note: the lua code for the http_serve runs in a nonblocking
|
||||
// context. It must produce a result instantly.
|
||||
//
|
||||
HttpServerResponse http_serve(const HttpParser &request);
|
||||
|
||||
// Run all unit tests.
|
||||
//
|
||||
void run_unittests();
|
||||
|
||||
// fetch_global_pointer
|
||||
//
|
||||
// Given a lua state, fetch the world model associated with
|
||||
// that lua state.
|
||||
//
|
||||
static World *fetch_global_pointer(lua_State *L);
|
||||
|
||||
// Check if the world is authoritative.
|
||||
//
|
||||
bool is_authoritative() const { return util::is_authoritative(world_type_); }
|
||||
|
||||
// Get a table showing all outstanding HTTP requests.
|
||||
//
|
||||
const HttpClientRequestMap &http_requests() const { return http_requests_; }
|
||||
|
||||
// Serialize and deserialize.
|
||||
//
|
||||
void serialize(StreamBuffer *sb);
|
||||
void deserialize(StreamBuffer *sb);
|
||||
|
||||
// Snapshot and rollback.
|
||||
//
|
||||
// These are used by the client to convert the synchronous model
|
||||
// to an asynchronous model and back.
|
||||
//
|
||||
void snapshot();
|
||||
void rollback();
|
||||
bool snapshot_empty() { return snapshot_.empty(); }
|
||||
|
||||
// Run any threads which according to the scheduler queue are ready.
|
||||
//
|
||||
void run_scheduled_threads();
|
||||
|
||||
// Check that the main thread has nothing on the stack
|
||||
//
|
||||
bool stack_is_clear() const { return lua_gettop(state()) == 0; }
|
||||
|
||||
// Set the lthread state.
|
||||
//
|
||||
// Whenever lua code is running, and ONLY when lua code is running,
|
||||
// we store the following information in the world model:
|
||||
//
|
||||
// * lthread_actor_id: current actor
|
||||
// * lthread_place_id: current place
|
||||
// * lthread_use_ppool: true if we should use the player ID pool.
|
||||
// * lthread_prints_: a stringstream which will collect 'print' statements.
|
||||
//
|
||||
// As soon as the lua code stops executing, these variables are
|
||||
// cleared.
|
||||
//
|
||||
void clear_lthread_state();
|
||||
void open_lthread_state(int64_t actor_id, int64_t place_id, int64_t thread_id, bool ppool, bool prints);
|
||||
void close_lthread_state();
|
||||
|
||||
std::ostream *lthread_print_stream() const;
|
||||
|
||||
// Set a global variable.
|
||||
//
|
||||
// This will throw lua errors if there's a problem.
|
||||
//
|
||||
void set_global(LuaCoreStack &LS0, const eng::string &gvar, LuaSlot value);
|
||||
|
||||
// Get the serialized value of a global variable.
|
||||
//
|
||||
// This accessor is used during difference transmission.
|
||||
//
|
||||
const eng::string &get_gvname_serial(const eng::string &gvar);
|
||||
|
||||
// Allocate a single ID.
|
||||
//
|
||||
// The rules are as follows:
|
||||
// * if lthread_use_ppool is false, uses the global pool.
|
||||
// * if lthread_actor_id is not a valid actor id, uses the global pool.
|
||||
// * otherwise, uses the player pool of lthread_actor_id.
|
||||
//
|
||||
int64_t alloc_id_predictable();
|
||||
|
||||
// If we're in a probe, generate an error.
|
||||
// If we're in a nonauthoritative model, do a nopredict yield.
|
||||
// Otherwise, return.
|
||||
void guard_blockable(lua_State *L, const char *fn);
|
||||
|
||||
// If we're in a probe, return.
|
||||
// If we're in a nonauthoritative model, do a nopredict yield.
|
||||
// Otherwise, return.
|
||||
void guard_nopredict(lua_State *L, const char *fn);
|
||||
|
||||
private:
|
||||
// Add a thread to the scheduler queue.
|
||||
//
|
||||
void schedule(int64_t clk, int64_t thid, int64_t plid);
|
||||
|
||||
// Store a pointer to a world model into a lua registry.
|
||||
//
|
||||
static void store_global_pointer(lua_State *L, World *w);
|
||||
|
||||
// Invoke the lua_call operation.
|
||||
//
|
||||
void invoke_lua_call(int64_t actor_id, int64_t place_id, std::string_view datapack);
|
||||
|
||||
// Invoke a lua string.
|
||||
//
|
||||
void invoke_lua_expr(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, std::string_view datapack);
|
||||
|
||||
// Invoke the tick operation.
|
||||
//
|
||||
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, std::string_view datapack);
|
||||
|
||||
// Low level spawn thread function.
|
||||
//
|
||||
bool spawn(LuaCoreStack &LS0, int64_t actor_id, int64_t place_id, LuaSlot func, bool passactorplace, int nargs, bool print);
|
||||
|
||||
public:
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TESTING SUPPORT
|
||||
//
|
||||
// The following functions are not designed to be useful for production
|
||||
// code, they're designed to be helpful for unit testing.
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Clear the animation queue and remove all persistent state.
|
||||
//
|
||||
void tangible_clear_anim_queue_to_empty(int64_t id);
|
||||
|
||||
// Clear the animation queue to a reasonable starting position.
|
||||
//
|
||||
void tangible_clear_plane_and_xyz(int64_t id, const eng::string &plane, const util::DXYZ &xyz);
|
||||
|
||||
// Add a 'walkto' animation to the specified tangible.
|
||||
//
|
||||
void tangible_walkto(int64_t id, float x, float y);
|
||||
|
||||
// Get the tangible's animation queue as a debug string.
|
||||
//
|
||||
eng::string tangible_anim_debug_string(int64_t id) const;
|
||||
|
||||
// Get the tangible's ID Pool as a debug string.
|
||||
//
|
||||
eng::string tangible_id_pool_debug_string(int64_t id) const;
|
||||
|
||||
// Get a list of all existing tangibles as a comma-separated string.
|
||||
//
|
||||
eng::string tangible_ids_debug_string() const;
|
||||
|
||||
// Get a list of all tangibles near the target as a string.
|
||||
//
|
||||
eng::string tangibles_near_debug_string(int64_t actor, int64_t distance);
|
||||
|
||||
// Shows the TID (table ID) of the tables that were numbered.
|
||||
// TIDs are in alphabetical order. Any table without a TID
|
||||
// shows up as "unknown"
|
||||
//
|
||||
eng::string numbered_tables_debug_string() const;
|
||||
|
||||
// Paired tables debug string. Shows TID=TID pairs, sorted alphabetically.
|
||||
//
|
||||
eng::string paired_tables_debug_string(lua_State *master) const;
|
||||
|
||||
// Store a string in the tangible's database.
|
||||
//
|
||||
void tangible_set_string(int64_t id, const eng::string &path, const eng::string &value);
|
||||
|
||||
// Copy a from the lua global environment into the tangible's database.
|
||||
//
|
||||
// This is for unit testing.
|
||||
//
|
||||
void tangible_copy_global(int64_t id, const eng::string &path, const eng::string &global);
|
||||
|
||||
// Pretty-print the entire tangible database and return it as a string.
|
||||
//
|
||||
eng::string tangible_pprint(int64_t id) const;
|
||||
|
||||
// Set the tangible's lua class.
|
||||
//
|
||||
void tangible_set_class(int64_t id, const eng::string &c) const;
|
||||
|
||||
// Get the tangible's lua class (returns empty string if none).
|
||||
//
|
||||
eng::string tangible_get_class(int64_t id) const;
|
||||
|
||||
// Store json in a global variable.
|
||||
//
|
||||
void set_global_json(const eng::string &gvar, const eng::string &json);
|
||||
|
||||
// Get a global variable as json.
|
||||
//
|
||||
eng::string get_global_json(const eng::string &gvar);
|
||||
|
||||
public:
|
||||
///////////////////////////////////////////////////////////
|
||||
//
|
||||
// world-difftab: Nonrecursive table comparison
|
||||
//
|
||||
// These routines compare tables in the master lua to the corresponding
|
||||
// tables in the synchronous lua. This is a nonrecursive process, because
|
||||
// the recursion has already been done during the table enumeration process.
|
||||
//
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
void patch_numbered_tables(StreamBuffer *sb, DebugCollector *dbc);
|
||||
void diff_numbered_tables(lua_State *master, StreamBuffer *sb);
|
||||
|
||||
void patch_tangible_databases(StreamBuffer *sb, DebugCollector *dbc);
|
||||
void diff_tangible_databases(const IdVector &basis, lua_State *master, StreamBuffer *sb);
|
||||
|
||||
void patch_tangible_classes(StreamBuffer *sb, DebugCollector *dbc);
|
||||
void diff_tangible_classes(const IdVector &basis, lua_State *master, StreamBuffer *sb);
|
||||
|
||||
public:
|
||||
///////////////////////////////////////////////////////////
|
||||
//
|
||||
// Difference transmission
|
||||
//
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
util::IdVector get_visible_union(int64_t actor_id, World *master);
|
||||
|
||||
int64_t patch_actor(StreamBuffer *sb, DebugCollector *dbc);
|
||||
void diff_actor(int64_t actor_id, World *master, StreamBuffer *sb);
|
||||
|
||||
void patch_visible(StreamBuffer *sb, DebugCollector *dbc);
|
||||
void diff_visible(const util::IdVector &ids, World *master, StreamBuffer *sb);
|
||||
|
||||
void patch_luatabs(StreamBuffer *sb, DebugCollector *dbc);
|
||||
void diff_luatabs(int64_t actor_id, World *master, StreamBuffer *sb);
|
||||
|
||||
void patch_tanclass(StreamBuffer *sb, DebugCollector *dbc);
|
||||
void diff_tanclass(int64_t actor_id, World *master, StreamBuffer *sb);
|
||||
|
||||
void patch_source(StreamBuffer *sb, DebugCollector *dbc);
|
||||
void diff_source(World *master, StreamBuffer *sb);
|
||||
|
||||
void patch_globals(StreamBuffer *sb, DebugCollector *dbc);
|
||||
void diff_globals(World *master, StreamBuffer *sb);
|
||||
|
||||
// This is the main entry point for difference transmission.
|
||||
//
|
||||
int64_t patch_everything(StreamBuffer *sb, DebugCollector *dbc);
|
||||
void diff_everything(int64_t actor, World *master, StreamBuffer *sb);
|
||||
|
||||
public:
|
||||
///////////////////////////////////////////////////////////
|
||||
//
|
||||
// world-pairtab: Numbering and pairing of lua tables.
|
||||
//
|
||||
// The following routines pair up tables in the synchronous
|
||||
// model with tables in the master model, by assigning matching
|
||||
// table numbers. This is not one subroutine but several, because
|
||||
// some of the steps happen on the server, some on the client,
|
||||
// and so forth.
|
||||
//
|
||||
// The goal of these routines is to build these data structures:
|
||||
//
|
||||
// Table-to-number mapping is stored in registry.tnmap
|
||||
// Number-to-table mapping is stored in registry.ntmap
|
||||
//
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
// In the synchronous models, number tables recursively.
|
||||
//
|
||||
// This is a simple recursive traversal, which numbers tables.
|
||||
// This creates the initial ntmap in the synchronous models.
|
||||
//
|
||||
int number_lua_tables(const IdVector &basis);
|
||||
|
||||
// Pair tables in the master model to tables in the synch model.
|
||||
//
|
||||
// Recursively walk the master and synchronous model in parallel,
|
||||
// copying table numbers from the synchronous ntmap into the master's ntmap.
|
||||
//
|
||||
void pair_lua_tables(const IdVector &basis, lua_State *master);
|
||||
|
||||
// Number previously unpaired tables in the master model.
|
||||
//
|
||||
// This finds every not-yet-numbered table in the master model,
|
||||
// and appends these tables to the master's ntmap. Once they're
|
||||
// in the ntmap, they can be paired by simply creating new tables
|
||||
// in the synchronous model.
|
||||
//
|
||||
int number_remaining_tables(const IdVector &basis, lua_State *master);
|
||||
|
||||
// Create new tables in the synchronous models.
|
||||
//
|
||||
// Creates new tables in the synchronous model and appends these
|
||||
// new tables to the synchronous model's ntmap.
|
||||
//
|
||||
void create_new_tables(int n);
|
||||
|
||||
// Delete the table numbering.
|
||||
//
|
||||
// This simply removes registry.tnmap and registry.ntmap
|
||||
//
|
||||
void unnumber_lua_tables();
|
||||
|
||||
private:
|
||||
// Type of model
|
||||
WorldType world_type_;
|
||||
|
||||
// A lua intepreter with snapshot function.
|
||||
//
|
||||
LuaSnap lua_snap_;
|
||||
|
||||
// The Global ID Pool.
|
||||
//
|
||||
IdGlobalPool id_global_pool_;
|
||||
|
||||
// Source Database.
|
||||
//
|
||||
SourceDB source_db_;
|
||||
PlaneMap plane_map_;
|
||||
|
||||
// Lua Global Variables
|
||||
//
|
||||
// assign_seqno: sequence number generator for global variable assignments (master and client)
|
||||
// gvname_to_serial: global variable name to serialized data. (master and client)
|
||||
// gvname_to_seqno: global variable name to sequence number. (master only)
|
||||
// seqno_to_gvname: sequence number to global variable name. (master only)
|
||||
// gvname_modified: set of global variables recently locally modified. (client only)
|
||||
//
|
||||
int64_t assign_seqno_;
|
||||
eng::map<eng::string, eng::string> gvname_to_serial_;
|
||||
eng::map<eng::string, int64_t> gvname_to_seqno_;
|
||||
eng::map<int64_t, eng::string> seqno_to_gvname_;
|
||||
eng::set<eng::string> gvname_modified_;
|
||||
|
||||
// Tangibles table.
|
||||
//
|
||||
eng::unordered_map<int64_t, UniqueTangible> tangibles_;
|
||||
|
||||
// Current time.
|
||||
//
|
||||
int64_t clock_;
|
||||
|
||||
// Thread schedule: must include every thread, except
|
||||
// for the one currently-executing thread.
|
||||
//
|
||||
Schedule thread_sched_;
|
||||
|
||||
// Outstanding HTTP requests, indexed by request ID.
|
||||
// Authoritative models only.
|
||||
//
|
||||
HttpClientRequestMap http_requests_;
|
||||
|
||||
// Serialized snapshot of world model.
|
||||
//
|
||||
StreamBuffer snapshot_;
|
||||
|
||||
// Redirects.
|
||||
//
|
||||
Redirects redirects_;
|
||||
|
||||
// lthread variables: see set_lthread_state for explanation.
|
||||
//
|
||||
int64_t lthread_actor_id_;
|
||||
int64_t lthread_place_id_;
|
||||
int64_t lthread_thread_id_;
|
||||
int64_t lthread_use_ppool_;
|
||||
std::unique_ptr<eng::ostringstream> lthread_prints_;
|
||||
|
||||
friend class Tangible;
|
||||
friend void global_set(LuaCoreStack &LS0, const eng::string &gvar, LuaSlot value);
|
||||
friend int lfn_tangible_animate(lua_State *L);
|
||||
friend int lfn_tangible_build(lua_State *L);
|
||||
friend int lfn_tangible_redirect(lua_State *L);
|
||||
friend int lfn_tangible_actor(lua_State *L);
|
||||
friend int lfn_tangible_place(lua_State *L);
|
||||
friend int lfn_tangible_nopredict(lua_State *L);
|
||||
friend int lfn_tangible_near(lua_State *L);
|
||||
friend int lfn_tangible_scan(lua_State *L);
|
||||
friend int lfn_tangible_find(lua_State *L);
|
||||
friend int lfn_tangible_start(lua_State *L);
|
||||
friend int lfn_math_random(lua_State *L);
|
||||
friend int lfn_math_randomstate(lua_State *L);
|
||||
friend int lfn_wait(lua_State *L);
|
||||
friend int lfn_nopredict(lua_State *L);
|
||||
friend int lfn_http_request(lua_State *L, const char *method);
|
||||
friend int lfn_global_set(lua_State *L);
|
||||
};
|
||||
|
||||
using UniqueWorld = std::unique_ptr<World>;
|
||||
|
||||
|
||||
#endif // WORLD_HPP
|
||||
271
luprex/cpp/drv/driver-linux.cpp
Normal file
271
luprex/cpp/drv/driver-linux.cpp
Normal file
@@ -0,0 +1,271 @@
|
||||
|
||||
|
||||
#include "drvutil.hpp"
|
||||
#include "osdrvutil.hpp"
|
||||
#include "sslutil.hpp"
|
||||
#include "readline.hpp"
|
||||
#include "../core/enginewrapper.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cassert>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include <poll.h>
|
||||
#include <sys/time.h>
|
||||
#include <fcntl.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/personality.h>
|
||||
#include <netdb.h>
|
||||
#include <malloc.h>
|
||||
#include <dlfcn.h>
|
||||
|
||||
using SOCKET=int;
|
||||
const int INVALID_SOCKET = -1;
|
||||
|
||||
struct termios orig_termios;
|
||||
|
||||
std::filesystem::path get_exe_path() {
|
||||
char result[ PATH_MAX ];
|
||||
ssize_t count = readlink( "/proc/self/exe", result, PATH_MAX );
|
||||
return std::filesystem::path(std::string( result, (count > 0) ? count : 0 ));
|
||||
}
|
||||
|
||||
void set_nonblocking(int fd) {
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
assert(flags != -1);
|
||||
int status = fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
||||
assert(status != -1);
|
||||
}
|
||||
|
||||
static void disable_tty_raw() {
|
||||
tcsetattr(0, TCSAFLUSH, &orig_termios);
|
||||
}
|
||||
|
||||
static void enable_tty_raw() {
|
||||
int status = tcgetattr(0, &orig_termios);
|
||||
assert(status >= 0);
|
||||
atexit(disable_tty_raw);
|
||||
struct termios raw = orig_termios;
|
||||
raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
|
||||
raw.c_lflag &= ~(ECHO | ICANON);
|
||||
raw.c_oflag |= OPOST;
|
||||
raw.c_cc[VMIN] = 0;
|
||||
raw.c_cc[VTIME] = 0;
|
||||
status = tcsetattr(0, TCSAFLUSH, &raw);
|
||||
assert(status >= 0);
|
||||
}
|
||||
|
||||
static SOCKET open_connection(const char *host, const char *port, std::string &err) {
|
||||
struct addrinfo *addrs = nullptr;
|
||||
struct addrinfo *goodaddr = nullptr;
|
||||
struct addrinfo hints;
|
||||
SOCKET sock = INVALID_SOCKET;
|
||||
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
hints.ai_protocol = IPPROTO_TCP;
|
||||
hints.ai_flags = AI_NUMERICSERV;
|
||||
|
||||
err.clear();
|
||||
int status = getaddrinfo(host, port, &hints, &addrs);
|
||||
if (status != 0) {
|
||||
err = gai_strerror(status);
|
||||
goto error_general;
|
||||
}
|
||||
if (addrs == nullptr) {
|
||||
err = "no such host found";
|
||||
goto error_general;
|
||||
}
|
||||
goodaddr = addrs;
|
||||
assert(goodaddr->ai_family == AF_INET);
|
||||
assert(goodaddr->ai_socktype == SOCK_STREAM);
|
||||
assert(goodaddr->ai_protocol == IPPROTO_TCP);
|
||||
sock = socket(goodaddr->ai_family, goodaddr->ai_socktype, goodaddr->ai_protocol);
|
||||
if (sock <= 0) goto error_errno;
|
||||
|
||||
set_nonblocking(sock);
|
||||
|
||||
status = connect(sock, goodaddr->ai_addr, goodaddr->ai_addrlen);
|
||||
if ((status != 0) && (errno != EINPROGRESS)) goto error_errno;
|
||||
|
||||
freeaddrinfo(addrs);
|
||||
return sock;
|
||||
|
||||
error_errno:
|
||||
err = drvutil::strerror_str(errno);
|
||||
error_general:
|
||||
if (sock != INVALID_SOCKET) close(sock);
|
||||
if (addrs != nullptr) freeaddrinfo(addrs);
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
|
||||
static SOCKET listen_on_port(int port, std::string &err) {
|
||||
int status, enable;
|
||||
err.clear();
|
||||
|
||||
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock <= 0) goto error_errno;
|
||||
|
||||
enable = 1;
|
||||
status = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
|
||||
if (status != 0) goto error_errno;
|
||||
|
||||
struct sockaddr_in server;
|
||||
server.sin_family = AF_INET;
|
||||
server.sin_addr.s_addr = INADDR_ANY;
|
||||
server.sin_port = htons(port);
|
||||
|
||||
status = bind(sock, (struct sockaddr *)&server, sizeof(server));
|
||||
if (status != 0) goto error_errno;
|
||||
|
||||
status = listen(sock, 10);
|
||||
if (status != 0) goto error_errno;
|
||||
|
||||
set_nonblocking(sock);
|
||||
return sock;
|
||||
|
||||
error_errno:
|
||||
err = drvutil::strerror_str(errno);
|
||||
if (sock >= 0) close(sock);
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
|
||||
static SOCKET accept_on_socket(SOCKET listen_socket, std::string &err) {
|
||||
err.clear();
|
||||
SOCKET chsock = accept(listen_socket, nullptr, nullptr);
|
||||
if (chsock >= 0) {
|
||||
set_nonblocking(chsock);
|
||||
return chsock;
|
||||
} else {
|
||||
if ((errno != EAGAIN) && (errno != EWOULDBLOCK) && (errno != ECONNABORTED)) {
|
||||
err = drvutil::strerror_str(errno);
|
||||
}
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
}
|
||||
|
||||
// the return values for socket_send:
|
||||
//
|
||||
// positive: sent bytes successfully
|
||||
// negative: error.
|
||||
// If the error message is empty, then it's "would block"
|
||||
// Any other error generates an error message.
|
||||
//
|
||||
static int socket_send(SOCKET socket, const char *bytes, int nbytes, std::string &err) {
|
||||
int wbytes = send(socket, bytes, nbytes, 0);
|
||||
if (wbytes < 0) {
|
||||
if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {
|
||||
err.clear();
|
||||
} else {
|
||||
err = drvutil::strerror_str(errno);
|
||||
}
|
||||
return -1;
|
||||
} else {
|
||||
err.clear();
|
||||
return wbytes;
|
||||
}
|
||||
}
|
||||
|
||||
static int socket_recv(SOCKET socket, char *bytes, int nbytes, std::string &err) {
|
||||
int nrecv = recv(socket, bytes, nbytes, 0);
|
||||
if (nrecv < 0) {
|
||||
if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {
|
||||
err.clear();
|
||||
} else {
|
||||
err = drvutil::strerror_str(errno);
|
||||
}
|
||||
return -1;
|
||||
} else if (nrecv == 0) {
|
||||
err.clear();
|
||||
return 0;
|
||||
} else {
|
||||
err.clear();
|
||||
return nrecv;
|
||||
}
|
||||
}
|
||||
|
||||
static int socket_close(SOCKET socket) {
|
||||
return close(socket);
|
||||
}
|
||||
|
||||
static int socket_poll(struct pollfd *pollvec, int pollcount, int mstimeout, std::string &err) {
|
||||
// socket_poll is implicitly expected to also poll stdin,
|
||||
// if the OS allows that. Linux does, so we add stdin to the
|
||||
// poll vector. The poll vector is required to have at
|
||||
// least one free space in order to do this.
|
||||
pollvec[pollcount].fd = 0;
|
||||
pollvec[pollcount].events = POLLIN;
|
||||
pollcount += 1;
|
||||
|
||||
// Do the poll.
|
||||
int status = poll(pollvec, pollcount, mstimeout);
|
||||
if (status < 0) {
|
||||
err = drvutil::strerror_str(errno);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Write unicode onto the console.
|
||||
static void console_write(const std::u32string &cps) {
|
||||
std::string utf8 = drvutil::utf32_to_utf8(cps);
|
||||
write(1, utf8.c_str(), utf8.size());
|
||||
}
|
||||
|
||||
static std::u32string console_read() {
|
||||
std::u32string result;
|
||||
char buffer[512];
|
||||
int nread = read(0, buffer, 512);
|
||||
if (nread > 0) {
|
||||
std::string_view s(buffer, nread);
|
||||
result = drvutil::utf8_to_utf32(s, nullptr);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void call_init_engine_wrapper(const std::filesystem::path &luprexroot, EngineWrapper *w) {
|
||||
using InitFn = void (*)(EngineWrapper *);
|
||||
InitFn initfn = (InitFn)dlsym(nullptr, "init_engine_wrapper");
|
||||
if (initfn == nullptr) {
|
||||
std::string path = luprexroot / "build/linux/luprexlib.so";
|
||||
void *dll_handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||
assert(dll_handle != nullptr);
|
||||
initfn = (InitFn)dlsym(dll_handle, "init_engine_wrapper");
|
||||
}
|
||||
assert(initfn != nullptr);
|
||||
initfn(w);
|
||||
}
|
||||
|
||||
static void ssl_load_certificate_authorities(SSL_CTX *ctx) {
|
||||
assert(SSL_CTX_set_default_verify_paths(ctx) == 1);
|
||||
}
|
||||
|
||||
static void disable_randomization(int argc, char *argv[]) {
|
||||
const int old_personality = personality(ADDR_NO_RANDOMIZE);
|
||||
if (!(old_personality & ADDR_NO_RANDOMIZE)) {
|
||||
const int new_personality = personality(ADDR_NO_RANDOMIZE);
|
||||
if (new_personality & ADDR_NO_RANDOMIZE) {
|
||||
execv(argv[0], argv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void os_initialize(int argc, char **argv) {
|
||||
disable_randomization(argc, argv);
|
||||
enable_tty_raw();
|
||||
}
|
||||
|
||||
|
||||
328
luprex/cpp/drv/driver-windows.cpp
Normal file
328
luprex/cpp/drv/driver-windows.cpp
Normal file
@@ -0,0 +1,328 @@
|
||||
#define WINVER 0x0600
|
||||
#define _WIN32_WINNT 0x0600
|
||||
|
||||
#include "drvutil.hpp"
|
||||
#include "osdrvutil.hpp"
|
||||
#include "sslutil.hpp"
|
||||
#include "readline.hpp"
|
||||
#include "../core/enginewrapper.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cassert>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <synchapi.h>
|
||||
#include <sysinfoapi.h>
|
||||
#include <libloaderapi.h>
|
||||
#include <windows.h>
|
||||
#include <openssl/ssl.h>
|
||||
#include <openssl/rsa.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/pem.h>
|
||||
|
||||
// OpenSSL requires plain ascii pathnames. Returns empty string
|
||||
// if the path cannot be converted to plain ascii.
|
||||
std::string path_to_plain_ascii(const std::filesystem::path &path) {
|
||||
std::wstring s = path.native();
|
||||
for (wchar_t c : s) {
|
||||
if ((c < 1) || (c > 127)) return "";
|
||||
}
|
||||
std::ostringstream oss;
|
||||
for (wchar_t c : s) {
|
||||
oss << ((char)c);
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::filesystem::path get_exe_path() {
|
||||
WCHAR exepath[MAX_PATH];
|
||||
DWORD status = GetModuleFileNameW( NULL, exepath, MAX_PATH );
|
||||
assert(status != 0);
|
||||
return std::filesystem::path(exepath);
|
||||
}
|
||||
|
||||
static void set_nonblocking(SOCKET sock) {
|
||||
u_long mode = 1; // 1 to enable non-blocking socket
|
||||
int status = ioctlsocket(sock, FIONBIO, &mode);
|
||||
assert(status == 0);
|
||||
}
|
||||
|
||||
static PADDRINFOA find_good_addr(PADDRINFOA addrinfo) {
|
||||
for (PADDRINFOA addr = addrinfo; addr != nullptr; addr = addr->ai_next) {
|
||||
if (addr->ai_family == AF_INET) {
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static SOCKET open_connection(const char *host, const char *port, std::string &err) {
|
||||
PADDRINFOA addrs = nullptr;
|
||||
PADDRINFOA goodaddr = nullptr;
|
||||
SOCKET sock = INVALID_SOCKET;
|
||||
|
||||
err.clear();
|
||||
int status = getaddrinfo(host, port, nullptr, &addrs);
|
||||
while (status == WSATRY_AGAIN) {
|
||||
status = getaddrinfo(host, port, nullptr, &addrs);
|
||||
}
|
||||
if (status == WSAHOST_NOT_FOUND) {
|
||||
err = "host not found";
|
||||
goto error;
|
||||
}
|
||||
if (status != 0) {
|
||||
err = "DNS resolution malfunction";
|
||||
goto error;
|
||||
}
|
||||
goodaddr = find_good_addr(addrs);
|
||||
if (goodaddr == nullptr) {
|
||||
err = "host not an internet host";
|
||||
goto error;
|
||||
}
|
||||
sock = socket(goodaddr->ai_family, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (sock == INVALID_SOCKET) {
|
||||
err = "could not create a socket";
|
||||
goto error;
|
||||
}
|
||||
set_nonblocking(sock);
|
||||
status = connect(sock, goodaddr->ai_addr, goodaddr->ai_addrlen);
|
||||
if (status != 0) {
|
||||
int errcode = WSAGetLastError();
|
||||
if (errcode != WSAEWOULDBLOCK) {
|
||||
err = "connect failure";
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
freeaddrinfo(addrs);
|
||||
return sock;
|
||||
|
||||
error:
|
||||
if (sock != INVALID_SOCKET) closesocket(sock);
|
||||
if (addrs != nullptr) freeaddrinfo(addrs);
|
||||
return SOCKET_ERROR;
|
||||
}
|
||||
|
||||
SOCKET listen_on_port(int port, std::string &err) {
|
||||
int status;
|
||||
err.clear();
|
||||
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock == INVALID_SOCKET) {
|
||||
err = "could not create a socket";
|
||||
goto error;
|
||||
}
|
||||
|
||||
struct sockaddr_in server;
|
||||
server.sin_family = AF_INET;
|
||||
server.sin_addr.s_addr = INADDR_ANY;
|
||||
server.sin_port = htons(port);
|
||||
|
||||
status = bind(sock, (struct sockaddr *)&server, sizeof(server));
|
||||
if (status < 0) {
|
||||
err = "could not bind port";
|
||||
goto error;
|
||||
}
|
||||
status = listen(sock, 10);
|
||||
if (status < 0) {
|
||||
err = "could not listen on socket";
|
||||
goto error;
|
||||
}
|
||||
set_nonblocking(sock);
|
||||
return sock;
|
||||
|
||||
error:
|
||||
if (sock != INVALID_SOCKET) closesocket(sock);
|
||||
return SOCKET_ERROR;
|
||||
}
|
||||
|
||||
static SOCKET accept_on_socket(SOCKET listen_socket, std::string &err) {
|
||||
SOCKET chsock = accept(listen_socket, nullptr, nullptr);
|
||||
if (chsock != INVALID_SOCKET) {
|
||||
set_nonblocking(chsock);
|
||||
return chsock;
|
||||
} else {
|
||||
int errcode = WSAGetLastError();
|
||||
if ((errcode == WSAEWOULDBLOCK) || (errcode == WSAECONNRESET)) {
|
||||
return INVALID_SOCKET;
|
||||
} else {
|
||||
err = "accept failed";
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the return values for socket_send:
|
||||
//
|
||||
// positive: sent bytes successfully
|
||||
// negative: error.
|
||||
// If the error message is empty, then it's "would block"
|
||||
// Any other error generates an error message.
|
||||
//
|
||||
|
||||
static int socket_send(SOCKET socket, const char *bytes, int nbytes, std::string &err) {
|
||||
int wbytes = send(socket, bytes, nbytes, 0);
|
||||
if (wbytes == SOCKET_ERROR) {
|
||||
int errcode = WSAGetLastError();
|
||||
if (errcode == WSAEWOULDBLOCK) {
|
||||
err.clear();
|
||||
} else {
|
||||
err = "send failure";
|
||||
}
|
||||
return -1;
|
||||
} else {
|
||||
assert(wbytes > 0);
|
||||
err.clear();
|
||||
return wbytes;
|
||||
}
|
||||
}
|
||||
|
||||
static int socket_recv(SOCKET socket, char *bytes, int nbytes, std::string &err) {
|
||||
int nrecv = recv(socket, bytes, nbytes, 0);
|
||||
if (nrecv < 0) {
|
||||
int errcode = WSAGetLastError();
|
||||
if (errcode == WSAEWOULDBLOCK) {
|
||||
err = "";
|
||||
} else {
|
||||
err = "recv failure";
|
||||
}
|
||||
return -1;
|
||||
} else if (nrecv == 0) {
|
||||
err.clear();
|
||||
return 0;
|
||||
} else {
|
||||
err.clear();
|
||||
return nrecv;
|
||||
}
|
||||
}
|
||||
|
||||
static int socket_close(SOCKET socket) {
|
||||
return closesocket(socket);
|
||||
}
|
||||
|
||||
static int socket_poll(struct pollfd *pollvec, int pollcount, int mstimeout, std::string &err) {
|
||||
if (pollcount == 0) {
|
||||
if (mstimeout > 0) Sleep(mstimeout);
|
||||
return 0;
|
||||
}
|
||||
int status = WSAPoll(pollvec, pollcount, mstimeout);
|
||||
if (status < 0) {
|
||||
err = drvutil::strerror_str(WSAGetLastError());
|
||||
return -1;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
static void init_winsock() {
|
||||
WSADATA data;
|
||||
int errcode = WSAStartup(2, &data);
|
||||
if (errcode != 0) {
|
||||
fprintf(stderr, "Winsock didn't initalize, error %d", errcode);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void console_write(const std::u32string &cps) {
|
||||
if (cps.size() == 0) return;
|
||||
// Convert to wstring. Any character not representable as a single wchar_t
|
||||
// is replaced with a box. It's not ideal, but it's pretty good.
|
||||
std::wstring ws(cps.size(), 0);
|
||||
for (int i = 0; i < int(cps.size()); i++) {
|
||||
char32_t c = cps[i];
|
||||
if (drvutil::is_single_wchar_t(c)) ws[i] = (wchar_t)c;
|
||||
else ws[i] = 0x2610;
|
||||
}
|
||||
HANDLE hstdout = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
assert(hstdout != INVALID_HANDLE_VALUE);
|
||||
DWORD nwrote;
|
||||
std::wstring_view v(ws);
|
||||
while (v.size() > 0) {
|
||||
int nwrite = v.size();
|
||||
if (nwrite > 10000) nwrite = 10000;
|
||||
assert(WriteConsoleW(hstdout, v.data(), nwrite, &nwrote, nullptr));
|
||||
assert(nwrote > 0);
|
||||
v.remove_prefix(nwrote);
|
||||
}
|
||||
}
|
||||
|
||||
static std::u32string console_read() {
|
||||
HANDLE hstdin = GetStdHandle(STD_INPUT_HANDLE);
|
||||
assert(hstdin != INVALID_HANDLE_VALUE);
|
||||
INPUT_RECORD inrecords[512];
|
||||
DWORD nread, nevents;
|
||||
if (GetNumberOfConsoleInputEvents(hstdin, &nevents)) {
|
||||
if (int(nevents) > 0) {
|
||||
if (int(nevents) > 512) nevents = 512;
|
||||
ReadConsoleInputW(hstdin, inrecords, nevents, &nread);
|
||||
std::u32string result(nread, 0);
|
||||
int len = 0;
|
||||
for (int i = 0; i < int(nread); i++) {
|
||||
const INPUT_RECORD &inr = inrecords[i];
|
||||
if (inr.EventType != KEY_EVENT) continue;
|
||||
const KEY_EVENT_RECORD &key = inr.Event.KeyEvent;
|
||||
if (!key.bKeyDown) continue;
|
||||
result[len++] = key.uChar.UnicodeChar;
|
||||
}
|
||||
return result.substr(0, len);
|
||||
}
|
||||
}
|
||||
return std::u32string();
|
||||
}
|
||||
|
||||
static void ssl_load_certificate_authorities(SSL_CTX *ctx) {
|
||||
HCERTSTORE hStore = CertOpenSystemStoreW(0, L"ROOT");
|
||||
PCCERT_CONTEXT pContext = NULL;
|
||||
X509 *x509;
|
||||
X509_STORE *store = SSL_CTX_get_cert_store(ctx);
|
||||
|
||||
if (!hStore) {
|
||||
fprintf(stderr, "Cannot open system certificate store.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
while ((pContext = CertEnumCertificatesInStore(hStore, pContext))) {
|
||||
const unsigned char *encoded_cert = pContext->pbCertEncoded;
|
||||
x509 = d2i_X509(NULL, &encoded_cert, pContext->cbCertEncoded);
|
||||
if (x509) {
|
||||
X509_STORE_add_cert(store, x509);
|
||||
X509_free(x509);
|
||||
}
|
||||
}
|
||||
|
||||
CertCloseStore(hStore, 0);
|
||||
}
|
||||
|
||||
static void call_init_engine_wrapper(const std::filesystem::path &luprexroot, EngineWrapper *w) {
|
||||
HMODULE exe = GetModuleHandleA(NULL);
|
||||
using InitFn = void (*)(EngineWrapper *);
|
||||
InitFn initfn = (InitFn)GetProcAddress(exe, "init_engine_wrapper");
|
||||
if (initfn == nullptr) {
|
||||
#if defined(_MSC_VER)
|
||||
std::wstring path = luprexroot / "build/visual/luprexlib.dll";
|
||||
#elif defined(__GNUC__)
|
||||
std::wstring path = luprexroot / "build/mingw/luprexlib.dll";
|
||||
#else
|
||||
#error "Cannot detect OS type"
|
||||
#endif
|
||||
HMODULE dll = LoadLibraryW(path.c_str());
|
||||
assert(dll != nullptr);
|
||||
initfn = (InitFn)GetProcAddress(dll, "init_engine_wrapper");
|
||||
}
|
||||
assert(initfn != nullptr);
|
||||
initfn(w);
|
||||
}
|
||||
|
||||
void os_initialize(int argc, char **argv) {
|
||||
init_winsock();
|
||||
}
|
||||
|
||||
689
luprex/cpp/drv/driver.cpp
Normal file
689
luprex/cpp/drv/driver.cpp
Normal file
@@ -0,0 +1,689 @@
|
||||
|
||||
#if defined(__linux__)
|
||||
#include "driver-linux.cpp"
|
||||
#elif defined(_WIN32)
|
||||
#include "driver-windows.cpp"
|
||||
#endif
|
||||
|
||||
#define POLLVEC_SIZE (DRV_MAX_CHAN + 1)
|
||||
#define MAX_BIO_BUFFER (128 * 1024)
|
||||
|
||||
|
||||
static void if_error_print_and_exit(const std::string_view str) {
|
||||
if (!str.empty()) {
|
||||
std::cerr << std::endl << "error: " << str << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
static void dprint_callback(const char *oneline, size_t size) {
|
||||
fwrite("**", 1, 2, stderr);
|
||||
fwrite(oneline, 1, size, stderr);
|
||||
fwrite("\n", 1, 1, stderr);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
inline bool file_exists(const std::filesystem::path &name) {
|
||||
std::ifstream f(name);
|
||||
return f.good();
|
||||
}
|
||||
|
||||
std::filesystem::path find_luprex_root(std::filesystem::path exepath) {
|
||||
std::filesystem::path pp = exepath.parent_path();
|
||||
if (file_exists(pp / "lua/control.lst")) {
|
||||
return pp;
|
||||
}
|
||||
pp = pp.parent_path();
|
||||
if (file_exists(pp / "lua/control.lst")) {
|
||||
return pp;
|
||||
}
|
||||
pp = pp.parent_path();
|
||||
if (file_exists(pp / "lua/control.lst")) {
|
||||
return pp;
|
||||
}
|
||||
assert(false && "Could not find lua/control.lst");
|
||||
return "";
|
||||
}
|
||||
|
||||
class Driver {
|
||||
public:
|
||||
enum ChanState {
|
||||
CHAN_INACTIVE,
|
||||
CHAN_PLAINTEXT,
|
||||
CHAN_SSL_CONNECTING,
|
||||
CHAN_SSL_ACCEPTING,
|
||||
CHAN_SSL_READWRITE,
|
||||
};
|
||||
|
||||
struct ChanInfo {
|
||||
int chid;
|
||||
SOCKET socket;
|
||||
SSL *ssl;
|
||||
BIO *recv_bio;
|
||||
BIO *send_bio;
|
||||
|
||||
// If recent_error is set, that means that a recent IO operation generated
|
||||
// an error. As a special case, EOF on read is considered an error, we use
|
||||
// the string "EOF" for this case.
|
||||
std::string recent_error;
|
||||
|
||||
// OpenSSL has a rule: if you try to SSL_write and it returns
|
||||
// SSL_ERROR_WANT_READ, then you have to retry the write with the same
|
||||
// number of bytes. In this event, we record how many bytes we
|
||||
// attempted to write, which will enable us to retry.
|
||||
int retry_write_nbytes;
|
||||
|
||||
// True if the channel needs to be advanced.
|
||||
bool need_advance;
|
||||
|
||||
ChanState state;
|
||||
uint32_t nbytes;
|
||||
const char *bytes;
|
||||
|
||||
bool marked_for_deletion() const { return state == CHAN_INACTIVE; }
|
||||
};
|
||||
|
||||
std::filesystem::path luprexroot;
|
||||
EngineWrapper engw;
|
||||
std::vector<ChanInfo> chans_;
|
||||
std::map<int, SOCKET> listen_sockets_;
|
||||
bool read_console_recently_;
|
||||
std::unique_ptr<struct pollfd[]> pollvec_;
|
||||
std::unique_ptr<char[]> chbuf_;
|
||||
ReadlineDevice readline_device_;
|
||||
|
||||
sslutil::UniqueCTX ssl_server_ctx_;
|
||||
sslutil::UniqueCTX ssl_client_secure_ctx_;
|
||||
sslutil::UniqueCTX ssl_client_insecure_ctx_;
|
||||
|
||||
// Return the amount of 'space left' in a BIO. This is a fiction,
|
||||
// because MEM BIOs technically have unlimited capacity. We're
|
||||
// artificially limiting them to a certain size because there's no
|
||||
// reason to buffer huge amounts of data.
|
||||
//
|
||||
int bio_space(BIO *bio) {
|
||||
int space = (MAX_BIO_BUFFER) - BIO_pending(bio);
|
||||
if (space < 0) space = 0;
|
||||
return space;
|
||||
}
|
||||
|
||||
// This is a terribly inefficient way to discard data that has
|
||||
// already been processed. There has to be something better.
|
||||
//
|
||||
void bio_discard(BIO *b, int nbytes) {
|
||||
while (nbytes > 0) {
|
||||
int nread = nbytes;
|
||||
if (nread > DRV_SHORTSTRING_SIZE) nread = DRV_SHORTSTRING_SIZE;
|
||||
int ndropped = BIO_read(b, chbuf_.get(), nread);
|
||||
assert(ndropped == nread);
|
||||
nbytes -= ndropped;
|
||||
}
|
||||
}
|
||||
|
||||
void make_channel(SOCKET sock, int chid, SSL_CTX *ctx, ChanState state) {
|
||||
ChanInfo newchan;
|
||||
newchan.chid = chid;
|
||||
newchan.socket = sock;
|
||||
newchan.recv_bio = BIO_new(BIO_s_mem());
|
||||
newchan.send_bio = BIO_new(BIO_s_mem());
|
||||
newchan.recent_error.clear();
|
||||
newchan.retry_write_nbytes = 0;
|
||||
newchan.need_advance = true;
|
||||
|
||||
if (state == CHAN_PLAINTEXT) {
|
||||
newchan.ssl = nullptr;
|
||||
} else {
|
||||
newchan.ssl = SSL_new(ctx);
|
||||
SSL_set_bio(newchan.ssl, newchan.recv_bio, newchan.send_bio);
|
||||
}
|
||||
|
||||
newchan.state = state;
|
||||
newchan.nbytes = 0;
|
||||
newchan.bytes = 0;
|
||||
chans_.push_back(newchan);
|
||||
}
|
||||
|
||||
void close_channel(ChanInfo &chan, std::string_view err) {
|
||||
// std::cerr << "Closing channel " << chan.chid << " with " << err << std::endl;
|
||||
assert(chan.state != CHAN_INACTIVE);
|
||||
|
||||
// Close and release the SSL channel.
|
||||
// This frees the BIO objects as well.
|
||||
if (chan.ssl != nullptr) {
|
||||
SSL_free(chan.ssl);
|
||||
chan.ssl = nullptr;
|
||||
}
|
||||
chan.recv_bio = nullptr;
|
||||
chan.send_bio = nullptr;
|
||||
chan.recent_error.clear();
|
||||
chan.retry_write_nbytes = 0;
|
||||
chan.need_advance = false;
|
||||
|
||||
// Close and release the socket.
|
||||
assert(chan.socket != INVALID_SOCKET);
|
||||
assert(socket_close(chan.socket) == 0);
|
||||
chan.socket = INVALID_SOCKET;
|
||||
|
||||
// Close everything else.
|
||||
engw.play_notify_close(&engw, chan.chid, err.size(), err.data());
|
||||
chan.state = CHAN_INACTIVE;
|
||||
chan.chid = -1;
|
||||
chan.nbytes = 0;
|
||||
chan.bytes = 0;
|
||||
}
|
||||
|
||||
void handle_listen_ports() {
|
||||
uint32_t nports; const uint32_t *ports;
|
||||
engw.get_listen_ports(&engw, &nports, &ports);
|
||||
for (uint32_t i = 0; i < nports; i++) {
|
||||
int port = ports[i];
|
||||
if (listen_sockets_.find(port) == listen_sockets_.end()) {
|
||||
std::string err;
|
||||
SOCKET sock = listen_on_port(port, err);
|
||||
if_error_print_and_exit(err);
|
||||
assert(sock != INVALID_SOCKET);
|
||||
listen_sockets_[port] = sock;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handle_lua_source() {
|
||||
if (engw.get_rescan_lua_source(&engw)) {
|
||||
drvutil::ostringstream oss;
|
||||
std::string err = drvutil::package_lua_source(".", &oss);
|
||||
if_error_print_and_exit(err);
|
||||
std::string_view ossv = oss.view();
|
||||
engw.play_call_function(&engw, InvocationKind::LUA_SOURCE, 0, ossv.size(), ossv.data(), nullptr, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void handle_console_output() {
|
||||
while (true) {
|
||||
uint32_t ndata; const char *data;
|
||||
engw.get_outgoing(&engw, 0, &ndata, &data);
|
||||
if (ndata == 0) break;
|
||||
if (ndata > DRV_SHORTSTRING_SIZE) ndata = DRV_SHORTSTRING_SIZE;
|
||||
std::string_view src(data, ndata);
|
||||
int consumed;
|
||||
std::u32string cps = drvutil::utf8_to_utf32(src, &consumed);
|
||||
readline_device_.print(cps);
|
||||
engw.play_sent_outgoing(&engw, 0, consumed);
|
||||
}
|
||||
}
|
||||
|
||||
void handle_console_input() {
|
||||
read_console_recently_ = false;
|
||||
uint32_t promptlen;
|
||||
const char *promptdata;
|
||||
engw.get_console_prompt(&engw, &promptlen, &promptdata);
|
||||
std::u32string prompt = drvutil::utf8_to_utf32(std::string_view(promptdata, promptlen), nullptr);
|
||||
readline_device_.set_prompt(prompt);
|
||||
while (true) {
|
||||
std::u32string cps = console_read();
|
||||
if (cps.size() == 0) break;
|
||||
read_console_recently_ = true;
|
||||
for (char32_t c : cps) {
|
||||
std::u32string line = readline_device_.putcode(c);
|
||||
if (!line.empty()) {
|
||||
std::string utf8 = drvutil::utf32_to_utf8(line);
|
||||
engw.play_recv_incoming(&engw, 0, utf8.size(), utf8.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handle_new_outgoing_sockets() {
|
||||
uint32_t nchids; const uint32_t *chids;
|
||||
engw.get_new_outgoing(&engw, &nchids, &chids);
|
||||
for (uint32_t i = 0; i < nchids; i++) {
|
||||
uint32_t chid = chids[i];
|
||||
std::string err, cert, host, port;
|
||||
const char *target = engw.get_target(&engw, chid);
|
||||
drvutil::split_target(target, cert, host, port);
|
||||
if (cert.empty() || host.empty() || port.empty()) {
|
||||
std::string message = "invalid target: ";
|
||||
message += target;
|
||||
engw.play_notify_close(&engw, chid, message.size(), message.c_str());
|
||||
continue;
|
||||
}
|
||||
SSL_CTX *ctx = nullptr;
|
||||
if (cert == "cert") {
|
||||
ctx = ssl_client_secure_ctx_.get();
|
||||
} else if (cert == "nocert") {
|
||||
ctx = ssl_client_insecure_ctx_.get();
|
||||
} else {
|
||||
std::string message = "invalid cert rule: ";
|
||||
message += target;
|
||||
engw.play_notify_close(&engw, chid, message.size(), message.c_str());
|
||||
continue;
|
||||
}
|
||||
SOCKET sock = open_connection(host.c_str(), port.c_str(), err);
|
||||
if (sock == INVALID_SOCKET) {
|
||||
engw.play_notify_close(&engw, chid, err.size(), err.c_str());
|
||||
continue;
|
||||
}
|
||||
make_channel(sock, chid, ctx, CHAN_SSL_CONNECTING);
|
||||
}
|
||||
engw.play_clear_new_outgoing(&engw);
|
||||
}
|
||||
|
||||
void accept_connection(int port, SOCKET sock) {
|
||||
std::string err;
|
||||
SOCKET socket = accept_on_socket(sock, err);
|
||||
if_error_print_and_exit(err);
|
||||
if (socket != INVALID_SOCKET) {
|
||||
uint32_t chid = engw.play_notify_accept(&engw, port);
|
||||
make_channel(socket, chid, ssl_server_ctx_.get(), CHAN_SSL_ACCEPTING);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy data from the socket into the recv bio.
|
||||
//
|
||||
// If it detects an error or EOF, sets the recent_errno flag.
|
||||
//
|
||||
void transfer_socket_to_recv_bio(ChanInfo &chan) {
|
||||
if ((chan.state == CHAN_INACTIVE) || (!chan.recent_error.empty())) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string err;
|
||||
int nread = socket_recv(chan.socket, chbuf_.get(), DRV_SHORTSTRING_SIZE, err);
|
||||
// std::cerr << "chan " << chan.chid << " recv " << nread << " err=" << err << std::endl;
|
||||
if (nread < 0) {
|
||||
chan.recent_error = err;
|
||||
} else {
|
||||
if (nread == 0) {
|
||||
chan.recent_error = "EOF";
|
||||
} else {
|
||||
int nstored = BIO_write(chan.recv_bio, chbuf_.get(), nread);
|
||||
assert(nstored == nread);
|
||||
chan.need_advance = true;
|
||||
// std::cerr << "chan " << chan.chid << " stored " << nread << " bytes" << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy data from the send BIO into the socket.
|
||||
//
|
||||
// If it detects an error, sets the recent_errno flag.
|
||||
// It is an error to call this when there is nothing in the send BIO.
|
||||
//
|
||||
void transfer_send_bio_to_socket(ChanInfo &chan) {
|
||||
if ((chan.state == CHAN_INACTIVE) || (!chan.recent_error.empty())) {
|
||||
return;
|
||||
}
|
||||
|
||||
char *data;
|
||||
int ndata = BIO_get_mem_data(chan.send_bio, &data);
|
||||
if (ndata > DRV_SHORTSTRING_SIZE) ndata = DRV_SHORTSTRING_SIZE;
|
||||
|
||||
// It is an error to call this function when there is nothing in the send BIO.
|
||||
assert(ndata > 0);
|
||||
|
||||
std::string err;
|
||||
int nwrote = socket_send(chan.socket, data, ndata, err);
|
||||
// std::cerr << "chan " << chan.chid << " send " << nwrote << " err=" << err << std::endl;
|
||||
if (nwrote < 0) {
|
||||
chan.recent_error = err;
|
||||
} else {
|
||||
assert(nwrote != 0);
|
||||
bio_discard(chan.send_bio, nwrote);
|
||||
chan.need_advance = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Close the channel if there's a serious OpenSSL error.
|
||||
//
|
||||
// The 'retval' is the return value of the SSL function that returned an
|
||||
// error.
|
||||
//
|
||||
// All errors are considered serious except for SSL_ERROR_WANT_READ, which
|
||||
// is not serious because it is transient. However, if you get an
|
||||
// SSL_ERROR_WANT_READ when there's tons of data available in the read
|
||||
// buffer, that's inexplicable and therefore serious.
|
||||
//
|
||||
void if_error_is_serious_close_channel(ChanInfo &chan, int retval) {
|
||||
int error = SSL_get_error(chan.ssl, retval);
|
||||
//std::cerr << "chan " << chan.chid << " ssl error = " << error << std::endl;
|
||||
|
||||
// Should never have write errors, because we're
|
||||
// using a memory BIO with unlimited capacity.
|
||||
assert(error != SSL_ERROR_WANT_WRITE);
|
||||
|
||||
// If we get a read error, make sure it's plausible:
|
||||
// if the recv bio is full, that makes no sense.
|
||||
if (error == SSL_ERROR_WANT_READ) {
|
||||
if (bio_space(chan.recv_bio) == 0) {
|
||||
close_channel(chan, "ssl waiting for data, but there's tons of data");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Any other error is an actual error. Close
|
||||
// the channel.
|
||||
std::string errstr = sslutil::error_string();
|
||||
if (errstr == "") errstr = "unknown error";
|
||||
close_channel(chan, errstr);
|
||||
}
|
||||
|
||||
void advance_plaintext(ChanInfo &chan) {
|
||||
uint32_t ndata; const char *data;
|
||||
|
||||
// Transfer all data from the recv BIO into the channel.
|
||||
ndata = BIO_get_mem_data(chan.recv_bio, &data);
|
||||
if (ndata > 0) {
|
||||
engw.play_recv_incoming(&engw, chan.chid, ndata, data);
|
||||
bio_discard(chan.recv_bio, ndata);
|
||||
}
|
||||
|
||||
// Transfer all data from the channel to the send BIO.
|
||||
engw.get_outgoing(&engw, chan.chid, &ndata, &data);
|
||||
if (ndata > 0) {
|
||||
int nwrote = BIO_write(chan.send_bio, data, ndata);
|
||||
assert(nwrote == int(ndata));
|
||||
engw.play_sent_outgoing(&engw, chan.chid, ndata);
|
||||
}
|
||||
}
|
||||
|
||||
void advance_ssl_connecting(ChanInfo &chan) {
|
||||
int retval = SSL_connect(chan.ssl);
|
||||
//std::cerr << "chan " << chan.chid << " ssl_connect returns " << retval << std::endl;
|
||||
if (retval == 1) {
|
||||
chan.state = CHAN_SSL_READWRITE;
|
||||
chan.need_advance = true;
|
||||
} else {
|
||||
if_error_is_serious_close_channel(chan, retval);
|
||||
}
|
||||
}
|
||||
|
||||
void advance_ssl_accepting(ChanInfo &chan) {
|
||||
int retval = SSL_accept(chan.ssl);
|
||||
//std::cerr << "chan " << chan.chid << " ssl_accept returns " << retval << std::endl;
|
||||
if (retval == 1) {
|
||||
chan.state = CHAN_SSL_READWRITE;
|
||||
chan.need_advance = true;
|
||||
} else {
|
||||
if_error_is_serious_close_channel(chan, retval);
|
||||
}
|
||||
}
|
||||
|
||||
void advance_ssl_readwrite(ChanInfo &chan) {
|
||||
// Read as much as we can, which of course will be limited
|
||||
// by the fact that the recv_bio contains finite data.
|
||||
while (true) {
|
||||
int read_result = SSL_read(chan.ssl, chbuf_.get(), DRV_SHORTSTRING_SIZE);
|
||||
if (read_result > 0) {
|
||||
engw.play_recv_incoming(&engw, chan.chid, read_result, chbuf_.get());
|
||||
} else {
|
||||
if_error_is_serious_close_channel(chan, read_result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The read process could have generated an error which could
|
||||
// have closed the channel. If so, don't try writing.
|
||||
if (chan.state == CHAN_INACTIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to write data.
|
||||
while (chan.nbytes) {
|
||||
uint32_t wbytes;
|
||||
if (chan.retry_write_nbytes > 0) {
|
||||
wbytes = chan.retry_write_nbytes;
|
||||
assert(wbytes < chan.nbytes);
|
||||
} else {
|
||||
wbytes = chan.nbytes;
|
||||
if (wbytes > DRV_SHORTSTRING_SIZE) wbytes = DRV_SHORTSTRING_SIZE;
|
||||
}
|
||||
if (wbytes == 0) break;
|
||||
int write_result = SSL_write(chan.ssl, chan.bytes, wbytes);
|
||||
if (write_result > 0) {
|
||||
engw.play_sent_outgoing(&engw, chan.chid, write_result);
|
||||
chan.retry_write_nbytes = 0;
|
||||
chan.nbytes -= write_result;
|
||||
chan.bytes += write_result;
|
||||
} else {
|
||||
if_error_is_serious_close_channel(chan, write_result);
|
||||
chan.retry_write_nbytes = wbytes;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void advance_channel(ChanInfo &chan) {
|
||||
sslutil::clear_all_errors();
|
||||
|
||||
// We set the need_advance flag to false here, but
|
||||
// the rest of the advance routine is allowed to set
|
||||
// it back to true in the event that the advance routine
|
||||
// only processes some of the available data.
|
||||
chan.need_advance = false;
|
||||
|
||||
switch (chan.state) {
|
||||
case CHAN_PLAINTEXT:
|
||||
advance_plaintext(chan);
|
||||
break;
|
||||
case CHAN_SSL_CONNECTING:
|
||||
advance_ssl_connecting(chan);
|
||||
break;
|
||||
case CHAN_SSL_ACCEPTING:
|
||||
advance_ssl_accepting(chan);
|
||||
break;
|
||||
case CHAN_SSL_READWRITE:
|
||||
advance_ssl_readwrite(chan);
|
||||
break;
|
||||
default:
|
||||
assert(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void handle_socket_input_output() {
|
||||
std::string err;
|
||||
int mstimeout = read_console_recently_ ? 10 : 100;
|
||||
|
||||
// Peek output buffers and determine channel release flags.
|
||||
bool any_released = false;
|
||||
for (ChanInfo &chan : chans_) {
|
||||
engw.get_outgoing(&engw, chan.chid, &chan.nbytes, &chan.bytes);
|
||||
if (chan.nbytes > 0) {
|
||||
chan.need_advance = true;
|
||||
}
|
||||
if (chan.nbytes == 0) {
|
||||
if (engw.get_channel_released(&engw, chan.chid)) {
|
||||
if (BIO_pending(chan.send_bio) == 0) {
|
||||
close_channel(chan, "");
|
||||
any_released = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete any released channels
|
||||
if (any_released) {
|
||||
drvutil::remove_marked_items(chans_);
|
||||
}
|
||||
|
||||
// Construct the struct pollfd vector.
|
||||
int pollsize = 0;
|
||||
for (const ChanInfo &chan : chans_) {
|
||||
struct pollfd &pfd = pollvec_[pollsize++];
|
||||
assert(chan.socket != INVALID_SOCKET);
|
||||
pfd.fd = chan.socket;
|
||||
pfd.events = 0;
|
||||
pfd.revents = 0;
|
||||
// If there's room in the receive buffer, set POLLIN
|
||||
if (bio_space(chan.recv_bio) > 0) {
|
||||
pfd.events |= POLLIN;
|
||||
}
|
||||
// If there's data in the outgoing buffer, set POLLOUT
|
||||
if (BIO_pending(chan.send_bio) > 0) {
|
||||
pfd.events |= POLLOUT;
|
||||
}
|
||||
if (chan.need_advance) {
|
||||
mstimeout = 0;
|
||||
}
|
||||
}
|
||||
for (const auto &p : listen_sockets_) {
|
||||
struct pollfd &pfd = pollvec_[pollsize++];
|
||||
pfd.fd = p.second;
|
||||
pfd.events = POLLIN;
|
||||
pfd.revents = 0;
|
||||
}
|
||||
|
||||
// Do the poll.
|
||||
socket_poll(pollvec_.get(), pollsize, mstimeout, err);
|
||||
if_error_print_and_exit(err);
|
||||
|
||||
// Advance channels where possible and then check listen sockets.
|
||||
int index = 0;
|
||||
for (ChanInfo &chan : chans_) {
|
||||
struct pollfd &pfd = pollvec_[index++];
|
||||
if ((pfd.revents & POLLIN) != 0) {
|
||||
transfer_socket_to_recv_bio(chan);
|
||||
}
|
||||
if ((pfd.revents & POLLOUT) != 0) {
|
||||
transfer_send_bio_to_socket(chan);
|
||||
}
|
||||
if (chan.need_advance || (!chan.recent_error.empty())) {
|
||||
advance_channel(chan);
|
||||
}
|
||||
if (!chan.recent_error.empty()) {
|
||||
if (chan.recent_error == "EOF") {
|
||||
close_channel(chan, "");
|
||||
} else {
|
||||
close_channel(chan, chan.recent_error);
|
||||
}
|
||||
chan.recent_error.clear();
|
||||
}
|
||||
chan.nbytes = 0;
|
||||
chan.bytes = 0;
|
||||
}
|
||||
for (auto &p : listen_sockets_) {
|
||||
struct pollfd &pfd = pollvec_[index++];
|
||||
if (pfd.revents & (POLLIN | POLLERR)) {
|
||||
accept_connection(p.first, p.second);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete any newly-inactive channels
|
||||
drvutil::remove_marked_items(chans_);
|
||||
}
|
||||
|
||||
int replay_logfile(const char *fn, bool verbose) {
|
||||
engw.replay_initialize(&engw, fn);
|
||||
if_error_print_and_exit(engw.error);
|
||||
while (engw.rlog) {
|
||||
engw.replay_step(&engw);
|
||||
}
|
||||
if_error_print_and_exit(engw.error);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void replay_cb_sent_outgoing(void *vp, int chid, int ndata, const char *data) {
|
||||
if (chid == 0) {
|
||||
std::cerr.write(data, ndata);
|
||||
}
|
||||
}
|
||||
|
||||
int drive(int argc, char *argv[]) {
|
||||
// Set up the console readline device.
|
||||
readline_device_.set_print_callback(console_write);
|
||||
|
||||
// Remove the program name from argv.
|
||||
std::string program = argv[0];
|
||||
argc -= 1;
|
||||
argv += 1;
|
||||
|
||||
// Find the root of the luprex tree.
|
||||
luprexroot = find_luprex_root(get_exe_path());
|
||||
|
||||
// Load the DLL and gain access to its functions.
|
||||
call_init_engine_wrapper(luprexroot, &engw);
|
||||
engw.replay_cb_sent_outgoing = replay_cb_sent_outgoing;
|
||||
engw.hook_dprint(dprint_callback);
|
||||
|
||||
// If argv contains "replay <filename>", do a replay,
|
||||
// and then skip everything else.
|
||||
if (argc >= 1) {
|
||||
std::string cmd(argv[0]);
|
||||
if ((cmd == "replay") || (cmd == "vreplay")) {
|
||||
if (argc != 2) {
|
||||
std::cerr << "usage: " << program << " replay <filename>"
|
||||
<< std::endl;
|
||||
return 1;
|
||||
}
|
||||
return replay_logfile(argv[1], cmd == "vreplay");
|
||||
}
|
||||
}
|
||||
|
||||
// If argv contains "record <filename>", start recording,
|
||||
// and remove the "record <filename>" from argv.
|
||||
std::string replaylogfn;
|
||||
if (argc >= 1) {
|
||||
std::string cmd = argv[0];
|
||||
if (cmd == "record") {
|
||||
if (argc < 2) {
|
||||
std::cerr << "The 'record' command must be followed by a filename" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
replaylogfn = argv[1];
|
||||
argc -= 2;
|
||||
argv += 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize state variables.
|
||||
read_console_recently_ = false;
|
||||
chbuf_.reset(new char[DRV_SHORTSTRING_SIZE]);
|
||||
pollvec_.reset(new struct pollfd[POLLVEC_SIZE]);
|
||||
|
||||
ssl_server_ctx_.reset(sslutil::new_context(SSL_VERIFY_NONE));
|
||||
ssl_client_secure_ctx_.reset(sslutil::new_context(SSL_VERIFY_PEER));
|
||||
ssl_client_insecure_ctx_.reset(sslutil::new_context(SSL_VERIFY_NONE));
|
||||
ssl_load_certificate_authorities(ssl_client_secure_ctx_.get());
|
||||
sslutil::ctx_load_dummy_cert(ssl_server_ctx_.get());
|
||||
|
||||
// Read the initial lua source code.
|
||||
drvutil::ostringstream srcpak;
|
||||
std::string srcpakerr = drvutil::package_lua_source(luprexroot, &srcpak);
|
||||
if_error_print_and_exit(srcpakerr);
|
||||
|
||||
// Initialize the engine.
|
||||
std::string_view srcpakv = srcpak.view();
|
||||
engw.play_initialize(&engw, argc, argv, srcpakv.size(), srcpakv.data(), replaylogfn.c_str());
|
||||
if_error_print_and_exit(engw.error);
|
||||
|
||||
// Set up listening ports.
|
||||
handle_listen_ports();
|
||||
|
||||
// Main loop.
|
||||
while (!engw.get_stop_driver(&engw)) {
|
||||
handle_lua_source();
|
||||
handle_console_output();
|
||||
handle_new_outgoing_sockets();
|
||||
handle_socket_input_output();
|
||||
handle_console_input();
|
||||
handle_console_output();
|
||||
engw.play_update(&engw, drvutil::get_monotonic_clock());
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
for (ChanInfo &chan : chans_) {
|
||||
close_channel(chan, "");
|
||||
}
|
||||
engw.release(&engw);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
os_initialize(argc, argv);
|
||||
assert(OPENSSL_init_ssl(0, NULL) == 1);
|
||||
sslutil::clear_all_errors();
|
||||
Driver driver;
|
||||
return driver.drive(argc, argv);
|
||||
}
|
||||
|
||||
357
luprex/cpp/drv/drvutil.cpp
Normal file
357
luprex/cpp/drv/drvutil.cpp
Normal file
@@ -0,0 +1,357 @@
|
||||
|
||||
#include "drvutil.hpp"
|
||||
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
|
||||
namespace drvutil {
|
||||
|
||||
|
||||
inline static bool ascii_isspace(char c) {
|
||||
return (c==' ')||(c=='\t')||(c=='\r')||(c=='\n')||(c=='\f')||(c=='\v');
|
||||
}
|
||||
|
||||
std::string_view trim(std::string_view v) {
|
||||
while ((!v.empty()) && (ascii_isspace(v.front()))) {
|
||||
v.remove_prefix(1);
|
||||
}
|
||||
while ((!v.empty()) && (ascii_isspace(v.back()))) {
|
||||
v.remove_suffix(1);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
static std::string_view read_to_line(std::string_view &source) {
|
||||
size_t pos = source.find('\n');
|
||||
std::string_view result;
|
||||
if (pos == std::string_view::npos) {
|
||||
result = source;
|
||||
source = std::string_view();
|
||||
} else {
|
||||
result = source.substr(0, pos);
|
||||
source = source.substr(pos + 1);
|
||||
}
|
||||
if ((!result.empty()) && (result.back() == '\r')) {
|
||||
result.remove_suffix(1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string_view> split_view(std::string_view v, char sep) {
|
||||
std::vector<std::string_view> result;
|
||||
while (true) {
|
||||
size_t pos = v.find(sep);
|
||||
if (pos == std::string_view::npos) break;
|
||||
result.push_back(v.substr(0, pos));
|
||||
v = v.substr(pos + 1);
|
||||
}
|
||||
result.push_back(v);
|
||||
return result;
|
||||
}
|
||||
|
||||
void split_target(std::string_view target, std::string &cert, std::string &host, std::string &port) {
|
||||
std::vector<std::string_view> split = split_view(target, ':');
|
||||
if (split.size() != 3) {
|
||||
cert.clear(); host.clear(); port.clear();
|
||||
return;
|
||||
}
|
||||
if (split[0].empty() || split[1].empty() || split[2].empty()) {
|
||||
cert.clear(); host.clear(); port.clear();
|
||||
return;
|
||||
}
|
||||
cert = std::string(split[0]);
|
||||
host = std::string(split[1]);
|
||||
port = std::string(split[2]);
|
||||
}
|
||||
|
||||
bool is_single_wchar_t(char32_t c) {
|
||||
if ((c >= 0xD800) && (c <= 0xDFFF)) return false;
|
||||
if ((c >= 0) && (c <= 0xFFFF)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static int buffer_codepoint_utf8(char32_t scp, char *buffer) {
|
||||
uint32_t cp = (uint32_t)scp;
|
||||
unsigned char *c = (unsigned char *)buffer;
|
||||
if (cp < 0) {
|
||||
return 0;
|
||||
}
|
||||
else if (cp <= 0x7F) {
|
||||
c[0] = cp;
|
||||
return 1;
|
||||
}
|
||||
else if (cp <= 0x7FF) {
|
||||
c[0] = (cp>>6)+192;
|
||||
c[1] = (cp&63)+128;
|
||||
return 2;
|
||||
}
|
||||
else if (cp <= 0xFFFF) {
|
||||
if ((cp >= 0xD800) && (cp <= 0xDFFF)) {
|
||||
return 0;
|
||||
}
|
||||
c[0] = (cp>>12)+224;
|
||||
c[1] = ((cp>>6)&63)+128;
|
||||
c[2] = (cp&63)+128;
|
||||
return 3;
|
||||
}
|
||||
else if (cp <= 0x10FFFF) {
|
||||
c[0] = (cp>>18)+240;
|
||||
c[1] = ((cp>>12)&63)+128;
|
||||
c[2] = ((cp>>6)&63)+128;
|
||||
c[3] = (cp&63)+128;
|
||||
return 4;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int32_t read_codepoint_utf16(std::u16string_view &source) {
|
||||
if (source.empty()) return -1;
|
||||
|
||||
int32_t word0 = ((const uint16_t *)source.data())[0];
|
||||
source.remove_prefix(1);
|
||||
|
||||
if (word0 < 0xD800) {
|
||||
return word0;
|
||||
} else if (word0 < 0xDC00) {
|
||||
if (source.empty()) {
|
||||
return -2;
|
||||
}
|
||||
int32_t word1 = ((const uint16_t *)source.data())[0];
|
||||
if ((word1 < 0xDC00)||(word1 > 0xDFFF)) {
|
||||
return -2;
|
||||
}
|
||||
int32_t part1 = word0 & 0x3FF;
|
||||
int32_t part2 = word1 & 0x3FF;
|
||||
int32_t result = ((part1 << 10) | part2) + 0x10000;
|
||||
source.remove_prefix(1);
|
||||
return result;
|
||||
} else if (word0 < 0xE000) {
|
||||
return -2;
|
||||
} else {
|
||||
return word0;
|
||||
}
|
||||
}
|
||||
|
||||
static int32_t read_codepoint_utf8(std::string_view &source) {
|
||||
size_t size = source.size();
|
||||
if (size == 0) return -1;
|
||||
|
||||
const unsigned char *bytes = (const unsigned char *)source.data();
|
||||
int codepoint;
|
||||
size_t seqlen;
|
||||
if ((bytes[0] & 0x80) == 0x00) {
|
||||
// U+0000 to U+007F
|
||||
codepoint = (bytes[0] & 0x7F);
|
||||
seqlen = 1;
|
||||
} else if ((bytes[0] & 0xE0) == 0xC0) {
|
||||
// U+0080 to U+07FF
|
||||
codepoint = (bytes[0] & 0x1F);
|
||||
seqlen = 2;
|
||||
} else if ((bytes[0] & 0xF0) == 0xE0) {
|
||||
// U+0800 to U+FFFF
|
||||
codepoint = (bytes[0] & 0x0F);
|
||||
seqlen = 3;
|
||||
} else if ((bytes[0] & 0xF8) == 0xF0) {
|
||||
// U+10000 to U+10FFFF
|
||||
codepoint = (bytes[0] & 0x07);
|
||||
seqlen = 4;
|
||||
} else {
|
||||
// Bad character. return invalid CP.
|
||||
return -2;
|
||||
}
|
||||
|
||||
if (seqlen > size) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (size_t i = 1; i < seqlen; ++i) {
|
||||
if ((bytes[i] & 0xC0) != 0x80) {
|
||||
// Bad character. return invalid CP.
|
||||
return -2;
|
||||
}
|
||||
codepoint = (codepoint << 6) | (bytes[i] & 0x3F);
|
||||
}
|
||||
|
||||
if ((codepoint > 0x10FFFF) ||
|
||||
((codepoint >= 0xD800) && (codepoint <= 0xDFFF)) ||
|
||||
((codepoint <= 0x007F) && (seqlen != 1)) ||
|
||||
((codepoint >= 0x0080) && (codepoint <= 0x07FF) && (seqlen != 2)) ||
|
||||
((codepoint >= 0x0800) && (codepoint <= 0xFFFF) && (seqlen != 3)) ||
|
||||
((codepoint >= 0x10000) && (codepoint <= 0x1FFFFF) && (seqlen != 4))) {
|
||||
// Bad character. return invalid CP.
|
||||
return -2;
|
||||
}
|
||||
|
||||
source.remove_prefix(seqlen);
|
||||
return codepoint;
|
||||
}
|
||||
|
||||
std::string utf32_to_utf8(const std::u32string &s) {
|
||||
std::string result(s.size() * 4, 0);
|
||||
char *buffer = &result[0];
|
||||
int len = 0;
|
||||
for (char32_t c : s) {
|
||||
int clen = buffer_codepoint_utf8(c, buffer + len);
|
||||
len += clen;
|
||||
}
|
||||
return result.substr(0, len);
|
||||
}
|
||||
|
||||
std::u32string utf8_to_utf32(std::string_view s, int *consumed) {
|
||||
std::string_view rest = s;
|
||||
std::u32string result(s.size(), 0);
|
||||
int len = 0;
|
||||
while (true) {
|
||||
int32_t c = read_codepoint_utf8(rest);
|
||||
if (c == -1) {
|
||||
break; // EOF reached;
|
||||
} else if (c < 0) {
|
||||
rest.remove_prefix(1);
|
||||
} else {
|
||||
result[len++] = (char32_t)c;
|
||||
}
|
||||
}
|
||||
if (consumed != nullptr) {
|
||||
*consumed = s.size() - rest.size();
|
||||
}
|
||||
return result.substr(0, len);
|
||||
}
|
||||
|
||||
std::u16string utf8_to_ucs2(std::string_view s, int *consumed) {
|
||||
std::string_view rest = s;
|
||||
std::u16string result(s.size(), 0);
|
||||
int len = 0;
|
||||
while (true) {
|
||||
int32_t c = read_codepoint_utf8(rest);
|
||||
if (c == -1) {
|
||||
break; // EOF reached;
|
||||
} else if (c < 0) {
|
||||
rest.remove_prefix(1);
|
||||
} else if ((c >= 0xD800) && (c <= 0xDFFF)) {
|
||||
result[len++] = 0x2610;
|
||||
} else if (c > 0xFFFF) {
|
||||
result[len++] = 0x2610;
|
||||
} else {
|
||||
result[len++] = (char16_t)c;
|
||||
}
|
||||
}
|
||||
if (consumed != nullptr) {
|
||||
*consumed = s.size() - rest.size();
|
||||
}
|
||||
return result.substr(0, len);
|
||||
}
|
||||
|
||||
std::string utf16_to_utf8(std::u16string_view s) {
|
||||
std::string result(s.size() * 4, 0);
|
||||
int len = 0;
|
||||
while (true) {
|
||||
int codepoint = read_codepoint_utf16(s);
|
||||
if (codepoint == -1) break;
|
||||
if (codepoint < 0) continue;
|
||||
len += buffer_codepoint_utf8(codepoint, &result[len]);
|
||||
}
|
||||
return result.substr(0, len);
|
||||
}
|
||||
|
||||
static std::vector<std::string> parse_control_lst(std::string_view ctrl) {
|
||||
std::vector<std::string> result;
|
||||
while (!ctrl.empty()) {
|
||||
std::string_view line = read_to_line(ctrl);
|
||||
std::string_view trimmed = trim(line);
|
||||
if ((trimmed.size() > 0) && (trimmed[0] != '#')) {
|
||||
result.emplace_back(trimmed);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Read a source file into a string.
|
||||
//
|
||||
static std::string read_file(const std::filesystem::path &fn, std::string &err) {
|
||||
std::ifstream t(fn);
|
||||
if (t.fail()) {
|
||||
err = std::string("Could not open ") + fn.u8string();
|
||||
return "";
|
||||
}
|
||||
t.seekg(0, std::ios::end);
|
||||
size_t size = t.tellg();
|
||||
std::string result(size, ' ');
|
||||
t.seekg(0);
|
||||
t.read(&result[0], size);
|
||||
if ((t.fail()) || (size_t(t.tellg()) != size)) {
|
||||
err = std::string("Could not read ") + fn.u8string();
|
||||
return "";
|
||||
}
|
||||
err = "";
|
||||
return result;
|
||||
}
|
||||
|
||||
// This encoding can be read by StreamBuffer::read_uint32.
|
||||
//
|
||||
static void sbwrite_uint32(std::ostream *s, uint32_t v) {
|
||||
s->write((const char *)&v, 4);
|
||||
}
|
||||
|
||||
// This encoding can be read by StreamBuffer::read_uint64.
|
||||
//
|
||||
static void sbwrite_uint64(std::ostream *s, uint64_t v) {
|
||||
s->write((const char *)&v, 8);
|
||||
}
|
||||
|
||||
// This encoding can be read by StreamBuffer::read_string.
|
||||
//
|
||||
static void sbwrite_string(std::ostream *s, std::string_view sv) {
|
||||
s->put('\xFF');
|
||||
sbwrite_uint64(s, sv.size());
|
||||
s->write(sv.data(), sv.size());
|
||||
}
|
||||
|
||||
// This encoding can be read by StreamBuffer::read_string.
|
||||
//
|
||||
static bool sbwrite_file(std::ostream *s, const std::filesystem::path &fn) {
|
||||
s->put('\xFF');
|
||||
uint64_t pos1 = s->tellp();
|
||||
sbwrite_uint64(s, 0);
|
||||
uint64_t pos2 = s->tellp();
|
||||
std::ifstream t(fn);
|
||||
if (t.fail()) {
|
||||
return false;
|
||||
}
|
||||
*s << t.rdbuf();
|
||||
if (t.fail()) {
|
||||
return false;
|
||||
}
|
||||
uint64_t pos3 = s->tellp();
|
||||
s->seekp(pos1);
|
||||
sbwrite_uint64(s, pos3 - pos2);
|
||||
s->seekp(pos3);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string package_lua_source(const std::filesystem::path &base, std::ostream *s) {
|
||||
std::string err;
|
||||
std::filesystem::path cfn = base / "lua/control.lst";
|
||||
std::string ctrl = read_file(cfn, err);
|
||||
if (!err.empty()) {
|
||||
return err;
|
||||
}
|
||||
|
||||
std::vector<std::string> names = parse_control_lst(ctrl);
|
||||
sbwrite_uint32(s, names.size());
|
||||
for (int i = 0; i < int(names.size()); i++) {
|
||||
sbwrite_string(s, names[i]);
|
||||
std::filesystem::path lfn = base / "lua" / names[i];
|
||||
if (!sbwrite_file(s, lfn)) {
|
||||
return std::string("Cannot read source file: ") + lfn.u8string();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
} // namespace drv
|
||||
137
luprex/cpp/drv/drvutil.hpp
Normal file
137
luprex/cpp/drv/drvutil.hpp
Normal file
@@ -0,0 +1,137 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DRIVER_UTIL
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
#ifndef DRVUTIL_HPP
|
||||
#define DRVUTIL_HPP
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <ostream>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
|
||||
namespace drvutil {
|
||||
|
||||
// Read the lua source from disk into an ostringstream.
|
||||
//
|
||||
// To pass the lua source into the DLL, here is what you do: Construct an
|
||||
// ostringstream. Use package_lua_source to package all the lua source into
|
||||
// the ostringstream. Fetch the packaged source code using ostringstream::str.
|
||||
// Pass the packaged source code into drv_set_lua_source.
|
||||
//
|
||||
// The DLL must then decode the source package. Here is how it does that:
|
||||
// It creates a StreamBuffer from the packaged up source. Then it must
|
||||
// call these StreamBuffer methods:
|
||||
//
|
||||
// - read the number of source files using read_uint32.
|
||||
// - for each file, read the filename using read_string.
|
||||
// - for each file, read the contents using read_string.
|
||||
//
|
||||
// If package_lua_source encounters an error reading the source code, then it
|
||||
// returns an error message. In this case, the ostream contains garbage. If
|
||||
// there is no error, returns the empty string.
|
||||
//
|
||||
std::string package_lua_source(const std::filesystem::path &base, std::ostream *oss);
|
||||
|
||||
// Parse a target designation.
|
||||
//
|
||||
// A target consists of 'cert::host::port'.
|
||||
//
|
||||
void split_target(std::string_view target, std::string &cert, std::string &host, std::string &port);
|
||||
|
||||
// Return true if the unicode codepoint can be converted to a single 16-bit wchar_t.
|
||||
//
|
||||
bool is_single_wchar_t(char32_t c);
|
||||
|
||||
// Convert a codepoint string into a UTF8-string.
|
||||
// If the codepoint string contains invalid codepoints, they're silently dropped.
|
||||
//
|
||||
std::string utf32_to_utf8(const std::u32string &cps);
|
||||
|
||||
// Convert a UTF8 string to a UTF32 string.
|
||||
//
|
||||
// If the UTF8 string contains invalid sequences, they're silently dropped.
|
||||
// Some of the bytes may not be consumed, if the source ends with an unfinished
|
||||
// utf-8 sequence. Returns the Codepoint string and the number of bytes consumed.
|
||||
//
|
||||
std::u32string utf8_to_utf32(std::string_view source, int *consumed);
|
||||
|
||||
// Convert a UTF8 string to a UCS-2 string.
|
||||
//
|
||||
// If the UTF8 string contains invalid sequences, they're silently dropped.
|
||||
// Some of the bytes may not be consumed, if the source ends with an unfinished
|
||||
// utf-8 sequence. Returns the UCS-2 string and the number of bytes consumed.
|
||||
// Of course, UCS-2 can't represent all of unicode, so this is lossy.
|
||||
// Any character that can't be represented is replaced with a box.
|
||||
//
|
||||
std::u16string utf8_to_ucs2(std::string_view source, int *consumed);
|
||||
|
||||
// Convert a UTF16 string to a UTF8 string.
|
||||
//
|
||||
// If the UTF16 string contains invalid sequences, they're silently dropped.
|
||||
//
|
||||
std::string utf16_to_utf8(std::u16string_view source);
|
||||
|
||||
// Get a system error message, in an OS-independent manner.
|
||||
//
|
||||
// These versions of strerror is thread-safe, and it never fails
|
||||
// to put a message into the buffer.
|
||||
//
|
||||
void strerror_safe(int errnum, char result[256]);
|
||||
std::string strerror_str(int errnum);
|
||||
|
||||
// Get the amount of time elapsed since program start.
|
||||
//
|
||||
// This is guaranteed to be monotonically increasing. It is not
|
||||
// guaranteed to be accurate. Error could gradually accumulate over
|
||||
// time.
|
||||
//
|
||||
double get_monotonic_clock();
|
||||
|
||||
// drvutil::ostringstream
|
||||
//
|
||||
// This is a variant of ostringstream in which it is possible
|
||||
// to get the contents without copying. To get the contents
|
||||
// without copying, use oss.size() and oss.c_str()
|
||||
//
|
||||
class ostringstream : public std::ostringstream {
|
||||
class rstringbuf : public std::basic_stringbuf<char_type, traits_type, allocator_type> {
|
||||
public:
|
||||
char *eback() const { return std::streambuf::eback(); }
|
||||
char *pptr() const { return std::streambuf::pptr(); }
|
||||
};
|
||||
rstringbuf rstringbuf_;
|
||||
public:
|
||||
ostringstream() {
|
||||
std::basic_ostream<char>::rdbuf(&rstringbuf_);
|
||||
}
|
||||
std::string_view view() const {
|
||||
char *p = rstringbuf_.eback();
|
||||
size_t size = rstringbuf_.pptr() - p;
|
||||
return std::string_view(p, size);
|
||||
}
|
||||
std::string str() const {
|
||||
return rstringbuf_.str();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Remove items from a vector that are marked for deletion.
|
||||
//
|
||||
template<class T>
|
||||
void remove_marked_items(T &vec) {
|
||||
auto iter = std::partition(vec.begin(), vec.end(), [] (const auto &x) { return !x.marked_for_deletion(); });
|
||||
vec.erase(iter, vec.end());
|
||||
}
|
||||
|
||||
|
||||
} // namespace drvutil
|
||||
|
||||
#endif // DRVUTIL_HPP
|
||||
116
luprex/cpp/drv/osdrvutil.cpp
Normal file
116
luprex/cpp/drv/osdrvutil.cpp
Normal file
@@ -0,0 +1,116 @@
|
||||
#include "osdrvutil.hpp"
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <time.h>
|
||||
#elif defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#include <profileapi.h>
|
||||
#else
|
||||
#error "Only support __linux__ or _WIN32"
|
||||
#endif
|
||||
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
|
||||
// strerror has to be the most overcomplicated function imaginable. The simple
|
||||
// version, 'strerror', is not thread-safe, and the improved versions are all
|
||||
// incompatible from OS to OS. Even different versions of linux aren't
|
||||
// compatible. A lot of conditional compilation is needed.
|
||||
|
||||
#if defined(__linux__)
|
||||
|
||||
inline static void strerror_helper(int status, int errnum, char errbuf[256]) {
|
||||
if (status != 0) {
|
||||
snprintf(errbuf, 256, "unknown errno %d", errnum);
|
||||
}
|
||||
}
|
||||
|
||||
inline static void strerror_helper(const char *result, int errnum, char errbuf[256]) {
|
||||
if (result != errbuf) {
|
||||
snprintf(errbuf, 256, "%s", result);
|
||||
}
|
||||
}
|
||||
|
||||
static void strerror_safe(int errnum, char errbuf[256]) {
|
||||
auto rval = strerror_r(errnum, errbuf, 256);
|
||||
strerror_helper(rval, errnum, errbuf);
|
||||
}
|
||||
|
||||
#elif defined(_WIN32)
|
||||
|
||||
static void strerror_safe(int errnum, char errbuf[256]) {
|
||||
int status = strerror_s(errbuf, 256, errnum);
|
||||
if (status != 0) {
|
||||
snprintf(errbuf, 256, "unknown errno %d", errnum);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// The monotonic clock is required to start at zero at initialization time,
|
||||
// advance steadily, and never go backwards. It is okay, however, if it is a
|
||||
// little inaccurate, or if it drifts a little over time.
|
||||
|
||||
#if defined(__linux__)
|
||||
|
||||
class MonoClock {
|
||||
private:
|
||||
struct timespec base_;
|
||||
public:
|
||||
MonoClock() {
|
||||
int status = clock_gettime(CLOCK_MONOTONIC, &base_);
|
||||
assert(status == 0);
|
||||
}
|
||||
double get() {
|
||||
struct timespec t;
|
||||
int status = clock_gettime(CLOCK_MONOTONIC, &t);
|
||||
assert(status == 0);
|
||||
double tv_sec = t.tv_sec - base_.tv_sec;
|
||||
double tv_nsec = t.tv_nsec - base_.tv_nsec;
|
||||
return tv_sec + (tv_nsec * 1.0E-9);
|
||||
}
|
||||
};
|
||||
|
||||
#elif defined(_WIN32)
|
||||
|
||||
class MonoClock {
|
||||
public:
|
||||
double freq_;
|
||||
LONGLONG base_;
|
||||
inline LONGLONG qpc() {
|
||||
LARGE_INTEGER x;
|
||||
BOOL status = QueryPerformanceCounter(&x);
|
||||
assert(status != 0);
|
||||
return x.QuadPart;
|
||||
}
|
||||
MonoClock() {
|
||||
LARGE_INTEGER x;
|
||||
BOOL status = QueryPerformanceFrequency(&x);
|
||||
assert(status != 0);
|
||||
freq_ = 1.0 / double(x.QuadPart);
|
||||
base_ = qpc();
|
||||
}
|
||||
double get() {
|
||||
return (qpc() - base_) * freq_;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
namespace drvutil {
|
||||
|
||||
static MonoClock monoclock;
|
||||
double get_monotonic_clock() {
|
||||
return monoclock.get();
|
||||
}
|
||||
|
||||
std::string strerror_str(int errnum) {
|
||||
char buf[256];
|
||||
strerror_safe(errnum, buf);
|
||||
return buf;
|
||||
|
||||
}
|
||||
} // namespace drvutil
|
||||
23
luprex/cpp/drv/osdrvutil.hpp
Normal file
23
luprex/cpp/drv/osdrvutil.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#ifndef OSDRVUTIL_HPP
|
||||
#define OSDRVUTIL_HPP
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace drvutil {
|
||||
|
||||
// Get a system error message, in an OS-independent manner.
|
||||
//
|
||||
std::string strerror_str(int errnum);
|
||||
|
||||
// Get the amount of time elapsed since program start.
|
||||
//
|
||||
// This is guaranteed to be monotonically increasing. It is not
|
||||
// guaranteed to be accurate. Error could gradually accumulate over
|
||||
// time.
|
||||
//
|
||||
double get_monotonic_clock();
|
||||
|
||||
} // namespace drvutil
|
||||
|
||||
#endif // OSDRVUTIL_HPP
|
||||
|
||||
111
luprex/cpp/drv/readline.cpp
Normal file
111
luprex/cpp/drv/readline.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
#include "readline.hpp"
|
||||
|
||||
#define MAXLINE 512
|
||||
|
||||
static std::u32string n_backspaces(int n) {
|
||||
std::u32string result(3 * n, 0);
|
||||
for (int i = 0; i < n; i++) {
|
||||
result[i*3 + 0] = '\b';
|
||||
result[i*3 + 1] = ' ';
|
||||
result[i*3 + 2] = '\b';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static int common_prefix_length(const std::u32string &a, const std::u32string &b) {
|
||||
int minlen = std::min(a.size(), b.size());
|
||||
for (int i = 0; i < minlen; i++) {
|
||||
if (a[i] != b[i]) return i;
|
||||
}
|
||||
return minlen;
|
||||
}
|
||||
|
||||
void ReadlineDevice::set_print_callback(print_callback cb) {
|
||||
print_cb_ = cb;
|
||||
}
|
||||
|
||||
void ReadlineDevice::set_prompt(const std::u32string &prompt) {
|
||||
desired_prompt_ = prompt;
|
||||
echo_command();
|
||||
}
|
||||
|
||||
void ReadlineDevice::erase_command() {
|
||||
int ccsize = current_prompt_.size() + current_command_.size();
|
||||
if (ccsize > 0) {
|
||||
print_cb_(n_backspaces(ccsize));
|
||||
current_prompt_.clear();
|
||||
current_command_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void ReadlineDevice::echo_command() {
|
||||
// If the prompt has changed, erase everything and start over.
|
||||
if (desired_prompt_ != current_prompt_) {
|
||||
int ccsize = current_prompt_.size() + current_command_.size();
|
||||
print_cb_(n_backspaces(ccsize));
|
||||
print_cb_(desired_prompt_);
|
||||
current_command_.clear();
|
||||
current_prompt_ = desired_prompt_;
|
||||
}
|
||||
|
||||
// Find out how much of the command matches.
|
||||
int match = common_prefix_length(current_command_, desired_command_);
|
||||
|
||||
// Echo backspaces to remove the non-matching part.
|
||||
int remove = current_command_.size() - match;
|
||||
if (remove > 0) {
|
||||
print_cb_(n_backspaces(remove));
|
||||
current_command_ = current_command_.substr(0, match);
|
||||
}
|
||||
|
||||
// Echo the new part.
|
||||
std::u32string newpart = desired_command_.substr(current_command_.size());
|
||||
if (!newpart.empty()) {
|
||||
print_cb_(newpart);
|
||||
current_command_ = desired_command_;
|
||||
}
|
||||
}
|
||||
|
||||
std::u32string ReadlineDevice::putcode(char32_t c) {
|
||||
if ((c == '\n') && (readline_lastc_ == '\r')) {
|
||||
// Ignore newline immediately after carriage return.
|
||||
// Otherwise, crlf produces two newlines.
|
||||
return std::u32string();
|
||||
} else if ((c == '\r') || (c == '\n')) {
|
||||
std::u32string white(1, ' ');
|
||||
std::u32string newline(1, '\n');
|
||||
echo_command();
|
||||
print_cb_(white + newline);
|
||||
std::u32string result = desired_command_ + newline;
|
||||
desired_command_.clear();
|
||||
current_prompt_.clear();
|
||||
current_command_.clear();
|
||||
echo_command();
|
||||
return result;
|
||||
} else if ((c == '\b') || (c == 127)) {
|
||||
int len = desired_command_.size();
|
||||
if (len > 0) {
|
||||
desired_command_ = desired_command_.substr(0, len-1);
|
||||
}
|
||||
echo_command();
|
||||
return std::u32string();
|
||||
} else if ((c >= 32)&&(c <= 0x10FFFF)) {
|
||||
int len = desired_command_.size();
|
||||
if (len < MAXLINE) {
|
||||
desired_command_ = desired_command_ + c;
|
||||
}
|
||||
echo_command();
|
||||
return std::u32string();
|
||||
}
|
||||
readline_lastc_ = c;
|
||||
return std::u32string();
|
||||
}
|
||||
|
||||
void ReadlineDevice::print(const std::u32string &s) {
|
||||
if (!s.empty()) {
|
||||
erase_command();
|
||||
print_cb_(s);
|
||||
echo_command();
|
||||
}
|
||||
}
|
||||
|
||||
43
luprex/cpp/drv/readline.hpp
Normal file
43
luprex/cpp/drv/readline.hpp
Normal file
@@ -0,0 +1,43 @@
|
||||
|
||||
#ifndef READLINE_HPP
|
||||
#define READLINE_HPP
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include "drvutil.hpp"
|
||||
|
||||
|
||||
class ReadlineDevice {
|
||||
public:
|
||||
using print_callback = void (*)(const std::u32string &text);
|
||||
|
||||
private:
|
||||
print_callback print_cb_;
|
||||
std::u32string desired_command_;
|
||||
std::u32string current_command_;
|
||||
std::u32string desired_prompt_;
|
||||
std::u32string current_prompt_;
|
||||
char32_t readline_lastc_;
|
||||
|
||||
void erase_command();
|
||||
void echo_command();
|
||||
|
||||
|
||||
public:
|
||||
// The callback must be set before using the readline device.
|
||||
void set_print_callback(print_callback cb);
|
||||
|
||||
// change the prompt.
|
||||
void set_prompt(const std::u32string &prompt);
|
||||
|
||||
// Use this to print anything on the console.
|
||||
void print(const std::u32string &cps);
|
||||
|
||||
// Whenever the user types a character, call 'putcode'. If the code is
|
||||
// newline, this returns the line of text that was entered, including the
|
||||
// newline. Otherwise returns empty string. Backspace is handled here.
|
||||
std::u32string putcode(char32_t codepoint);
|
||||
};
|
||||
|
||||
|
||||
#endif // READLINE_HPP
|
||||
217
luprex/cpp/drv/sslutil.cpp
Normal file
217
luprex/cpp/drv/sslutil.cpp
Normal file
@@ -0,0 +1,217 @@
|
||||
#include "drvutil.hpp"
|
||||
#include "osdrvutil.hpp"
|
||||
#include "sslutil.hpp"
|
||||
#include <iostream>
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
|
||||
namespace sslutil {
|
||||
|
||||
const char *dummy_cert =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIDezCCAmOgAwIBAgIUajKmxrLMr9zBMlphrTJU5qKG8FgwDQYJKoZIhvcNAQEL\n"
|
||||
"BQAwTDELMAkGA1UEBhMCVVMxFTATBgNVBAgMDFBlbm5zeWx2YW5pYTESMBAGA1UE\n"
|
||||
"CgwJbG9jYWxob3N0MRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMjIwMzIyMTczMzA4\n"
|
||||
"WhgPMjEyMjAyMjYxNzMzMDhaMEwxCzAJBgNVBAYTAlVTMRUwEwYDVQQIDAxQZW5u\n"
|
||||
"c3lsdmFuaWExEjAQBgNVBAoMCWxvY2FsaG9zdDESMBAGA1UEAwwJbG9jYWxob3N0\n"
|
||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5OWIaKqYae4nPxvu5EP3\n"
|
||||
"VilcjApYcMT4+2ypfQoB6PEep5lwguA929rNsTKnhGsEiQAZ0eZPEZN7VhUwf/hz\n"
|
||||
"26jIyTT43ELkt6k97wwSZSXuT65RpSiemwEs6g2mMwzpgP6nv+yam4HjE9AKiHGN\n"
|
||||
"YeTV72Nw1EN70t6IjIf4jsJRXqDJkUx5sSSD6j0WBTOhzozIDgZHTDwiLhatE66m\n"
|
||||
"SNoD8oWC0PscbUgOJkFpbaCAS8RJmpsdgkTFae2rzL9cOFLGw6OgV/BV1J1s0ks8\n"
|
||||
"+veoMMtIO6fese+OZ+DyQbuGaoaltZUXzY6QjD5l34m2mGplelT7BrpcqJTBHwmh\n"
|
||||
"CwIDAQABo1MwUTAdBgNVHQ4EFgQUXQM5TVfJ9gpUXg8fZ8yfuUVcBP8wHwYDVR0j\n"
|
||||
"BBgwFoAUXQM5TVfJ9gpUXg8fZ8yfuUVcBP8wDwYDVR0TAQH/BAUwAwEB/zANBgkq\n"
|
||||
"hkiG9w0BAQsFAAOCAQEAqYX/ZGv0Qh/xdXppjnqojm8mH0giDW4tvwMqHcW3YRa3\n"
|
||||
"9J2yYot+rHjU5g4n6HEmWDBE0eqLz9n3Y3fkFzT8RWZwBaST965CgsfGofyuA2hC\n"
|
||||
"Ddn4Am3B5tTPmi8WWRZg8amhpGVD/mwkoVFIK0M337b1aZUJYPE+Kc9WetSL2KqB\n"
|
||||
"EhqSQpkAWhVadzP85dq2T9EDjAvhlFTFlDEBx1GDUcc8M0KQ9NEvLT7LgoUcbMiT\n"
|
||||
"PerlSZQTB0crchXTRSERgiwu80r7D6STn/RcPL9Fg5PkA94/d87jGbmV4sxSRsvM\n"
|
||||
"z+DnJGjHrV1J/jHPrnVvVLpigBlGno3C5O/sRw3gcQ==\n"
|
||||
"-----END CERTIFICATE-----\n";
|
||||
|
||||
const char *dummy_key =
|
||||
"-----BEGIN PRIVATE KEY-----\n"
|
||||
"MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDk5Yhoqphp7ic/\n"
|
||||
"G+7kQ/dWKVyMClhwxPj7bKl9CgHo8R6nmXCC4D3b2s2xMqeEawSJABnR5k8Rk3tW\n"
|
||||
"FTB/+HPbqMjJNPjcQuS3qT3vDBJlJe5PrlGlKJ6bASzqDaYzDOmA/qe/7JqbgeMT\n"
|
||||
"0AqIcY1h5NXvY3DUQ3vS3oiMh/iOwlFeoMmRTHmxJIPqPRYFM6HOjMgOBkdMPCIu\n"
|
||||
"Fq0TrqZI2gPyhYLQ+xxtSA4mQWltoIBLxEmamx2CRMVp7avMv1w4UsbDo6BX8FXU\n"
|
||||
"nWzSSzz696gwy0g7p96x745n4PJBu4ZqhqW1lRfNjpCMPmXfibaYamV6VPsGulyo\n"
|
||||
"lMEfCaELAgMBAAECggEBAJa1AiFX4U4tva1xqNKmZV1XklWqIhzts7lnDBkF08gZ\n"
|
||||
"qcNT5Z5mIpR09eVropwvEidZ56Yp63l5D0XYYbyAS1gfQ0QnGot7h7fdOKgB3MK4\n"
|
||||
"PLY94gfKPNN17KqWHg2SvNNv1+cn04v78xUCb0zy5tHDp5Acexdm70ohtupARElJ\n"
|
||||
"LSHdS7ebsqZUFXbbM3BpPEsQLi3PrzNs1DrKkZ3rR6eMGrsDqExXx8/foi9aZKsd\n"
|
||||
"BGM2/kcTJ5aY6NhSv5iqO1oK46sbMrjVW/bYNsOyl0eFjwTRahn+Zhp/JMewZYeu\n"
|
||||
"715g6kzbZNwEzBLgrhNPF6E2ycEr/C6z5bE78g5QCkECgYEA8s07UUY25bjYiWWy\n"
|
||||
"W38pT7d/OXBSyKnq16N6MjVahl29r7nezFiDeLhLC0QiwXu/+qyxVZkB95MMGZXS\n"
|
||||
"AsaKFNis3AJ6eR4SYyhpSScYKNvlKIiW37TtR4FDcy7y5LL6tFpiDDIGH3LuyWNo\n"
|
||||
"d76142MBpv5aStnLGYU3pcZj43sCgYEA8VbNM4nqgSCQcbnHYjvsgphEMNSaoVie\n"
|
||||
"xob2uigXdV6Te0ayoUFBnVNKVsRhk+sswuTV4k1pK/On+USVl2tQ16tcaVMjTfSD\n"
|
||||
"HLYTJLmt6s4DcywWj5dfkbDoe5PulGXNZE960qXmOC62Lf0VMRwJ5x4FBRvGTjKC\n"
|
||||
"zvekI2/kO7ECgYEAhBGeclb/BXXGUvY+TgadMf9d9KBkZ0IFu8Xwcd8TnoLe6vbv\n"
|
||||
"ebery75zE228egIWKwREcYsIxuH1cvVLhrb35N73J7UxaTAyUD1rB598RL1XqPSj\n"
|
||||
"HIwNhReK2NxwwnWYaQHA02FiczjRKjooWPojdcwk2fEArDZLg1YzLrj7HIECgYEA\n"
|
||||
"htdx1Y8ESFtyeShMv5UtoxYCW6oeL3H9XH0CE6bc3IYYLvOkULbOO2HTEkGtJ2Fp\n"
|
||||
"5AbJfiS0U4tS2dI5Jp4eUDH9cxexjRfFvd/5ODbKdnver5X9kQMJsbQ/YPSZg66R\n"
|
||||
"oK9Lt7Bbvh5TScSy93psCgba1SzckspkDdGNkwMsaTECgYEAnFWaxormLUpXQRLs\n"
|
||||
"tKzMMHgVnHlsHiqXH432zmT2fpGZHYoWbsGuQjjrHGnSiu3QbDhnzM6y/T2GRs6z\n"
|
||||
"zHteIo/tzIyxg4MvJGJ9qANA7HoiKBdQ7G/I/NLJIyWAjj+e7/hgzKFcf+dpjpDq\n"
|
||||
"HcKc9a4WXhC7yu79e5BnKWltHXY=\n"
|
||||
"-----END PRIVATE KEY-----\n";
|
||||
|
||||
std::string error_string() {
|
||||
// Get the last code.
|
||||
int code = 0;
|
||||
while (true) {
|
||||
int icode = ERR_get_error();
|
||||
if (icode == 0) break;
|
||||
code = icode;
|
||||
}
|
||||
|
||||
// Fetch and clear errno.
|
||||
int terrno = errno;
|
||||
errno = 0;
|
||||
|
||||
if (code != 0) {
|
||||
const char *rc = ERR_reason_error_string(code);
|
||||
if (rc != nullptr) {
|
||||
return rc;
|
||||
} else {
|
||||
return drvutil::strerror_str(ERR_GET_REASON(code));
|
||||
}
|
||||
} else if (terrno != 0) {
|
||||
return drvutil::strerror_str(terrno);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the path only contains printable ascii characters,
|
||||
// and no control characters or unicode characters.
|
||||
std::string path_to_plain_printable_ascii(const std::filesystem::path &path) {
|
||||
auto s = path.native();
|
||||
std::string result(' ', s.size());
|
||||
for (int i = 0; i < int(s.size()); i++) {
|
||||
auto c = s[i];
|
||||
if ((c < 32) || (c > 126)) return "";
|
||||
result[i] = c;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void clear_all_errors() {
|
||||
ERR_clear_error();
|
||||
errno = 0;
|
||||
}
|
||||
|
||||
SSL_CTX *new_context(int verify) {
|
||||
SSL_CTX *ctx = SSL_CTX_new(TLS_method());
|
||||
SSL_CTX_set_mode(ctx, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
|
||||
SSL_CTX_set_mode(ctx, SSL_MODE_ENABLE_PARTIAL_WRITE);
|
||||
SSL_CTX_set_verify(ctx, verify, nullptr);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
static int ctx_use_certificate_str(SSL_CTX *ctx, const char *str) {
|
||||
UniqueBIO bio(BIO_new(BIO_s_mem()));
|
||||
BIO_puts(bio.get(), str);
|
||||
UniqueX509 certificate(PEM_read_bio_X509(bio.get(), NULL, NULL, NULL));
|
||||
return SSL_CTX_use_certificate(ctx, certificate.get());
|
||||
}
|
||||
|
||||
static int ctx_use_privatekey_str(SSL_CTX *ctx, const char *str) {
|
||||
UniqueBIO bio(BIO_new(BIO_s_mem()));
|
||||
BIO_puts(bio.get(), str);
|
||||
UniquePKEY pkey(PEM_read_bio_PrivateKey(bio.get(), NULL, NULL, NULL));
|
||||
return SSL_CTX_use_PrivateKey(ctx, pkey.get());
|
||||
}
|
||||
|
||||
void ctx_load_dummy_cert(SSL_CTX *ctx) {
|
||||
ERR_clear_error();
|
||||
if (ctx_use_certificate_str(ctx, dummy_cert) <= 0) {
|
||||
ERR_print_errors_fp(stderr);
|
||||
exit(1);
|
||||
}
|
||||
if (ctx_use_privatekey_str(ctx, dummy_key) <= 0) {
|
||||
ERR_print_errors_fp(stderr);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
static int count_certificates(const char *fn) {
|
||||
static char null_passwd;
|
||||
ErrClearErrorOnExit ece;
|
||||
UniqueBIO bio(BIO_new(BIO_s_file()));
|
||||
assert(bio != nullptr);
|
||||
if (BIO_read_filename(bio.get(), fn) <= 0) {
|
||||
std::cerr << "Cannot open file: " << fn << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
int total = 0;
|
||||
while (true) {
|
||||
UniqueX509 x(PEM_read_bio_X509_AUX(bio.get(), nullptr, nullptr, &null_passwd));
|
||||
if (x == nullptr) break;
|
||||
total += 1;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
static bool contains_privatekey(const char *fn) {
|
||||
static char null_passwd;
|
||||
ErrClearErrorOnExit ece;
|
||||
UniqueBIO bio(BIO_new(BIO_s_file()));
|
||||
assert(bio != nullptr);
|
||||
if (BIO_read_filename(bio.get(), fn) <= 0) {
|
||||
std::cerr << "Cannot open file: " << fn << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
UniquePKEY k(PEM_read_bio_PrivateKey(bio.get(), nullptr, nullptr, &null_passwd));
|
||||
return k != nullptr;
|
||||
}
|
||||
|
||||
void ctx_load_cert_from_directory(SSL_CTX *ctx, const std::string &dir) {
|
||||
std::vector<std::string> key_paths;
|
||||
std::vector<std::string> cert_paths;
|
||||
|
||||
for (const auto & entry : std::filesystem::directory_iterator(dir)) {
|
||||
std::string fn = path_to_plain_printable_ascii(entry.path());
|
||||
if (fn.empty()) {
|
||||
std::cerr << "Ignoring file with non-ascii filename: " << entry.path() << std::endl;
|
||||
} else {
|
||||
if (count_certificates(fn.c_str()) >= 1) {
|
||||
cert_paths.push_back(fn);
|
||||
}
|
||||
if (contains_privatekey(fn.c_str())) {
|
||||
key_paths.push_back(fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cert_paths.size() > 1) {
|
||||
std::cerr << "Directory contains multiple certs: " << dir << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
if (key_paths.size() > 1) {
|
||||
std::cerr << "Directory contains multiple keys: " << dir << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
if (cert_paths.empty()) {
|
||||
std::cerr << "Directory doesn't contain a cert: " << dir << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
if (key_paths.empty()) {
|
||||
std::cerr << "Directory doesn't contain a key: " << dir << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
int status;
|
||||
status = SSL_CTX_use_PrivateKey_file(ctx, key_paths[0].c_str(), SSL_FILETYPE_PEM);
|
||||
assert(status == 1);
|
||||
status = SSL_CTX_use_certificate_chain_file(ctx, cert_paths[0].c_str());
|
||||
assert(status == 1);
|
||||
}
|
||||
|
||||
} // namespace sslutil
|
||||
|
||||
59
luprex/cpp/drv/sslutil.hpp
Normal file
59
luprex/cpp/drv/sslutil.hpp
Normal file
@@ -0,0 +1,59 @@
|
||||
#ifndef SSLUTIL_HPP
|
||||
#define SSLUTIL_HPP
|
||||
|
||||
#include <openssl/ssl.h>
|
||||
#include <openssl/rsa.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/conf.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace sslutil {
|
||||
|
||||
struct SSL_Deleter {
|
||||
void operator()(SSL *ssl) { SSL_free(ssl); }
|
||||
};
|
||||
|
||||
struct CTX_Deleter {
|
||||
void operator()(SSL_CTX *ctx) { SSL_CTX_free(ctx); }
|
||||
};
|
||||
|
||||
struct BIO_Deleter {
|
||||
void operator()(BIO *bio) { BIO_free(bio); }
|
||||
};
|
||||
|
||||
struct X509_Deleter {
|
||||
void operator()(X509 *x) { X509_free(x); }
|
||||
};
|
||||
|
||||
struct PKEY_Deleter {
|
||||
void operator()(EVP_PKEY *p) { EVP_PKEY_free(p); }
|
||||
};
|
||||
|
||||
using UniqueSSL = std::unique_ptr<SSL, SSL_Deleter>;
|
||||
using UniqueCTX = std::unique_ptr<SSL_CTX, CTX_Deleter>;
|
||||
using UniqueBIO = std::unique_ptr<BIO, BIO_Deleter>;
|
||||
using UniqueX509 = std::unique_ptr<X509, X509_Deleter>;
|
||||
using UniquePKEY = std::unique_ptr<EVP_PKEY, PKEY_Deleter>;
|
||||
|
||||
struct ErrClearErrorOnExit {
|
||||
~ErrClearErrorOnExit() {
|
||||
ERR_clear_error();
|
||||
}
|
||||
};
|
||||
|
||||
// Return the OpenSSL error as a string.
|
||||
std::string error_string();
|
||||
void clear_all_errors();
|
||||
SSL_CTX *new_context(int verify);
|
||||
void ctx_load_dummy_cert(SSL_CTX *ctx);
|
||||
void ctx_load_cert_from_directory(SSL_CTX *ctx, const std::string &dir);
|
||||
|
||||
} // namespace sslutil
|
||||
|
||||
#endif // SSLUTIL_HPP
|
||||
|
||||
19
luprex/cpp/wrap/mkstub.py
Executable file
19
luprex/cpp/wrap/mkstub.py
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import sys
|
||||
|
||||
base=sys.argv[1]
|
||||
ubase=base.upper()
|
||||
dash=base.replace("_", "-")
|
||||
|
||||
with open(f"wrap-{dash}.hpp", "w") as f:
|
||||
print(f"#ifndef WRAP_{ubase}_HPP", file=f)
|
||||
print(f"#define WRAP_{ubase}_HPP", file=f)
|
||||
print("", file=f)
|
||||
print('#include "eng-malloc.hpp"', file=f)
|
||||
print(f"#include <{base}>", file=f)
|
||||
print("", file=f)
|
||||
print("namespace eng {", file=f)
|
||||
print("} // namespace eng", file=f)
|
||||
print("", file=f)
|
||||
print(f"#endif // WRAP_{ubase}_HPP", file=f)
|
||||
14
luprex/cpp/wrap/wrap-bytell-hash-map.hpp
Normal file
14
luprex/cpp/wrap/wrap-bytell-hash-map.hpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef WRAP_BYTELL_HASH_MAP_HPP
|
||||
#define WRAP_BYTELL_HASH_MAP_HPP
|
||||
|
||||
#include "eng-malloc.hpp"
|
||||
#include "bytell-hash-map.hpp"
|
||||
|
||||
namespace eng {
|
||||
template<class K, class V, class H=std::hash<K>, class E=std::equal_to<K>>
|
||||
class bytell_hash_map : public ska::bytell_hash_map<K, V, H, E, eng::allocator<std::pair<const K, V>>>, public eng::opnew {
|
||||
using ska::bytell_hash_map<K, V, H, E, eng::allocator<std::pair<const K, V>>>::bytell_hash_map;
|
||||
};
|
||||
} // namespace eng
|
||||
|
||||
#endif // WRAP_BYTELL_HASH_MAP_HPP
|
||||
14
luprex/cpp/wrap/wrap-deque.hpp
Normal file
14
luprex/cpp/wrap/wrap-deque.hpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef WRAP_DEQUE_HPP
|
||||
#define WRAP_DEQUE_HPP
|
||||
|
||||
#include "eng-malloc.hpp"
|
||||
#include <deque>
|
||||
|
||||
namespace eng {
|
||||
template<class T>
|
||||
class deque : public std::deque<T, eng::allocator<T>>, public eng::opnew {
|
||||
using std::deque<T, eng::allocator<T>>::deque;
|
||||
};
|
||||
} // namespace eng
|
||||
|
||||
#endif // WRAP_DEQUE_HPP
|
||||
14
luprex/cpp/wrap/wrap-map.hpp
Normal file
14
luprex/cpp/wrap/wrap-map.hpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef WRAP_MAP_HPP
|
||||
#define WRAP_MAP_HPP
|
||||
|
||||
#include "eng-malloc.hpp"
|
||||
#include <map>
|
||||
|
||||
namespace eng {
|
||||
template<class K, class V, class C=std::less<K>>
|
||||
class map : public std::map<K, V, C, eng::allocator<std::pair<const K, V>>>, eng::opnew {
|
||||
using std::map<K, V, C, eng::allocator<std::pair<const K, V>>>::map;
|
||||
};
|
||||
} // namespace eng
|
||||
|
||||
#endif // WRAP_MAP_HPP
|
||||
14
luprex/cpp/wrap/wrap-set.hpp
Normal file
14
luprex/cpp/wrap/wrap-set.hpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef WRAP_SET_HPP
|
||||
#define WRAP_SET_HPP
|
||||
|
||||
#include "eng-malloc.hpp"
|
||||
#include <set>
|
||||
|
||||
namespace eng {
|
||||
template<class K, class C=std::less<K>>
|
||||
class set : public std::set<K, C, eng::allocator<K>>, public eng::opnew {
|
||||
using std::set<K, C, eng::allocator<K>>::set;
|
||||
};
|
||||
} // namespace eng
|
||||
|
||||
#endif // WRAP_SET_HPP
|
||||
21
luprex/cpp/wrap/wrap-sstream.hpp
Normal file
21
luprex/cpp/wrap/wrap-sstream.hpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef WRAP_SSTREAM_HPP
|
||||
#define WRAP_SSTREAM_HPP
|
||||
|
||||
#include "eng-malloc.hpp"
|
||||
#include "wrap-string.hpp"
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
|
||||
namespace eng {
|
||||
template<class C, class T=std::char_traits<C>>
|
||||
class basic_ostringstream : public std::basic_ostringstream<C, T, eng::allocator<C>>, public eng::opnew {
|
||||
using underlying = std::basic_ostringstream<C, T, eng::allocator<C>>;
|
||||
using underlying::basic_ostringstream;
|
||||
};
|
||||
//template<class C, class T=std::char_traits<C>>
|
||||
//using basic_stringbuf = std::basic_stringbuf<C, T, eng::allocator<C>>;
|
||||
using ostringstream = basic_ostringstream<char>;
|
||||
//using stringbuf = basic_stringbuf<char>;
|
||||
} // namespace eng
|
||||
|
||||
#endif // WRAP_SSTREAM_HPP
|
||||
13
luprex/cpp/wrap/wrap-string.hpp
Normal file
13
luprex/cpp/wrap/wrap-string.hpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#ifndef WRAP_STRING_HPP
|
||||
#define WRAP_STRING_HPP
|
||||
|
||||
#include "eng-malloc.hpp"
|
||||
#include <string>
|
||||
|
||||
namespace eng {
|
||||
template<class C, class T=std::char_traits<C>>
|
||||
using basic_string = std::basic_string<C, T, eng::allocator<C>>;
|
||||
using string = basic_string<char>;
|
||||
} // namespace eng
|
||||
|
||||
#endif // WRAP_STRING_HPP
|
||||
14
luprex/cpp/wrap/wrap-unordered-map.hpp
Normal file
14
luprex/cpp/wrap/wrap-unordered-map.hpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef WRAP_UNORDERED_MAP_HPP
|
||||
#define WRAP_UNORDERED_MAP_HPP
|
||||
|
||||
#include "eng-malloc.hpp"
|
||||
#include <unordered_map>
|
||||
|
||||
namespace eng {
|
||||
template<class K, class V, class H=std::hash<K>, class E=std::equal_to<K>>
|
||||
class unordered_map : public std::unordered_map<K, V, H, E, eng::allocator<std::pair<const K, V>>>, public eng::opnew {
|
||||
using std::unordered_map<K, V, H, E, eng::allocator<std::pair<const K, V>>>::unordered_map;
|
||||
};
|
||||
} // namespace eng
|
||||
|
||||
#endif // WRAP_UNORDERED_MAP_HPP
|
||||
14
luprex/cpp/wrap/wrap-unordered-set.hpp
Normal file
14
luprex/cpp/wrap/wrap-unordered-set.hpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef WRAP_UNORDERED_SET_HPP
|
||||
#define WRAP_UNORDERED_SET_HPP
|
||||
|
||||
#include "eng-malloc.hpp"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace eng {
|
||||
template<class K, class H=std::hash<K>, class E=std::equal_to<K>>
|
||||
class unordered_set : public std::unordered_set<K, H, E, eng::allocator<K>>, public eng::opnew {
|
||||
using std::unordered_set<K, H, E, eng::allocator<K>>::unordered_set;
|
||||
};
|
||||
} // namespace eng
|
||||
|
||||
#endif // WRAP_UNORDERED_SET_HPP
|
||||
14
luprex/cpp/wrap/wrap-vector.hpp
Normal file
14
luprex/cpp/wrap/wrap-vector.hpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef WRAP_VECTOR_HPP
|
||||
#define WRAP_VECTOR_HPP
|
||||
|
||||
#include "eng-malloc.hpp"
|
||||
#include <vector>
|
||||
|
||||
namespace eng {
|
||||
template<class T>
|
||||
class vector : public std::vector<T, eng::allocator<T>>, public eng::opnew {
|
||||
using std::vector<T, eng::allocator<T>>::vector;
|
||||
};
|
||||
} // namespace eng
|
||||
|
||||
#endif // WRAP_VECTOR_HPP
|
||||
0
luprex/diffs
Normal file
0
luprex/diffs
Normal file
771
luprex/ext/base-buffer.hpp
Normal file
771
luprex/ext/base-buffer.hpp
Normal file
@@ -0,0 +1,771 @@
|
||||
#pragma once
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// IMPORTANT: This is a header-only library that is included
|
||||
// by the graphics engine as well. It cannot contain references
|
||||
// to anything else in the engine.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <string_view>
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
//
|
||||
// SimpleDynamic
|
||||
//
|
||||
// A struct that holds a dynamically typed value.
|
||||
// This can hold a string, number, vector, or boolean.
|
||||
//
|
||||
// The type is stored in the 'type' field.
|
||||
//
|
||||
// If it's a STRING, the value is in the field s
|
||||
// If it's a NUMBER, the value is in the field x
|
||||
// If it's a BOOLEAN, it's true if (x==1.0)
|
||||
// If it's a VECTOR, the value is in x,y,z
|
||||
//
|
||||
///////////////////////////////////////////////////////////////
|
||||
|
||||
enum class SimpleDynamicTag {
|
||||
UNINITIALIZED,
|
||||
AUTO,
|
||||
STRING,
|
||||
NUMBER,
|
||||
BOOLEAN,
|
||||
VECTOR,
|
||||
};
|
||||
|
||||
template<class STRING>
|
||||
struct SimpleDynamic {
|
||||
using string = STRING;
|
||||
SimpleDynamicTag type;
|
||||
double x, y, z;
|
||||
string s;
|
||||
|
||||
SimpleDynamic() {
|
||||
type = SimpleDynamicTag::UNINITIALIZED;
|
||||
x=y=z=0;
|
||||
}
|
||||
|
||||
static const char *type_name_of(SimpleDynamicTag t) {
|
||||
switch (t) {
|
||||
case SimpleDynamicTag::UNINITIALIZED: return "uninitialized";
|
||||
case SimpleDynamicTag::BOOLEAN: return "boolean";
|
||||
case SimpleDynamicTag::NUMBER: return "number";
|
||||
case SimpleDynamicTag::STRING: return "string";
|
||||
case SimpleDynamicTag::VECTOR: return "vector";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
const char *type_name() const {
|
||||
return type_name_of(type);
|
||||
}
|
||||
|
||||
void set_uninitialized() {
|
||||
type=SimpleDynamicTag::UNINITIALIZED; s.clear(); x=y=z=0;
|
||||
}
|
||||
|
||||
void set_auto() {
|
||||
type=SimpleDynamicTag::AUTO; s.clear(); x=y=z=0;
|
||||
}
|
||||
|
||||
void set_string(std::string_view is) {
|
||||
type=SimpleDynamicTag::STRING; s=is; x=y=z=0;
|
||||
}
|
||||
|
||||
void set_number(double n) {
|
||||
type = SimpleDynamicTag::NUMBER; s.clear(); x=n; y=z=0;
|
||||
}
|
||||
|
||||
void set_boolean(bool b) {
|
||||
type = SimpleDynamicTag::BOOLEAN; s.clear(); x=(b?1:0); y=z=0;
|
||||
}
|
||||
|
||||
void set_vector(double ix, double iy, double iz) {
|
||||
type = SimpleDynamicTag::VECTOR; s.clear(); x=ix; y=iy; z=iz;
|
||||
}
|
||||
|
||||
void copy_value(const SimpleDynamic &other) {
|
||||
type = other.type;
|
||||
s=other.s; x=other.x; y=other.y; z=other.z;
|
||||
}
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
//
|
||||
// BaseWriter
|
||||
//
|
||||
// This base class provides the following methods:
|
||||
//
|
||||
// void write_uint8(uint64_t data)
|
||||
// void write_uint16(uint64_t data)
|
||||
// void write_uint32(uint64_t data)
|
||||
// void write_uint64(uint64_t data)
|
||||
// void write_int8(int64_t data)
|
||||
// void write_int16(int64_t data)
|
||||
// void write_int32(int64_t data)
|
||||
// void write_int64(int64_t data)
|
||||
// void write_char(char c)
|
||||
// void write_float(float data)
|
||||
// void write_double(double data)
|
||||
// void write_length(size_t data)
|
||||
// void write_string(std::string_view data)
|
||||
// void write_simple_dynamic(const SimpleDynamic &sd);
|
||||
//
|
||||
// You should derive from BaseWriter using the CRTP pattern:
|
||||
//
|
||||
// class DerivedWriter : public BaseWriter<DerivedWriter>
|
||||
//
|
||||
// You must provide two methods in the derived class:
|
||||
//
|
||||
// write_bytes(const char *n, size_t size)
|
||||
// raise_truncated()
|
||||
//
|
||||
///////////////////////////////////////////////////////////////
|
||||
|
||||
template<class Derived>
|
||||
class BaseWriter {
|
||||
protected:
|
||||
template<class T>
|
||||
void write_value_core(T arg) {
|
||||
static_cast<Derived*>(this)->write_bytes((const char *)&arg, sizeof(arg));
|
||||
}
|
||||
|
||||
template<class T, class XT>
|
||||
void write_int_core(XT arg) {
|
||||
T reduced = arg;
|
||||
if (XT(reduced) != arg) static_cast<Derived*>(this)->raise_truncated();
|
||||
write_value_core(reduced);
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
void write_uint8(uint64_t data) { write_int_core<uint8_t, uint64_t>(data); }
|
||||
void write_uint16(uint64_t data) { write_int_core<uint16_t, uint64_t>(data); }
|
||||
void write_uint32(uint64_t data) { write_int_core<uint32_t, uint64_t>(data); }
|
||||
void write_uint64(uint64_t data) { write_int_core<uint64_t, uint64_t>(data); }
|
||||
void write_int8(int64_t data) { write_int_core<int8_t, int64_t>(data); }
|
||||
void write_int16(int64_t data) { write_int_core<int16_t, int64_t>(data); }
|
||||
void write_int32(int64_t data) { write_int_core<int32_t, int64_t>(data); }
|
||||
void write_int64(int64_t data) { write_int_core<int64_t, int64_t>(data); }
|
||||
|
||||
void write_bool(bool b) { write_uint8(b ? 1:0); }
|
||||
void write_char(char c) { write_value_core(c); }
|
||||
void write_float(float arg) { write_value_core(arg); }
|
||||
void write_double(double arg) { write_value_core(arg); }
|
||||
|
||||
void write_length(size_t len) {
|
||||
if (len >= 255) {
|
||||
write_uint8(0xFF);
|
||||
write_uint64(len);
|
||||
} else {
|
||||
write_uint8(len);
|
||||
}
|
||||
}
|
||||
|
||||
void write_string(std::string_view s) {
|
||||
write_length(s.size());
|
||||
static_cast<Derived*>(this)->write_bytes(s.data(), s.size());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
//
|
||||
// BaseReader
|
||||
//
|
||||
// This base class provides the following methods:
|
||||
//
|
||||
// uint8_t read_uint8();
|
||||
// uint16_t read_uint16();
|
||||
// uint32_t read_uint32();
|
||||
// uint64_t read_uint64();
|
||||
// int8_t read_int8();
|
||||
// int16_t read_int16();
|
||||
// int32_t read_int32();
|
||||
// int64_t read_int64();
|
||||
// bool read_bool();
|
||||
// char read_char();
|
||||
// float read_float();
|
||||
// double read_double();
|
||||
// size_t read_length();
|
||||
// String read_string_limit(uint64_t size);
|
||||
// String read_string();
|
||||
// SimpleDynamic read_simple_dynamic();
|
||||
//
|
||||
// You should derive from BaseReader using the CRTP pattern:
|
||||
//
|
||||
// class DerivedReader : public BaseReader<DerivedReader>
|
||||
//
|
||||
// The derived class must provide:
|
||||
//
|
||||
// using read_string_type = std::string; // or compatible
|
||||
// void read_bytes_into(char *n, size_t size)
|
||||
// void handle_string_too_long();
|
||||
//
|
||||
// Error Handling:
|
||||
//
|
||||
// It is up to the derived class whether it wants
|
||||
// to report errors using exceptions or flags.
|
||||
//
|
||||
// If read_bytes_into discovers there's not enough bytes,
|
||||
// there are two valid options: throw an exception, OR,
|
||||
// set an error flag and fill the buffer with zeros.
|
||||
//
|
||||
// If read_string discovers that the string is longer than
|
||||
// the allowed limit, it will call handle_string_too_long.
|
||||
// This function may either throw an exception, or set an
|
||||
// error flag.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////
|
||||
|
||||
template<class Derived>
|
||||
class BaseReader {
|
||||
protected:
|
||||
template<class T>
|
||||
T read_value_core() {
|
||||
T result;
|
||||
Derived *dthis = static_cast<Derived*>(this);
|
||||
dthis->read_bytes_into((char *)(&result), sizeof(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
uint8_t read_uint8() { return read_value_core<uint8_t>(); }
|
||||
uint16_t read_uint16() { return read_value_core<uint16_t>(); }
|
||||
uint32_t read_uint32() { return read_value_core<uint32_t>(); }
|
||||
uint64_t read_uint64() { return read_value_core<uint64_t>(); }
|
||||
int8_t read_int8() { return read_value_core<int8_t>(); }
|
||||
int16_t read_int16() { return read_value_core<int16_t>(); }
|
||||
int32_t read_int32() { return read_value_core<int32_t>(); }
|
||||
int64_t read_int64() { return read_value_core<int64_t>(); }
|
||||
|
||||
bool read_bool() { return (bool)read_uint8(); }
|
||||
char read_char() { return read_value_core<char>(); }
|
||||
float read_float() { return read_value_core<float>(); }
|
||||
double read_double() { return read_value_core<double>(); }
|
||||
|
||||
size_t read_length() {
|
||||
uint64_t len = read_uint8();
|
||||
if (len == 255) {
|
||||
len = read_uint64();
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
auto read_string_limit(uint64_t limit) {
|
||||
size_t len = read_length();
|
||||
Derived *dthis = static_cast<Derived*>(this);
|
||||
if (len > limit) {
|
||||
dthis->raise_string_too_long();
|
||||
len = 0;
|
||||
}
|
||||
typename Derived::read_string_type result(len, ' ');
|
||||
dthis->read_bytes_into(&(result[0]), len);
|
||||
return result;
|
||||
}
|
||||
|
||||
auto read_string() { return read_string_limit(0x1000000); } // 16MB limit default
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Class BaseBuffer
|
||||
//
|
||||
// You must supply a CoreHandler which must define these
|
||||
// methods:
|
||||
//
|
||||
// void *basebuffer_malloc(size_t size);
|
||||
// void basebuffer_free(void *data);
|
||||
// void raise_eof_on_read();
|
||||
// void raise_string_too_long();
|
||||
// void raise_integer_truncated();
|
||||
//
|
||||
// You must also select a StringType. Typically this would
|
||||
// be std::string. This only affects the return value of
|
||||
// read_string. You can always use read_string_view to read
|
||||
// strings into other string types.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////
|
||||
|
||||
template<class CoreHandler, class StringType>
|
||||
class BaseBuffer : public CoreHandler {
|
||||
private:
|
||||
// True if we own this buffer.
|
||||
bool owned_;
|
||||
|
||||
// True if we're not allowed to expand this buffer.
|
||||
bool fixed_size_;
|
||||
|
||||
// Start and end of the allocated block.
|
||||
char *buf_lo_;
|
||||
char *buf_hi_;
|
||||
|
||||
// The write and read cursors.
|
||||
char *write_cursor_;
|
||||
char *read_cursor_;
|
||||
|
||||
// Number of bytes read before buffer was last aligned.
|
||||
int64_t pre_read_count_;
|
||||
|
||||
private:
|
||||
void init(bool fixed, bool owned, char *buf, int64_t size) {
|
||||
CoreHandler::clear_error_flags();
|
||||
owned_ = owned;
|
||||
fixed_size_ = fixed;
|
||||
buf_lo_ = buf;
|
||||
buf_hi_ = buf_lo_ + size;
|
||||
read_cursor_ = buf_lo_;
|
||||
write_cursor_ = buf_lo_;
|
||||
pre_read_count_ = 0;
|
||||
}
|
||||
|
||||
public:
|
||||
using string_type = StringType;
|
||||
|
||||
// Construct an empty buffer.
|
||||
//
|
||||
BaseBuffer() {
|
||||
init(false, true, 0, 0);
|
||||
}
|
||||
|
||||
// Construct an empty buffer, preallocate the specified amount of space.
|
||||
//
|
||||
BaseBuffer(int64_t size, bool fixed) {
|
||||
assert(size >= 0);
|
||||
init(fixed, true, (char *)CoreHandler::basebuffer_malloc(size), size);
|
||||
}
|
||||
|
||||
// Construct a streambuffer that reads from an external block of bytes.
|
||||
//
|
||||
BaseBuffer(std::string_view data) {
|
||||
init(true, false, const_cast<char *>(data.data()), data.size());
|
||||
write_cursor_ = buf_hi_;
|
||||
}
|
||||
|
||||
// Modify an existing streambuffer to read from an external block of bytes.
|
||||
//
|
||||
void open(std::string_view data) {
|
||||
if (owned_ && (buf_lo_ != 0)) CoreHandler::basebuffer_free(buf_lo_);
|
||||
init(true, false, const_cast<char *>(data.data()), data.size());
|
||||
write_cursor_ = buf_hi_;
|
||||
}
|
||||
|
||||
// Destructor. Frees the buffer, if any.
|
||||
//
|
||||
~BaseBuffer() {
|
||||
if (owned_ && (buf_lo_ != 0)) CoreHandler::basebuffer_free(buf_lo_);
|
||||
}
|
||||
|
||||
// Return the total number of bytes ever read.
|
||||
//
|
||||
int64_t total_reads() const {
|
||||
return (read_cursor_ - buf_lo_) + pre_read_count_;
|
||||
}
|
||||
|
||||
// Return the total number of bytes ever written.
|
||||
//
|
||||
int64_t total_writes() const {
|
||||
return (write_cursor_ - buf_lo_) + pre_read_count_;
|
||||
}
|
||||
|
||||
// Return the total bytes in the buffer.
|
||||
//
|
||||
int64_t fill() const {
|
||||
return write_cursor_ - read_cursor_;
|
||||
}
|
||||
|
||||
// Checks to see if the buffer is empty.
|
||||
//
|
||||
bool empty() const {
|
||||
return write_cursor_ == read_cursor_;
|
||||
}
|
||||
|
||||
// Return the contents as a string_view.
|
||||
//
|
||||
std::string_view view() const {
|
||||
return std::string_view(read_cursor_, write_cursor_ - read_cursor_);
|
||||
}
|
||||
|
||||
// Make the specified amount of space in the buffer for writing.
|
||||
//
|
||||
char *make_space(int64_t bytes) {
|
||||
int64_t available = buf_hi_ - write_cursor_;
|
||||
if (available < bytes) make_space_slow(bytes);
|
||||
return write_cursor_;
|
||||
}
|
||||
|
||||
// Used after calling make_space then filling the space.
|
||||
//
|
||||
void wrote_space(int64_t bytes) {
|
||||
int64_t available = buf_hi_ - write_cursor_;
|
||||
assert(bytes >= 0);
|
||||
assert(available >= bytes);
|
||||
write_cursor_ += bytes;
|
||||
}
|
||||
|
||||
// Rewind the read cursor to a previous position.
|
||||
//
|
||||
void unread_to(int64_t rd_count) {
|
||||
assert(rd_count >= pre_read_count_);
|
||||
assert(rd_count <= total_reads());
|
||||
read_cursor_ = buf_lo_ + (rd_count - pre_read_count_);
|
||||
}
|
||||
|
||||
// Rewind the write cursor to a previous position.
|
||||
//
|
||||
void unwrite_to(int64_t wr_count) {
|
||||
assert(wr_count >= total_reads());
|
||||
assert(wr_count <= total_writes());
|
||||
write_cursor_ = buf_lo_ + (wr_count - pre_read_count_);
|
||||
}
|
||||
|
||||
// Discard all data. Reset total read and write counts.
|
||||
// May release the allocated buffer, if it is large.
|
||||
//
|
||||
void clear() {
|
||||
if (!owned_) {
|
||||
open("");
|
||||
} else {
|
||||
if ((!fixed_size_) && (buf_lo_ != nullptr) && ((buf_hi_ - buf_lo_) > 100000)) {
|
||||
CoreHandler::basebuffer_free(buf_lo_);
|
||||
buf_lo_ = nullptr;
|
||||
buf_hi_ = nullptr;
|
||||
}
|
||||
read_cursor_ = buf_lo_;
|
||||
write_cursor_ = buf_lo_;
|
||||
pre_read_count_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Write block of bytes into the buffer.
|
||||
//
|
||||
void write_bytes(std::string_view s) {
|
||||
int64_t len = s.size();
|
||||
make_space(len);
|
||||
memcpy(write_cursor_, s.data(), len);
|
||||
write_cursor_ += len;
|
||||
}
|
||||
|
||||
// Write integers.
|
||||
//
|
||||
void write_uint8(uint64_t data) { write_uint_core<uint8_t>(data); }
|
||||
void write_uint16(uint64_t data) { write_uint_core<uint16_t>(data); }
|
||||
void write_uint32(uint64_t data) { write_uint_core<uint32_t>(data); }
|
||||
void write_uint64(uint64_t data) { write_uint_core<uint64_t>(data); }
|
||||
void write_int8(int64_t data) { write_int_core<int8_t>(data); }
|
||||
void write_int16(int64_t data) { write_int_core<int16_t>(data); }
|
||||
void write_int32(int64_t data) { write_int_core<int32_t>(data); }
|
||||
void write_int64(int64_t data) { write_int_core<int64_t>(data); }
|
||||
|
||||
// Write other primitive types.
|
||||
//
|
||||
void write_bool(bool b) { write_uint8(b ? 1:0); }
|
||||
void write_char(char c) { write_value_core(c); }
|
||||
void write_float(float arg) { write_value_core(arg); }
|
||||
void write_double(double arg) { write_value_core(arg); }
|
||||
|
||||
// Write lengths.
|
||||
//
|
||||
// Lengths are usually short, so we have a special way of storing
|
||||
// lengths that minimizes the number of bytes when the length is short.
|
||||
//
|
||||
void write_length(size_t len) {
|
||||
if (len >= 255) {
|
||||
write_uint8(0xFF);
|
||||
write_uint64(len);
|
||||
} else {
|
||||
write_uint8(len);
|
||||
}
|
||||
}
|
||||
|
||||
// Write a string.
|
||||
//
|
||||
void write_string(std::string_view s) {
|
||||
write_length(s.size());
|
||||
write_bytes(s);
|
||||
}
|
||||
|
||||
// Write a SimpleDynamicTag.
|
||||
//
|
||||
void write_simple_dynamic_tag(SimpleDynamicTag tag) {
|
||||
write_uint8(uint8_t(tag));
|
||||
}
|
||||
|
||||
// Write a SimpleDynamic value.
|
||||
//
|
||||
// This works regardless of what kind of string is present in the
|
||||
// SimpleDynamic.
|
||||
//
|
||||
template<class STRING>
|
||||
void write_simple_dynamic(const SimpleDynamic<STRING> &sd) {
|
||||
write_simple_dynamic_tag(sd.type);
|
||||
switch(sd.type) {
|
||||
case SimpleDynamicTag::NUMBER: write_double(sd.x); break;
|
||||
case SimpleDynamicTag::BOOLEAN: write_bool(sd.x == 1.0); break;
|
||||
case SimpleDynamicTag::VECTOR: write_double(sd.x); write_double(sd.y); write_double(sd.z); break;
|
||||
case SimpleDynamicTag::STRING: write_string(sd.s); break;
|
||||
default: assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Read a block of bytes from the buffer.
|
||||
//
|
||||
// Caution: the pointer returned is a pointer to the stream's buffer.
|
||||
// It is only valid until you mutate the buffer. If the bytes aren't
|
||||
// there, calls 'raise_eof_on_read', and then returns nullptr.
|
||||
//
|
||||
const char *read_bytes(int64_t bytes) {
|
||||
int64_t avail = write_cursor_ - read_cursor_;
|
||||
if (avail < bytes) {
|
||||
CoreHandler::raise_eof_on_read();
|
||||
return nullptr;
|
||||
}
|
||||
char *data = read_cursor_;
|
||||
read_cursor_ += bytes;
|
||||
return data;
|
||||
}
|
||||
|
||||
// Read integers.
|
||||
//
|
||||
uint8_t read_uint8() { return read_value_core<uint8_t>(); }
|
||||
uint16_t read_uint16() { return read_value_core<uint16_t>(); }
|
||||
uint32_t read_uint32() { return read_value_core<uint32_t>(); }
|
||||
uint64_t read_uint64() { return read_value_core<uint64_t>(); }
|
||||
int8_t read_int8() { return read_value_core<int8_t>(); }
|
||||
int16_t read_int16() { return read_value_core<int16_t>(); }
|
||||
int32_t read_int32() { return read_value_core<int32_t>(); }
|
||||
int64_t read_int64() { return read_value_core<int64_t>(); }
|
||||
|
||||
// Read other primitive types.
|
||||
//
|
||||
bool read_bool() { return (bool)read_uint8(); }
|
||||
char read_char() { return read_value_core<char>(); }
|
||||
float read_float() { return read_value_core<float>(); }
|
||||
double read_double() { return read_value_core<double>(); }
|
||||
|
||||
// Read a length.
|
||||
//
|
||||
size_t read_length() {
|
||||
uint64_t len = read_uint8();
|
||||
if (len == 255) {
|
||||
len = read_uint64();
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
// Read a string as a string_view.
|
||||
//
|
||||
// If the string in the buffer is longer than the limit,
|
||||
// calls 'raise_string_too_long' and returns an empty string.
|
||||
//
|
||||
// If the buffer doesn't contain a complete string, calls
|
||||
// 'raise_eof_on_read' and returns an empty string.
|
||||
//
|
||||
std::string_view read_string_view_limit(uint64_t limit) {
|
||||
size_t length = read_length();
|
||||
if (length > limit) {
|
||||
CoreHandler::raise_string_too_long();
|
||||
return std::string_view();
|
||||
}
|
||||
int64_t avail = write_cursor_ - read_cursor_;
|
||||
if (avail < int64_t(length)) {
|
||||
CoreHandler::raise_eof_on_read();
|
||||
return std::string_view();
|
||||
}
|
||||
std::string_view result(read_cursor_, length);
|
||||
read_cursor_ += length;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Read a string as a string_view.
|
||||
//
|
||||
std::string_view read_string_view() {
|
||||
return read_string_view_limit(0x1000000);
|
||||
}
|
||||
|
||||
// Read a string.
|
||||
//
|
||||
string_type read_string_limit(uint64_t limit) {
|
||||
size_t len = read_length();
|
||||
if (len > limit) {
|
||||
CoreHandler::raise_string_too_long();
|
||||
return string_type();
|
||||
}
|
||||
int64_t avail = write_cursor_ - read_cursor_;
|
||||
if (avail < int64_t(len)) {
|
||||
CoreHandler::raise_eof_on_read();
|
||||
return string_type();
|
||||
}
|
||||
string_type result(len, ' ');
|
||||
memcpy(&result[0], read_cursor_, len);
|
||||
read_cursor_ += len;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Read a string.
|
||||
//
|
||||
string_type read_string() {
|
||||
return read_string_limit(0x1000000);
|
||||
}
|
||||
|
||||
// Read a SimpleDynamicTag
|
||||
//
|
||||
SimpleDynamicTag read_simple_dynamic_tag() {
|
||||
return SimpleDynamicTag(read_uint8());
|
||||
}
|
||||
|
||||
// Read a SimpleDynamic
|
||||
//
|
||||
template<class STRING>
|
||||
void read_simple_dynamic(SimpleDynamic<STRING> *result) {
|
||||
SimpleDynamicTag type = read_simple_dynamic_tag();
|
||||
switch (type) {
|
||||
case SimpleDynamicTag::NUMBER: result->set_number(read_double()); break;
|
||||
case SimpleDynamicTag::BOOLEAN: result->set_boolean(read_bool()); break;
|
||||
case SimpleDynamicTag::VECTOR: {
|
||||
double x=read_double();
|
||||
double y=read_double();
|
||||
double z=read_double();
|
||||
result->set_vector(x,y,z);
|
||||
break;
|
||||
}
|
||||
case SimpleDynamicTag::STRING: result->set_string(read_string()); break;
|
||||
default: result->set_uninitialized(); break;
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to do a "readline". If there is no newline in
|
||||
// the buffer, returns empty string. If there is a newline,
|
||||
// returns a block of text that ends in newline.
|
||||
//
|
||||
string_type readline() {
|
||||
char *p = read_cursor_;
|
||||
while ((p < write_cursor_) && (*p != '\n')) p++;
|
||||
if (p == write_cursor_) {
|
||||
return string_type();
|
||||
} else {
|
||||
p++;
|
||||
string_type result(read_cursor_, p - read_cursor_);
|
||||
read_cursor_ = p;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Overwrite values previously written to the buffer.
|
||||
//
|
||||
// See the comment at the top of this file for an explanation.
|
||||
//
|
||||
void overwrite_int8(int64_t write_count_after, int64_t v) { overwrite_int_core<int8_t>(write_count_after, v); }
|
||||
void overwrite_int16(int64_t write_count_after, int64_t v) { overwrite_int_core<int16_t>(write_count_after, v); }
|
||||
void overwrite_int32(int64_t write_count_after, int64_t v) { overwrite_int_core<int32_t>(write_count_after, v); }
|
||||
void overwrite_int64(int64_t write_count_after, int64_t v) { overwrite_int_core<int64_t>(write_count_after, v); }
|
||||
void overwrite_uint8(int64_t write_count_after, uint64_t v) { overwrite_uint_core<uint8_t>(write_count_after, v); }
|
||||
void overwrite_uint16(int64_t write_count_after, uint64_t v) { overwrite_uint_core<uint16_t>(write_count_after, v); }
|
||||
void overwrite_uint32(int64_t write_count_after, uint64_t v) { overwrite_uint_core<uint32_t>(write_count_after, v); }
|
||||
void overwrite_uint64(int64_t write_count_after, uint64_t v) { overwrite_uint_core<uint64_t>(write_count_after, v); }
|
||||
|
||||
// This is for unit testing.
|
||||
//
|
||||
bool layout_is(int64_t a, int64_t b, int64_t c) {
|
||||
if (read_cursor_ - buf_lo_ != a) return false;
|
||||
if (write_cursor_ - read_cursor_ != b) return false;
|
||||
if (buf_hi_ - write_cursor_ != c) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
void make_space_slow(int64_t bytes) {
|
||||
assert(owned_ && "We don't own this buffer, can't grow it");
|
||||
|
||||
// Decide whether the current buffer is big enough.
|
||||
int64_t data_size = (write_cursor_ - read_cursor_);
|
||||
int64_t existing_size = (buf_hi_ - buf_lo_);
|
||||
int64_t desired_size = 8192 + ((data_size + bytes) * 2);
|
||||
|
||||
// Update some simple things.
|
||||
pre_read_count_ += (read_cursor_ - buf_lo_);
|
||||
|
||||
// Move the data to the beginning of the buffer, or to
|
||||
// the beginning of a new buffer.
|
||||
if (fixed_size_) {
|
||||
assert((data_size + bytes <= existing_size) && "Not enough space in fixed-size buffer");
|
||||
if (data_size > 0) memcpy(buf_lo_, read_cursor_, data_size);
|
||||
} else if (existing_size >= desired_size) {
|
||||
if (data_size > 0) memcpy(buf_lo_, read_cursor_, data_size);
|
||||
} else {
|
||||
char *nbuf = (char *)CoreHandler::basebuffer_malloc(desired_size);
|
||||
if (data_size > 0) memcpy(nbuf, read_cursor_, data_size);
|
||||
if (buf_lo_ != nullptr) CoreHandler::basebuffer_free(buf_lo_);
|
||||
buf_lo_ = nbuf;
|
||||
buf_hi_ = nbuf + desired_size;
|
||||
}
|
||||
|
||||
// Update the pointers to the data region.
|
||||
read_cursor_ = buf_lo_;
|
||||
write_cursor_ = buf_lo_ + data_size;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void write_value_core(T arg) {
|
||||
make_space(sizeof(arg));
|
||||
memcpy(write_cursor_, &arg, sizeof(arg));
|
||||
write_cursor_ += sizeof(arg);
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void write_int_core(int64_t arg) {
|
||||
T reduced = arg;
|
||||
if (int64_t(reduced) != arg) CoreHandler::raise_integer_truncated();
|
||||
write_value_core(reduced);
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void write_uint_core(uint64_t arg) {
|
||||
T reduced = arg;
|
||||
if (uint64_t(reduced) != arg) CoreHandler::raise_integer_truncated();
|
||||
write_value_core(reduced);
|
||||
}
|
||||
|
||||
template<class T>
|
||||
T read_value_core() {
|
||||
T result;
|
||||
int64_t avail = write_cursor_ - read_cursor_;
|
||||
if (avail < int64_t(sizeof(result))) {
|
||||
CoreHandler::raise_eof_on_read();
|
||||
return 0;
|
||||
}
|
||||
memcpy(&result, read_cursor_, sizeof(result));
|
||||
read_cursor_ += sizeof(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void overwrite_int_core(int64_t write_count_after, int64_t vv) {
|
||||
T v = vv;
|
||||
assert(int64_t(v) == vv);
|
||||
int64_t write_count_before = write_count_after - sizeof(v);
|
||||
assert(write_count_before >= total_reads());
|
||||
assert(write_count_after <= total_writes());
|
||||
void *target = buf_lo_ + (write_count_before - pre_read_count_);
|
||||
memcpy(target, &v, sizeof(v));
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void overwrite_uint_core(int64_t write_count_after, uint64_t vv) {
|
||||
T v = vv;
|
||||
assert(uint64_t(v) == vv);
|
||||
int64_t write_count_before = write_count_after - sizeof(v);
|
||||
assert(write_count_before >= total_reads());
|
||||
assert(write_count_after <= total_writes());
|
||||
void *target = buf_lo_ + (write_count_before - pre_read_count_);
|
||||
memcpy(target, &v, sizeof(v));
|
||||
}
|
||||
};
|
||||
BIN
luprex/ext/cv2pdb.exe
LFS
Normal file
BIN
luprex/ext/cv2pdb.exe
LFS
Normal file
Binary file not shown.
6251
luprex/ext/dlmalloc.c
Normal file
6251
luprex/ext/dlmalloc.c
Normal file
File diff suppressed because it is too large
Load Diff
239
luprex/ext/eris-master/FILEFORMAT
Normal file
239
luprex/ext/eris-master/FILEFORMAT
Normal file
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
The format is largely equal to that of Pluto, with some changes to reduce
|
||||
file size, and where necessary due to changes in Lua's architecture.
|
||||
|
||||
Pseudo-C is used to express the file format. Padding is assumed to be
|
||||
nonexistent. The keyword "one_of" is used to express a concept similar to
|
||||
"union", except that its size is the size of the actual datatype chosen. Thus,
|
||||
objects which contain, directly or indirectly, a one_of, may vary in size. The
|
||||
keyword "if" is used to express the fact that the following block may or may
|
||||
not be present, usually indicated by the type of a previous "one_of".
|
||||
*/
|
||||
|
||||
struct PersistedData {
|
||||
Header header; /* The header used for basic validation. */
|
||||
Object rootobj; /* The root object that was persisted. */
|
||||
};
|
||||
|
||||
struct Header {
|
||||
char header[4] = "ERIS"; /* Header signature for rudimentary validation */
|
||||
uint8_t sizeof_number; /* sizeof(lua_Number) to check type compatibility */
|
||||
lua_Number test; /* -1.234567890 to check representation compatibility */
|
||||
uint8_t sizeof_int; /* sizeof(int) in persisted data */
|
||||
uint8_t sizeof_size_t; /* sizeof(size_t) in persisted data */
|
||||
/* Note that the last two fields determine the size of the int and size_t
|
||||
* fields in the following definitions. We write each value in the native
|
||||
* "size" and check for truncation when reading, if necessary. */
|
||||
};
|
||||
|
||||
struct Object {
|
||||
one_of {
|
||||
int type; /* if the value is one of LUA_TXXX or ERIS_PERMANENT */
|
||||
Reference r; /* otherwise */
|
||||
/* Note that the types LUA_TNIL, LUA_TBOOLEAN, LUA_TNUMBER and
|
||||
* LUA_TLIGHTUSERDATA will never be "referenced", but always be written
|
||||
* directly. */
|
||||
}
|
||||
if (type) {
|
||||
RealObject o; /* if we have a type, not a reference */
|
||||
/* If the object is not primitive (see list above) we remember it and
|
||||
* increment the reference counter, and point any future occurrences of
|
||||
* it to this one via a reference (see above, Reference r). */
|
||||
}
|
||||
};
|
||||
|
||||
struct Reference {
|
||||
int ref; /* The index the object was registered with */
|
||||
};
|
||||
|
||||
struct RealObject {
|
||||
one_of {
|
||||
uint8_t b; /* If type == LUA_TBOOLEAN */
|
||||
size_t l; /* If type == LUA_TLIGHTUSERDATA */
|
||||
Number n; /* If type == LUA_TNUMBER */
|
||||
String s; /* If type == LUA_TSTRING */
|
||||
Table t; /* If type == LUA_TTABLE */
|
||||
Closure f; /* If type == LUA_TFUNCTION */
|
||||
Userdata u; /* If type == LUA_TUSERDATA */
|
||||
Thread th; /* If type == LUA_TTHREAD */
|
||||
Proto p; /* If type == LUA_TPROTO (from lobject.h) */
|
||||
UpVal uv; /* If type == LUA_TUPVAL (from lobject.h) */
|
||||
PermKey pk; /* if type == ERIS_PERMANENT */
|
||||
};
|
||||
};
|
||||
|
||||
struct Number {
|
||||
if(sizeof(lua_Number) == sizeof(uint32_t)) {
|
||||
uint32_t rep; /* binary representation of the number, stored as
|
||||
* integer to re-use endian-agnosticism */
|
||||
}
|
||||
else if (sizeof(lua_Number) == sizeof(uint64_t)) {
|
||||
uint64_t rep; /* binary representation of the number, stored as
|
||||
* integer to re-use endian-agnosticism */
|
||||
}
|
||||
else {
|
||||
/* unsupported float type -- error if asserts are enabled */
|
||||
}
|
||||
};
|
||||
|
||||
struct String {
|
||||
size_t length; /* The length of the string */
|
||||
char str[length]; /* The actual string (not always null terminated) */
|
||||
};
|
||||
|
||||
struct Table {
|
||||
uint8_t isSpecial; /* 1 if SP is used; 0 otherwise */
|
||||
one_of {
|
||||
Object c; /* if isspecial == 1; closure to refill the table */
|
||||
LiteralTable t; /* if isspecial == 0; literal table info */
|
||||
};
|
||||
};
|
||||
|
||||
struct LiteralTable {
|
||||
Pair p[]; /* key/value pairs */
|
||||
Object nil = nil; /* Nil reference to terminate */
|
||||
Object metatable; /* The metatable (nil for none, otherwise LUA_TTABLE) */
|
||||
};
|
||||
|
||||
struct Pair {
|
||||
Object key; /* never nil, since that indicates the end */
|
||||
Object value; /* never nil, since the entry wouldn't exist then */
|
||||
};
|
||||
|
||||
struct Userdata {
|
||||
uint8_t isSpecial; /* 1 for special persistence, 0 for literal */
|
||||
one_of {
|
||||
Object c; /* if isspecial == 1; closure to recreate the udata */
|
||||
LiteralUserdata lu; /* if is_special is 0 */
|
||||
};
|
||||
};
|
||||
|
||||
struct LiteralUserdata {
|
||||
size_t length; /* Size of the data */
|
||||
char data[length]; /* The actual data */
|
||||
Object metatable; /* The metatable (nil for none, otherwise LUA_TTABLE) */
|
||||
};
|
||||
|
||||
struct Closure {
|
||||
uint8_t isCClosure; /* 1 if the closure is a C closure; 0 otherwise */
|
||||
uint8_t nups; /* Number of upvalues the function uses */
|
||||
one_of {
|
||||
CClosure ccl; /* if isCClosure == 1 */
|
||||
LClosure lcl; /* if isCClosure == 0; it's a Lua closure */
|
||||
};
|
||||
};
|
||||
|
||||
struct CClosure {
|
||||
Object f; /* The actual C function. Must be available via the
|
||||
* permanents table on persist and unpersist. */
|
||||
Object upvals[Closure.nups]; /* All upvalues */
|
||||
/* Note that here the upvalues are the actual objects, i.e. these are not
|
||||
* of type LUA_TUPVAL, since C closures' upalues are always closed. */
|
||||
};
|
||||
|
||||
struct LClosure {
|
||||
Object proto; /* The proto this function uses */
|
||||
Object upvals[Closure.nups]; /* All upvalues */
|
||||
};
|
||||
|
||||
struct UpVal {
|
||||
Object obj; /* The object this upval refers to; we proxy it with
|
||||
* the LUA_TUPVAL type to keep shared upvalues intact */
|
||||
}
|
||||
|
||||
struct Proto {
|
||||
int linedefined; /* Start of line range */
|
||||
int lastlinedefined; /* End of line range */
|
||||
uint8_t numparams; /* Number of parameters taken */
|
||||
uint8_t is_vararg; /* 1 if function accepts varargs, 0 otherwise */
|
||||
uint8_t maxstacksize; /* Size of stack reserved for the function */
|
||||
|
||||
int sizecode; /* Number of instructions in code */
|
||||
Instruction code[sizecode]; /* The proto's code */
|
||||
|
||||
int sizek; /* Number of constants referenced */
|
||||
Object k[sizek]; /* Constants referenced */
|
||||
|
||||
int sizep; /* Number of inner Protos referenced */
|
||||
Object p[sizep]; /* Inner Protos referenced */
|
||||
|
||||
int sizeupvalues; /* Number of upvalues used */
|
||||
Upvaldesc upvalues[sizeupvalues]; /* The locations of the upvalues */
|
||||
|
||||
uint8_t debug; /* 1 if debug data is present; 0 otherwise */
|
||||
if (debug) {
|
||||
Object source; /* The source code string */
|
||||
|
||||
int sizelineinfo; /* Number of opcode-line mappings */
|
||||
int lineinfo[sizelineinfo]; /* opcode-line mappings */
|
||||
|
||||
int sizelocvars; /* Number of local variable names */
|
||||
LocVar[sizelocvars]; /* Local variable names */
|
||||
|
||||
Object upvalnames[sizeupvalues]; /* Upvalue names */
|
||||
}
|
||||
};
|
||||
|
||||
struct Upvaldesc {
|
||||
uint8_t instack; /* whether it is in stack */
|
||||
uint8_t idx; /* index of upvalue (in stack or in outer function's list) */
|
||||
};
|
||||
|
||||
struct LocVar {
|
||||
int startpc; /* Point where variable is active */
|
||||
int endpc; /* Point where variable is dead */
|
||||
Object name; /* Name of the local variable */
|
||||
};
|
||||
|
||||
struct Thread {
|
||||
int stacksize; /* The overall size of the stack filled with objects,
|
||||
* including all stack frames. */
|
||||
size_t top; /* top = L->top - L->stack; */
|
||||
Object stack[stacksize]; /* All stack values, bottom up */
|
||||
|
||||
uint8_t status; /* current thread status (ok, yield) */
|
||||
size_t errfunc; /* current error handling function (stack index) */
|
||||
|
||||
CallInfo ci[]; /* The CallInfo stack, starting with base_ci */
|
||||
if (status == LUA_YIELD) {
|
||||
size_t extra; /* value of thread->ci->extra, which is the original
|
||||
* value of thread->ci->func */
|
||||
}
|
||||
|
||||
OpenUpval openupvals[]; /* Upvalues to open */
|
||||
};
|
||||
|
||||
struct CallInfo {
|
||||
size_t func; /* func = ci->func - thread->stack */
|
||||
size_t top; /* top = ci->top - thread->stack */
|
||||
int16_t nresults; /* expected number of results from this function */
|
||||
uint8_t callstatus;
|
||||
if (callstatus & CIST_YPCALL) {
|
||||
size_t extra; /* the stack level of the function being pcalled */
|
||||
}
|
||||
if (callstatus & CIST_LUA) {
|
||||
size_t base; /* base = ci->u.l.base - thread-stack */
|
||||
size_t savedpc; /* savedpc = ci->u.l.savedpc - ci_func(ci)->p->code */
|
||||
}
|
||||
else {
|
||||
uint8_t status;
|
||||
if (callstatus & (CIST_YPCALL | CIST_YIELDED)) {
|
||||
int ctx; /* context info. in case of yields */
|
||||
Object k; /* C function, callback for resuming */
|
||||
}
|
||||
}
|
||||
uint8_t hasNext; /* 1 if there's another CI to read; 0 otherwise */
|
||||
};
|
||||
|
||||
struct OpenUpval {
|
||||
size_t idx; /* stack index of the value + 1; 0 if end of list */
|
||||
Object upval; /* The upvalue */
|
||||
};
|
||||
|
||||
struct PermKey {
|
||||
uint8_t type; /* The actual LUA_TXXX of the original value. */
|
||||
Object key; /* The value to use as a key when unpersisting. */
|
||||
/* Note that we store the type of the original value (replaced by the
|
||||
* permanent table value used as a key when unpersisting) to ensure the
|
||||
* value in the permanents table when unpersisting has the correct type. */
|
||||
};
|
||||
45
luprex/ext/eris-master/LICENSE
Normal file
45
luprex/ext/eris-master/LICENSE
Normal file
@@ -0,0 +1,45 @@
|
||||
Eris
|
||||
Copyright (c) 2013-2015 Florian Nücke.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
Lua 5.2.3 from http://www.lua.org/ with patches http://www.lua.org/bugs.html
|
||||
slightly modified to access internal library functions for persistence.
|
||||
|
||||
Copyright © 1994–2013 Lua.org, PUC-Rio.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
113
luprex/ext/eris-master/Makefile
Normal file
113
luprex/ext/eris-master/Makefile
Normal file
@@ -0,0 +1,113 @@
|
||||
# Makefile for installing Lua
|
||||
# See doc/readme.html for installation and customization instructions.
|
||||
|
||||
# == CHANGE THE SETTINGS BELOW TO SUIT YOUR ENVIRONMENT =======================
|
||||
|
||||
# Your platform. See PLATS for possible values.
|
||||
PLAT= none
|
||||
|
||||
# Where to install. The installation starts in the src and doc directories,
|
||||
# so take care if INSTALL_TOP is not an absolute path. See the local target.
|
||||
# You may want to make INSTALL_LMOD and INSTALL_CMOD consistent with
|
||||
# LUA_ROOT, LUA_LDIR, and LUA_CDIR in luaconf.h.
|
||||
INSTALL_TOP= $(DESTDIR)/usr/local
|
||||
INSTALL_BIN= $(INSTALL_TOP)/bin
|
||||
INSTALL_INC= $(INSTALL_TOP)/include
|
||||
INSTALL_LIB= $(INSTALL_TOP)/lib
|
||||
INSTALL_MAN= $(INSTALL_TOP)/man/man1
|
||||
INSTALL_LMOD= $(INSTALL_TOP)/share/lua/$V
|
||||
INSTALL_CMOD= $(INSTALL_TOP)/lib/lua/$V
|
||||
|
||||
# How to install. If your install program does not support "-p", then
|
||||
# you may have to run ranlib on the installed liblua.a.
|
||||
INSTALL= install -p
|
||||
INSTALL_EXEC= $(INSTALL) -m 0755
|
||||
INSTALL_DATA= $(INSTALL) -m 0644
|
||||
#
|
||||
# If you don't have "install" you can use "cp" instead.
|
||||
# INSTALL= cp -p
|
||||
# INSTALL_EXEC= $(INSTALL)
|
||||
# INSTALL_DATA= $(INSTALL)
|
||||
|
||||
# Other utilities.
|
||||
MKDIR= mkdir -p
|
||||
RM= rm -f
|
||||
|
||||
# == END OF USER SETTINGS -- NO NEED TO CHANGE ANYTHING BELOW THIS LINE =======
|
||||
|
||||
# Convenience platforms targets.
|
||||
PLATS= aix ansi bsd freebsd generic linux macosx mingw posix solaris
|
||||
|
||||
# What to install.
|
||||
TO_BIN= lua luac
|
||||
TO_INC= lua.h luaconf.h lualib.h lauxlib.h lua.hpp eris.h
|
||||
TO_LIB= liblua.a
|
||||
TO_MAN= lua.1 luac.1
|
||||
|
||||
# Lua version and release.
|
||||
V= 5.2
|
||||
R= $V.4
|
||||
|
||||
# Targets start here.
|
||||
all: $(PLAT)
|
||||
|
||||
$(PLATS) clean:
|
||||
cd src && $(MAKE) $@
|
||||
|
||||
test: dummy
|
||||
src/lua -v
|
||||
"test/persist" test/persist.lua test.eris && "test/unpersist" test/unpersist.lua test.eris
|
||||
|
||||
install: dummy
|
||||
cd src && $(MKDIR) $(INSTALL_BIN) $(INSTALL_INC) $(INSTALL_LIB) $(INSTALL_MAN) $(INSTALL_LMOD) $(INSTALL_CMOD)
|
||||
cd src && $(INSTALL_EXEC) $(TO_BIN) $(INSTALL_BIN)
|
||||
cd src && $(INSTALL_DATA) $(TO_INC) $(INSTALL_INC)
|
||||
cd src && $(INSTALL_DATA) $(TO_LIB) $(INSTALL_LIB)
|
||||
|
||||
uninstall:
|
||||
cd src && cd $(INSTALL_BIN) && $(RM) $(TO_BIN)
|
||||
cd src && cd $(INSTALL_INC) && $(RM) $(TO_INC)
|
||||
cd src && cd $(INSTALL_LIB) && $(RM) $(TO_LIB)
|
||||
|
||||
local:
|
||||
$(MAKE) install INSTALL_TOP=../install
|
||||
|
||||
none:
|
||||
@echo "Please do 'make PLATFORM' where PLATFORM is one of these:"
|
||||
@echo " $(PLATS)"
|
||||
@echo "See doc/readme.html for complete instructions."
|
||||
|
||||
# make may get confused with test/ and install/
|
||||
dummy:
|
||||
|
||||
# echo config parameters
|
||||
echo:
|
||||
@cd src && $(MAKE) -s echo
|
||||
@echo "PLAT= $(PLAT)"
|
||||
@echo "V= $V"
|
||||
@echo "R= $R"
|
||||
@echo "TO_BIN= $(TO_BIN)"
|
||||
@echo "TO_INC= $(TO_INC)"
|
||||
@echo "TO_LIB= $(TO_LIB)"
|
||||
@echo "TO_MAN= $(TO_MAN)"
|
||||
@echo "INSTALL_TOP= $(INSTALL_TOP)"
|
||||
@echo "INSTALL_BIN= $(INSTALL_BIN)"
|
||||
@echo "INSTALL_INC= $(INSTALL_INC)"
|
||||
@echo "INSTALL_LIB= $(INSTALL_LIB)"
|
||||
@echo "INSTALL_MAN= $(INSTALL_MAN)"
|
||||
@echo "INSTALL_LMOD= $(INSTALL_LMOD)"
|
||||
@echo "INSTALL_CMOD= $(INSTALL_CMOD)"
|
||||
@echo "INSTALL_EXEC= $(INSTALL_EXEC)"
|
||||
@echo "INSTALL_DATA= $(INSTALL_DATA)"
|
||||
|
||||
# echo pkg-config data
|
||||
pc:
|
||||
@echo "version=$R"
|
||||
@echo "prefix=$(INSTALL_TOP)"
|
||||
@echo "libdir=$(INSTALL_LIB)"
|
||||
@echo "includedir=$(INSTALL_INC)"
|
||||
|
||||
# list targets that do not create files (but not all makes understand .PHONY)
|
||||
.PHONY: all $(PLATS) clean test install local none dummy echo pecho lecho
|
||||
|
||||
# (end of Makefile)
|
||||
202
luprex/ext/eris-master/README.md
Normal file
202
luprex/ext/eris-master/README.md
Normal file
@@ -0,0 +1,202 @@
|
||||
Eris - Heavy Duty Persistence for Lua
|
||||
=====================================
|
||||
|
||||
First things first: this is essentially a rewrite of [Pluto][], targeting Lua 5.2. If you need something like this for Lua 5.1 I strongly suggest you have a look at that. In fact, have a look at it anyway, because the documentation isn't half bad, and a lot of it applies to Eris, too.
|
||||
|
||||
Although my implementation is strongly influenced by Pluto, in my humble opinion there are still sufficient changes in the architecture that a new name is appropriate. So, to stick with the theme, I named it Eris. Note that all those changes are internal. The API is almost left untouched to make a transition easier.
|
||||
|
||||
Eris can serialize almost anything you can have in a Lua VM and can unserialize it again at a later point, even in a different VM. This in particular includes yielded coroutines, which is very handy for saving long running, multi-session scripting systems, for example in games: just persist the state into the save file and unpersist it upon load, and the scripts won't even know the game was quit inbetween.
|
||||
|
||||
Eris currently comes with Lua 5.2.3. It should also build with 5.2.2. It may build with earlier 5.2.x versions, but I have not tried it. It *won't* work with other Lua implementations, such as LuaJIT, because Eris works directly on some internal structures of vanilla Lua and uses non-API macros and functions.
|
||||
|
||||
I think Eris can be considered stable. I have distributed it in a piece of software that has been used by many users on varying platforms, and there were no problems that had to do with Eris at all. There are also the testcases that came with Pluto - extended with some Lua 5.2 specific ones, such as yielded `pcall`s - run through successfully.
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
Think of Eris as a custom "distribution" of Lua. Compile and install Eris like you would Lua. You will get a shared and/or static library that will contain both Lua and Eris. The version string has been adjusted to reflect this: `Lua+Eris 5.2.3`.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
C API
|
||||
-----
|
||||
|
||||
Like Pluto, Eris offers two functions to persist to or read from an arbitrary source. Eris uses the Lua typedefs `lua_Writer` and `lua_Reader` for this purpose, the replacements of `lua_Chunkwriter` and `lua_Chunkreader` used by Pluto.
|
||||
|
||||
* `void eris_dump(lua_State *L, lua_Writer writer, void *ud);` `[-0, +0, e]`
|
||||
This provides an interface to Eris' persist functionality for writing in an arbitrary way, using a writer. When called, the stack in `L` must look like this:
|
||||
1. `perms:table`
|
||||
2. `value:any`
|
||||
|
||||
That is, `perms` must be a table at stack position 1. This table is used as the permanent object table. `value` is the value that should be persisted. It can be any persistable Lua value. 'writer' is the writer function used to store all data, `ud` is passed to the writer function whenever it is called.
|
||||
|
||||
This function is equivalent to `pluto_persist`.
|
||||
|
||||
* `void eris_undump(lua_State *L, lua_Reader reader, void *ud);` `[-0, +1, e]`
|
||||
This provides an interface to Eris' unpersist functionality for reading in an arbitrary way, using a reader. When called, the stack in `L` must look like this:
|
||||
1. `perms:table`
|
||||
|
||||
That is, `perms` must be a table at stack position 1. This table is used as the permanent object table. This must hold the inverse mapping present in the permanent object table used when persisting. `reader` is the reader function used to read all data, `ud` is passed to the reader function whenever it is called. The result of the operation will be pushed onto the stack.
|
||||
|
||||
This function is equivalent to `pluto_unpersist`. The function will only check the stack's top. Like Pluto, Eris uses Lua's own ZIO to handle buffered reading. Note that unlike with Pluto, the value can in fact be `nil`.
|
||||
|
||||
In addition to these, Eris also offers two more convenient functions, if you simply wish to persist an object to a string. These behave like the functions exposed to Lua.
|
||||
|
||||
* `void eris_persist(lua_State *L, int perms, int value);` `[-0, +1, e]`
|
||||
It expects the permanent object table at the specified index `perms` and the value to persist at the specified index `value`. It will push the resulting binary string onto the stack on success.
|
||||
|
||||
* `void eris_unpersist(lua_State *L, int perms, int value);` `[-0, +1, e]`
|
||||
It expects the permanent object table at the specified index `perms` and the binary string containing persisted data at the specified index `value`. It will push the resulting value onto the stack on success.
|
||||
|
||||
The subtle name change from Pluto was done because Lua's own dump/undump works with a writer/reader, so it felt more consistent this way.
|
||||
|
||||
Finally, you can change some of Eris' behavior via settings that can be queried and adjusted via the following functions:
|
||||
|
||||
* `void eris_get_setting(lua_State *L, const char *name);` `[-0, +1, e]`
|
||||
This will push the current value of the setting with the specified name onto the stack. If there is no setting with the specified name an error will be thrown. Available settings are:
|
||||
- `debug`, a boolean value indicating whether to persist debug information of function prototypes, such as line numbers and variable names. The default is `true`.
|
||||
- `maxrec`, an unsigned integer value indicating the maximum complexity of objects: the number of "persist" recursions, for example for nested tables. If an object has a higher complexity we throw an error. This can be used to avoid segfaults due to deep recursion when handling user-provided data. The default is `10000`.
|
||||
- `path`, a boolean value indicating whether to generate the "path" in the object to display it when an error occurs. **Important:** this adds significant overhead and should only be used to debug errors. The default is `false`.
|
||||
- `spio`, a boolean value indicating whether to pass IO objects along as light userdata to special persistence functions. When enabled, this will pass the `lua_Writer` and its associated `void*` in addition to the original object when persisting, and the `ZIO*` when unpersisting. The default is `false`.
|
||||
- `spkey`, a string that is the name of the field in the metatable of tables and userdata used to control persistence (see Special Persistence). The default is `__persist`.
|
||||
|
||||
* `void eris_set_setting(lua_State *L, const char *name, int value);` `[-0, +0, e]`
|
||||
This will change the value of the setting with the specified name to the value at the specified stack index `value`. The names are the same as described in `eris_get_setting()`. If the specified is invalid, an error will be thrown. If the value at the specified index has the wrong type for the specified setting, an error will be thrown. Specify a `nil` value to reset the setting to its default value.
|
||||
|
||||
Note that all settings are stored in the registry of the provided Lua state, so they will be shared across all coroutines.
|
||||
|
||||
Lua
|
||||
---
|
||||
|
||||
You can either load Eris as a table onto the Lua stack via `luaopen_eris()` or just call `luaL_openlibs()` which will open it in addition to all other libraries and register the global table `eris`. As with Pluto, there are the two functions for persisting/unpersisting in this table. In addition, there is a function that allows configuring Eris on the fly:
|
||||
|
||||
* `string eris.persist([perms,] value)`
|
||||
This persists the provided value and returns the resulting data as a binary string. Note that passing the permanent object table is optional. If only one argument is given Eris assumes it's the object to persist, and the permanent object table is empty. If given, `perms` must be a table. `value` can be any type.
|
||||
|
||||
* `any eris.unpersist([perms,] value)`
|
||||
This unpersists the provided binary string that resulted from an earlier call to `eris.persist()` and return the unpersisted value. Note that passing the permanent object table is optional. If only one argument is given Eris assumes it's the data representing a persisted object, and the permanent object table is empty. If given, `perms` must be a table. `value` must be a string.
|
||||
|
||||
* `[value] eris.settings(name[, value])`
|
||||
This allows changing Eris' settings for the Lua VM the script runs in (Eris stores its settings in the registry). For available settings see the documentation of the corresponding C functions above. If this function is called with only a name it will return the current value of that setting. If it is called with a value also, it will set the setting to that value and return nothing.
|
||||
|
||||
Concepts
|
||||
========
|
||||
|
||||
Persistence
|
||||
-----------
|
||||
|
||||
Eris will persist most objects out of the box. This includes basic value types (nil, boolean, light userdata (as the literal pointer value), number) as well as strings, tables, closures and threads. For tables and userdata, metatables are also persisted. Tables, functions and threads are persisted recursively, i.e. each value referenced in them (key/value, upvalue, stack) will in turn be persisted.
|
||||
|
||||
C closures are only supported if the underlying C function is in the permanent object table. Userdata is only supported if the special persistence metafield is present (see below).
|
||||
|
||||
Like Pluto, Eris will store objects only once and reference this first occasion whenever the object should be persisted again. This ensures that references are kept across persisting an object, and has the nice side effect of keeping the size of the output small.
|
||||
|
||||
Permanent Objects
|
||||
-----------------
|
||||
|
||||
Whenever Eris tries to persist any non-basic value (i.e. *not* nil, boolean, light userdata or number) it will check if the value it is about to persist is a key in the table of permanent objects. If it is, Eris will write the associated value instead (the "permanent key"). When unpersisting, the opposite process takes place: for each value persisted this way Eris will look for the permanent key in the permanent object table, and use the value associated with that key.
|
||||
|
||||
For example, this allows persisting values that reference API functions, i.e. C functions provided by the native host. These cannot be persisted literally, because what it boils down to is that those are function pointers, and these may change over multiple program runs or between machines.
|
||||
|
||||
Note that Eris requires the value type to be the same before persisting and after unpersisting. That is, if the value replaced with a key from the permanent object table was a string, it must be a string that is stored permanent object table used for unpersisting at that key:
|
||||
```lua
|
||||
> eris.unpersist({[1] = "asd"}, eris.persist({["asd"] = 1}, "asd")) -- OK
|
||||
> eris.unpersist({[1] = true}, eris.persist({["asd"] = 1}, "asd"))
|
||||
stdin:1: bad permanent value (string expected, got boolean)
|
||||
```
|
||||
|
||||
The permanent object table must not contain two identical values, i.e. two different objects must not be persisted with the same permanent key. This would lead to errors when unpersisting if the original types mismatch, and weird behavior otherwise. You must ensure this yourself, Eris does no checks in this regard.
|
||||
|
||||
Permanent keys starting with `__eris.` are reserved for internal use. For now they are used for functions internal to Lua's libraries, namely the resume functions for yieldable C functions such as `pcall`.
|
||||
|
||||
Special Persistence
|
||||
-------------------
|
||||
|
||||
Another concept carried over from Pluto is "special persistence". Tables and userdata can be annotated with a metatable field (named `__persist` per default). This field can be one of three types: `nil` for default behavior, `boolean` or `function`:
|
||||
|
||||
* `true` marks the object for literal persistence. This is the default for tables. Trying to literally persist userdata without this will result in an error. If set, however, the userdata's memory block will be written as-is, and read back as-is. Note that the table's or userdata's metatable will also be persisted, if present. If you do not want that you'll have to use special persistence (see `function`).
|
||||
* `false` marks the object as "forbidden". When such an object is encountered an error will be thrown.
|
||||
* `function` is very special indeed. If present, this function will called when the object should be persisted. It must return a closure, which is persisted in place of the original object. The returned closure is then run when unpersisting, and is expected to return an object of the object's original type (i.e. table or userdata). Per default, the function stored in the metatable will receive one parameter, the original object. You can configure Eris to also pass along the used writer and userdata associated with it (see `eris_dump()`).
|
||||
|
||||
Here is an example, originally from the Pluto documentation:
|
||||
```lua
|
||||
vec = { x = 2, y = 1, z = 4 }
|
||||
setmetatable(vec, { __persist = function(oldtbl)
|
||||
local x = oldtbl.x
|
||||
local y = oldtbl.y
|
||||
local z = oldtbl.z
|
||||
local mt = getmetatable(oldtbl)
|
||||
return function()
|
||||
newtbl = {}
|
||||
newtbl.x = x
|
||||
newtbl.y = y
|
||||
newtbl.z = z
|
||||
setmetatable(newtbl, mt)
|
||||
return newtbl
|
||||
end
|
||||
end })
|
||||
```
|
||||
|
||||
Also from the Pluto documentation:
|
||||
> It is important that the fixup closure returned not reference the original table directly, as that table would again be persisted as an upvalue, leading to an infinite loop. Also note that the object's metatable is *not* automatically persisted; it is necessary for the fixup closure to reset it, if it wants.
|
||||
|
||||
Cross-platform compatibility
|
||||
============================
|
||||
|
||||
There are three components to this: byte order (little endian, big endian, ...), architecture (16 Bit, 32 Bit, 64 Bit) and floating point representation.
|
||||
- The way values are persisted is endian-agnostic (big endianness tested on a [MIPS Debian][mips] running in [QEMU][]). Note that the testcase for literal userdata persistence fails when performed cross-platform. This is to be expected, since that data is actually an integer and Eris has no concept of the actual data stored in a userdatum.
|
||||
- All potentially architecture-specific values are persisted as they are, i.e. when persisting in a 32-bit application `size_t` will usually be 4 bytes long, when persisting in a 64-bit application it'll be 8 bytes long. This means data will never be truncated when writing. We write the sizes of these types (`int` and `size_t`) in the persisting application into the header and check for truncation when reading. If there is data loss due to this, Eris will throw an error. At least errors when reading 64-bit data in a 32-bit application should be quite unlikely, however, since usually `int` will still be 4 bytes long, and `size_t` is only used for stack size and locations - and the maximum stack size is usually smaller than the maximum value of a 32-bit unsigned integer - for string lengths and userdata size, both of which should rarely be that large. What *could* be an issue, is light userdata.
|
||||
- The binary floating point representation is expected to be the same on all systems sharing persisted data, which will usually be [IEEE 754][floats]. Also, all systems have to use the same type, i.e. `float` or `double`. You cannot load data persisted from a Lua installation using `float` into one that uses `double`. Eris performs a small check as part of its header and if the local model is incompatible to the data it will throw an error.
|
||||
|
||||
So all in all, Eris should be largely cross-platform compatible.
|
||||
|
||||
If you need more control over how things are persisted on your system: it should be relatively easy to adjust Eris in that regard, since reading and writing of the different elementary types is already split up into a couple of functions.
|
||||
|
||||
Limitations
|
||||
===========
|
||||
|
||||
* C functions cannot be persisted. Put them in the permanent object table.
|
||||
- C closures can only be persisted if the underlying C function is in the permanent object table.
|
||||
* Hook information is not persisted. You have to re-install hooks for persisted threads after unpersisting them.
|
||||
* Metatables of basic types are not persisted. A workaround may be to add a dummy table with a `__persist` callback, which in turn stores these metatables, reinstalls them and returns a dummy table upon unpersisting.
|
||||
* Neither the thread running Eris nor its parents can be persisted. More generally: no *running* thread can be persisted, only suspended ones - either because they were not started yet, or because they yielded.
|
||||
* Threads that yielded out of a hook function cannot be persisted. I have not even tested this, since this seems to only be possible for certain hooks (line and count) and when the hook function yields from C itself.
|
||||
* When the loaded data contains functions, this is like loading compiled code, and may therefore be potentially unsafe. You should only unpersist trusted data.
|
||||
* Loops in the permanent value table (e.g. `{a = "b", b = "a"}`) will result in an infinite loop when persisting. Make sure there aren't any.
|
||||
|
||||
Core library
|
||||
============
|
||||
|
||||
You will notice that I decided to bundle this with Lua directly, instead of as a standalone library. This is in part due to how deeply Eris has to interact with Lua. It has to manually create internal data structures and populate them. Later versions of Pluto worked around this by extracting the used internal functions and only bundling those with the library. I decided against this for now, since with Lua 5.2 there's another thing that I need access to: the internally used C resume functions, used for yieldable C calls in the libraries (e.g. `pcall`). I had to patch the library sources by adding a function that pushed these C functions to the table with permanent values. These patches are very unintrusive: they just add a couple of lines to the end of the relevant library files. This means it is possible to persist a yielded `pcall` out of the box.
|
||||
|
||||
Testing
|
||||
=======
|
||||
|
||||
Eris uses the same test suite Pluto uses, extended with a couple of Lua 5.2 specific tests, such as persisting yielded `pcall`s. The executables are built automatically into the test folder when building normally and can be run using `make test`. I only tested this on Windows (MinGW 32 and 64 bit) and Linux (Mint 64 bit). If this causes issues on other platforms please submit a patch, thanks.
|
||||
|
||||
Differences to Pluto
|
||||
====================
|
||||
|
||||
Quite obviously most design choices were taken from Pluto, partially to make it easier to migrate for people already familiar with Pluto, largely because they are pretty good as they are. There are some minute differences however.
|
||||
|
||||
* The resulting persisted data, while using the same basic idea, is structured slightly differently.
|
||||
* On the C side, Eris provides two new functions, `eris_persist` and `eris_unpersist` which work on with the stack, so you don't have to write your own `lua_Writer`/`lua_Reader` implementation.
|
||||
* Better error reporting. When debugging, I recommend enabling path generation, e.g. from Lua via `eris.settings("path", true)`. This will result in all error messages containing a "path" that specifies where in the object the error occurred. For example:
|
||||
|
||||
```lua
|
||||
> eris.persist({
|
||||
>> good = true,
|
||||
>> [false] = setmetatable({}, {
|
||||
>> __index = setmetatable({}, {__persist = false})
|
||||
>> })})
|
||||
stdin:1: attempt to persist forbidden table (root[false]@metatable.__index)
|
||||
```
|
||||
This is disabled per default due to the additional processing and memory overhead.
|
||||
|
||||
|
||||
[Pluto]: https://github.com/hoelzro/pluto
|
||||
[mips]: http://people.debian.org/~aurel32/qemu/mips/
|
||||
[QEMU]: http://qemu.org/
|
||||
[floats]: http://en.wikipedia.org/wiki/IEEE_floating_point
|
||||
[maxstack]: https://github.com/fnuecke/eris/blob/c1674d99c1ee76c5fe1c30e64bfbb1ace52b011e/src/luaconf.h#L351
|
||||
213
luprex/ext/eris-master/src/Makefile
Normal file
213
luprex/ext/eris-master/src/Makefile
Normal file
@@ -0,0 +1,213 @@
|
||||
# Makefile for building Lua+Eris, based on the original Lua Makefile.
|
||||
|
||||
# == CHANGE THE SETTINGS BELOW TO SUIT YOUR ENVIRONMENT =======================
|
||||
|
||||
# Your platform. See PLATS for possible values.
|
||||
PLAT= none
|
||||
|
||||
CC= gcc
|
||||
CFLAGS= -O2 -Wall -DLUA_COMPAT_ALL $(SYSCFLAGS) $(MYCFLAGS)
|
||||
LDFLAGS= $(SYSLDFLAGS) $(MYLDFLAGS)
|
||||
LIBS= -lm $(SYSLIBS) $(MYLIBS)
|
||||
|
||||
AR= ar rcu
|
||||
RANLIB= ranlib
|
||||
RM= rm -f
|
||||
|
||||
SYSCFLAGS=
|
||||
SYSLDFLAGS=
|
||||
SYSLIBS=
|
||||
|
||||
MYCFLAGS=
|
||||
MYLDFLAGS=
|
||||
MYLIBS=
|
||||
MYOBJS=eris.o
|
||||
|
||||
# == END OF USER SETTINGS -- NO NEED TO CHANGE ANYTHING BELOW THIS LINE =======
|
||||
|
||||
PLATS= aix ansi bsd freebsd generic linux macosx mingw posix solaris win-gcc-static
|
||||
|
||||
LUA_A= liblua.a
|
||||
CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o \
|
||||
lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o \
|
||||
ltm.o lundump.o lvm.o lzio.o
|
||||
LIB_O= lauxlib.o lbaselib.o lbitlib.o lcorolib.o ldblib.o liolib.o \
|
||||
lmathlib.o loslib.o lstrlib.o ltablib.o loadlib.o linit.o
|
||||
BASE_O= $(CORE_O) $(LIB_O) $(MYOBJS)
|
||||
|
||||
LUA_T= lua
|
||||
LUA_O= lua.o
|
||||
|
||||
LUAC_T= luac
|
||||
LUAC_O= luac.o
|
||||
|
||||
TESTP_T= ../test/persist
|
||||
TESTP_O= ../test/persist.o
|
||||
|
||||
TESTUP_T= ../test/unpersist
|
||||
TESTUP_O= ../test/unpersist.o
|
||||
|
||||
ALL_O= $(BASE_O) $(LUA_O) $(LUAC_O) $(TESTP_O) $(TESTUP_O)
|
||||
ALL_T= $(LUA_A) $(LUA_T) $(LUAC_T) $(TESTP_T) $(TESTUP_T)
|
||||
ALL_A= $(LUA_A)
|
||||
|
||||
# Targets start here.
|
||||
default: $(PLAT)
|
||||
|
||||
all: $(ALL_T)
|
||||
|
||||
o: $(ALL_O)
|
||||
|
||||
a: $(ALL_A)
|
||||
|
||||
$(LUA_A): $(BASE_O)
|
||||
$(AR) $@ $(BASE_O)
|
||||
$(RANLIB) $@
|
||||
|
||||
$(LUA_T): $(LUA_O) $(LUA_A)
|
||||
$(CC) -o $@ $(LDFLAGS) $(LUA_O) $(LUA_A) $(LIBS)
|
||||
|
||||
$(LUAC_T): $(LUAC_O) $(LUA_A)
|
||||
$(CC) -o $@ $(LDFLAGS) $(LUAC_O) $(LUA_A) $(LIBS)
|
||||
|
||||
$(TESTP_T): $(TESTP_O) $(LUA_A)
|
||||
$(CC) -o $@ $(LDFLAGS) $(TESTP_O) $(LUA_A) $(LIBS)
|
||||
|
||||
$(TESTUP_T): $(TESTUP_O) $(LUA_A)
|
||||
$(CC) -o $@ $(LDFLAGS) $(TESTUP_O) $(LUA_A) $(LIBS)
|
||||
|
||||
$(TESTP_O): lua.h lualib.h lauxlib.h
|
||||
$(CC) -c -o $@ ../test/persist.c -I../src
|
||||
|
||||
$(TESTUP_O): ../test/unpersist.c lua.h lualib.h lauxlib.h
|
||||
$(CC) -c -o $@ ../test/unpersist.c -I../src
|
||||
|
||||
clean:
|
||||
$(RM) $(ALL_T) $(ALL_O)
|
||||
|
||||
depend:
|
||||
@$(CC) $(CFLAGS) -MM l*.c
|
||||
|
||||
echo:
|
||||
@echo "PLAT= $(PLAT)"
|
||||
@echo "CC= $(CC)"
|
||||
@echo "CFLAGS= $(CFLAGS)"
|
||||
@echo "LDFLAGS= $(SYSLDFLAGS)"
|
||||
@echo "LIBS= $(LIBS)"
|
||||
@echo "AR= $(AR)"
|
||||
@echo "RANLIB= $(RANLIB)"
|
||||
@echo "RM= $(RM)"
|
||||
|
||||
# Convenience targets for popular platforms
|
||||
ALL= all
|
||||
|
||||
none:
|
||||
@echo "Please do 'make PLATFORM' where PLATFORM is one of these:"
|
||||
@echo " $(PLATS)"
|
||||
|
||||
aix:
|
||||
$(MAKE) $(ALL) CC="xlc" CFLAGS="-O2 -DLUA_USE_POSIX -DLUA_USE_DLOPEN" SYSLIBS="-ldl" SYSLDFLAGS="-brtl -bexpall"
|
||||
|
||||
ansi:
|
||||
$(MAKE) $(ALL) SYSCFLAGS="-DLUA_ANSI"
|
||||
|
||||
bsd:
|
||||
$(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_POSIX -DLUA_USE_DLOPEN" SYSLIBS="-Wl,-E"
|
||||
|
||||
freebsd:
|
||||
$(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_LINUX" SYSLIBS="-Wl,-E -lreadline"
|
||||
|
||||
generic: $(ALL)
|
||||
|
||||
linux:
|
||||
$(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_LINUX" SYSLIBS="-Wl,-E -ldl -lreadline"
|
||||
|
||||
macosx:
|
||||
$(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_MACOSX" SYSLIBS="-lreadline" CC=cc
|
||||
|
||||
mingw:
|
||||
$(MAKE) "LUA_T=lua.exe" \
|
||||
"AR=$(CC) -shared -o" "RANLIB=strip --strip-unneeded" \
|
||||
"SYSCFLAGS=-DLUA_BUILD_AS_DLL" "SYSLIBS=" "SYSLDFLAGS=-s" lua.exe
|
||||
$(MAKE) "LUAC_T=luac.exe" luac.exe
|
||||
$(MAKE) "TESTP_T=../test/persist.exe" ../test/persist.exe
|
||||
$(MAKE) "TESTUP_T=../test/unpersist.exe" ../test/unpersist.exe
|
||||
|
||||
win-gcc-static:
|
||||
$(MAKE) "LUA_T=lua.exe" "SYSLIBS=" "SYSLDFLAGS=-s" lua.exe
|
||||
$(MAKE) "LUAC_T=luac.exe" luac.exe
|
||||
$(MAKE) "TESTP_T=../test/persist.exe" ../test/persist.exe
|
||||
$(MAKE) "TESTUP_T=../test/unpersist.exe" ../test/unpersist.exe
|
||||
|
||||
posix:
|
||||
$(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_POSIX"
|
||||
|
||||
solaris:
|
||||
$(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_POSIX -DLUA_USE_DLOPEN" SYSLIBS="-ldl"
|
||||
|
||||
# list targets that do not create files (but not all makes understand .PHONY)
|
||||
.PHONY: all $(PLATS) default o a clean depend echo none
|
||||
|
||||
# DO NOT DELETE
|
||||
|
||||
lapi.o: lapi.c lua.h luaconf.h lapi.h llimits.h lstate.h lobject.h ltm.h \
|
||||
lzio.h lmem.h ldebug.h ldo.h lfunc.h lgc.h lstring.h ltable.h lundump.h \
|
||||
lvm.h
|
||||
lauxlib.o: lauxlib.c lua.h luaconf.h lauxlib.h
|
||||
lbaselib.o: lbaselib.c lua.h luaconf.h lauxlib.h lualib.h
|
||||
lbitlib.o: lbitlib.c lua.h luaconf.h lauxlib.h lualib.h
|
||||
lcode.o: lcode.c lua.h luaconf.h lcode.h llex.h lobject.h llimits.h \
|
||||
lzio.h lmem.h lopcodes.h lparser.h ldebug.h lstate.h ltm.h ldo.h lgc.h \
|
||||
lstring.h ltable.h lvm.h
|
||||
lcorolib.o: lcorolib.c lua.h luaconf.h lauxlib.h lualib.h
|
||||
lctype.o: lctype.c lctype.h lua.h luaconf.h llimits.h
|
||||
ldblib.o: ldblib.c lua.h luaconf.h lauxlib.h lualib.h
|
||||
ldebug.o: ldebug.c lua.h luaconf.h lapi.h llimits.h lstate.h lobject.h \
|
||||
ltm.h lzio.h lmem.h lcode.h llex.h lopcodes.h lparser.h ldebug.h ldo.h \
|
||||
lfunc.h lstring.h lgc.h ltable.h lvm.h
|
||||
ldo.o: ldo.c lua.h luaconf.h lapi.h llimits.h lstate.h lobject.h ltm.h \
|
||||
lzio.h lmem.h ldebug.h ldo.h lfunc.h lgc.h lopcodes.h lparser.h \
|
||||
lstring.h ltable.h lundump.h lvm.h
|
||||
ldump.o: ldump.c lua.h luaconf.h lobject.h llimits.h lstate.h ltm.h \
|
||||
lzio.h lmem.h lundump.h
|
||||
lfunc.o: lfunc.c lua.h luaconf.h lfunc.h lobject.h llimits.h lgc.h \
|
||||
lstate.h ltm.h lzio.h lmem.h
|
||||
lgc.o: lgc.c lua.h luaconf.h ldebug.h lstate.h lobject.h llimits.h ltm.h \
|
||||
lzio.h lmem.h ldo.h lfunc.h lgc.h lstring.h ltable.h
|
||||
linit.o: linit.c lua.h luaconf.h lualib.h lauxlib.h
|
||||
liolib.o: liolib.c lua.h luaconf.h lauxlib.h lualib.h
|
||||
llex.o: llex.c lua.h luaconf.h lctype.h llimits.h ldo.h lobject.h \
|
||||
lstate.h ltm.h lzio.h lmem.h llex.h lparser.h lstring.h lgc.h ltable.h
|
||||
lmathlib.o: lmathlib.c lua.h luaconf.h lauxlib.h lualib.h
|
||||
lmem.o: lmem.c lua.h luaconf.h ldebug.h lstate.h lobject.h llimits.h \
|
||||
ltm.h lzio.h lmem.h ldo.h lgc.h
|
||||
loadlib.o: loadlib.c lua.h luaconf.h lauxlib.h lualib.h
|
||||
lobject.o: lobject.c lua.h luaconf.h lctype.h llimits.h ldebug.h lstate.h \
|
||||
lobject.h ltm.h lzio.h lmem.h ldo.h lstring.h lgc.h lvm.h
|
||||
lopcodes.o: lopcodes.c lopcodes.h llimits.h lua.h luaconf.h
|
||||
loslib.o: loslib.c lua.h luaconf.h lauxlib.h lualib.h
|
||||
lparser.o: lparser.c lua.h luaconf.h lcode.h llex.h lobject.h llimits.h \
|
||||
lzio.h lmem.h lopcodes.h lparser.h ldebug.h lstate.h ltm.h ldo.h lfunc.h \
|
||||
lstring.h lgc.h ltable.h
|
||||
lstate.o: lstate.c lua.h luaconf.h lapi.h llimits.h lstate.h lobject.h \
|
||||
ltm.h lzio.h lmem.h ldebug.h ldo.h lfunc.h lgc.h llex.h lstring.h \
|
||||
ltable.h
|
||||
lstring.o: lstring.c lua.h luaconf.h lmem.h llimits.h lobject.h lstate.h \
|
||||
ltm.h lzio.h lstring.h lgc.h
|
||||
lstrlib.o: lstrlib.c lua.h luaconf.h lauxlib.h lualib.h
|
||||
ltable.o: ltable.c lua.h luaconf.h ldebug.h lstate.h lobject.h llimits.h \
|
||||
ltm.h lzio.h lmem.h ldo.h lgc.h lstring.h ltable.h lvm.h
|
||||
ltablib.o: ltablib.c lua.h luaconf.h lauxlib.h lualib.h
|
||||
ltm.o: ltm.c lua.h luaconf.h lobject.h llimits.h lstate.h ltm.h lzio.h \
|
||||
lmem.h lstring.h lgc.h ltable.h
|
||||
lua.o: lua.c lua.h luaconf.h lauxlib.h lualib.h
|
||||
luac.o: luac.c lua.h luaconf.h lauxlib.h lobject.h llimits.h lstate.h \
|
||||
ltm.h lzio.h lmem.h lundump.h ldebug.h lopcodes.h
|
||||
lundump.o: lundump.c lua.h luaconf.h ldebug.h lstate.h lobject.h \
|
||||
llimits.h ltm.h lzio.h lmem.h ldo.h lfunc.h lstring.h lgc.h lundump.h
|
||||
lvm.o: lvm.c lua.h luaconf.h ldebug.h lstate.h lobject.h llimits.h ltm.h \
|
||||
lzio.h lmem.h ldo.h lfunc.h lgc.h lopcodes.h lstring.h ltable.h lvm.h
|
||||
lzio.o: lzio.c lua.h luaconf.h llimits.h lmem.h lstate.h lobject.h ltm.h \
|
||||
lzio.h
|
||||
eris.o: eris.c lua.h lauxlib.h lualib.h ldebug.h ldo.h lfunc.h lobject.h \
|
||||
lstate.h lstring.h lzio.h eris.h
|
||||
2758
luprex/ext/eris-master/src/eris.c
Normal file
2758
luprex/ext/eris-master/src/eris.c
Normal file
File diff suppressed because it is too large
Load Diff
153
luprex/ext/eris-master/src/eris.h
Normal file
153
luprex/ext/eris-master/src/eris.h
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
Eris - Heavy-duty persistence for Lua 5.2.4 - Based on Pluto
|
||||
Copyright (c) 2013-2015 by Florian Nuecke.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/* lua.h must be included before this file */
|
||||
|
||||
#ifndef ERIS_H
|
||||
#define ERIS_H
|
||||
|
||||
#define ERIS_VERSION_MAJOR "1"
|
||||
#define ERIS_VERSION_MINOR "1"
|
||||
#define ERIS_VERSION_NUM 101
|
||||
#define ERIS_VERSION_RELEASE "1"
|
||||
|
||||
/*
|
||||
** ==================================================================
|
||||
** API
|
||||
** ==================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* This provides an interface to Eris' persist functionality for writing in
|
||||
* an arbitrary way, using a writer.
|
||||
*
|
||||
* When called, the stack in 'L' must look like this:
|
||||
* 1: perms:table
|
||||
* 2: value:any
|
||||
*
|
||||
* 'writer' is the writer function used to store all data, 'ud' is passed to
|
||||
* the writer function whenever it is called.
|
||||
*
|
||||
* [-0, +0, e]
|
||||
*/
|
||||
LUA_API void eris_dump(lua_State* L, lua_Writer writer, void* ud);
|
||||
|
||||
/**
|
||||
* This provides an interface to Eris' unpersist functionality for reading
|
||||
* in an arbitrary way, using a reader.
|
||||
*
|
||||
* When called, the stack in 'L' must look like this:
|
||||
* 1: perms:table
|
||||
*
|
||||
* 'reader' is the reader function used to read all data, 'ud' is passed to
|
||||
* the reader function whenever it is called.
|
||||
*
|
||||
* The result of the operation will be pushed onto the stack.
|
||||
*
|
||||
* [-0, +1, e]
|
||||
*/
|
||||
LUA_API void eris_undump(lua_State* L, lua_Reader reader, void* ud);
|
||||
|
||||
/**
|
||||
* This is a stack-based alternative to eris_dump.
|
||||
*
|
||||
* It expects the perms table at the specified index 'perms' and the value to
|
||||
* persist at the specified index 'value'. It will push the resulting string
|
||||
* onto the stack on success.
|
||||
*
|
||||
* [-0, +1, e]
|
||||
*/
|
||||
LUA_API void eris_persist(lua_State* L, int perms, int value);
|
||||
|
||||
/**
|
||||
* This is a stack-based alternative to eris_undump.
|
||||
*
|
||||
* It expects the perms table at the specified index 'perms' and the string
|
||||
* containing persisted data at the specified index 'value'. It will push the
|
||||
* resulting value onto the stack on success.
|
||||
*
|
||||
* [-0, +1, e]
|
||||
*/
|
||||
LUA_API void eris_unpersist(lua_State* L, int perms, int value);
|
||||
|
||||
/**
|
||||
* Pushes the current value of a setting onto the stack.
|
||||
*
|
||||
* The name is the name of the setting to get the value for:
|
||||
* - 'debug' whether to write debug information when persisting function
|
||||
* prototypes (line numbers, local variable names, upvalue names).
|
||||
* - 'maxrec' the maximum complexity of objects we support (the nesting level
|
||||
* of tables, for example). This can be useful to avoid segmentation
|
||||
* faults due to too deep recursion when working with user-provided
|
||||
* data.
|
||||
* - 'path' whether to generate a "path" used to indicate where in an object
|
||||
* an error occurred. This adds considerable overhead and should
|
||||
* only be used to debug errors as they appear.
|
||||
* - 'spio' whether to pass IO objects along as light userdata to special
|
||||
* persistence functions. When persisting this will pass along the
|
||||
* lua_Writer and its void* in addition to the original object, when
|
||||
* unpersisting it will pass along a ZIO*.
|
||||
* - 'spkey' the name of the field in the metatable of tables and userdata
|
||||
* used to control persistence (on/off or special persistence).
|
||||
*
|
||||
* If an unknown name is specified this will throw an error.
|
||||
*
|
||||
* [-0, +1, e]
|
||||
*/
|
||||
LUA_API void eris_get_setting(lua_State *L, const char *name);
|
||||
|
||||
/**
|
||||
* Changes the value of a setting to a value on the stack.
|
||||
*
|
||||
* For the available settings see eris_set_setting(). This will get the new
|
||||
* value from the specified stack index 'value'. If the type does not match
|
||||
* this will throw an error. Specify a nil value to reset the setting to its
|
||||
* default value.
|
||||
*
|
||||
* [-0, +0, e]
|
||||
*/
|
||||
LUA_API void eris_set_setting(lua_State *L, const char *name, int value);
|
||||
|
||||
/*
|
||||
** ==================================================================
|
||||
** Library installer
|
||||
** ==================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* This pushes a table with the two functions 'persist' and 'unpersist':
|
||||
* persist([perms,] value)
|
||||
* Where 'perms' is a table with "permanent" objects and 'value' is the
|
||||
* value that should be persisted. Returns the string with persisted data.
|
||||
* If only one value is given, the perms table is assumed to be empty.
|
||||
*
|
||||
* unpersist([perms,] value)
|
||||
* Where 'perms' is a table with the inverse mapping as used when
|
||||
* persisting the data via persist() and 'value' is the string with the
|
||||
* persisted data returned by persist(). Returns the unpersisted value.
|
||||
* If only one value is given, the perms table is assumed to be empty.
|
||||
*/
|
||||
LUA_API int luaopen_eris(lua_State* L);
|
||||
|
||||
#endif
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user