mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-24 00:35:59 +02:00
A mod with a reskate-plugin.json is a plugin: Luau scripts that run in their own VM, with only
the reskate API ReSkate registers for them. Luau 0.741 is vendored in External/luau (the VM and
compiler only: no native code generation, no loadstring, io or C modules).
- Limits: 64 MB of memory, 50 ms a call, 2 ms a tick on average (over it for 3 s stops it),
more than 5 errors in 10 s stops it, 50 log lines a second. A plugin over a limit cannot catch
it in a pcall and carry on.
- API: reskate.log, on (load, unload, tick, level_loaded, level_unloading), timers, the level,
its own package's files and JSON, require inside the package; console.command and print
("console"); storage get, set, add and keys ("storage"), 1 MB in
%LOCALAPPDATA%\ReSkate\Plugins\<mod>\storage.json.
- Found every 2 s among the mods mods.json leaves on: switching a mod, or changing a plugin's
files, loads, unloads or reloads it without a restart. A package holding a program is refused.
- plugin list, commands, reload, off and on in the console. A plugin cannot take a name of
ReSkate's own commands.
- Tests: DINGOSDK_BUILD_PLUGIN_TESTS, including plugins written to break the sandbox.
509 lines
22 KiB
C++
509 lines
22 KiB
C++
#include "plugin_api.h"
|
|
#include "lua.h"
|
|
#include "lualib.h"
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <format>
|
|
#include <optional>
|
|
|
|
// The reskate API, v1 base set plus console and storage (docs: ReSkate Plugin System Design).
|
|
// Every function checks what it is given before acting on it, and no value a plugin passes
|
|
// reaches anything outside its own VM unchecked. Errors are raised the way Lua reports them:
|
|
// luaL_error, or a C++ exception, which Luau turns into a Lua error with its message.
|
|
namespace dingosdk::plugins {
|
|
namespace {
|
|
constexpr std::array<std::string_view, 5> events{"load", "unload", "tick", "level_loaded", "level_unloading"};
|
|
constexpr std::size_t maximum_key = 128;
|
|
constexpr std::size_t maximum_command = 48;
|
|
constexpr std::size_t maximum_listing = 1000;
|
|
|
|
// A plugin that caught its own memory or time error in a pcall gets no further: nothing it asks
|
|
// for is done, and the next safe point stops it.
|
|
Plugin& plugin_of(lua_State* L) {
|
|
auto& plugin = *static_cast<Plugin*>(lua_callbacks(L)->userdata);
|
|
if (plugin.halted()) luaL_error(L, "the plugin was stopped");
|
|
return plugin;
|
|
}
|
|
|
|
// The table being filled is on top of the stack.
|
|
void set_function(lua_State* L, const char* name, lua_CFunction function) {
|
|
lua_pushcfunction(L, function, name);
|
|
lua_setfield(L, -2, name);
|
|
}
|
|
|
|
std::string text_of(lua_State* L, int index) {
|
|
std::size_t length = 0;
|
|
const char* text = luaL_tolstring(L, index, &length);
|
|
std::string result(text, length);
|
|
lua_pop(L, 1);
|
|
return result;
|
|
}
|
|
|
|
// What turning Lua into JSON may cost outside the VM's own heap.
|
|
struct Budget {
|
|
std::size_t values = 1u << 20;
|
|
std::size_t bytes = 16u << 20;
|
|
void spend(std::size_t count, std::size_t size) {
|
|
if (count > values || size > bytes) throw std::runtime_error("the value is too large to turn into JSON");
|
|
values -= count;
|
|
bytes -= size;
|
|
}
|
|
};
|
|
|
|
Json to_json(lua_State* L, int index, int depth, Budget& budget) {
|
|
if (depth > 32) throw std::runtime_error("tables nested deeper than 32 cannot be turned into JSON");
|
|
index = lua_absindex(L, index);
|
|
budget.spend(1, 0);
|
|
switch (lua_type(L, index)) {
|
|
case LUA_TNIL: return nullptr;
|
|
case LUA_TBOOLEAN: return lua_toboolean(L, index) != 0;
|
|
case LUA_TNUMBER: {
|
|
const double value = lua_tonumber(L, index);
|
|
if (!std::isfinite(value)) throw std::runtime_error("JSON has no NaN or infinity");
|
|
if (value == std::floor(value) && std::fabs(value) <= 9007199254740992.0) return static_cast<std::int64_t>(value);
|
|
return value;
|
|
}
|
|
case LUA_TSTRING: {
|
|
std::size_t length = 0;
|
|
const char* text = lua_tolstring(L, index, &length);
|
|
budget.spend(0, length);
|
|
return std::string(text, length);
|
|
}
|
|
case LUA_TTABLE: {
|
|
luaL_checkstack(L, 4, "tables nested too deep");
|
|
const int length = lua_objlen(L, index);
|
|
bool list = length > 0;
|
|
int count = 0;
|
|
lua_pushnil(L);
|
|
while (lua_next(L, index)) {
|
|
++count;
|
|
if (lua_type(L, -2) != LUA_TNUMBER) list = false;
|
|
else {
|
|
const double key = lua_tonumber(L, -2);
|
|
if (key != std::floor(key) || key < 1 || key > length) list = false;
|
|
}
|
|
lua_pop(L, 1);
|
|
}
|
|
if (list && count == length) {
|
|
auto result = Json::array();
|
|
for (int i = 1; i <= length; ++i) {
|
|
lua_rawgeti(L, index, i);
|
|
result.push_back(to_json(L, -1, depth + 1, budget));
|
|
lua_pop(L, 1);
|
|
}
|
|
return result;
|
|
}
|
|
auto result = Json::object();
|
|
lua_pushnil(L);
|
|
while (lua_next(L, index)) {
|
|
if (lua_type(L, -2) != LUA_TSTRING)
|
|
throw std::runtime_error("only a list (keys 1 to n) or a table with string keys can be turned into JSON");
|
|
std::size_t size = 0;
|
|
const char* key = lua_tolstring(L, -2, &size);
|
|
budget.spend(0, size);
|
|
result[std::string_view(key, size)] = to_json(L, -1, depth + 1, budget);
|
|
lua_pop(L, 1);
|
|
}
|
|
return result;
|
|
}
|
|
default:
|
|
throw std::runtime_error(std::string("a ") + lua_typename(L, lua_type(L, index)) + " cannot be turned into JSON");
|
|
}
|
|
}
|
|
|
|
void push_json_value(lua_State* L, const Json& value, int depth) {
|
|
luaL_checkstack(L, 4, "JSON nested too deep");
|
|
if (depth > 64) throw std::runtime_error("JSON nested too deep");
|
|
if (value.is_null()) lua_pushnil(L);
|
|
else if (value.is_boolean()) lua_pushboolean(L, value.get<bool>());
|
|
else if (value.is_number()) lua_pushnumber(L, value.get<double>());
|
|
else if (value.is_string()) lua_pushlstring(L, value.string().data(), value.string().size());
|
|
else if (value.is_array()) {
|
|
lua_createtable(L, static_cast<int>(value.size()), 0);
|
|
int index = 0;
|
|
for (const auto& item : value) {
|
|
push_json_value(L, item, depth + 1);
|
|
lua_rawseti(L, -2, ++index);
|
|
}
|
|
} else {
|
|
lua_createtable(L, 0, static_cast<int>(value.size()));
|
|
for (const auto& [key, item] : value.items()) {
|
|
lua_pushlstring(L, key.data(), key.size());
|
|
push_json_value(L, item, depth + 1);
|
|
lua_rawset(L, -3);
|
|
}
|
|
}
|
|
}
|
|
|
|
void need(lua_State* L, Plugin& plugin, const char* permission, const char* what) {
|
|
if (!plugin.has(permission))
|
|
luaL_error(L, "%s needs the \"%s\" permission in reskate-plugin.json", what, permission);
|
|
}
|
|
|
|
std::string key_of(lua_State* L, int index) {
|
|
std::size_t length = 0;
|
|
const char* key = luaL_checklstring(L, index, &length);
|
|
if (length == 0 || length > maximum_key) luaL_error(L, "a storage key must be 1 to %d characters", static_cast<int>(maximum_key));
|
|
return std::string(key, length);
|
|
}
|
|
|
|
// A file or folder of the plugin's own package, never outside it: the path is checked as text,
|
|
// then once more after links and junctions are resolved.
|
|
std::filesystem::path package_path(lua_State* L, Plugin& plugin, std::string_view path, bool folder) {
|
|
std::string relative = folder && path.empty() ? std::string{} : safe_relative_path(path);
|
|
if (relative.empty() && !(folder && path.empty()))
|
|
luaL_error(L, "\"%s\" is not a path inside the plugin's package (use / between folders, no ..)", std::string(path).c_str());
|
|
const auto root = plugin.directory();
|
|
const auto full = relative.empty() ? root : root / std::filesystem::path(relative);
|
|
std::error_code code;
|
|
const auto real_root = std::filesystem::weakly_canonical(root, code);
|
|
const auto real = std::filesystem::weakly_canonical(full, code);
|
|
const auto inside = real.lexically_relative(real_root);
|
|
if (code || (!relative.empty() && (inside.empty() || *inside.begin() == ".."))) luaL_error(L, "that path leads outside the package");
|
|
return full;
|
|
}
|
|
}
|
|
|
|
struct ApiFunctions {
|
|
static int log(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
const auto message = text_of(L, 1);
|
|
const std::string level = luaL_optstring(L, 2, "info");
|
|
if (level != "info" && level != "warn" && level != "error") luaL_argerror(L, 2, "the level must be \"info\", \"warn\" or \"error\"");
|
|
plugin.log(level == "error" ? LogLevel::error : level == "warn" ? LogLevel::warn : LogLevel::info, message);
|
|
return 0;
|
|
}
|
|
static int print(lua_State* L) {
|
|
std::string line;
|
|
const int count = lua_gettop(L);
|
|
for (int i = 1; i <= count; ++i) {
|
|
if (i > 1) line += '\t';
|
|
line += text_of(L, i);
|
|
}
|
|
plugin_of(L).log(LogLevel::info, line);
|
|
return 0;
|
|
}
|
|
static int on(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
const std::string event = luaL_checkstring(L, 1);
|
|
luaL_checktype(L, 2, LUA_TFUNCTION);
|
|
if (std::find(events.begin(), events.end(), event) == events.end())
|
|
luaL_error(L, "there is no event \"%s\" in this ReSkate", event.c_str());
|
|
auto& list = plugin.handlers_[event];
|
|
if (list.size() >= plugin.limits_.handlers) luaL_error(L, "more than %d handlers for one event", static_cast<int>(plugin.limits_.handlers));
|
|
lua_pushvalue(L, 2);
|
|
list.push_back(lua_ref(L, -1));
|
|
lua_pop(L, 1);
|
|
return 0;
|
|
}
|
|
static int now(lua_State* L) {
|
|
lua_pushnumber(L, plugin_of(L).services_.now_ms());
|
|
return 1;
|
|
}
|
|
static int timer(lua_State* L, bool repeat) {
|
|
auto& plugin = plugin_of(L);
|
|
const double ms = luaL_checknumber(L, 1);
|
|
luaL_checktype(L, 2, LUA_TFUNCTION);
|
|
if (!std::isfinite(ms) || ms < (repeat ? 1 : 0)) luaL_argerror(L, 1, repeat ? "must be 1 ms or more" : "must be 0 ms or more");
|
|
if (plugin.timers_.size() >= plugin.limits_.timers) luaL_error(L, "more than %d timers", static_cast<int>(plugin.limits_.timers));
|
|
lua_pushvalue(L, 2);
|
|
const int function = lua_ref(L, -1);
|
|
lua_pop(L, 1);
|
|
const int id = plugin.next_timer_++;
|
|
plugin.timers_[id] = {plugin.services_.now_ms() + ms, repeat ? ms : 0, function};
|
|
lua_pushinteger(L, id);
|
|
return 1;
|
|
}
|
|
static int after(lua_State* L) { return timer(L, false); }
|
|
static int every(lua_State* L) { return timer(L, true); }
|
|
static int cancel(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
const auto found = plugin.timers_.find(luaL_checkinteger(L, 1));
|
|
if (found != plugin.timers_.end()) {
|
|
lua_unref(L, found->second.function);
|
|
plugin.timers_.erase(found);
|
|
}
|
|
return 0;
|
|
}
|
|
static int level(lua_State* L) {
|
|
const auto name = plugin_of(L).services_.level();
|
|
if (name.empty()) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
lua_createtable(L, 0, 1);
|
|
lua_pushlstring(L, name.data(), name.size());
|
|
lua_setfield(L, -2, "name");
|
|
return 1;
|
|
}
|
|
static int read(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
const auto file = package_path(L, plugin, luaL_checkstring(L, 1), false);
|
|
std::string error;
|
|
const auto text = read_package_file(file, plugin.limits_.file_bytes, error);
|
|
if (error == "not found") {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
if (!error.empty()) luaL_error(L, "%s %s", luaL_checkstring(L, 1), error.c_str());
|
|
lua_pushlstring(L, text.data(), text.size());
|
|
return 1;
|
|
}
|
|
static int read_json(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
const auto file = package_path(L, plugin, luaL_checkstring(L, 1), false);
|
|
std::string error;
|
|
const auto text = read_package_file(file, plugin.limits_.file_bytes, error);
|
|
if (error == "not found") {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
if (!error.empty()) luaL_error(L, "%s %s", luaL_checkstring(L, 1), error.c_str());
|
|
push_json(L, Json::parse(text, {plugin.limits_.file_bytes, 64, 1u << 20}));
|
|
return 1;
|
|
}
|
|
static int list(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
const auto folder = package_path(L, plugin, luaL_optstring(L, 1, ""), true);
|
|
std::vector<std::string> names;
|
|
std::error_code code;
|
|
for (std::filesystem::directory_iterator it(folder, code), end; !code && it != end; it.increment(code)) {
|
|
const auto name = it->path().filename().wstring();
|
|
if (!std::all_of(name.begin(), name.end(), [](wchar_t c) { return c >= 0x20 && c < 0x7f; })) continue;
|
|
std::string ascii;
|
|
for (const wchar_t c : name) ascii += static_cast<char>(c);
|
|
if (it->is_directory(code)) ascii += '/';
|
|
names.push_back(std::move(ascii));
|
|
if (names.size() > maximum_listing) luaL_error(L, "the folder holds more than %d entries", static_cast<int>(maximum_listing));
|
|
}
|
|
std::sort(names.begin(), names.end());
|
|
lua_createtable(L, static_cast<int>(names.size()), 0);
|
|
for (std::size_t i = 0; i < names.size(); ++i) {
|
|
lua_pushlstring(L, names[i].data(), names[i].size());
|
|
lua_rawseti(L, -2, static_cast<int>(i + 1));
|
|
}
|
|
return 1;
|
|
}
|
|
static int encode(lua_State* L) {
|
|
luaL_checkany(L, 1);
|
|
const auto text = lua_to_json(L, 1).dump(lua_toboolean(L, 2) ? 2 : -1);
|
|
lua_pushlstring(L, text.data(), text.size());
|
|
return 1;
|
|
}
|
|
static int decode(lua_State* L) {
|
|
std::size_t length = 0;
|
|
const char* text = luaL_checklstring(L, 1, &length);
|
|
push_json(L, Json::parse(std::string_view(text, length), {16u << 20, 64, 1u << 20}));
|
|
return 1;
|
|
}
|
|
static int command(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "console", "reskate.console.command");
|
|
std::size_t length = 0;
|
|
const char* text = luaL_checklstring(L, 1, &length);
|
|
const std::string help = luaL_checkstring(L, 2);
|
|
luaL_checktype(L, 3, LUA_TFUNCTION);
|
|
const std::string name(text, length);
|
|
// Lowercase words of letters, digits, _ and -, one space between them, at most three.
|
|
bool valid = !name.empty() && name.size() <= maximum_command && name.front() != ' ' && name.back() != ' ' &&
|
|
name.find(" ") == std::string::npos && std::count(name.begin(), name.end(), ' ') < 3;
|
|
for (const char c : name) valid = valid && ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == ' ');
|
|
if (!valid) luaL_error(L, "\"%s\" is not a command name: 1 to 3 lowercase words of a-z, 0-9, _ and -", name.c_str());
|
|
if (plugin.commands_.size() >= plugin.limits_.commands) luaL_error(L, "more than %d commands", static_cast<int>(plugin.limits_.commands));
|
|
if (plugin.commands_.contains(name)) luaL_error(L, "the command \"%s\" is registered already", name.c_str());
|
|
if (const auto refusal = plugin.services_.claim_command(plugin.id(), name); !refusal.empty())
|
|
luaL_error(L, "%s", refusal.c_str());
|
|
lua_pushvalue(L, 3);
|
|
plugin.commands_[name] = {help.substr(0, 200), lua_ref(L, -1)};
|
|
lua_pop(L, 1);
|
|
return 0;
|
|
}
|
|
static int console_print(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "console", "reskate.console.print");
|
|
plugin.print(text_of(L, 1));
|
|
return 0;
|
|
}
|
|
static int storage_get(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "storage", "reskate.storage");
|
|
const auto key = key_of(L, 1);
|
|
const auto found = plugin.storage_.find(key);
|
|
if (found == plugin.storage_.end()) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
push_json(L, *found);
|
|
return 1;
|
|
}
|
|
// Sets a key and checks the whole store against the quota; the value it had comes back on failure.
|
|
static void store(lua_State* L, Plugin& plugin, const std::string& key, Json value) {
|
|
std::optional<Json> previous;
|
|
if (const auto found = plugin.storage_.find(key); found != plugin.storage_.end()) previous = *found;
|
|
if (value.is_null()) plugin.storage_.erase(key);
|
|
else plugin.storage_[key] = std::move(value);
|
|
if (const auto size = plugin.storage_.dump().size(); size > plugin.limits_.storage_bytes) {
|
|
if (previous) plugin.storage_[key] = std::move(*previous);
|
|
else plugin.storage_.erase(key);
|
|
luaL_error(L, "storage is full: that would make it %d KB, and a plugin has %d KB", static_cast<int>(size >> 10),
|
|
static_cast<int>(plugin.limits_.storage_bytes >> 10));
|
|
}
|
|
plugin.storage_dirty_ = true;
|
|
}
|
|
static int storage_set(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "storage", "reskate.storage");
|
|
const auto key = key_of(L, 1);
|
|
store(L, plugin, key, lua_to_json(L, 2));
|
|
return 0;
|
|
}
|
|
static int storage_add(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "storage", "reskate.storage");
|
|
const auto key = key_of(L, 1);
|
|
const double by = luaL_optnumber(L, 2, 1);
|
|
if (!std::isfinite(by)) luaL_argerror(L, 2, "must be a finite number");
|
|
double value = 0;
|
|
if (const auto found = plugin.storage_.find(key); found != plugin.storage_.end()) {
|
|
if (!found->is_number()) luaL_error(L, "storage key \"%s\" does not hold a number", key.c_str());
|
|
value = found->get<double>();
|
|
}
|
|
value += by;
|
|
store(L, plugin, key, value == std::floor(value) && std::fabs(value) <= 9007199254740992.0
|
|
? Json(static_cast<std::int64_t>(value)) : Json(value));
|
|
lua_pushnumber(L, value);
|
|
return 1;
|
|
}
|
|
static int storage_keys(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "storage", "reskate.storage");
|
|
lua_createtable(L, static_cast<int>(plugin.storage_.size()), 0);
|
|
int index = 0;
|
|
for (const auto& item : plugin.storage_.items()) {
|
|
lua_pushlstring(L, item.first.data(), item.first.size());
|
|
lua_rawseti(L, -2, ++index);
|
|
}
|
|
return 1;
|
|
}
|
|
// Modules come from the plugin's own package, beside its entry: require("./rules") or
|
|
// require("rules") is <entry folder>/rules.luau or rules.lua. Each runs once.
|
|
static int require(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
std::string name = luaL_checkstring(L, 1);
|
|
if (name.starts_with("./")) name.erase(0, 2);
|
|
const auto relative = safe_relative_path(name);
|
|
if (relative.empty()) luaL_error(L, "require(\"%s\"): a module is a path inside the plugin's package, with no ..", name.c_str());
|
|
const auto in_root = [&](const std::string& path) { return plugin.code_root_.empty() ? path : plugin.code_root_ + "/" + path; };
|
|
std::vector<std::string> candidates;
|
|
if (relative.ends_with(".lua") || relative.ends_with(".luau")) candidates.push_back(in_root(relative));
|
|
else {
|
|
candidates.push_back(in_root(relative + ".luau"));
|
|
candidates.push_back(in_root(relative + ".lua"));
|
|
}
|
|
std::string key;
|
|
std::filesystem::path file;
|
|
for (const auto& candidate : candidates) {
|
|
const auto path = package_path(L, plugin, candidate, false);
|
|
std::error_code code;
|
|
if (std::filesystem::is_regular_file(path, code)) {
|
|
key = candidate;
|
|
file = path;
|
|
break;
|
|
}
|
|
}
|
|
if (key.empty()) luaL_error(L, "require(\"%s\"): there is no %s", name.c_str(), candidates.front().c_str());
|
|
lua_getref(L, plugin.modules_);
|
|
lua_getfield(L, -1, key.c_str());
|
|
if (!lua_isnil(L, -1)) return 1;
|
|
lua_pop(L, 1);
|
|
if (plugin.loading_modules_.contains(key)) luaL_error(L, "require(\"%s\"): it requires itself, through another module", name.c_str());
|
|
plugin.loading_modules_.insert(key);
|
|
struct Done {
|
|
Plugin& plugin;
|
|
std::string key;
|
|
~Done() { plugin.loading_modules_.erase(key); }
|
|
} done{plugin, key};
|
|
if (!plugin.load_chunk(L, key, file)) lua_error(L);
|
|
lua_call(L, 0, 1);
|
|
if (lua_isnil(L, -1)) {
|
|
lua_pop(L, 1);
|
|
lua_pushboolean(L, 1);
|
|
}
|
|
lua_pushvalue(L, -1);
|
|
lua_setfield(L, -3, key.c_str());
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
Json lua_to_json(lua_State* L, int index) {
|
|
Budget budget;
|
|
return to_json(L, index, 0, budget);
|
|
}
|
|
|
|
void push_json(lua_State* L, const Json& value) { push_json_value(L, value, 0); }
|
|
|
|
void Api::install(Plugin& plugin, lua_State* L) {
|
|
const auto readonly = [&] { lua_setreadonly(L, -1, true); };
|
|
lua_createtable(L, 0, 16);
|
|
lua_pushinteger(L, api_version);
|
|
lua_setfield(L, -2, "api_version");
|
|
lua_pushlstring(L, plugin.id().data(), plugin.id().size());
|
|
lua_setfield(L, -2, "id");
|
|
set_function(L, "log", ApiFunctions::log);
|
|
set_function(L, "on", ApiFunctions::on);
|
|
|
|
lua_createtable(L, 0, 4);
|
|
set_function(L, "now", ApiFunctions::now);
|
|
set_function(L, "after", ApiFunctions::after);
|
|
set_function(L, "every", ApiFunctions::every);
|
|
set_function(L, "cancel", ApiFunctions::cancel);
|
|
readonly();
|
|
lua_setfield(L, -2, "time");
|
|
|
|
lua_createtable(L, 0, 1);
|
|
set_function(L, "current", ApiFunctions::level);
|
|
readonly();
|
|
lua_setfield(L, -2, "level");
|
|
|
|
lua_createtable(L, 0, 3);
|
|
set_function(L, "read", ApiFunctions::read);
|
|
set_function(L, "read_json", ApiFunctions::read_json);
|
|
set_function(L, "list", ApiFunctions::list);
|
|
readonly();
|
|
lua_setfield(L, -2, "package");
|
|
|
|
lua_createtable(L, 0, 2);
|
|
set_function(L, "encode", ApiFunctions::encode);
|
|
set_function(L, "decode", ApiFunctions::decode);
|
|
readonly();
|
|
lua_setfield(L, -2, "json");
|
|
|
|
lua_createtable(L, 0, 2);
|
|
set_function(L, "command", ApiFunctions::command);
|
|
lua_pushcfunction(L, ApiFunctions::console_print, "print");
|
|
lua_setfield(L, -2, "print");
|
|
readonly();
|
|
lua_setfield(L, -2, "console");
|
|
|
|
lua_createtable(L, 0, 4);
|
|
lua_pushcfunction(L, ApiFunctions::storage_get, "get");
|
|
lua_setfield(L, -2, "get");
|
|
lua_pushcfunction(L, ApiFunctions::storage_set, "set");
|
|
lua_setfield(L, -2, "set");
|
|
lua_pushcfunction(L, ApiFunctions::storage_add, "add");
|
|
lua_setfield(L, -2, "add");
|
|
lua_pushcfunction(L, ApiFunctions::storage_keys, "keys");
|
|
lua_setfield(L, -2, "keys");
|
|
readonly();
|
|
lua_setfield(L, -2, "storage");
|
|
|
|
readonly();
|
|
lua_setglobal(L, "reskate");
|
|
lua_pushcfunction(L, ApiFunctions::print, "print");
|
|
lua_setglobal(L, "print");
|
|
lua_pushcfunction(L, ApiFunctions::require, "require");
|
|
lua_setglobal(L, "require");
|
|
}
|
|
}
|