diff --git a/LUA_API.rst b/LUA_API.rst index 12b25bac8..bf6c81ec0 100644 --- a/LUA_API.rst +++ b/LUA_API.rst @@ -127,10 +127,15 @@ They implement the following features: Valid fields of the structure may be accessed by subscript. - In case of inheritance, *superclass* fields have precedence + Primitive typed fields, i.e. numbers & strings, are converted + to/from matching lua values. The value of a pointer is a reference + to the target, or nil/NULL. Complex types are represented by + a reference to the field within the structure; unless recursive + lua table assignment is used, such fields can only be read. + + **NOTE:** In case of inheritance, *superclass* fields have precedence over the subclass, but fields shadowed in this way can still be accessed as ``ref['subclasstype.field']``. - This shadowing order is necessary because vtable-based classes are automatically exposed in their exact type, and the reverse rule would make access to superclass fields unreliable. @@ -138,8 +143,8 @@ They implement the following features: * ``ref._field(field)`` Returns a reference to a valid field. That is, unlike regular - subscript, it returns a pointer reference even for primitive - typed fields. + subscript, it returns a reference to the field within the structure + even for primitive typed fields and pointers. * ``ref:vmethod(args...)`` @@ -297,3 +302,198 @@ The ``df`` table itself contains the following functions and values: * ``df.is_instance(type,obj)`` Equivalent to the method, but also allows a reference as proxy for its type. + +Recursive table assignment +========================== + +Recursive assignment is invoked when a lua table is assigned +to a C++ object or field, i.e. one of: + +* ``ref:assign{...}`` +* ``ref.field = {...}`` + +The general mode of operation is that all fields of the table +are assigned to the fields of the target structure, roughly +emulating the following code:: + + function rec_assign(ref,table) + for key,value in pairs(table) do + ref[key] = value + end + end + +Since assigning a table to a field using = invokes the same +process, it is recursive. + +There are however some variations to this process depending +on the type of the field being assigned to: + +1. If the table contains an ``assign`` field, it is + applied first, using the ``ref:assign(value)`` method. + It is never assigned as a usual field. + +2. When a table is assigned to a non-NULL pointer field + using the ``ref.field = {...}`` syntax, it is applied + to the target of the pointer instead. + + If the pointer is NULL, the table is checked for a ``new`` field: + + a. If it is *nil* or *false*, assignment fails with an error. + + b. If it is *true*, the pointer is initialized with a newly + allocated object of the declared target type of the pointer. + + c. Otherwise, ``table.new`` must be a named type, or an + object of a type compatible with the pointer. The pointer + is initialized with the result of calling ``table.new:new()``. + + After this auto-vivification process, assignment proceeds + as if the pointer wasn't NULL. + + Obviously, the ``new`` field inside the table is always skipped + during the actual per-field assignment processing. + +3. If the target of the assignment is a container, a separate + rule set is used: + + a. If the table contains neither ``assign`` nor ``resize`` + fields, it is interpreted as an ordinary *1-based* lua + array. The container is resized to the #-size of the + table, and elements are assigned in numeric order:: + + ref:resize(#table); + for i=1,#table do ref[i-1] = table[i] end + + b. Otherwise, ``resize`` must be *true*, *false*, or + an explicit number. If it is not false, the container + is resized. After that the usual struct-like 'pairs' + assignment is performed. + + In case ``resize`` is *true*, the size is computed + by scanning the table for the largest numeric key. + + This means that in order to reassign only one element of + a container using this system, it is necessary to use:: + + { resize=false, [idx]=value } + +Since nil inside a table is indistinguishable from missing key, +it is necessary to use ``df.NULL`` as a null pointer value. + +This system is intended as a way to define a nested object +tree using pure lua data structures, and then materialize it in +C++ memory in one go. Note that if pointer auto-vivification +is used, an error in the middle of the recursive walk would +not destroy any objects allocated in this way, so the user +should be prepared to catch the error and do the necessary +cleanup. + +================ +DFHack utilities +================ + +DFHack utility functions are placed in the ``dfhack`` global tree. + +Currently it defines the following features: + +* ``dfhack.print(args...)`` + + Output tab-separated args as standard lua print would do, + but without a newline. + +* ``print(args...)``, ``dfhack.println(args...)`` + + A replacement of the standard library print function that + works with DFHack output infrastructure. + +* ``dfhack.printerr(args...)`` + + Same as println; intended for errors. Uses red color and logs to stderr.log. + +* ``dfhack.color([color])`` + + Sets the current output color. If color is *nil* or *-1*, resets to default. + +* ``dfhack.is_interactive()`` + + Checks if the thread can access the interactive console and returns *true* or *false*. + +* ``dfhack.lineedit([prompt[,history_filename]])`` + + If the thread owns the interactive console, shows a prompt + and returns the entered string. Otherwise returns *nil, error*. + +* ``dfhack.interpreter([prompt[,env[,history_filename]]])`` + + Starts an interactive lua interpreter, using the specified prompt + string, global environment and command-line history file. + + If the interactive console is not accessible, returns *nil, error*. + +* ``dfhack.pcall(f[,args...])`` + + Invokes f via xpcall, using an error function that attaches + a stack trace to the error. The same function is used by SafeCall + in C++, and dfhack.safecall. + + The returned error is a table with separate ``message`` and + ``stacktrace`` string fields; it implements ``__tostring``. + +* ``safecall(f[,args...])``, ``dfhack.safecall(f[,args...])`` + + Just like pcall, but also prints the error using printerr before + returning. Intended as a convenience function. + +* ``dfhack.with_suspend(f[,args...])`` + + Calls ``f`` with arguments after grabbing the DF core suspend lock. + Suspending is necessary for accessing a consistent state of DF memory. + + Returned values and errors are propagated through after releasing + the lock. It is safe to nest suspends. + + Every thread is allowed only one suspend per DF frame, so it is best + to group operations together in one big critical section. A plugin + can choose to run all lua code inside a C++-side suspend lock. + +Persistent configuration storage +================================ + +This api is intended for storing configuration options in the world itself. +It probably should be restricted to data that is world-dependent. + +Entries are identified by a string ``key``, but it is also possible to manage +multiple entries with the same key; their identity is determined by ``entry_id``. +Every entry has a mutable string ``value``, and an array of 7 mutable ``ints``. + +* ``dfhack.persistent.get(key)``, ``entry:get()`` + + Retrieves a persistent config record with the given string key, + or refreshes an already retrieved entry. If there are multiple + entries with the same key, it is undefined which one is retrieved + by the first version of the call. + + Returns entry, or *nil* if not found. + +* ``dfhack.persistent.delete(key)``, ``entry:delete()`` + + Removes an existing entry. Returns *true* if succeeded. + +* ``dfhack.persistent.get_all(key[,match_prefix])`` + + Retrieves all entries with the same key, or starting with key..'/'. + Calling ``get_all('',true)`` will match all entries. + + If none found, returns nil; otherwise returns an array of entries. + +* ``dfhack.persistent.save({key=str1, ...}[,new])``, ``entry:save([new])`` + + Saves changes in an entry, or creates a new one. Passing true as + new forces creation of a new entry even if one already exists; + otherwise the existing one is simply updated. + Returns *entry, did_create_new* + +Since the data is hidden in data structures owned by the DF world, +and automatically stored in the save game, these save and retrieval +functions can just copy values in memory without doing any actual I/O. +However, currently every entry has a 180+-byte dead-weight overhead. diff --git a/library/Core.cpp b/library/Core.cpp index ffc174721..b3c91034f 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -868,6 +868,15 @@ int Core::Update() color_ostream_proxy out(con); + // Pretend this thread has suspended the core in the usual way + { + lock_guard lock(d->AccessMutex); + + assert(d->df_suspend_depth == 0); + d->df_suspend_thread = this_thread::get_id(); + d->df_suspend_depth = 1000; + } + // detect if the game was loaded or unloaded in the meantime void *new_wdata = NULL; void *new_mapdata = NULL; @@ -922,6 +931,14 @@ int Core::Update() // notify all the plugins that a game tick is finished plug_mgr->OnUpdate(out); + // Release the fake suspend lock + { + lock_guard lock(d->AccessMutex); + + assert(d->df_suspend_depth == 1000); + d->df_suspend_depth = 0; + } + out << std::flush; // wake waiting tools diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index f81396f95..5a50d0dd2 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -76,6 +76,27 @@ static void set_dfhack_output(lua_State *L, color_ostream *p) lua_rawsetp(L, LUA_REGISTRYINDEX, &DFHACK_OSTREAM_TOKEN); } +static Console *get_console(lua_State *state) +{ + color_ostream *pstream = Lua::GetOutput(state); + + if (!pstream) + { + lua_pushnil(state); + lua_pushstring(state, "no output stream"); + return NULL; + } + + if (!pstream->is_console()) + { + lua_pushnil(state); + lua_pushstring(state, "not an interactive console"); + return NULL; + } + + return static_cast(pstream); +} + static std::string lua_print_fmt(lua_State *L) { /* Copied from lua source to fully replicate builtin print */ @@ -120,37 +141,233 @@ static int lua_dfhack_println(lua_State *S) return 0; } -static int lua_dfhack_printerr(lua_State *S) +static void dfhack_printerr(lua_State *S, const std::string &str) { - std::string str = lua_print_fmt(S); if (color_ostream *out = Lua::GetOutput(S)) out->printerr("%s\n", str.c_str()); else Core::printerr("%s\n", str.c_str()); +} + +static int lua_dfhack_printerr(lua_State *S) +{ + std::string str = lua_print_fmt(S); + dfhack_printerr(S, str); return 0; } -static int traceback (lua_State *L) { - const char *msg = lua_tostring(L, 1); - if (msg) - luaL_traceback(L, L, msg, 1); - else if (!lua_isnoneornil(L, 1)) { /* is there an error object? */ - if (!luaL_callmeta(L, 1, "__tostring")) /* try its 'tostring' metamethod */ - lua_pushliteral(L, "(no error message)"); - } +static int lua_dfhack_color(lua_State *S) +{ + int cv = luaL_optint(S, 1, -1); + + if (cv < -1 || cv > color_ostream::COLOR_MAX) + luaL_argerror(S, 1, "invalid color value"); + + color_ostream *out = Lua::GetOutput(S); + if (out) + out->color(color_ostream::color_value(cv)); + return 0; +} + +static int lua_dfhack_is_interactive(lua_State *S) +{ + lua_pushboolean(S, get_console(S) != NULL); return 1; } -static void report_error(color_ostream &out, lua_State *L) +static int lua_dfhack_lineedit(lua_State *S) { + const char *prompt = luaL_optstring(S, 1, ">> "); + const char *hfile = luaL_optstring(S, 2, NULL); + + Console *pstream = get_console(S); + if (!pstream) + return 2; + + DFHack::CommandHistory hist; + if (hfile) + hist.load(hfile); + + std::string ret; + int rv = pstream->lineedit(prompt, ret, hist); + + if (rv < 0) + { + lua_pushnil(S); + lua_pushstring(S, "input error"); + return 2; + } + else + { + if (hfile) + hist.save(hfile); + lua_pushlstring(S, ret.data(), ret.size()); + return 1; + } +} + +static int DFHACK_EXCEPTION_META_TOKEN = 0; + +static void error_tostring(lua_State *L) +{ + lua_getglobal(L, "tostring"); + lua_pushvalue(L, -2); + bool ok = lua_pcall(L, 1, 1, 0) == LUA_OK; + + const char *msg = lua_tostring(L, -1); + if (!msg) + { + msg = "tostring didn't return a string"; + ok = false; + } + + if (!ok) + { + lua_pushfstring(L, "(invalid error: %s)", msg); + lua_remove(L, -2); + } +} + +static void report_error(lua_State *L, color_ostream *out = NULL) +{ + lua_dup(L); + error_tostring(L); + const char *msg = lua_tostring(L, -1); - if (msg) - out.printerr("%s\n", msg); + assert(msg); + + if (out) + out->printerr("%s\n", msg); else - out.printerr("In Lua::SafeCall: error message is not a string.\n", msg); + dfhack_printerr(L, msg); + lua_pop(L, 1); } +static bool convert_to_exception(lua_State *L) +{ + int base = lua_gettop(L); + + bool force_unknown = false; + + if (lua_istable(L, base) && lua_getmetatable(L, base)) + { + lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_EXCEPTION_META_TOKEN); + bool is_exception = lua_rawequal(L, -1, -2); + lua_settop(L, base); + + // If it is an exception, return as is + if (is_exception) + return false; + + force_unknown = true; + } + + if (!lua_istable(L, base) || force_unknown) + { + lua_newtable(L); + lua_swap(L); + + if (lua_isstring(L, -1)) + lua_setfield(L, base, "message"); + else + { + error_tostring(L); + lua_setfield(L, base, "message"); + lua_setfield(L, base, "object"); + } + } + else + { + lua_getfield(L, base, "message"); + + if (!lua_isstring(L, -1)) + { + error_tostring(L); + lua_setfield(L, base, "message"); + } + + lua_settop(L, base); + } + + lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_EXCEPTION_META_TOKEN); + lua_setmetatable(L, base); + return true; +} + +static int dfhack_onerror(lua_State *L) +{ + luaL_checkany(L, 1); + lua_settop(L, 1); + + bool changed = convert_to_exception(L); + if (!changed) + return 1; + + luaL_traceback(L, L, NULL, 1); + lua_setfield(L, 1, "stacktrace"); + + return 1; +} + +static int dfhack_exception_tostring(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + + int base = lua_gettop(L); + + lua_getfield(L, 1, "message"); + if (!lua_isstring(L, -1)) + { + lua_pop(L, 1); + lua_pushstring(L, "(error message is not a string)"); + } + + lua_pushstring(L, "\n"); + lua_getfield(L, 1, "stacktrace"); + if (!lua_isstring(L, -1)) + lua_pop(L, 2); + + lua_concat(L, lua_gettop(L) - base); + return 1; +} + +static int finish_dfhack_safecall (lua_State *L, bool success) +{ + if (!lua_checkstack(L, 2)) + { + lua_settop(L, 0); /* create space for return values */ + lua_pushboolean(L, 0); + lua_pushstring(L, "stack overflow in dfhack.safecall()"); + success = false; + } + else + { + lua_pushboolean(L, success); + lua_replace(L, 1); /* put first result in first slot */ + } + + if (!success) + report_error(L); + + return lua_gettop(L); +} + +static int safecall_cont (lua_State *L) +{ + int status = lua_getctx(L, NULL); + return finish_dfhack_safecall(L, (status == LUA_YIELD)); +} + +static int lua_dfhack_safecall (lua_State *L) +{ + luaL_checkany(L, 1); + lua_pushcfunction(L, dfhack_onerror); + lua_insert(L, 1); + int status = lua_pcallk(L, lua_gettop(L) - 2, LUA_MULTRET, 1, 0, safecall_cont); + return finish_dfhack_safecall(L, (status == LUA_OK)); +} + bool DFHack::Lua::SafeCall(color_ostream &out, lua_State *L, int nargs, int nres, bool perr) { int base = lua_gettop(L) - nargs; @@ -158,17 +375,20 @@ bool DFHack::Lua::SafeCall(color_ostream &out, lua_State *L, int nargs, int nres color_ostream *cur_out = Lua::GetOutput(L); set_dfhack_output(L, &out); - lua_pushcfunction(L, traceback); + lua_pushcfunction(L, dfhack_onerror); lua_insert(L, base); bool ok = lua_pcall(L, nargs, nres, base) == LUA_OK; + if (!ok && perr) + { + report_error(L, &out); + lua_pop(L, 1); + } + lua_remove(L, base); set_dfhack_output(L, cur_out); - if (!ok && perr) - report_error(out, L); - return ok; } @@ -189,24 +409,51 @@ bool DFHack::Lua::Require(color_ostream &out, lua_State *state, return true; } -static bool load_with_env(color_ostream &out, lua_State *state, const std::string &code, int eidx) +bool DFHack::Lua::AssignDFObject(color_ostream &out, lua_State *state, + type_identity *type, void *target, int val_index, bool perr) { - if (luaL_loadbuffer(state, code.data(), code.size(), "=(interactive)") != LUA_OK) + val_index = lua_absindex(state, val_index); + lua_getfield(state, LUA_REGISTRYINDEX, DFHACK_ASSIGN_NAME); + PushDFObject(state, type, target); + lua_pushvalue(state, val_index); + return Lua::SafeCall(out, state, 2, 0, perr); +} + +bool DFHack::Lua::SafeCallString(color_ostream &out, lua_State *state, const std::string &code, + int nargs, int nres, bool perr, + const char *debug_tag, int env_idx) +{ + if (!debug_tag) + debug_tag = code.c_str(); + if (env_idx) + env_idx = lua_absindex(state, env_idx); + + int base = lua_gettop(state); + + // Parse the code + if (luaL_loadbuffer(state, code.data(), code.size(), debug_tag) != LUA_OK) { - report_error(out, state); + if (perr) + { + report_error(state, &out); + lua_pop(state, 1); + } + return false; } // Replace _ENV - lua_pushvalue(state, eidx); - - if (!lua_setupvalue(state, -2, 1)) + if (env_idx) { - out.printerr("No _ENV upvalue.\n"); - return false; + lua_pushvalue(state, env_idx); + lua_setupvalue(state, -2, 1); + assert(lua_gettop(state) == base+1); } - return true; + if (nargs > 0) + lua_insert(state, -1-nargs); + + return Lua::SafeCall(out, state, nargs, nres, perr); } bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, @@ -240,6 +487,8 @@ bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, // Make a proxy global environment. lua_newtable(state); + int base = lua_gettop(state); + lua_newtable(state); if (env) lua_pushvalue(state, env); @@ -249,7 +498,6 @@ bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, lua_setmetatable(state, -2); // Main interactive loop - int base = lua_gettop(state); int vcnt = 1; string curline; string prompt_str = "[" + string(prompt) + "]# "; @@ -272,9 +520,7 @@ bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, { curline = "return " + curline.substr(1); - if (!load_with_env(out, state, curline, base)) - continue; - if (!SafeCall(out, state, 0, LUA_MULTRET)) + if (!Lua::SafeCallString(out, state, curline, 0, LUA_MULTRET, true, "=(interactive)", base)) continue; int numret = lua_gettop(state) - base; @@ -308,9 +554,7 @@ bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, } else { - if (!load_with_env(out, state, curline, base)) - continue; - if (!SafeCall(out, state, 0, 0)) + if (!Lua::SafeCallString(out, state, curline, 0, LUA_MULTRET, true, "=(interactive)", base)) continue; } } @@ -323,9 +567,9 @@ bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, static int lua_dfhack_interpreter(lua_State *state) { - color_ostream *pstream = Lua::GetOutput(state); + Console *pstream = get_console(state); if (!pstream) - luaL_error(state, "Cannot use dfhack.interpreter() without output."); + return 2; int argc = lua_gettop(state); @@ -339,8 +583,7 @@ static int lua_dfhack_interpreter(lua_State *state) static int lua_dfhack_with_suspend(lua_State *L) { - int ctx; - int rv = lua_getctx(L, &ctx); + int rv = lua_getctx(L, NULL); // Non-resume entry point: if (rv == LUA_OK) @@ -351,7 +594,7 @@ static int lua_dfhack_with_suspend(lua_State *L) Core::getInstance().Suspend(); - lua_pushcfunction(L, traceback); + lua_pushcfunction(L, dfhack_onerror); lua_insert(L, 1); rv = lua_pcallk(L, nargs-1, LUA_MULTRET, 1, 0, lua_dfhack_with_suspend); @@ -372,8 +615,12 @@ static const luaL_Reg dfhack_funcs[] = { { "print", lua_dfhack_print }, { "println", lua_dfhack_println }, { "printerr", lua_dfhack_printerr }, - { "traceback", traceback }, + { "color", lua_dfhack_color }, + { "is_interactive", lua_dfhack_is_interactive }, + { "lineedit", lua_dfhack_lineedit }, { "interpreter", lua_dfhack_interpreter }, + { "safecall", lua_dfhack_safecall }, + { "onerror", dfhack_onerror }, { "with_suspend", lua_dfhack_with_suspend }, { NULL, NULL } }; @@ -459,6 +706,8 @@ static PersistentDataItem get_persistent(lua_State *state) static int dfhack_persistent_get(lua_State *state) { + CoreSuspender suspend; + auto ref = get_persistent(state); return read_persistent(state, ref, !lua_istable(state, 1)); @@ -466,6 +715,8 @@ static int dfhack_persistent_get(lua_State *state) static int dfhack_persistent_delete(lua_State *state) { + CoreSuspender suspend; + auto ref = get_persistent(state); bool ok = Core::getInstance().getWorld()->DeletePersistentData(ref); @@ -476,6 +727,8 @@ static int dfhack_persistent_delete(lua_State *state) static int dfhack_persistent_get_all(lua_State *state) { + CoreSuspender suspend; + const char *str = luaL_checkstring(state, 1); bool prefix = (lua_gettop(state)>=2 ? lua_toboolean(state,2) : false); @@ -501,6 +754,8 @@ static int dfhack_persistent_get_all(lua_State *state) static int dfhack_persistent_save(lua_State *state) { + CoreSuspender suspend; + lua_settop(state, 2); luaL_checktype(state, 1, LUA_TTABLE); bool add = lua_toboolean(state, 2); @@ -597,8 +852,18 @@ lua_State *DFHack::Lua::Open(color_ostream &out, lua_State *state) lua_pushcfunction(state, lua_dfhack_println); lua_setglobal(state, "print"); - // Create and initialize the dfhack global + // Create the dfhack global lua_newtable(state); + + // Create the metatable for exceptions + lua_newtable(state); + lua_pushcfunction(state, dfhack_exception_tostring); + lua_setfield(state, -2, "__tostring"); + lua_dup(state); + lua_rawsetp(state, LUA_REGISTRYINDEX, &DFHACK_EXCEPTION_META_TOKEN); + lua_setfield(state, -2, "exception"); + + // Initialize the dfhack global luaL_setfuncs(state, dfhack_funcs, 0); OpenPersistent(state); diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 6f95e7f6e..7e3ed1683 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -79,6 +79,13 @@ namespace DFHack { namespace Lua { */ DFHACK_EXPORT void *GetDFObject(lua_State *state, type_identity *type, int val_index, bool exact_type = false); + /** + * Assign the value at val_index to the target of given identity using df.assign(). + * Return behavior is of SafeCall below. + */ + DFHACK_EXPORT bool AssignDFObject(color_ostream &out, lua_State *state, + type_identity *type, void *target, int val_index, bool perr = true); + /** * Push the pointer onto the stack as a wrapped DF object of a specific type. */ @@ -95,12 +102,28 @@ namespace DFHack { namespace Lua { return (T*)GetDFObject(state, df::identity_traits::get(), val_index, exact_type); } + /** + * Assign the value at val_index to the target using df.assign(). + */ + template + bool AssignDFObject(color_ostream &out, lua_State *state, T *target, int val_index, bool perr = true) { + return AssignDFObject(out, state, df::identity_traits::get(), target, val_index, perr); + } + /** * Invoke lua function via pcall. Returns true if success. * If an error is signalled, and perr is true, it is printed and popped from the stack. */ DFHACK_EXPORT bool SafeCall(color_ostream &out, lua_State *state, int nargs, int nres, bool perr = true); + /** + * Parse code from string with debug_tag and env_idx, then call it using SafeCall. + * In case of error, it is either left on the stack, or printed like SafeCall does. + */ + DFHACK_EXPORT bool SafeCallString(color_ostream &out, lua_State *state, const std::string &code, + int nargs, int nres, bool perr = true, + const char *debug_tag = NULL, int env_idx = 0); + /** * Returns the ostream passed to SafeCall. */ diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index b7052b2ea..67652ff15 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -1,6 +1,12 @@ -- Common startup file for all dfhack plugins with lua support -- The global dfhack table is already created by C++ init code. +safecall = dfhack.safecall + +function dfhack.pcall(f, ...) + return xpcall(f, dfhack.onerror, ...) +end + function mkmodule(module,env) local pkg = package.loaded[module] if pkg == nil then