[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 15:45:23 -07:00
committed by GitHub
co-authored by Mothblocks
parent a6723944c1
commit 4e6e1f090e
46 changed files with 2161 additions and 15 deletions
+3
View File
@@ -123,6 +123,9 @@ jobs:
sudo apt update || true
sudo apt install -o APT::Immediate-Configure=false libssl1.1:i386
bash tools/ci/install_rust_g.sh
- name: Install auxlua
run: |
bash tools/ci/install_auxlua.sh
- name: Compile Tests
run: |
bash tools/ci/install_byond.sh
+5
View File
@@ -197,6 +197,11 @@ Temporary Items
/tools/LinuxOneShot/TGS_Instances
/tools/LinuxOneShot/TGS_Logs
# Built auxtools libraries and intermediate files
aux*.dll
libaux*.so
aux*.pdb
# JavaScript tools
**/node_modules
+12
View File
@@ -1,6 +1,18 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug External Libraries",
"type": "cppvsdbg",
"request": "launch",
"program": "${command:dreammaker.returnDreamDaemonPath}",
"cwd": "${workspaceRoot}",
"args": [
"${command:dreammaker.getFilenameDmb}",
"-trusted"
],
"preLaunchTask": "Build All"
},
{
"type": "byond",
"request": "launch",
+2 -1
View File
@@ -39,5 +39,6 @@
"editor.rulers": [80],
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}
},
"Lua.diagnostics.enable": false
}
Executable
BIN
View File
Binary file not shown.
+2
View File
@@ -66,6 +66,8 @@
#define ADMIN_VERBOSEJMP(src) "[src ? src.Admin_Coordinates_Readable(TRUE, TRUE) : "nonexistent location"]"
#define ADMIN_INDIVIDUALLOG(user) "(<a href='?_src_=holder;[HrefToken(forceGlobal = TRUE)];individuallog=[REF(user)]'>LOGS</a>)"
#define ADMIN_TAG(datum) "(<A href='?src=[REF(src)];[HrefToken(forceGlobal = TRUE)];tag_datum=[REF(datum)]'>TAG</a>)"
#define ADMIN_LUAVIEW(state) "(<a href='?_src_=holder;[HrefToken(forceGlobal = TRUE)];lua_state=[REF(state)]'>VIEW STATE</a>)"
#define ADMIN_LUAVIEW_CHUNK(state, log_index) "(<a href='?_src_=holder;[HrefToken(forceGlobal = TRUE)];lua_state=[REF(state)];log_index=[log_index]'>VIEW CODE</a>)"
/atom/proc/Admin_Coordinates_Readable(area_name, admin_jump_ref)
var/turf/T = Safe_COORD_Location()
-5
View File
@@ -37,8 +37,3 @@
/proc/enable_debugging(mode, port)
CRASH("auxtools not loaded")
/world/Del()
var/debug_server = world.GetConfig("env", "AUXTOOLS_DEBUG_DLL")
if (debug_server)
call(debug_server, "auxtools_shutdown")()
. = ..()
+30
View File
@@ -0,0 +1,30 @@
#define AUXTOOLS_FULL_INIT 2
#define AUXTOOLS_PARTIAL_INIT 1
GLOBAL_LIST_EMPTY(auxtools_initialized)
#define AUXTOOLS_CHECK(LIB)\
if (GLOB.auxtools_initialized[LIB] != AUXTOOLS_FULL_INIT) {\
if (fexists(LIB)) {\
var/string = call(LIB,"auxtools_init")();\
if(findtext(string, "SUCCESS")) {\
GLOB.auxtools_initialized[LIB] = AUXTOOLS_FULL_INIT;\
} else {\
CRASH(string);\
}\
} else {\
CRASH("No file named [LIB] found!")\
}\
}\
#define AUXTOOLS_SHUTDOWN(LIB)\
if (GLOB.auxtools_initialized[LIB] == AUXTOOLS_FULL_INIT && fexists(LIB)){\
call(LIB,"auxtools_shutdown")();\
GLOB.auxtools_initialized[LIB] = AUXTOOLS_PARTIAL_INIT;\
}\
#define AUXTOOLS_FULL_SHUTDOWN(LIB)\
if (GLOB.auxtools_initialized[LIB] && fexists(LIB)){\
call(LIB,"auxtools_full_shutdown")();\
GLOB.auxtools_initialized[LIB] = FALSE;\
}\
+87
View File
@@ -811,3 +811,90 @@
return ref.resolve()
else
return element
#define REFIFY_KVPIFY_MAX_LENGTH 1000
/// Returns a copy of the list where any element that is a datum or the world is converted into a ref
/proc/refify_list(list/target_list)
if(length(target_list) > REFIFY_KVPIFY_MAX_LENGTH)
return "list\[[length(target_list)]\]"
var/list/ret = list()
for(var/i in 1 to target_list.len)
var/key = target_list[i]
var/new_key = key
if(isdatum(key))
new_key = "[key] [REF(key)]"
else if(key == world)
new_key = "world [REF(world)]"
else if(islist(key))
new_key = refify_list(key)
var/value
if(istext(key) || islist(key) || ispath(key) || isdatum(key) || key == world)
value = target_list[key]
if(isdatum(value))
value = "[value] [REF(value)]"
else if(value == world)
value = "world [REF(world)]"
else if(islist(value))
value = refify_list(value)
var/list/to_add = list(new_key)
if(value)
to_add[new_key] = value
ret += to_add
return ret
/**
* Converts a list into a list of assoc lists of the form ("key" = key, "value" = value)
* so that list keys that are themselves lists can be fully json-encoded
*/
/proc/kvpify_list(list/target_list, depth = INFINITY)
if(length(target_list) > REFIFY_KVPIFY_MAX_LENGTH)
return "list\[[length(target_list)]\]"
var/list/ret = list()
for(var/i in 1 to target_list.len)
var/key = target_list[i]
var/new_key = key
if(islist(key) && depth)
new_key = kvpify_list(key, depth-1)
var/value
if(istext(key) || islist(key) || ispath(key) || isdatum(key) || key == world)
value = target_list[key]
if(islist(value) && depth)
value = kvpify_list(value, depth-1)
if(value)
ret += list(list("key" = new_key, "value" = value))
else
ret += list(list("key" = i, "value" = new_key))
return ret
#undef REFIFY_KVPIFY_MAX_LENGTH
/// Compares 2 lists, returns TRUE if they are the same
/proc/deep_compare_list(list/list_1, list/list_2)
if(!islist(list_1) || !islist(list_2))
return FALSE
if(list_1 == list_2)
return TRUE
if(list_1.len != list_2.len)
return FALSE
for(var/i in 1 to list_1.len)
var/key_1 = list_1[i]
var/key_2 = list_2[i]
if (islist(key_1) && islist(key_2))
if(!deep_compare_list(key_1, key_2))
return FALSE
else if(key_1 != key_2)
return FALSE
if(istext(key_1) || islist(key_1) || ispath(key_1) || isdatum(key_1) || key_1 == world)
var/value_1 = list_1[key_1]
var/value_2 = list_2[key_1]
if (islist(value_1) && islist(value_2))
if(!deep_compare_list(value_1, value_2))
return FALSE
else if(value_1 != value_2)
return FALSE
return TRUE
+12
View File
@@ -0,0 +1,12 @@
/// Macro for getting the auxtools library file
#define AUXLUA (world.system_type == MS_WINDOWS ? "auxlua.dll" : __detect_auxtools("auxlua"))
/proc/__detect_auxtools(library)
if(IsAdminAdvancedProcCall())
return
if (fexists("./lib[library].so"))
return "./lib[library].so"
else if (fexists("[world.GetConfig("env", "HOME")]/.byond/bin/lib[library].so"))
return "[world.GetConfig("env", "HOME")]/.byond/bin/lib[library].so"
else
CRASH("Could not find lib[library].so")
+4
View File
@@ -58,3 +58,7 @@
WRITE_LOG(GLOB.world_runtime_log, text)
#endif
SEND_TEXT(world.log, text)
/// Logging for lua scripting
/proc/log_lua(text)
WRITE_LOG(GLOB.lua_log, text)
+3
View File
@@ -67,6 +67,9 @@ GLOBAL_PROTECT(perf_log)
GLOBAL_VAR(demo_log)
GLOBAL_PROTECT(demo_log)
GLOBAL_VAR(lua_log)
GLOBAL_PROTECT(lua_log)
GLOBAL_LIST_EMPTY(bombers)
GLOBAL_PROTECT(bombers)
GLOBAL_LIST_EMPTY(admin_log)
@@ -399,3 +399,4 @@
integer = FALSE
/datum/config_entry/flag/disallow_circuit_sounds
@@ -0,0 +1,2 @@
/datum/config_entry/str_list/lua_path
protection = CONFIG_ENTRY_LOCKED | CONFIG_ENTRY_HIDDEN
+166
View File
@@ -0,0 +1,166 @@
//world/proc/shelleo
#define SHELLEO_ERRORLEVEL 1
#define SHELLEO_STDOUT 2
#define SHELLEO_STDERR 3
#define SSLUA_INIT_FAILED 2
SUBSYSTEM_DEF(lua)
name = "Lua Scripting"
runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT
wait = 0.1 SECONDS
/// A list of all lua states
var/list/datum/lua_state/states = list()
/// A list of open editors, with each key in the list associated with a list of editors.
/// Tracks which UIs are open for each state so that they can be updated whenever
/// code is run in the state.
var/list/editors
var/list/sleeps = list()
var/list/resumes = list()
var/list/current_run = list()
/// Protects return values from getting GCed before getting converted to lua values
var/gc_guard
/datum/controller/subsystem/lua/Initialize(start_timeofday)
try
// Initialize the auxtools library
AUXTOOLS_CHECK(AUXLUA)
// Set the wrappers for setting vars and calling procs
__lua_set_set_var_wrapper("/proc/wrap_lua_set_var")
__lua_set_datum_proc_call_wrapper("/proc/wrap_lua_datum_proc_call")
__lua_set_global_proc_call_wrapper("/proc/wrap_lua_global_proc_call")
__lua_set_print_wrapper("/proc/wrap_lua_print")
return ..()
catch(var/exception/e)
// Something went wrong, best not allow the subsystem to run
initialized = SSLUA_INIT_FAILED
can_fire = FALSE
var/time = (REALTIMEOFDAY - start_timeofday) / 10
var/msg = "Failed to initialize [name] subsystem after [time] seconds!"
to_chat(world, span_boldwarning("[msg]"))
warning(e.name)
return time
/datum/controller/subsystem/lua/OnConfigLoad()
// Get the current working directory - we need it to set the LUAU_PATH environment variable
var/here = world.shelleo(world.system_type == MS_WINDOWS ? "cd" : "pwd")[SHELLEO_STDOUT]
here = replacetext(here, "\n", "")
var/last_char = copytext_char(here, -1)
if(last_char != "/" && last_char != "\\")
here += "/"
// Read the paths from the config file
var/list/lua_path = list()
var/list/config_paths = CONFIG_GET(str_list/lua_path)
for(var/path in config_paths)
if(path[1] != "/")
path = here + path
lua_path += path
world.SetConfig("env", "LUAU_PATH", jointext(lua_path, ";"))
/datum/controller/subsystem/lua/Shutdown()
AUXTOOLS_SHUTDOWN(AUXLUA)
/datum/controller/subsystem/lua/proc/queue_resume(datum/lua_state/state, index, arguments)
if(initialized != TRUE)
return
if(!istype(state))
return
if(!arguments)
arguments = list()
else if(!islist(arguments))
arguments = list(arguments)
resumes += list(list("state" = state, "index" = index, "arguments" = arguments))
/datum/controller/subsystem/lua/proc/kill_task(datum/lua_state/state, list/task_info)
if(!istype(state))
return
if(!islist(task_info))
return
if(!(istext(task_info["name"]) && istext(task_info["status"]) && isnum(task_info["index"])))
return
switch(task_info["status"])
if("sleep")
var/task_index = task_info["index"]
var/state_index = 1
// Get the nth sleep in the sleep list corresponding to the target state
for(var/i in 1 to length(sleeps))
var/datum/lua_state/sleeping_state = sleeps[i]
if(sleeping_state == state)
if(state_index == task_index)
sleeps.Cut(i, i+1)
break
state_index++
if("yield")
// Remove the resumt from the resumt list
for(var/i in 1 to length(resumes))
var/resume = resumes[i]
if(resume["state"] == state && resume["index"] == task_info["index"])
resumes.Cut(i, i+1)
break
state.kill_task(task_info)
/datum/controller/subsystem/lua/fire(resumed)
// Each fire of SSlua awakens every sleeping task in the order they slept,
// then resumes every yielded task in the order their resumes were queued
if(!resumed)
current_run = list("sleeps" = sleeps.Copy(), "resumes" = resumes.Copy())
sleeps.Cut()
resumes.Cut()
var/list/current_sleeps = current_run["sleeps"]
var/list/affected_states = list()
while(length(current_sleeps))
var/datum/lua_state/state = current_sleeps[1]
current_sleeps.Cut(1,2)
if(!istype(state))
continue
affected_states |= state
var/result = state.awaken()
state.log_result(result, verbose = FALSE)
if(MC_TICK_CHECK)
break
if(!length(current_sleeps))
var/list/current_resumes = current_run["resumes"]
while(length(current_resumes))
var/list/resume_params = current_resumes[1]
current_resumes.Cut(1,2)
var/datum/lua_state/state = resume_params["state"]
if(!istype(state))
continue
var/index = resume_params["index"]
if(isnull(index) || !isnum(index))
continue
var/arguments = resume_params["arguments"]
if(!islist(arguments))
continue
affected_states |= state
var/result = state.resume(arglist(list(index) + arguments))
state.log_result(result, verbose = FALSE)
if(MC_TICK_CHECK)
break
// Update every lua editor TGUI open for each state that had a task awakened or resumed
for(var/state in affected_states)
var/list/editor_list = LAZYACCESS(editors, "\ref[state]")
if(editor_list)
for(var/datum/lua_editor/editor in editor_list)
SStgui.update_uis(editor)
//world/proc/shelleo
#undef SHELLEO_ERRORLEVEL
#undef SHELLEO_STDOUT
#undef SHELLEO_STDERR
#undef SSLUA_INIT_FAILED
+4
View File
@@ -124,6 +124,8 @@
else
calling_arguments = args
if(datum_flags & DF_VAR_EDITED)
if(usr && usr != GLOB.AdminProcCallHandler && !usr.client?.ckey) //This ONLY happens when usr's client switches mobs just before the callback gets invoked.
return HandleUserlessProcCall(usr, object, delegate, calling_arguments)
return WrapAdminProcCall(object, delegate, calling_arguments)
if (object == GLOBAL_PROC)
return call(delegate)(arglist(calling_arguments))
@@ -159,6 +161,8 @@
else
calling_arguments = args
if(datum_flags & DF_VAR_EDITED)
if(usr != GLOB.AdminProcCallHandler && !usr?.client?.ckey) //This happens when a timer invokes a callback
return HandleUserlessProcCall(usr, object, delegate, calling_arguments)
return WrapAdminProcCall(object, delegate, calling_arguments)
if (object == GLOBAL_PROC)
return call(delegate)(arglist(calling_arguments))
+10
View File
@@ -159,6 +159,8 @@ GLOBAL_VAR(restart_counter)
GLOB.demo_log = "[GLOB.log_directory]/demo.log"
GLOB.lua_log = "[GLOB.log_directory]/lua.log"
#ifdef UNIT_TESTS
GLOB.test_log = "[GLOB.log_directory]/tests.log"
start_log(GLOB.test_log)
@@ -292,8 +294,16 @@ GLOBAL_VAR(restart_counter)
TgsReboot()
shutdown_logging() // Past this point, no logging procs can be used, at risk of data loss.
AUXTOOLS_FULL_SHUTDOWN(AUXLUA)
..()
/world/Del()
AUXTOOLS_FULL_SHUTDOWN(AUXLUA)
var/debug_server = world.GetConfig("env", "AUXTOOLS_DEBUG_DLL")
if (debug_server)
call(debug_server, "auxtools_shutdown")()
. = ..()
/world/proc/update_status()
var/list/features = list()
+1
View File
@@ -199,6 +199,7 @@ GLOBAL_PROTECT(admin_verbs_debug)
/client/proc/cmd_admin_toggle_fov,
/client/proc/cmd_admin_debug_traitor_objectives,
/client/proc/spawn_debug_full_crew,
/client/proc/open_lua_editor,
/client/proc/validate_puzzgrids,
/client/proc/debug_spell_requirements,
)
+19
View File
@@ -1977,3 +1977,22 @@
if(!datum_to_mark)
return
return usr.client?.mark_datum(datum_to_mark)
else if(href_list["lua_state"])
if(!check_rights(R_DEBUG))
return
var/datum/lua_state/state_to_view = locate(href_list["lua_state"])
if(!state_to_view)
return
var/datum/lua_editor/editor = new(state_to_view)
var/log_index = href_list["log_index"]
if(log_index)
log_index = text2num(log_index)
if(log_index <= state_to_view.log.len)
var/list/log_entry = state_to_view.log[log_index]
if(log_entry["chunk"])
LAZYINITLIST(editor.tgui_shared_states)
editor.tgui_shared_states["viewedChunk"] = json_encode(log_entry["chunk"])
editor.tgui_shared_states["modal"] = json_encode("viewChunk")
editor.ui_interact(usr)
editor.tgui_shared_states = null
@@ -3,11 +3,11 @@
/proc/_abs(A)
return abs(A)
/proc/_animate(atom/A, set_vars, time = 10, loop = 1, easing = LINEAR_EASING, flags = null)
var/mutable_appearance/MA = new()
for(var/v in set_vars)
MA.vars[v] = set_vars[v]
animate(A, appearance = MA, time, loop, easing, flags)
/proc/_animate(atom/target, set_vars, time = 10, loop = 1, easing = LINEAR_EASING, flags = null)
if(target)
animate(target, appearance = set_vars, time, loop, easing, flags)
else
animate(appearance = set_vars, time, easing = easing, flags)
/proc/_arccos(A)
return arccos(A)
@@ -253,7 +253,34 @@
/proc/_winset(player, control_id, params)
winset(player, control_id, params)
/proc/_winget(player, control_id, params)
winget(player, control_id, params)
/proc/_text2path(text)
return text2path(text)
/proc/_turn(dir, angle)
return turn(dir, angle)
/// For some reason, an atom's contents are a kind of list that auxtools can't work with as a list
/// This proc returns a copy of contents that is an ordinary list
/proc/_contents(atom/thing)
var/list/ret = list()
if(istype(thing))
ret += thing.contents
return ret
/// Auxtools REALLY doesn't know how to handle filters as values;
/// when passed as arguments to auxtools-called procs, they aren't simply treated as nulls -
/// they don't even count towards the length of args.
/// For example, calling some_proc([a filter], foo, bar) from auxtools
/// is equivalent to calling some_proc(foo, bar). Thus, we can't use _animate directly on filters.
/// Use this to perform animation steps on a filter. Consecutive steps on the same filter can be
/// achieved by calling _animate with no target.
/proc/_animate_filter(atom/target, filter_index, set_vars, time = 10, loop = 1, easing = LINEAR_EASING, flags = null)
if(!istype(target))
return
if(!filter_index || filter_index < 1 || filter_index > length(target.filters))
return
animate(target.filters[filter_index], appearance = set_vars, time, loop, easing, flags)
+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
@@ -95,7 +95,7 @@ GLOBAL_PROTECT(VVpixelmovement)
if (O)
L = L.Copy()
L += var_value
L += list(var_value) //var_value could be a list
switch(tgui_alert(usr,"Would you like to associate a value with the list entry?",,list("Yes","No")))
if("Yes")
+1
View File
@@ -5,6 +5,7 @@ $include dbconfig.txt
$include comms.txt
$include resources.txt
$include interviews.txt
$include lua.txt
# You can use the @ character at the beginning of a config option to lock it from being edited in-game
# Example usage:
+9
View File
@@ -0,0 +1,9 @@
## This file allows server hosts to specify the paths that the lua function "require" searches for modules in.
## The character '?' will be replaced with the name of the module being searched.
LUA_PATH ?
LUA_PATH ?.lua
LUA_PATH ?.luau
LUA_PATH lua/?
LUA_PATH lua/?.lua
LUA_PATH lua/?.luau
+6
View File
@@ -19,3 +19,9 @@ export SPACEMAN_DMM_VERSION=suite-1.7.1
# Python version for mapmerge and other tools
export PYTHON_VERSION=3.7.9
#auxlua repo
export AUXLUA_REPO=tgstation/auxlua
#auxlua git tag
export AUXLUA_VERSION=0.1.5
+153
View File
@@ -0,0 +1,153 @@
local SS13 = {}
SS13.SSlua = dm.global_vars:get_var("SSlua")
SS13.global_proc = "some_magic_bullshit"
local states = SS13.SSlua:get_var("states"):to_table()
for _, state in states do
if state:get_var("internal_id") == dm.state_id then
SS13.state = state
break
end
end
function SS13.istype(thing, type)
return dm.global_proc("_istype", thing, dm.global_proc("_text2path", type)) == 1
end
function SS13.new(type, ...)
local datum = dm.global_proc("_new", type, {...})
local references = SS13.state:get_var("references")
references:add(datum)
return datum
end
function SS13.await(thing_to_call, proc_to_call, ...)
if not SS13.istype(thing_to_call, "/datum") then
thing_to_call = SS13.global_proc
end
if thing_to_call == SS13.global_proc then
proc_to_call = "/proc/" .. proc_to_call
end
local promise = SS13.new("/datum/auxtools_promise", thing_to_call, proc_to_call, ...)
while promise:get_var("status") == 0 do
sleep()
end
return promise:get_var("return_value"), promise:get_var("runtime_message")
end
function SS13.wait(time, timer)
local index = #__yield_table + 1
local callback = SS13.new("/datum/callback", SS13.SSlua, "queue_resume", SS13.state, index)
local timedevent = dm.global_proc("_addtimer", callback, time*10, 8, timer, debug.info(1, "sl"))
coroutine.yield()
dm.global_proc("deltimer", timedevent, timer)
end
function SS13.register_signal(datum, signal, func, make_easy_clear_function)
if not SS13.signal_handlers then
SS13.signal_handlers = {}
end
if not SS13.istype(datum, "/datum") then return end
local ref = dm.global_proc("REF", datum)
if not SS13.signal_handlers[ref] then
SS13.signal_handlers[ref] = {}
end
if signal == "_cleanup" then return end
if not SS13.signal_handlers[ref][signal] then
SS13.signal_handlers[ref][signal] = {}
end
local callback = SS13.new("/datum/callback", SS13.state, "call_function_return_first")
callback:call_proc("RegisterSignal", datum, signal, "Invoke")
local callback_ref = dm.global_proc("REF", callback)
local path = {"SS13", "signal_handlers", ref, signal, callback_ref, "func"}
callback:set_var("arguments", {path})
if not SS13.signal_handlers[ref]["_cleanup"] then
local cleanup_path = {"SS13", "signal_handlers", ref, "_cleanup"}
local cleanup_callback = SS13.new("/datum/callback", SS13.state, "call_function_return_first", cleanup_path)
cleanup_callback:call_proc("RegisterSignal", datum, "parent_qdeleting", "Invoke")
SS13.signal_handlers[ref]["_cleanup"] = function(datum)
SS13.signal_handler_cleanup(datum)
dm.global_proc("qdel", cleanup_callback)
end
end
SS13.signal_handlers[ref][signal][callback_ref] = {func=func, callback=callback}
if make_easy_clear_function then
local clear_function_name = "clear_signal_" .. ref .. "_" .. signal .. "_" .. callback_ref
SS13[clear_function_name] = function()
if callback then
SS13.unregister_signal(datum, signal, callback)
end
SS13[clear_function_name] = nil
end
end
return callback
end
function SS13.unregister_signal(datum, signal, callback)
local function clear_handler(handler_info)
if not handler_info then return end
if not handler_info.callback then return end
local handler_callback = handler_info.callback
handler_callback:call_proc("UnregisterSignal", datum, signal)
dm.global_proc("qdel", handler_callback)
end
if not SS13.signal_handlers then return end
local ref = dm.global_proc("REF", datum)
local function clear_easy_clear_function(callback_ref)
local clear_function_name = "clear_signal_" .. ref .. "_" .. signal .. "_" .. callback_ref
SS13[clear_function_name] = nil
end
if not SS13.signal_handlers[ref] then return end
if signal == "_cleanup" then return end
if not SS13.signal_handlers[ref][signal] then return end
if not callback then
for handler_key, handler_info in SS13.signal_handlers[ref][signal] do
clear_easy_clear_function(handler_key)
clear_handler(handler_info)
end
SS13.signal_handlers[ref][signal] = nil
else
if not SS13.istype(callback, "/datum/callback") then return end
local callback_ref = dm.global_proc("REF", callback)
clear_easy_clear_function(callback_ref)
clear_handler(SS13.signal_handlers[ref][signal][callback_ref])
SS13.signal_handlers[ref][signal][callback_ref] = nil
end
end
function SS13.signal_handler_cleanup(datum)
if not SS13.signal_handlers then return end
local ref = dm.global_proc("REF", datum)
if not SS13.signal_handlers[ref] then return end
for signal, _ in SS13.signal_handlers[ref] do
SS13.unregister_signal(datum, signal)
end
SS13.signal_handlers[ref] = nil
end
function SS13.set_timeout(time, func)
if not SS13.timeouts then
SS13.timeouts = {}
end
local callback = SS13.new("/datum/callback", SS13.state, "call_function")
local callback_ref = dm.global_proc("REF", callback)
SS13.timeouts[callback_ref] = function()
SS13.timeouts[callback_ref] = nil
func()
end
local path = {"SS13", "timeouts", callback_ref}
callback:set_var("arguments", {path})
dm.global_proc("_addtimer", callback, time*10, 8, nil, debug.info(1, "sl"))
end
return SS13
+9
View File
@@ -276,6 +276,7 @@
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_silicon.dm"
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_simple.dm"
#include "code\__DEFINES\research\anomalies.dm"
#include "code\__HELPERS\_auxtools_api.dm"
#include "code\__HELPERS\_lists.dm"
#include "code\__HELPERS\_string_lists.dm"
#include "code\__HELPERS\admin.dm"
@@ -283,6 +284,7 @@
#include "code\__HELPERS\areas.dm"
#include "code\__HELPERS\atmospherics.dm"
#include "code\__HELPERS\atoms.dm"
#include "code\__HELPERS\auxtools.dm"
#include "code\__HELPERS\bitflag_lists.dm"
#include "code\__HELPERS\bodyparts.dm"
#include "code\__HELPERS\chat.dm"
@@ -459,6 +461,7 @@
#include "code\controllers\configuration\entries\game_options.dm"
#include "code\controllers\configuration\entries\general.dm"
#include "code\controllers\configuration\entries\interview.dm"
#include "code\controllers\configuration\entries\lua.dm"
#include "code\controllers\configuration\entries\resources.dm"
#include "code\controllers\subsystem\achievements.dm"
#include "code\controllers\subsystem\addiction.dm"
@@ -496,6 +499,7 @@
#include "code\controllers\subsystem\language.dm"
#include "code\controllers\subsystem\library.dm"
#include "code\controllers\subsystem\lighting.dm"
#include "code\controllers\subsystem\lua.dm"
#include "code\controllers\subsystem\machines.dm"
#include "code\controllers\subsystem\mapping.dm"
#include "code\controllers\subsystem\materials.dm"
@@ -2064,6 +2068,11 @@
#include "code\modules\admin\verbs\server.dm"
#include "code\modules\admin\verbs\shuttlepanel.dm"
#include "code\modules\admin\verbs\spawnobjasmob.dm"
#include "code\modules\admin\verbs\lua\_hooks.dm"
#include "code\modules\admin\verbs\lua\_wrappers.dm"
#include "code\modules\admin\verbs\lua\helpers.dm"
#include "code\modules\admin\verbs\lua\lua_editor.dm"
#include "code\modules\admin\verbs\lua\lua_state.dm"
#include "code\modules\admin\verbs\SDQL2\SDQL_2.dm"
#include "code\modules\admin\verbs\SDQL2\SDQL_2_parser.dm"
#include "code\modules\admin\verbs\SDQL2\SDQL_2_wrappers.dm"
+52
View File
@@ -309,3 +309,55 @@ export class ButtonInput extends Component {
}
Button.Input = ButtonInput;
export class ButtonFile extends Component {
constructor() {
super();
this.inputRef = createRef();
}
async read(files) {
const promises = Array.from(files).map((file) => {
let reader = new FileReader();
return new Promise((resolve) => {
reader.onload = () => resolve(reader.result);
reader.readAsText(file);
});
});
return await Promise.all(promises);
}
render() {
const { onSelectFiles, accept, multiple, ...rest } = this.props;
const filePicker = (
<input
hidden
type="file"
ref={this.inputRef}
accept={accept}
multiple={multiple}
onChange={async () => {
const files = this.inputRef.current.files;
if (files.length) {
const readFiles = await this.read(files);
onSelectFiles(multiple ? readFiles : readFiles[0]);
}
}}
/>
);
return (
<>
<Button
{...rest}
onClick={() => {
this.inputRef.current.click();
}}
/>
{filePicker}
</>
);
}
}
Button.File = ButtonFile;
+7 -2
View File
@@ -16,6 +16,7 @@ type SectionProps = BoxProps & {
fill?: boolean;
fitted?: boolean;
scrollable?: boolean;
scrollableHorizontal?: boolean;
/** @deprecated This property no longer works, please remove it. */
level?: boolean;
/** @deprecated Please use `scrollable` property */
@@ -25,21 +26,23 @@ type SectionProps = BoxProps & {
export class Section extends Component<SectionProps> {
scrollableRef: RefObject<HTMLDivElement>;
scrollable: boolean;
scrollableHorizontal: boolean;
constructor(props) {
super(props);
this.scrollableRef = createRef();
this.scrollable = props.scrollable;
this.scrollableHorizontal = props.scrollableHorizontal;
}
componentDidMount() {
if (this.scrollable) {
if (this.scrollable || this.scrollableHorizontal) {
addScrollableNode(this.scrollableRef.current);
}
}
componentWillUnmount() {
if (this.scrollable) {
if (this.scrollable || this.scrollableHorizontal) {
removeScrollableNode(this.scrollableRef.current);
}
}
@@ -52,6 +55,7 @@ export class Section extends Component<SectionProps> {
fill,
fitted,
scrollable,
scrollableHorizontal,
children,
...rest
} = this.props;
@@ -64,6 +68,7 @@ export class Section extends Component<SectionProps> {
fill && 'Section--fill',
fitted && 'Section--fitted',
scrollable && 'Section--scrollable',
scrollableHorizontal && 'Section--scrollableHorizontal',
className,
computeBoxClassName(rest),
])}
@@ -0,0 +1,42 @@
import { useBackend, useLocalState } from '../../backend';
import { Button, Modal, Section } from '../../components';
import { ListMapper } from './ListMapper';
export const CallModal = (props, context) => {
const { act, data } = useBackend(context);
const { callArguments } = data;
const [, setModal] = useLocalState(context, 'modal');
const [toCall, setToCall] = useLocalState(context, 'toCallTaskInfo');
const { type, params } = toCall;
return (
<Modal height="600px" width="600px">
<Section
fill
scrollable
scrollableHorizontal
title="Call Function/Task"
buttons={
<Button
color="red"
icon="window-close"
onClick={() => {
setModal(null);
setToCall(null);
act('clearArgs');
}}>
Cancel
</Button>
}>
<ListMapper name="Arguments" list={callArguments} editable />
<Button
onClick={() => {
setModal(null);
setToCall(null);
act(type, params);
}}>
Call
</Button>
</Section>
</Modal>
);
};
@@ -0,0 +1,38 @@
import { useLocalState } from '../../backend';
import { Button, Modal, Section, Box } from '../../components';
import { sanitizeText } from '../../sanitize';
import hljs from 'highlight.js/lib/core';
export const ChunkViewModal = (props, context) => {
const [, setModal] = useLocalState(context, 'modal');
const [viewedChunk, setViewedChunk] = useLocalState(context, 'viewedChunk');
return (
<Modal height="600px" width="500px">
<Section
fill
scrollable
scrollableHorizontal
title="Chunk"
buttons={
<Button
color="red"
icon="window-close"
onClick={() => {
setModal(null);
setViewedChunk(null);
}}>
Close
</Button>
}>
<Box
as="pre"
dangerouslySetInnerHTML={{
__html: hljs.highlight(sanitizeText(viewedChunk), {
language: 'lua',
}).value,
}}
/>
</Section>
</Modal>
);
};
@@ -0,0 +1,163 @@
import { useBackend, useLocalState } from '../../backend';
import { Box, Button, Collapsible, LabeledList, Section } from '../../components';
const RefRegex = RegExp('^.+ \\[0x[0-9a-fA-F]+]$');
const FunctionRegex = RegExp('^function: 0x[0-9a-fA-F]+$');
const UnconvertibleLuaValueRegex = RegExp(
'^(table|function|thread): 0x[0-9a-fA-F]+$'
);
export const ListMapper = (props, context) => {
const { act } = useBackend(context);
const {
list,
path,
editable,
name,
callType,
vvAct,
skipNulls,
collapsible,
...rest
} = props;
const [, setToCall] = useLocalState(context, 'toCallTaskInfo');
const [, setModal] = useLocalState(context, 'modal');
const ThingNode = (thing, path, overrideProps) => {
if (Array.isArray(thing)) {
return (
<ListMapper
{...props}
list={thing}
name={`List[${thing.length}]`}
path={path}
collapsible
{...rest}
{...overrideProps}
/>
);
} else if (typeof thing === 'string') {
if (FunctionRegex.test(thing) && callType) {
return (
<Button
tooltip="Click to call"
onClick={() => {
setToCall({
type: callType,
params: {
indices: path.map((v) => v.index),
},
});
setModal('call');
}}>
{thing.charAt(0).toUpperCase() + thing.substring(1)}
</Button>
);
} else if (UnconvertibleLuaValueRegex.test(thing)) {
return <b>{thing.charAt(0).toUpperCase() + thing.substring(1)}</b>;
} else if (RefRegex.test(thing)) {
return (
<Button tooltip="Click to VV Datum" onClick={() => vvAct(path)}>
{thing}
</Button>
);
} else if (thing === null) {
return <b>Nil</b>;
} else {
return thing;
}
} else {
return <Box {...rest}>{thing}</Box>;
}
};
const ListMapperInner = (element, i) => {
const { key, value } = element;
const basePath = path ? path : [];
let keyPath = [...basePath, { index: i + 1, type: 'key' }];
let valuePath = [...basePath, { index: i + 1, type: 'value' }];
let entryPath = [...basePath, { index: i + 1, type: 'entry' }];
if (key === null && skipNulls) {
return;
}
/*
* Finding a function only accessible as a table's key is too awkward to
* deal with for now
*/
let keyNode = ThingNode(key, keyPath, { callType: null });
/*
* Likewise, since table, thread, and userdata equality is tested by
* reference rather than value, we can't find functions whose keys
* within the table are tables, threads, or userdata
*/
const uniquelyIndexable =
(typeof key === 'string' &&
!(UnconvertibleLuaValueRegex.test(key) || RefRegex.test(key))) ||
typeof key === 'number';
let valueNode = ThingNode(value, valuePath, {
callType: uniquelyIndexable && callType,
});
return (
<LabeledList.Item
label={keyNode}
buttons={
editable && (
<>
<Button
icon="arrow-up"
disabled={i === 0}
tooltip="Move Up"
onClick={() => act('moveArgUp', { path: entryPath })}
/>
<Button
icon="arrow-down"
disabled={i === list.length - 1}
tooltip="Move Down"
onClick={() => act('moveArgDown', { path: entryPath })}
/>
<Button
icon="window-close"
color="red"
tooltip="Remove"
onClick={() => act('removeArg', { path: entryPath })}
/>
</>
)
}>
{valueNode}
</LabeledList.Item>
);
};
const inner = (
<>
{list && list.map(ListMapperInner)}
{editable && (
<Button
icon="plus"
tooltip="Add"
onClick={() => act('addArg', { path: path })}
/>
)}
</>
);
const buttons = vvAct && list?.length > 0 && (
<Button icon="search" tooltip="VV List" onClick={() => vvAct(path)} />
);
return collapsible ? (
<Collapsible title={name} buttons={buttons}>
{inner}
</Collapsible>
) : (
<Section title={name} buttons={buttons}>
{inner}
</Section>
);
};
@@ -0,0 +1,141 @@
import { useBackend, useLocalState } from '../../backend';
import { logger } from '../../logging';
import { Box, Button, Divider } from '../../components';
import { ListMapper } from './ListMapper';
export const Log = (props, context) => {
const { act, data } = useBackend(context);
const { stateLog } = data;
const [, setViewedChunk] = useLocalState(context, 'viewedChunk');
const [, setModal] = useLocalState(context, 'modal');
// We only kvpify logs so that the return values are kvpified.
const mappedLog = stateLog.map(({ value }) =>
Object.fromEntries(value.map(({ key, value }) => [key, value]))
);
return mappedLog.map((element, i) => {
const { name, status, param, chunk, repeats } = element;
let message;
let messageColor;
switch (status) {
case 'sleeping':
if (chunk) {
messageColor = 'blue';
message = (
<>
<b>{name}</b> slept.
</>
);
}
break;
case 'yielded':
message = (
<>
<b>{name}</b> yielded
{param.length
? ` ${param.length} value${param.length > 1 ? 's' : ''}`
: ''}
.
{param.length ? (
<ListMapper
list={param}
skipNulls
name="Return Value"
collapsible
vvAct={(path) =>
act('vvReturnValue', {
entryIndex: i + 1,
tableIndices: path,
})
}
/>
) : (
<br />
)}
</>
);
messageColor = 'yellow';
break;
case 'finished':
message = (
<>
<b>{name}</b> returned
{param.length
? ` ${param.length} value${param.length > 1 ? 's' : ''}`
: ''}
.
<Box color="default">
{param.length ? (
<ListMapper
list={param}
skipNulls
name="Return Value"
collapsible
vvAct={(path) =>
act('vvReturnValue', {
entryIndex: i + 1,
tableIndices: path,
})
}
/>
) : (
<br />
)}
</Box>
</>
);
messageColor = 'green';
break;
case 'errored':
case 'bad return':
message = (
<>
<b>{name}</b> errored with the message &quot;{param}&quot;
</>
);
messageColor = 'red';
break;
case 'print':
message = param;
break;
default:
logger.warn(`unknown log status ${status}`);
}
if (message === undefined) {
return;
}
if (chunk) {
message = (
<>
{message}
<Button
onClick={() => {
setViewedChunk(chunk);
setModal('viewChunk');
}}>
View Source
</Button>
</>
);
}
return (
<>
{i > 0 && <Divider />}
<Box width="100%" key={i} color={messageColor}>
{message}
</Box>
{repeats && (
<Box
inline
px="0.25rem"
mt="0.25rem"
style={{
'border-radius': '0.5em',
}}
backgroundColor={messageColor}>
x{repeats + 1}
</Box>
)}
</>
);
});
};
@@ -0,0 +1,58 @@
import { useBackend, useLocalState } from '../../backend';
import { Button, Modal, Input, Section, Stack } from '../../components';
export const StateSelectModal = (props, context) => {
const { act, data } = useBackend(context);
const [, setModal] = useLocalState(context, 'modal', 'states');
const [input, setInput] = useLocalState(context, 'newStateName', '');
const { states } = data;
return (
<Modal height="400px" width="300px">
<Section
fill
title="States"
buttons={
<Button
color="red"
icon="window-close"
onClick={() => {
setModal(null);
}}>
Cancel
</Button>
}>
{states.map((value, i) => (
<Button
key={i}
onClick={() => {
setModal(null);
act('switchState', { index: i + 1 });
}}>
{value}
</Button>
))}
<Stack fill>
<Stack.Item shrink basis="100%">
<Input
fluid
placeholder="New State"
value={input}
onInput={(_, value) => {
setInput(value);
}}
/>
</Stack.Item>
<Stack.Item>
<Button
icon="plus"
onClick={() => {
setModal(null);
act('newState', { name: input });
}}
/>
</Stack.Item>
</Stack>
</Section>
</Modal>
);
};
@@ -0,0 +1,68 @@
import { useBackend, useLocalState } from '../../backend';
import { Button, LabeledList, Section, Stack } from '../../components';
export const TaskManager = (props, context) => {
const { act, data } = useBackend(context);
const [, setToCall] = useLocalState(context, 'toCallTaskInfo');
const [, setModal] = useLocalState(context, 'modal');
let { tasks } = data;
tasks?.sort((a, b) => {
if (a.status < b.status) {
return -1;
} else if (a.status > b.status) {
return 1;
} else {
return 0;
}
});
const sleeps = tasks.filter((info) => info.status === 'sleep');
const yields = tasks.filter((info) => info.status === 'yield');
return (
<Stack fill width="100%" justify="space-around">
<Stack.Item grow="1" shrink="1">
<Section title="Sleeps" fill>
<LabeledList>
{sleeps.map((info, i) => (
<LabeledList.Item key={i} label={info.name}>
<Button
color="red"
icon="window-close"
onClick={() => act('killTask', { info: info })}>
Kill
</Button>
</LabeledList.Item>
))}
</LabeledList>
</Section>
</Stack.Item>
<Stack.Item grow="1" shrink="1">
<Section title="Yields" fill>
<LabeledList>
{yields.map((info, i) => (
<LabeledList.Item key={i} label={info.name}>
<Button
onClick={() => {
setToCall({
type: 'resumeTask',
params: { index: info.index },
});
setModal('call');
}}>
Call
</Button>
<Button
color="red"
icon="window-close"
onClick={() => {
act('killTask', { info: info });
}}>
Kill
</Button>
</LabeledList.Item>
))}
</LabeledList>
</Section>
</Stack.Item>
</Stack>
);
};
@@ -0,0 +1,159 @@
import { useBackend, useLocalState } from '../../backend';
import { Box, Button, Flex, Section, Tabs, TextArea, Modal, Stack } from '../../components';
import { Window } from '../../layouts';
import { CallModal } from './CallModal';
import { ChunkViewModal } from './ChunkViewModal';
import { StateSelectModal } from './StateSelectModal';
import { ListMapper } from './ListMapper';
import { Log } from './Log';
import { TaskManager } from './TaskManager';
import { sanitizeText } from '../../sanitize';
import { marked } from 'marked';
import hljs from 'highlight.js/lib/core';
import lua from 'highlight.js/lib/languages/lua';
hljs.registerLanguage('lua', lua);
export const LuaEditor = (props, context) => {
const { act, data } = useBackend(context);
const { noStateYet, globals, documentation } = data;
const [modal, setModal] = useLocalState(
context,
'modal',
noStateYet ? 'states' : null
);
const [activeTab, setActiveTab] = useLocalState(
context,
'activeTab',
'globals'
);
const [input, setInput] = useLocalState(context, 'scriptInput', '');
let tabContent;
switch (activeTab) {
case 'globals': {
tabContent = (
<ListMapper
list={globals}
skipNulls
vvAct={(path) => act('vvGlobal', { indices: path })}
callType="callFunction"
/>
);
break;
}
case 'tasks': {
tabContent = <TaskManager />;
break;
}
case 'log': {
tabContent = <Log />;
break;
}
}
return (
<Window width={1280} height={720}>
<Window.Content>
<Button icon="file" onClick={() => setModal('states')}>
States
</Button>
{noStateYet ? (
<Flex
width="100%"
height="100%"
align="center"
justify="space-around">
<h1>Please select or create a lua state to get started.</h1>
</Flex>
) : (
<Stack>
<Stack.Item>
<Section
fill
title="Input"
buttons={
<>
<Button.File
onSelectFiles={(file) => setInput(file)}
accept=".lua,.luau">
Import
</Button.File>
<Button onClick={() => setModal('documentation')}>
Help
</Button>
</>
}>
<TextArea
fluid
width="700px"
height="590px"
value={input}
onInput={(_, value) => setInput(value)}
/>
<Button onClick={() => act('runCode', { code: input })}>
Run
</Button>
</Section>
</Stack.Item>
<Stack.Item grow>
<Section fill height="95%" width="100%">
<Tabs>
<Tabs.Tab
selected={activeTab === 'globals'}
onClick={() => {
setActiveTab('globals');
}}>
Globals
</Tabs.Tab>
<Tabs.Tab
selected={activeTab === 'tasks'}
onClick={() => setActiveTab('tasks')}>
Tasks
</Tabs.Tab>
<Tabs.Tab
selected={activeTab === 'log'}
onClick={() => {
setActiveTab('log');
}}>
Log
</Tabs.Tab>
</Tabs>
<Section fill scrollable scrollableHorizontal width="100%">
{tabContent}
</Section>
</Section>
</Stack.Item>
</Stack>
)}
</Window.Content>
{modal === 'states' && <StateSelectModal />}
{modal === 'viewChunk' && <ChunkViewModal />}
{modal === 'call' && <CallModal />}
{modal === 'documentation' && (
<Modal>
<Button
color="red"
icon="window-close"
onClick={() => {
setModal(null);
}}>
Close
</Button>
<Section height="500px" width="700px" fill scrollable>
<Box
dangerouslySetInnerHTML={{
__html: marked(sanitizeText(documentation), {
breaks: true,
smartypants: true,
smartLists: true,
langPrefix: 'hljs language-',
highlight: (code) => {
return hljs.highlight(code, { language: 'lua' }).value;
},
}),
}}
/>
</Section>
</Modal>
)}
</Window>
);
};
+1
View File
@@ -7,6 +7,7 @@
"common": "workspace:*",
"dateformat": "^4.5.1",
"dompurify": "^2.3.1",
"highlight.js": "^11.5.1",
"inferno": "^7.4.8",
"inferno-vnode-flags": "^7.4.8",
"js-yaml": "^4.1.0",
@@ -100,6 +100,26 @@ $separator-color: colors.$primary !default;
}
}
.Section--scrollableHorizontal {
overflow-x: hidden;
overflow-y: hidden;
& > .Section__rest > .Section__content {
overflow-y: hidden;
overflow-x: scroll;
}
}
.Section--scrollable.Section--scrollableHorizontal {
overflow-x: hidden;
overflow-y: hidden;
& > .Section__rest > .Section__content {
overflow-y: scroll;
overflow-x: scroll;
}
}
// Nested sections
.Section .Section {
background-color: transparent;
+2
View File
@@ -77,6 +77,8 @@
@include meta.load-css('./layouts/TitleBar.scss');
@include meta.load-css('./layouts/Window.scss');
@include meta.load-css('highlight.js/scss/github-dark.scss');
// NT Theme
.Layout__content {
background-image: url('../assets/bg-nanotrasen.svg');
+8
View File
@@ -4975,6 +4975,13 @@ __metadata:
languageName: node
linkType: hard
"highlight.js@npm:^11.5.1":
version: 11.5.1
resolution: "highlight.js@npm:11.5.1"
checksum: bff556101d7691c6275ad19318e368fc971cd0621ef3d86222f5373df7d8191a2fc1ffd47f118138cbcf85e5fe91cfeefaecd6184f49a3ec18090955efc9edef
languageName: node
linkType: hard
"html-element-attributes@npm:2.3.0":
version: 2.3.0
resolution: "html-element-attributes@npm:2.3.0"
@@ -9142,6 +9149,7 @@ resolve@^2.0.0-next.3:
common: "workspace:*"
dateformat: ^4.5.1
dompurify: ^2.3.1
highlight.js: ^11.5.1
inferno: ^7.4.8
inferno-vnode-flags: ^7.4.8
js-yaml: ^4.1.0
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
source dependencies.sh
mkdir -p ~/.byond/bin
wget -nv -O ~/.byond/bin/libauxlua.so "https://github.com/$AUXLUA_REPO/releases/download/$AUXLUA_VERSION/libauxlua.so"
chmod +x ~/.byond/bin/libauxlua.so
ldd ~/.byond/bin/libauxlua.so