[Ready for Review] Admin lua scripting (#65635)

Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com>
This commit is contained in:
Y0SH1M4S73R
2022-07-19 18:45:23 -04:00
committed by GitHub
parent a6723944c1
commit 4e6e1f090e
46 changed files with 2161 additions and 15 deletions
+181
View File
@@ -0,0 +1,181 @@
# Auxlua
---
## Datums
DM datums are treated as Lua userdata, and can be stored in fields. Regular datums are referenced weakly, so if a datum has been deleted, the corresponding userdata will evaluate to `nil` when used in comparisons or functions.
Keep in mind that BYOND can't see that a datum is referenced in a Lua field, and will garbage collect it if it is not referenced anywhere in DM.
### datum:get_var(var)
Equivalent to DM's `datum.var`
### datum:set_var(var, value)
Equivalent to DM's `datum.var = value`
### datum:call_proc(procName, ...)
Equivalent to DM's `datum.procName(...)`
---
## Lists
In order to allow lists to be modified in-place across the DM-to-Lua language barrier, lists are treated as userdata. Whenever running code that expects a DM value, auxlua will attempt to convert tables into lists.
List references are subject to the same limitations as datum userdata, but you are less likely to encounter these limitations.
### list.len
Equivalent to DM's `list.len`
### list:get(index)
Equivalent to DM's `list[index]`
### list:set(index, value)
Equivalent to DM's `list[index] = value`
### list:add(value)
Equivalent to DM's `list.Add(value)`
### list:to_table()
Converts a DM list into a Lua table.
---
## The dm table
The `dm` table consists of the basic hooks into the DM language.
### dm.state_id
The address of the Lua state in memory. This is a copy of the internal value used by auxlua to locate the Lua state in a global hash map.
### dm.global_proc(proc, ...)
Calls the global proc `/proc/[proc]` with `...` as its arguments.
### dm.world
A reference to DM's `world`, in the form of datum userdata. This reference will never evaluate to `nil`, since `world` always exists.
Due to limitations inherent in the wrapper functions used on tgstation, `world:set_var` and `world:call_proc` will raise an error.
### dm.global_vars
A reference to DM's `global`, in the form of datum userdata. Subject to the same limitations as `dm.world`
### dm.usr
A weak reference to DM's `usr`. As a rule of thumb, this is a reference to the mob of the client who triggered the chain of procs leading to the execution of Lua code. The following is a list of what `usr` is for the most common ways of executing Lua code:
- For resumes and awakens, which are generally executed by the MC, `usr` is (most likely) null.
- `SS13.wait` queues a resume, which gets executed by the MC. Therefore, `usr` is null after `SS13.wait` finishes.
- For chunk loads, `usr` is generally the current mob of the admin that loaded that chunk.
- For function calls done from the Lua editor, `usr` is the current mob of the admin calling the function.
- `SS13.register_signal` creates a `/datum/callback` that gets executed by the `SEND_SIGNAL` macro for the corresponding signal. As such, `usr` is the mob that triggered the chain of procs leading to the invocation of `SEND_SIGNAL`.
---
## Task management
The Lua Scripting subsystem manages the execution of tasks for each Lua state. A single fire of the subsystem behaves as follows:
- All tasks that slept since the last fire are resumed in the order they slept.
- For each queued resume, the corresponding task is resumed.
### sleep()
Yields the current thread, scheduling it to be resumed during the next fire of SSlua. Use this function to prevent your Lua code from exceeding its allowed execution duration. Under the hood, `sleep` performs the following:
- Sets the global flag `__sleep_flag`
- Calls `coroutine.yield()`
- Clears the sleep flag when determining whether the task slept or yielded
- Ignores the return values of `coroutine.yield()` once resumed
---
## The SS13 package
The `SS13` package contains various helper functions that use code specific to tgstation.
### SS13.state
A reference to the state datum (`/datum/lua_state`) handling this Lua state.
### SS13.global_proc
A wrapper for the magic string used to tell `WrapAdminProcCall` to call a global proc.
For instance, `/datum/callback` must be instantiated with `SS13.global_proc` as its first argument to specify that it will be invoking a global proc.
The following example declares a callback which will execute the global proc `to_chat`:
```lua
local callback = SS13.new("/datum/callback", SS13.global_proc, "to_chat", dm.world, "Hello World")
```
### SS13.istype(thing, type)
Equivalent to the DM statement `istype(thing, text2path(type))`.
### SS13.new(type, ...)
Instantiates a datum of type `type` with `...` as the arguments passed to `/proc/_new`
The following example spawns a singularity at the caller's current turf:
```lua
SS13.new("/obj/singularity", dm.global_proc("_get_step", dm.usr, 0))
```
### SS13.await(thing_to_call, proc_to_call, ...)
Calls `proc_to_call` on `thing_to_call`, with `...` as its arguments, and sleeps until that proc returns.
Returns two return values - the first is the return value of the proc, and the second is the message of any runtime exception thrown by the called proc.
The following example calls and awaits the return of `poll_ghost_candidates`:
```lua
local ghosts, runtime = SS13.await(SS13.global_proc, "poll_ghost_candidates", "Would you like to be considered for something?")
```
### SS13.wait(time, timer)
Waits for a number of **seconds** specified with the `time` argument. You can optionally specify a timer subsystem using the `timer` argument.
Internally, this function creates a timer that will resume the current task after `time` seconds, then yields the current task by calling `coroutine.yield` with no arguments and ignores the return values. If the task is prematurely resumed, the timer will be safely deleted.
### SS13.register_signal(datum, signal, func, make_easy_clear_function)
Registers the Lua function `func` as a handler for `signal` on `datum`.
Like with signal handlers written in DM, Lua signal handlers should not sleep (either by calling `sleep` or `coroutine.yield`).
If `make_easy_clear_function` is truthy, a member function taking no arguments will be created in the `SS13` table to easily unregister the signal handler.
This function returns the `/datum/callback` created to call `func` from DM.
The following example defines a function which will register a signal that makes `target` make a honking sound any time it moves:
```lua
function honk(target)
SS13.register_signal(target, "movable_moved", function(source)
dm.global_proc("playsound", target, "sound/items/bikehorn.ogg", 100, true)
end)
end
```
### SS13.unregister_signal(datum, signal, callback)
Unregister a signal previously registered using `SS13.register_signal`. `callback` should be a `datum/callback` previously returned by `SS13.register_signal`. If `callback` is not specified, **ALL** signal handlers registered on `datum` for `signal` will be unregistered.
### SS13.set_timeout(time, func)
Creates a timer which will execute `func` after `time` **seconds**. `func` should not expect to be passed any arguments, as it will not be passed any. Unlike `SS13.wait`, `SS13.set_timeout` does not yield or sleep the current task, making it suitable for use in signal handlers for `SS13.register_signal`
The following example will output a message to chat after 5 seconds:
```lua
SS13.set_timeout(5, function()
dm.global_proc("to_chat", dm.world, "Hello World!")
end)
```
---
## Internal globals
Auxlua defines several globals for internal use. These are read-only.
### __sleep_flag
This flag is used to designate that a yielding task should be put in the sleep queue instead of the yield table. Once auxlua determines that a task should sleep, `__sleep_flag` is cleared.
### __set_sleep_flag(value)
A function that sets `__sleep_flag` to `value`. Calling this directly is not recommended, as doing so muddies the distinction between sleeps and yields.
### __sleep_queue
A sequence of threads, each corresponding to a task that has slept. When calling `/proc/__lua_awaken`, auxlua will dequeue the first thread from the sequence and resume it. Threads in this queue can be resumed from Lua code, but doing so is heavily advised against.
### __yield_table
A table of threads, each corresponding to a coroutine that has yielded. When calling `/proc/__lua_resume`, auxlua will look for a thread at the index specified in the `index` argument, and resume it with the arguments specified in the `arguments` argument. Threads in this table can be resumed from Lua code, but doing so is heavily advised against.
### __task_info
A table of key-value-pairs, where the keys are threads, and the values are tables consisting of the following fields:
- name: A string containing the name of the task
- status: A string, either "sleep" or "yield"
- index: The task's index in `__sleep_queue` or `__yield_table`
The threads constituting this table's keys can be resumed from Lua code, but doing so is heavily advised against.
+239
View File
@@ -0,0 +1,239 @@
/datum
var/__auxtools_weakref_id //used by auxtools for weak references
/**
* Sets a global proc to call in place of just outright setting a datum's var to a given value
*
* The proc will be called with the arguments (datum/datum_to_modify, var_name, value)
*
* required wrapper text the name of the proc to use as the wrapper
*/
/proc/__lua_set_set_var_wrapper(wrapper)
CRASH("auxlua not loaded")
/**
* Sets a global proc to call in place of just outright calling a given proc on a datum
*
* The proc will be called with the arguments (datum/thing_to_call, proc_to_call, list/arguments)
*
* required wrapper text the name of the proc to use as the wrapper
*/
/proc/__lua_set_datum_proc_call_wrapper(wrapper)
CRASH("auxlua not loaded")
/**
* Sets a global proc to call in place of just outright calling a given global proc
*
* The proc will be called with the arguments (proc_to_call, list/arguments)
*
* required wrapper text the name of the proc to use as the wrapper
*/
/proc/__lua_set_global_proc_call_wrapper(wrapper)
CRASH("auxlua not loaded")
/**
* Sets a global proc as a wrapper for lua's print function
*
* The proc will be called with the arguments (state_id, list/arguments)
*
* required wrapper text the name of the proc to use as the wrapper
*/
/proc/__lua_set_print_wrapper(wrapper)
CRASH("auxlua not loaded")
/**
* Sets the maximum amount of time a lua chunk or function can execute without sleeping or yielding.
* Chunks/functions that exceed this duration will produce an error.
*
* required limit number the execution limit, in milliseconds
*/
/proc/__lua_set_execution_limit(limit)
CRASH("auxlua not loaded")
/**
* Creates a new lua state.
*
* return text a pointer to the created state.
*/
/proc/__lua_new_state()
CRASH("auxlua not loaded")
/**
* Loads a chunk of lua source code and executes it
*
* required state text a pointer to the state
* in which to execute the code
* required script text the lua source code to execute
* optional name text a name to give to the chunk
*
* return list|text a list of lua return information
* or an error message if the state was corrupted
*
* Lua return information is formatted as followed:
* - ["status"]: How the chunk or function stopped code execution
* - "sleeping": The chunk or function called dm.sleep,
* placing it in the sleep queue. Items in the sleep
* queue can be resumed using /proc/__lua_awaken
* - "yielded": The chunk or function called coroutine.yield,
* placing it in the yield table. Items in the yield
* table can can be resumed by passing their index
* to /proc/__lua_resume
* - "finished": The chunk or function finished
* - "errored": The chunk or function produced an error
* - "bad return": The chunk or function yielded or finished,
* but its return value could not be converted to DM values
* - ["param"]: Depends on status.
* - "sleeping": null
* - "yielded" or "finished": The return/yield value(s)
* - "errored" or "bad return": The error message
* - ["yield_index"]: The index in the yield table where the
* chunk or function is located, for calls to __lua_resume
* - ["name"]: The name of the chunk or function, for logging
*/
/proc/__lua_load(state, script, name)
CRASH("auxlua not loaded")
/**
* Calls a lua function
*
* required state text a pointer to the state
* in which to call the function
* required function text the name of the function to call
* optional arguments list arguments to pass to the function
*
* return list|text a list of lua return information
* or an error message if the state was corrupted
*
* Lua return information is formatted as followed:
* - ["status"]: How the chunk or function stopped code execution
* - "sleeping": The chunk or function called dm.sleep,
* placing it in the sleep queue. Items in the sleep
* queue can be resumed using /proc/__lua_awaken
* - "yielded": The chunk or function called coroutine.yield,
* placing it in the yield table. Items in the yield
* table can can be resumed by passing their index
* to /proc/__lua_resume
* - "finished": The chunk or function finished
* - "errored": The chunk or function produced an error
* - "bad return": The chunk or function yielded or finished,
* but its return value could not be converted to DM values
* - ["param"]: Depends on status.
* - "sleeping": null
* - "yielded" or "finished": The return/yield value(s)
* - "errored" or "bad return": The error message
* - ["yield_index"]: The index in the yield table where the
* chunk or function is located, for calls to __lua_resume
* - ["name"]: The name of the chunk or function, for logging
*/
/proc/__lua_call(state, function, arguments)
CRASH("auxlua not loaded")
/**
* Dequeues the task at the front of the sleep queue and resumes it
*
* required state text a pointer to the state in which
* to resume a task
*
* return list|text|null a list of lua return information,
* an error message if the state is corrupted,
* or null if the sleep queue is empty
*
* Lua return information is formatted as followed:
* - ["status"]: How the chunk or function stopped code execution
* - "sleeping": The chunk or function called dm.sleep,
* placing it in the sleep queue. Items in the sleep
* queue can be resumed using /proc/__lua_awaken
* - "yielded": The chunk or function called coroutine.yield,
* placing it in the yield table. Items in the yield
* table can can be resumed by passing their index
* to /proc/__lua_resume
* - "finished": The chunk or function finished
* - "errored": The chunk or function produced an error
* - "bad return": The chunk or function yielded or finished,
* but its return value could not be converted to DM values
* - ["param"]: Depends on status.
* - "sleeping": null
* - "yielded" or "finished": The return/yield value(s)
* - "errored" or "bad return": The error message
* - ["yield_index"]: The index in the yield table where the
* chunk or function is located, for calls to __lua_resume
* - ["name"]: The name of the chunk or function, for logging
*/
/proc/__lua_awaken(state)
CRASH("auxlua not loaded")
/**
* Removes the task at the specified index from the yield table
* and resumes it
*
* required state text a pointer to the state in which to
* resume a task
* required index number the index in the yield table of the
* task to resume
* optional arguments list the arguments to resume the task with
*
* return list|text|null a list of lua return information,
* an error message if the state is corrupted,
* or null if there is no task at the specified index
*
* Lua return information is formatted as followed:
* - ["status"]: How the chunk or function stopped code execution
* - "sleeping": The chunk or function called dm.sleep,
* placing it in the sleep queue. Items in the sleep
* queue can be resumed using /proc/__lua_awaken
* - "yielded": The chunk or function called coroutine.yield,
* placing it in the yield table. Items in the yield
* table can can be resumed by passing their index
* to /proc/__lua_resume
* - "finished": The chunk or function finished
* - "errored": The chunk or function produced an error
* - "bad return": The chunk or function yielded or finished,
* but its return value could not be converted to DM values
* - ["param"]: Depends on status.
* - "sleeping": null
* - "yielded" or "finished": The return/yield value(s)
* - "errored" or "bad return": The error message
* - ["yield_index"]: The index in the yield table where the
* chunk or function is located, for calls to __lua_resume
* - ["name"]: The name of the chunk or function, for logging
*/
/proc/__lua_resume(state, index, arguments)
CRASH("auxlua not loaded")
/**
* Get the variables within a state's environment.
* Values not convertible to DM values are substituted
* for their types as text
*
* required state text a pointer to the state
* to get the variables from
*
* return list the variables of the state's environment
*/
/proc/__lua_get_globals(state)
CRASH("auxlua not loaded")
/**
* Get a list of all tasks currently in progress within a state
*
* required state text a pointer to the state
* to get the tasks from
*
* return list a list of the state's tasks, formatted as follows:
* - name: The name of the task
* - status: Whether the task is sleeping or yielding
* - index: The index of the task in the sleep queue
* or yield table, whichever is applicable
*/
/proc/__lua_get_tasks(state)
CRASH("auxlua not loaded")
/**
* Kills a task in progress
*
* required state text a pointer to the state
* in which to kill a task
* required info list the task info
*/
/proc/__lua_kill_task(state, info)
CRASH("auxlua not loaded")
+33
View File
@@ -0,0 +1,33 @@
/proc/wrap_lua_set_var(datum/thing_to_set, var_name, value)
thing_to_set.vv_edit_var(var_name, value)
/proc/wrap_lua_datum_proc_call(datum/thing_to_call, proc_name, list/arguments)
if(!usr)
usr = GLOB.lua_usr
if(usr)
SSlua.gc_guard = WrapAdminProcCall(thing_to_call, proc_name, arguments)
else
SSlua.gc_guard = HandleUserlessProcCall("lua", thing_to_call, proc_name, arguments)
return SSlua.gc_guard
/proc/wrap_lua_global_proc_call(proc_name, list/arguments)
if(!usr)
usr = GLOB.lua_usr
if(usr)
SSlua.gc_guard = WrapAdminProcCall(GLOBAL_PROC, proc_name, arguments)
else
SSlua.gc_guard = HandleUserlessProcCall("lua", GLOBAL_PROC, proc_name, arguments)
return SSlua.gc_guard
/proc/wrap_lua_print(state_id, list/arguments)
var/datum/lua_state/target_state
for(var/datum/lua_state/state as anything in SSlua.states)
if(state.internal_id == state_id)
target_state = state
break
if(!target_state)
return
var/print_message = jointext(arguments, "\t")
var/result = list("status" = "print", "param" = print_message)
target_state.log_result(result, verbose = TRUE)
log_lua("[target_state]: [print_message]")
+32
View File
@@ -0,0 +1,32 @@
#define PROMISE_PENDING 0
#define PROMISE_RESOLVED 1
#define PROMISE_REJECTED 2
/**
* Auxtools hooks act as "set waitfor = 0" procs. This means that whenever
* a proc directly called from auxtools sleeps, the hook returns with whatever
* the called proc had as its return value at the moment it slept. This may not
* be desired behavior, so this datum exists to wrap these procs.
*/
/datum/auxtools_promise
var/datum/callback/callback
var/return_value
var/runtime_message
var/status = PROMISE_PENDING
/datum/auxtools_promise/New(...)
callback = CALLBACK(arglist(args))
perform()
/datum/auxtools_promise/proc/perform()
set waitfor = 0
try
return_value = callback.Invoke()
status = PROMISE_RESOLVED
catch(var/exception/e)
runtime_message = e.name
status = PROMISE_REJECTED
#undef PROMISE_PENDING
#undef PROMISE_RESOLVED
#undef PROMISE_REJECTED
+196
View File
@@ -0,0 +1,196 @@
/datum/lua_editor
var/datum/lua_state/current_state
/// Code imported from the user's system
var/imported_code
/// Arguments for a function call or coroutine resume
var/list/arguments = list()
/datum/lua_editor/New(state, _quick_log_index)
. = ..()
if(state)
current_state = state
LAZYADDASSOCLIST(SSlua.editors, "\ref[current_state]", src)
/datum/lua_editor/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "LuaEditor", "Lua")
ui.set_autoupdate(FALSE)
ui.open()
/datum/lua_editor/Destroy(force, ...)
. = ..()
if(current_state)
LAZYREMOVEASSOC(SSlua.editors, "\ref[current_state]", src)
/datum/lua_editor/ui_state(mob/user)
return GLOB.debug_state
/datum/lua_editor/ui_static_data(mob/user)
var/list/data = list()
data["documentation"] = file2text('code/modules/admin/verbs/lua/README.md')
return data
/datum/lua_editor/ui_data(mob/user)
var/list/data = list()
data["noStateYet"] = !current_state
if(current_state)
current_state.get_globals()
if(current_state.log)
data["stateLog"] = kvpify_list(refify_list(current_state.log))
data["tasks"] = current_state.get_tasks()
if(current_state.globals)
data["globals"] = kvpify_list(refify_list(current_state.globals))
data["states"] = SSlua.states
data["callArguments"] = kvpify_list(refify_list(arguments))
return data
/datum/lua_editor/proc/traverse_list(list/path, list/root, traversal_depth_offset = 0)
var/top_affected_list_depth = LAZYLEN(path)-traversal_depth_offset // The depth of the element to get
if(top_affected_list_depth)
var/list/current_list = root
// We kvpify the list to the depth of the element to get - this allows us to reach list elements contained within a assoc list's key
var/list/path_list = kvpify_list(current_list, top_affected_list_depth-1)
while(LAZYLEN(path) > traversal_depth_offset)
// Navigate to the index of the next path element within the current path element
var/list/path_element = popleft(path)
var/list/list_element = path_list[path_element["index"]]
// Enter the next path element - be it the key or the value
switch(path_element["type"])
if("key")
path_list = list_element["key"]
if("value")
path_list = list_element["value"]
else
to_chat(usr, span_warning("invalid path element type \[[path_element["type"]]] for list traversal (expected \"key\" or \"value\""))
return
// The element we are entering SHOULD be a list, unless we're at the end of the path
if(!islist(path_list) && LAZYLEN(path))
to_chat(usr, span_warning("invalid path element \[[path_list]] for list traversal (expected a list)"))
return
current_list = path_list
return current_list
else
return root
/datum/lua_editor/ui_act(action, list/params)
. = ..()
if(.)
return
if(!check_rights_for(usr.client, R_DEBUG))
return
switch(action)
if("newState")
var/state_name = params["name"]
var/datum/lua_state/new_state = new(state_name)
SSlua.states += new_state
LAZYREMOVEASSOC(SSlua.editors, "\ref[current_state]", src)
current_state = new_state
LAZYADDASSOCLIST(SSlua.editors, "\ref[current_state]", src)
return TRUE
if("switchState")
var/state_index = params["index"]
LAZYREMOVEASSOC(SSlua.editors, "\ref[current_state]", src)
current_state = SSlua.states[state_index]
LAZYADDASSOCLIST(SSlua.editors, "\ref[current_state]", src)
return TRUE
if("runCode")
var/code = params["code"]
var/result = current_state.load_script(code)
current_state.log_result(result)
return TRUE
if("moveArgUp")
var/list/path = params["path"]
var/list/target_list = traverse_list(path, arguments, traversal_depth_offset = 1)
var/index = popleft(path)["index"]
target_list.Swap(index-1, index)
return TRUE
if("moveArgDown")
var/list/path = params["path"]
var/list/target_list = traverse_list(path, arguments, traversal_depth_offset = 1)
var/index = popleft(path)["index"]
target_list.Swap(index, index+1)
return TRUE
if("removeArg")
var/list/path = params["path"]
var/list/target_list = traverse_list(path, arguments, traversal_depth_offset = 1)
var/index = popleft(path)["index"]
target_list.Cut(index, index+1)
return TRUE
if("addArg")
var/list/path = params["path"]
var/list/target_list = traverse_list(path, arguments)
if(target_list != arguments)
usr?.client?.mod_list_add(target_list, null, "a lua editor", "arguments")
else
var/list/vv_val = usr?.client?.vv_get_value(restricted_classes = list(VV_RESTORE_DEFAULT))
var/class = vv_val["class"]
if(!class)
return
LAZYADD(arguments, list(vv_val["value"]))
return TRUE
if("callFunction")
var/list/recursive_indices = params["indices"]
var/list/current_list = kvpify_list(current_state.globals)
var/function = list()
while(LAZYLEN(recursive_indices))
var/index = popleft(recursive_indices)
var/list/element = current_list[index]
var/key = element["key"]
var/value = element["value"]
if(!(istext(key) || isnum(key)))
to_chat(usr, span_warning("invalid key \[[key]] for function call (expected text or num)"))
return
function += key
if(islist(value))
current_list = value
else
var/regex/function_regex = regex("^function: 0x\[0-9a-fA-F]+$")
if(function_regex.Find(value))
break
to_chat(usr, span_warning("invalid path element \[[value]] for function call (expected list or text matching [function_regex])"))
return
var/result = current_state.call_function(arglist(list(function) + arguments))
current_state.log_result(result)
arguments.Cut()
return TRUE
if("resumeTask")
var/task_index = params["index"]
SSlua.queue_resume(current_state, task_index, arguments)
arguments.Cut()
return TRUE
if("killTask")
var/task_info = params["info"]
SSlua.kill_task(current_state, task_info)
return TRUE
if("vvReturnValue")
var/log_entry_index = params["entryIndex"]
var/list/log_entry = current_state.log[log_entry_index]
var/thing_to_debug = traverse_list(params["tableIndices"], log_entry["param"])
INVOKE_ASYNC(usr.client, /client.proc/debug_variables, thing_to_debug)
return FALSE
if("vvGlobal")
var/thing_to_debug = traverse_list(params["indices"], current_state.globals)
INVOKE_ASYNC(usr.client, /client.proc/debug_variables, thing_to_debug)
return FALSE
if("clearArgs")
arguments.Cut()
return TRUE
/datum/lua_editor/ui_close(mob/user)
. = ..()
qdel(src)
/client/proc/open_lua_editor()
set name = "Open Lua Editor"
set category = "Debug"
if(!check_rights_for(src, R_DEBUG))
return
if(SSlua.initialized != TRUE)
to_chat(usr, span_warning("SSlua is not initialized!"))
return
var/datum/lua_editor/editor = new()
editor.ui_interact(usr)
+137
View File
@@ -0,0 +1,137 @@
#define MAX_LOG_REPEAT_LOOKBACK 5
GLOBAL_VAR_INIT(IsLuaCall, FALSE)
GLOBAL_PROTECT(IsLuaCall)
GLOBAL_DATUM(lua_usr, /mob)
GLOBAL_PROTECT(lua_usr)
/datum/lua_state
var/name
/// The internal ID of the lua state stored in auxlua's global map
var/internal_id
/// A log of every return, yield, and error for each chunk execution and function call
var/list/log = list()
/// A list of all the variables in the state's environment
var/list/globals = list()
/// A list in which to store datums and lists instantiated in lua, ensuring that they don't get garbage collected
var/list/references = list()
/datum/lua_state/vv_edit_var(var_name, var_value)
. = ..()
if(var_name == NAMEOF(src, internal_id))
return FALSE
/datum/lua_state/New(_name)
if(SSlua.initialized != TRUE)
qdel(src)
return
name = _name
internal_id = __lua_new_state()
/datum/lua_state/proc/check_if_slept(result)
if(result["status"] == "sleeping")
SSlua.sleeps += src
/datum/lua_state/proc/log_result(result, verbose = TRUE)
if(!verbose && result["status"] != "errored" && result["status"] != "bad return" \
&& !(result["name"] == "input" && (result["status"] == "finished" || length(result["param"]))))
return
var/append_to_log = TRUE
if(log.len)
for(var/index in log.len to max(log.len - MAX_LOG_REPEAT_LOOKBACK, 1) step -1)
var/list/entry = log[index]
if(entry["status"] == result["status"] \
&& entry["chunk"] == result["chunk"] \
&& entry["name"] == result["name"] \
&& ((entry["param"] == result["param"]) || deep_compare_list(entry["param"], result["param"])))
if(!entry["repeats"])
entry["repeats"] = 0
entry["repeats"]++
append_to_log = FALSE
break
if(append_to_log)
log += list(result)
/datum/lua_state/proc/load_script(script)
GLOB.IsLuaCall = TRUE
var/tmp_usr = GLOB.lua_usr
GLOB.lua_usr = usr
var/result = __lua_load(internal_id, script)
GLOB.IsLuaCall = FALSE
GLOB.lua_usr = tmp_usr
// Internal errors unrelated to the code being executed are returned as text rather than lists
if(istext(result))
result = list("status" = "errored", "param" = result, "name" = "input")
result["chunk"] = script
check_if_slept(result)
message_admins("[key_name(usr)] executed [length(script)] bytes of lua code. [ADMIN_LUAVIEW_CHUNK(src, log.len)]")
log_lua("[key_name(usr)] executed the following lua code:\n<code>[script]</code>")
return result
/datum/lua_state/proc/call_function(function, ...)
var/call_args = length(args) > 1 ? args.Copy(2) : list()
var/msg = "[key_name(usr)] called the lua function \"[function]\" with arguments: [english_list(call_args)]"
log_lua(msg)
var/tmp_usr = GLOB.lua_usr
GLOB.lua_usr = usr
GLOB.IsLuaCall = TRUE
var/result = __lua_call(internal_id, function, call_args)
GLOB.IsLuaCall = FALSE
GLOB.lua_usr = tmp_usr
if(istext(result))
result = list("status" = "errored", "param" = result, "name" = islist(function) ? jointext(function, ".") : function)
check_if_slept(result)
return result
/datum/lua_state/proc/call_function_return_first(function, ...)
var/list/result = call_function(arglist(args))
log_result(result, verbose = FALSE)
if(length(result))
if(islist(result["param"]) && length(result["param"]))
return result["param"][1]
/datum/lua_state/proc/awaken()
GLOB.IsLuaCall = TRUE
var/result = __lua_awaken(internal_id)
GLOB.IsLuaCall = FALSE
if(istext(result))
result = list("status" = "errored", "param" = result, "name" = "An attempted awaken")
check_if_slept(result)
return result
/// Prefer calling SSlua.queue_resume over directly calling this
/datum/lua_state/proc/resume(index, ...)
var/call_args = length(args) > 1 ? args.Copy(2) : list()
var/msg = "[key_name(usr)] resumed a lua coroutine with arguments: [english_list(call_args)]"
log_lua(msg)
GLOB.IsLuaCall = TRUE
var/result = __lua_resume(internal_id, index, call_args)
GLOB.IsLuaCall = FALSE
if(istext(result))
result = list("status" = "errored", "param" = result, "name" = "An attempted resume")
check_if_slept(result)
return result
/datum/lua_state/proc/get_globals()
globals = __lua_get_globals(internal_id)
/datum/lua_state/proc/get_tasks()
return __lua_get_tasks(internal_id)
/datum/lua_state/proc/kill_task(task_info)
__lua_kill_task(internal_id, task_info)
#undef MAX_LOG_REPEAT_LOOKBACK